Implement lexing, basic parsing, and pretty printing
This commit is contained in:
parent
87a4d7084f
commit
f4278a29b7
1
src/color.rs
Normal file
1
src/color.rs
Normal file
@ -0,0 +1 @@
|
||||
|
1052
src/common.rs
Normal file
1052
src/common.rs
Normal file
File diff suppressed because it is too large
Load Diff
1
src/error.rs
Normal file
1
src/error.rs
Normal file
@ -0,0 +1 @@
|
||||
|
159
src/format.rs
Normal file
159
src/format.rs
Normal file
@ -0,0 +1,159 @@
|
||||
use std::io::{self, Write};
|
||||
|
||||
use crate::{RuleSet, Stmt, Style, StyleSheet};
|
||||
|
||||
pub(crate) struct PrettyPrinter<W: Write> {
|
||||
buf: W,
|
||||
scope: usize,
|
||||
}
|
||||
|
||||
impl<W: Write> PrettyPrinter<W> {
|
||||
pub(crate) fn new(buf: W) -> Self {
|
||||
PrettyPrinter { buf, scope: 0 }
|
||||
}
|
||||
|
||||
fn pretty_print_stmt(&mut self, stmt: &Stmt) -> io::Result<()> {
|
||||
let padding = vec![' '; self.scope * 2].iter().collect::<String>();
|
||||
match stmt {
|
||||
Stmt::RuleSet(RuleSet {
|
||||
selector, rules, ..
|
||||
}) => {
|
||||
writeln!(self.buf, "{}{} {{", padding, selector)?;
|
||||
self.scope += 1;
|
||||
for rule in rules {
|
||||
self.pretty_print_stmt(rule)?;
|
||||
}
|
||||
writeln!(self.buf, "{}}}", padding)?;
|
||||
self.scope -= 1;
|
||||
}
|
||||
Stmt::Style(Style { property, value }) => {
|
||||
writeln!(
|
||||
self.buf,
|
||||
"{}{}: {};",
|
||||
padding,
|
||||
property,
|
||||
value
|
||||
.iter()
|
||||
.map(ToString::to_string)
|
||||
.collect::<Vec<String>>()
|
||||
.join(" ")
|
||||
)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn pretty_print(&mut self, s: &StyleSheet) -> io::Result<()> {
|
||||
for rule in &s.rules {
|
||||
self.pretty_print_stmt(rule)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::StyleSheet;
|
||||
macro_rules! test {
|
||||
($func:ident, $input:literal) => {
|
||||
#[test]
|
||||
fn $func() {
|
||||
let mut buf = Vec::new();
|
||||
StyleSheet::new($input)
|
||||
.pretty_print(&mut buf)
|
||||
.expect(concat!("failed to pretty print on ", $input));
|
||||
assert_eq!(
|
||||
String::from($input),
|
||||
String::from_utf8(buf).expect("produced invalid utf8")
|
||||
);
|
||||
}
|
||||
};
|
||||
($func:ident, $input:literal, $output:literal) => {
|
||||
#[test]
|
||||
fn $func() {
|
||||
let mut buf = Vec::new();
|
||||
StyleSheet::new($input)
|
||||
.pretty_print(&mut buf)
|
||||
.expect(concat!("failed to pretty print on ", $input));
|
||||
assert_eq!(
|
||||
String::from($output),
|
||||
String::from_utf8(buf).expect("produced invalid utf8")
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
test!(empty, "");
|
||||
test!(basic_nesting, "a {\n b {\n }\n}\n");
|
||||
test!(ident_with_num, "el1 {\n}\n");
|
||||
|
||||
test!(selector_element, "a {\n}\n");
|
||||
test!(selector_id, "#id {\n}\n");
|
||||
test!(selector_class, ".class {\n}\n");
|
||||
test!(selector_el_descendant, "a a {\n}\n");
|
||||
test!(selector_universal, "* {\n}\n");
|
||||
test!(selector_el_class_and, "a.class {\n}\n");
|
||||
test!(selector_el_id_and, "a#class {\n}\n");
|
||||
test!(selector_el_class_descendant, "a .class {\n}\n");
|
||||
test!(selector_el_id_descendant, "a #class {\n}\n");
|
||||
test!(selector_el_universal_descendant, "a * {\n}\n");
|
||||
test!(selector_universal_el_descendant, "* a {\n}\n");
|
||||
test!(selector_attribute_any, "[attr] {\n}\n");
|
||||
test!(selector_attribute_equals, "[attr=val] {\n}\n");
|
||||
test!(selector_attribute_in, "[attr~=val] {\n}\n");
|
||||
test!(
|
||||
selector_attribute_begins_hyphen_or_exact,
|
||||
"[attr|=val] {\n}\n"
|
||||
);
|
||||
test!(selector_attribute_starts_with, "[attr^=val] {\n}\n");
|
||||
test!(selector_attribute_ends_with, "[attr$=val] {\n}\n");
|
||||
test!(selector_attribute_contains, "[attr*=val] {\n}\n");
|
||||
test!(selector_el_attribute_and, "a[attr] {\n}\n");
|
||||
test!(selector_el_attribute_descendant, "a [attr] {\n}\n");
|
||||
test!(selector_el_mul_el, "a, b {\n}\n");
|
||||
test!(selector_el_immediate_child_el, "a > b {\n}\n");
|
||||
test!(selector_el_following_el, "a + b {\n}\n");
|
||||
test!(selector_el_preceding_el, "a ~ b {\n}\n");
|
||||
test!(selector_pseudo, ":pseudo {\n}\n");
|
||||
test!(selector_el_pseudo_and, "a:pseudo {\n}\n");
|
||||
test!(selector_el_pseudo_descendant, "a :pseudo {\n}\n");
|
||||
|
||||
test!(basic_style, "a {\n color: red;\n}\n");
|
||||
test!(two_styles, "a {\n color: red;\n color: blue;\n}\n");
|
||||
test!(hyphenated_style_property, "a {\n font-family: Arial;\n}\n");
|
||||
test!(hyphenated_style_value, "a {\n color: Open-Sans;\n}\n");
|
||||
test!(
|
||||
space_separated_style_value,
|
||||
"a {\n border: solid red;\n}\n"
|
||||
);
|
||||
test!(
|
||||
single_quoted_style_value,
|
||||
"a {\n font: 'Open-Sans';\n}\n",
|
||||
"a {\n font: Open-Sans;\n}\n"
|
||||
);
|
||||
test!(
|
||||
double_quoted_style_value,
|
||||
"a {\n font: \"Open-Sans\";\n}\n",
|
||||
"a {\n font: Open-Sans;\n}\n"
|
||||
);
|
||||
// test!(comma_style_value, "a {\n font: Open-Sans, sans-serif;\n}\n");
|
||||
test!(
|
||||
nested_style_in_parent,
|
||||
"a {\n color: red;\n b {\n }\n}\n"
|
||||
);
|
||||
test!(
|
||||
nested_style_in_child,
|
||||
"a {\n b {\n color: red;\n }\n}\n"
|
||||
);
|
||||
test!(
|
||||
nested_style_in_both,
|
||||
"a {\n color: red;\n b {\n color: red;\n }\n}\n"
|
||||
);
|
||||
|
||||
test!(unit_none, "a {\n height: 1;\n}\n");
|
||||
test!(unit_not_attached, "a {\n height: 1 px;\n}\n");
|
||||
test!(unit_px, "a {\n height: 1px;\n}\n");
|
||||
test!(unit_em, "a {\n height: 1em;\n}\n");
|
||||
test!(unit_rem, "a {\n height: 1rem;\n}\n");
|
||||
test!(unit_percent, "a {\n height: 1%;\n}\n");
|
||||
}
|
305
src/lexer.rs
Normal file
305
src/lexer.rs
Normal file
@ -0,0 +1,305 @@
|
||||
use std::convert::TryFrom;
|
||||
use std::iter::Peekable;
|
||||
use std::str::Chars;
|
||||
|
||||
use crate::common::{Keyword, Pos, Symbol};
|
||||
use crate::selector::{Attribute, AttributeKind, Selector};
|
||||
use crate::units::Unit;
|
||||
use crate::{Token, TokenKind, Whitespace};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Lexer<'a> {
|
||||
tokens: Vec<Token>,
|
||||
buf: Peekable<Chars<'a>>,
|
||||
pos: Pos,
|
||||
}
|
||||
|
||||
impl<'a> Iterator for Lexer<'a> {
|
||||
type Item = Token;
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
macro_rules! symbol {
|
||||
($self:ident, $symbol:ident) => {{
|
||||
$self.buf.next();
|
||||
$self.pos.next_char();
|
||||
TokenKind::Symbol(Symbol::$symbol)
|
||||
}};
|
||||
}
|
||||
macro_rules! whitespace {
|
||||
($self:ident, $whitespace:ident) => {{
|
||||
$self.buf.next();
|
||||
$self.pos.next_char();
|
||||
TokenKind::Whitespace(Whitespace::$whitespace)
|
||||
}};
|
||||
}
|
||||
let kind: TokenKind = match self.buf.peek().unwrap_or(&'\0') {
|
||||
'a'..='z' | 'A'..='Z' => self.lex_ident(),
|
||||
'@' => self.lex_at_rule(),
|
||||
'0'..='9' => self.lex_num(),
|
||||
'$' => self.lex_variable(),
|
||||
':' => symbol!(self, Colon),
|
||||
',' => symbol!(self, Comma),
|
||||
'.' => symbol!(self, Period),
|
||||
';' => symbol!(self, SemiColon),
|
||||
'+' => symbol!(self, Plus),
|
||||
'~' => symbol!(self, Tilde),
|
||||
'\'' => symbol!(self, SingleQuote),
|
||||
'"' => symbol!(self, DoubleQuote),
|
||||
' ' => whitespace!(self, Space),
|
||||
'\t' => whitespace!(self, Tab),
|
||||
'\n' => whitespace!(self, Newline),
|
||||
'\r' => whitespace!(self, CarriageReturn),
|
||||
'#' => symbol!(self, Hash),
|
||||
'{' => symbol!(self, OpenBrace),
|
||||
'*' => symbol!(self, Mul),
|
||||
'}' => symbol!(self, CloseBrace),
|
||||
'%' => {
|
||||
self.buf.next();
|
||||
self.pos.next_char();
|
||||
TokenKind::Unit(Unit::Percent)
|
||||
}
|
||||
'[' => {
|
||||
self.buf.next();
|
||||
self.pos.next_char();
|
||||
self.lex_attr()
|
||||
}
|
||||
'<' => symbol!(self, Lt),
|
||||
'>' => symbol!(self, Gt),
|
||||
'\0' => return None,
|
||||
_ => todo!("unknown char"),
|
||||
};
|
||||
self.pos.next_char();
|
||||
Some(Token {
|
||||
kind,
|
||||
pos: self.pos,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn is_whitespace(c: char) -> bool {
|
||||
c == ' ' || c == '\n' || c == '\r'
|
||||
}
|
||||
|
||||
impl<'a> Lexer<'a> {
|
||||
pub fn new(buf: &'a str) -> Lexer<'a> {
|
||||
Lexer {
|
||||
tokens: Vec::with_capacity(buf.len()),
|
||||
buf: buf.chars().peekable(),
|
||||
pos: Pos::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn devour_whitespace(&mut self) {
|
||||
while let Some(c) = self.buf.peek() {
|
||||
if !is_whitespace(*c) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn lex_at_rule(&mut self) -> TokenKind {
|
||||
let mut string = String::with_capacity(99);
|
||||
while let Some(c) = self.buf.peek() {
|
||||
if !c.is_alphabetic() && c != &'-' {
|
||||
break;
|
||||
}
|
||||
let tok = self
|
||||
.buf
|
||||
.next()
|
||||
.expect("this is impossible because we have already peeked");
|
||||
self.pos.next_char();
|
||||
string.push(tok);
|
||||
}
|
||||
|
||||
if let Ok(kw) = Unit::try_from(string.as_ref()) {
|
||||
TokenKind::Unit(kw)
|
||||
} else {
|
||||
panic!("expected ident after `@`")
|
||||
}
|
||||
}
|
||||
|
||||
fn lex_num(&mut self) -> TokenKind {
|
||||
let mut string = String::with_capacity(99);
|
||||
while let Some(c) = self.buf.peek() {
|
||||
if !c.is_numeric() && c != &'.' {
|
||||
break;
|
||||
}
|
||||
let tok = self
|
||||
.buf
|
||||
.next()
|
||||
.expect("this is impossible because we have already peeked");
|
||||
self.pos.next_char();
|
||||
string.push(tok);
|
||||
}
|
||||
|
||||
TokenKind::Number(string)
|
||||
}
|
||||
|
||||
fn lex_hash(&mut self) -> TokenKind {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn lex_attr(&mut self) -> TokenKind {
|
||||
let mut attr = String::with_capacity(99);
|
||||
while let Some(c) = self.buf.peek() {
|
||||
if !c.is_alphabetic() && c != &'-' {
|
||||
break;
|
||||
}
|
||||
let tok = self
|
||||
.buf
|
||||
.next()
|
||||
.expect("this is impossible because we have already peeked");
|
||||
self.pos.next_char();
|
||||
attr.push(tok);
|
||||
}
|
||||
|
||||
self.devour_whitespace();
|
||||
|
||||
let kind = match self
|
||||
.buf
|
||||
.next()
|
||||
.expect("todo! expected kind (should be error)")
|
||||
{
|
||||
']' => {
|
||||
return TokenKind::Selector(Selector::Attribute(Attribute {
|
||||
kind: AttributeKind::Any,
|
||||
attr,
|
||||
value: String::new(),
|
||||
case_sensitive: true,
|
||||
}))
|
||||
}
|
||||
'i' => {
|
||||
self.devour_whitespace();
|
||||
assert!(self.buf.next() == Some(']'));
|
||||
return TokenKind::Selector(Selector::Attribute(Attribute {
|
||||
kind: AttributeKind::Any,
|
||||
attr,
|
||||
value: String::new(),
|
||||
case_sensitive: false,
|
||||
}));
|
||||
}
|
||||
'=' => AttributeKind::Equals,
|
||||
'~' => AttributeKind::InList,
|
||||
'|' => AttributeKind::BeginsWithHyphenOrExact,
|
||||
'^' => AttributeKind::StartsWith,
|
||||
'$' => AttributeKind::EndsWith,
|
||||
'*' => AttributeKind::Contains,
|
||||
_ => todo!("expected kind (should be error)"),
|
||||
};
|
||||
|
||||
if kind != AttributeKind::Equals {
|
||||
assert!(self.buf.next() == Some('='));
|
||||
}
|
||||
|
||||
self.devour_whitespace();
|
||||
|
||||
match self
|
||||
.buf
|
||||
.peek()
|
||||
.expect("todo! expected either value or quote")
|
||||
{
|
||||
'\'' | '"' => {
|
||||
self.buf.next();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
let mut value = String::with_capacity(99);
|
||||
let mut case_sensitive = true;
|
||||
|
||||
while let Some(c) = self.buf.peek() {
|
||||
if !c.is_alphabetic() && c != &'-' {
|
||||
break;
|
||||
}
|
||||
|
||||
if c == &'i' {
|
||||
let tok = self
|
||||
.buf
|
||||
.next()
|
||||
.expect("this is impossible because we have already peeked");
|
||||
self.pos.next_char();
|
||||
self.devour_whitespace();
|
||||
match self.buf.next() {
|
||||
Some(']') => case_sensitive = false,
|
||||
Some(val) => {
|
||||
self.pos.next_char();
|
||||
value.push(tok);
|
||||
value.push(val);
|
||||
}
|
||||
None => todo!("expected something to come after "),
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
let tok = self
|
||||
.buf
|
||||
.next()
|
||||
.expect("this is impossible because we have already peeked");
|
||||
self.pos.next_char();
|
||||
value.push(tok);
|
||||
}
|
||||
|
||||
match self
|
||||
.buf
|
||||
.peek()
|
||||
.expect("todo! expected either value or quote")
|
||||
{
|
||||
'\'' | '"' => {
|
||||
self.buf.next();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
self.devour_whitespace();
|
||||
|
||||
assert!(self.buf.next() == Some(']'));
|
||||
|
||||
TokenKind::Selector(Selector::Attribute(Attribute {
|
||||
kind,
|
||||
attr,
|
||||
value,
|
||||
case_sensitive,
|
||||
}))
|
||||
}
|
||||
|
||||
fn lex_variable(&mut self) -> TokenKind {
|
||||
let mut string = String::with_capacity(99);
|
||||
while let Some(c) = self.buf.peek() {
|
||||
if !c.is_alphabetic() && c != &'-' {
|
||||
break;
|
||||
}
|
||||
let tok = self
|
||||
.buf
|
||||
.next()
|
||||
.expect("this is impossible because we have already peeked");
|
||||
self.pos.next_char();
|
||||
string.push(tok);
|
||||
}
|
||||
TokenKind::Variable(string)
|
||||
}
|
||||
|
||||
fn lex_ident(&mut self) -> TokenKind {
|
||||
let mut string = String::with_capacity(99);
|
||||
while let Some(c) = self.buf.peek() {
|
||||
// we know that the first char is alphabetic from peeking
|
||||
if !c.is_alphanumeric() && c != &'-' {
|
||||
break;
|
||||
}
|
||||
let tok = self
|
||||
.buf
|
||||
.next()
|
||||
.expect("this is impossible because we have already peeked");
|
||||
self.pos.next_char();
|
||||
string.push(tok);
|
||||
}
|
||||
|
||||
if let Ok(kw) = Keyword::try_from(string.as_ref()) {
|
||||
return TokenKind::Keyword(kw);
|
||||
}
|
||||
|
||||
if let Ok(kw) = Unit::try_from(string.as_ref()) {
|
||||
return TokenKind::Unit(kw);
|
||||
}
|
||||
|
||||
TokenKind::Ident(string)
|
||||
}
|
||||
}
|
303
src/main.rs
Normal file
303
src/main.rs
Normal file
@ -0,0 +1,303 @@
|
||||
#![warn(
|
||||
clippy::all,
|
||||
clippy::restriction,
|
||||
clippy::pedantic,
|
||||
clippy::nursery,
|
||||
// clippy::cargo
|
||||
)]
|
||||
#![deny(missing_debug_implementations)]
|
||||
#![allow(
|
||||
dead_code,
|
||||
clippy::pub_enum_variant_names,
|
||||
clippy::implicit_return,
|
||||
clippy::use_self,
|
||||
clippy::missing_docs_in_private_items,
|
||||
clippy::todo,
|
||||
clippy::dbg_macro,
|
||||
clippy::unreachable,
|
||||
clippy::wildcard_enum_match_arm,
|
||||
clippy::option_expect_used,
|
||||
clippy::panic,
|
||||
clippy::unused_self,
|
||||
clippy::too_many_lines,
|
||||
clippy::integer_arithmetic,
|
||||
clippy::missing_errors_doc
|
||||
)]
|
||||
use std::collections::HashMap;
|
||||
use std::fs;
|
||||
use std::io;
|
||||
use std::iter::Iterator;
|
||||
use std::{
|
||||
fmt::{self, Display},
|
||||
iter::Peekable,
|
||||
};
|
||||
|
||||
use crate::common::{Keyword, Pos, Symbol, Whitespace};
|
||||
use crate::format::PrettyPrinter;
|
||||
use crate::lexer::Lexer;
|
||||
use crate::selector::Selector;
|
||||
use crate::units::Unit;
|
||||
|
||||
mod color;
|
||||
mod common;
|
||||
mod error;
|
||||
mod format;
|
||||
mod lexer;
|
||||
mod selector;
|
||||
mod units;
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub enum TokenKind {
|
||||
Ident(String),
|
||||
Symbol(Symbol),
|
||||
Function(String, Vec<String>),
|
||||
AtRule(String),
|
||||
Keyword(Keyword),
|
||||
Number(String),
|
||||
Unit(Unit),
|
||||
Whitespace(Whitespace),
|
||||
Variable(String),
|
||||
Selector(Selector),
|
||||
Style(Vec<Token>),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub enum StyleToken {
|
||||
Ident(String),
|
||||
Function(String, Vec<String>),
|
||||
Keyword(Keyword),
|
||||
Symbol(Symbol),
|
||||
Dimension(String, Unit),
|
||||
}
|
||||
|
||||
impl Display for StyleToken {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
StyleToken::Ident(s) => write!(f, "{}", s),
|
||||
StyleToken::Symbol(s) => write!(f, "{}", s),
|
||||
StyleToken::Function(name, args) => write!(f, "{}({})", name, args.join(", ")),
|
||||
StyleToken::Keyword(kw) => write!(f, "{}", kw),
|
||||
StyleToken::Dimension(val, unit) => write!(f, "{}{}", val, unit),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct Token {
|
||||
pos: Pos,
|
||||
pub kind: TokenKind,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
pub struct StyleSheet {
|
||||
rules: Vec<Stmt>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct Style {
|
||||
property: String,
|
||||
value: Vec<StyleToken>,
|
||||
}
|
||||
|
||||
impl Display for Style {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{}: {};",
|
||||
self.property,
|
||||
self.value
|
||||
.iter()
|
||||
.map(|x| format!("{}", x))
|
||||
.collect::<Vec<String>>()
|
||||
.join(" ")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Style {
|
||||
fn from_tokens(raw: &[Token]) -> Result<Style, ()> {
|
||||
let mut iter = raw.iter();
|
||||
let property: String;
|
||||
loop {
|
||||
if let Some(tok) = iter.next() {
|
||||
match tok.kind {
|
||||
TokenKind::Whitespace(_) => continue,
|
||||
TokenKind::Ident(ref s) => {
|
||||
property = s.clone();
|
||||
break;
|
||||
}
|
||||
_ => todo!(),
|
||||
};
|
||||
} else {
|
||||
return Err(());
|
||||
}
|
||||
}
|
||||
|
||||
while let Some(tok) = iter.next() {
|
||||
match tok.kind {
|
||||
TokenKind::Whitespace(_) => continue,
|
||||
TokenKind::Symbol(Symbol::Colon) => break,
|
||||
_ => todo!("found tokens before style value"),
|
||||
}
|
||||
}
|
||||
|
||||
let mut value = Vec::new();
|
||||
|
||||
while let Some(tok) = iter.next() {
|
||||
match tok.kind {
|
||||
TokenKind::Whitespace(_)
|
||||
| TokenKind::Symbol(Symbol::SingleQuote)
|
||||
| TokenKind::Symbol(Symbol::DoubleQuote) => continue,
|
||||
TokenKind::Ident(ref s) => value.push(StyleToken::Ident(s.clone())),
|
||||
TokenKind::Symbol(s) => value.push(StyleToken::Symbol(s)),
|
||||
TokenKind::Unit(u) => value.push(StyleToken::Ident(u.into())),
|
||||
TokenKind::Number(ref num) => {
|
||||
if let Some(t) = iter.next() {
|
||||
match &t.kind {
|
||||
&TokenKind::Unit(unit) => {
|
||||
value.push(StyleToken::Dimension(num.clone(), unit))
|
||||
}
|
||||
TokenKind::Ident(ref s) => {
|
||||
value.push(StyleToken::Dimension(num.clone(), Unit::None));
|
||||
value.push(StyleToken::Ident(s.clone()));
|
||||
}
|
||||
TokenKind::Whitespace(_) => {
|
||||
value.push(StyleToken::Dimension(num.clone(), Unit::None))
|
||||
}
|
||||
_ => todo!(),
|
||||
}
|
||||
} else {
|
||||
value.push(StyleToken::Dimension(num.clone(), Unit::None))
|
||||
}
|
||||
}
|
||||
_ => todo!("style value not ident or dimension"),
|
||||
}
|
||||
}
|
||||
Ok(Style { property, value })
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub enum Stmt {
|
||||
Style(Style),
|
||||
RuleSet(RuleSet),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct RuleSet {
|
||||
selector: Selector,
|
||||
rules: Vec<Stmt>,
|
||||
// potential optimization: we don't *need* to own the selector
|
||||
super_selector: Option<Selector>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
enum Expr {
|
||||
Style(Style),
|
||||
Selector(Selector),
|
||||
}
|
||||
|
||||
impl StyleSheet {
|
||||
#[must_use]
|
||||
pub fn new(input: &str) -> StyleSheet {
|
||||
StyleSheetParser {
|
||||
variables: HashMap::new(),
|
||||
lexer: Lexer::new(input).peekable(),
|
||||
rules: Vec::new(),
|
||||
}
|
||||
.parse_toplevel()
|
||||
}
|
||||
|
||||
pub fn pretty_print<W: std::io::Write>(&self, buf: W) -> io::Result<()> {
|
||||
PrettyPrinter::new(buf).pretty_print(self)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct StyleSheetParser<'a> {
|
||||
variables: HashMap<String, String>,
|
||||
lexer: Peekable<Lexer<'a>>,
|
||||
rules: Vec<Stmt>,
|
||||
}
|
||||
|
||||
impl<'a> StyleSheetParser<'a> {
|
||||
fn parse_toplevel(&'a mut self) -> StyleSheet {
|
||||
let mut rules = Vec::new();
|
||||
while let Some(tok) = self.lexer.peek() {
|
||||
match tok.kind {
|
||||
TokenKind::Ident(_)
|
||||
| TokenKind::Selector(_)
|
||||
| TokenKind::Symbol(Symbol::Hash)
|
||||
| TokenKind::Symbol(Symbol::Colon)
|
||||
| TokenKind::Symbol(Symbol::Mul)
|
||||
| TokenKind::Symbol(Symbol::Period) => rules.extend(self.eat_rules(&None)),
|
||||
TokenKind::Whitespace(_) | TokenKind::Symbol(_) => {
|
||||
self.lexer.next();
|
||||
continue;
|
||||
}
|
||||
_ => todo!("unexpected toplevel token"),
|
||||
};
|
||||
}
|
||||
StyleSheet { rules }
|
||||
}
|
||||
|
||||
fn eat_rules(&mut self, super_selector: &Option<Selector>) -> Vec<Stmt> {
|
||||
let mut stmts = Vec::new();
|
||||
while let Ok(tok) = self.eat_expr() {
|
||||
match tok {
|
||||
Expr::Style(s) => stmts.push(Stmt::Style(s)),
|
||||
Expr::Selector(s) => {
|
||||
let rules = self.eat_rules(&Some(s.clone()));
|
||||
stmts.push(Stmt::RuleSet(RuleSet {
|
||||
super_selector: super_selector.clone(),
|
||||
selector: s,
|
||||
rules,
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
stmts
|
||||
}
|
||||
|
||||
fn eat_expr(&mut self) -> Result<Expr, ()> {
|
||||
let mut values = Vec::with_capacity(5);
|
||||
while let Some(tok) = self.lexer.next() {
|
||||
match tok.kind {
|
||||
TokenKind::Symbol(Symbol::SemiColon) | TokenKind::Symbol(Symbol::CloseBrace) => {
|
||||
self.devour_whitespace();
|
||||
return Ok(Expr::Style(Style::from_tokens(&values)?));
|
||||
}
|
||||
TokenKind::Symbol(Symbol::OpenBrace) => {
|
||||
self.devour_whitespace();
|
||||
return Ok(Expr::Selector(Selector::from_tokens(
|
||||
values.iter().peekable(),
|
||||
)));
|
||||
}
|
||||
_ => values.push(tok.clone()),
|
||||
};
|
||||
}
|
||||
Err(())
|
||||
}
|
||||
|
||||
fn devour_whitespace(&mut self) {
|
||||
while let Some(tok) = self.lexer.peek() {
|
||||
match tok.kind {
|
||||
TokenKind::Whitespace(_) => {
|
||||
self.lexer.next();
|
||||
}
|
||||
_ => break,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> io::Result<()> {
|
||||
let input = fs::read_to_string("input.scss")?;
|
||||
let mut stdout = std::io::stdout();
|
||||
let s = StyleSheet::new(&input);
|
||||
// dbg!(s);
|
||||
s.pretty_print(&mut stdout)?;
|
||||
// println!("{}", s);
|
||||
// drop(input);
|
||||
Ok(())
|
||||
}
|
247
src/selector.rs
Normal file
247
src/selector.rs
Normal file
@ -0,0 +1,247 @@
|
||||
use crate::common::Symbol;
|
||||
use crate::{Token, TokenKind};
|
||||
use std::iter::Peekable;
|
||||
use std::{
|
||||
fmt::{self, Display},
|
||||
slice::Iter,
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub enum Selector {
|
||||
/// An element selector: `button`
|
||||
Element(String),
|
||||
/// An id selector: `#footer`
|
||||
Id(String),
|
||||
/// A single class selector: `.button-active`
|
||||
Class(String),
|
||||
/// A universal selector: `*`
|
||||
Universal,
|
||||
/// A simple child selector: `ul li`
|
||||
Descendant(Box<Selector>, Box<Selector>),
|
||||
/// And selector: `button.active`
|
||||
And(Box<Selector>, Box<Selector>),
|
||||
/// Multiple unrelated selectors: `button, .active`
|
||||
Multiple(Box<Selector>, Box<Selector>),
|
||||
/// Select all immediate children: `ul > li`
|
||||
ImmediateChild(Box<Selector>, Box<Selector>),
|
||||
/// Select all elements immediately following: `div + p`
|
||||
Following(Box<Selector>, Box<Selector>),
|
||||
/// Select elements preceeded by: `p ~ ul`
|
||||
Preceding(Box<Selector>, Box<Selector>),
|
||||
/// Select elements with attribute: `html[lang|=en]`
|
||||
Attribute(Attribute),
|
||||
/// Pseudo selector: `:hover`
|
||||
Pseudo(String),
|
||||
}
|
||||
|
||||
impl Display for Selector {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Selector::Element(s) => write!(f, "{}", s),
|
||||
Selector::Id(s) => write!(f, "#{}", s),
|
||||
Selector::Class(s) => write!(f, ".{}", s),
|
||||
Selector::Universal => write!(f, "*"),
|
||||
Selector::Descendant(lhs, rhs) => write!(f, "{} {}", lhs, rhs),
|
||||
Selector::And(lhs, rhs) => write!(f, "{}{}", lhs, rhs),
|
||||
Selector::Multiple(lhs, rhs) => write!(f, "{}, {}", lhs, rhs),
|
||||
Selector::ImmediateChild(lhs, rhs) => write!(f, "{} > {}", lhs, rhs),
|
||||
Selector::Following(lhs, rhs) => write!(f, "{} + {}", lhs, rhs),
|
||||
Selector::Preceding(lhs, rhs) => write!(f, "{} ~ {}", lhs, rhs),
|
||||
Selector::Attribute(attr) => write!(f, "{}", attr),
|
||||
Selector::Pseudo(s) => write!(f, ":{}", s),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct SelectorParser<'a> {
|
||||
tokens: Peekable<Iter<'a, Token>>,
|
||||
}
|
||||
|
||||
impl<'a> SelectorParser<'a> {
|
||||
const fn new(tokens: Peekable<Iter<'a, Token>>) -> SelectorParser<'a> {
|
||||
SelectorParser { tokens }
|
||||
}
|
||||
|
||||
fn all_selectors(&mut self) -> Selector {
|
||||
self.devour_whitespace();
|
||||
let left = self
|
||||
.consume_selector()
|
||||
.expect("expected left handed selector");
|
||||
let whitespace: bool = self.devour_whitespace();
|
||||
match self.tokens.peek() {
|
||||
Some(tok) => match tok.kind {
|
||||
TokenKind::Ident(_) => {
|
||||
return Selector::Descendant(Box::new(left), Box::new(self.all_selectors()))
|
||||
}
|
||||
TokenKind::Symbol(Symbol::Plus) => {
|
||||
self.tokens.next();
|
||||
self.devour_whitespace();
|
||||
return Selector::Following(Box::new(left), Box::new(self.all_selectors()));
|
||||
}
|
||||
TokenKind::Symbol(Symbol::Tilde) => {
|
||||
self.tokens.next();
|
||||
self.devour_whitespace();
|
||||
return Selector::Preceding(Box::new(left), Box::new(self.all_selectors()));
|
||||
}
|
||||
TokenKind::Symbol(Symbol::Comma) => {
|
||||
self.tokens.next();
|
||||
self.devour_whitespace();
|
||||
return Selector::Multiple(Box::new(left), Box::new(self.all_selectors()));
|
||||
}
|
||||
TokenKind::Symbol(Symbol::Gt) => {
|
||||
self.tokens.next();
|
||||
self.devour_whitespace();
|
||||
return Selector::ImmediateChild(
|
||||
Box::new(left),
|
||||
Box::new(self.all_selectors()),
|
||||
);
|
||||
}
|
||||
TokenKind::Symbol(Symbol::Colon)
|
||||
| TokenKind::Symbol(Symbol::Period)
|
||||
| TokenKind::Symbol(Symbol::Mul)
|
||||
| TokenKind::Selector(_)
|
||||
| TokenKind::Symbol(Symbol::Hash) => {
|
||||
if whitespace {
|
||||
return Selector::Descendant(
|
||||
Box::new(left),
|
||||
Box::new(self.all_selectors()),
|
||||
);
|
||||
} else {
|
||||
return Selector::And(Box::new(left), Box::new(self.all_selectors()));
|
||||
}
|
||||
}
|
||||
TokenKind::Symbol(Symbol::Lt) => {}
|
||||
_ => todo!(),
|
||||
},
|
||||
None => return left,
|
||||
}
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn devour_whitespace(&mut self) -> bool {
|
||||
let mut found_whitespace = false;
|
||||
while let Some(tok) = self.tokens.peek() {
|
||||
match tok.kind {
|
||||
TokenKind::Whitespace(_) => {
|
||||
self.tokens.next();
|
||||
found_whitespace = true;
|
||||
}
|
||||
_ => break,
|
||||
}
|
||||
}
|
||||
found_whitespace
|
||||
}
|
||||
|
||||
fn consume_selector(&mut self) -> Option<Selector> {
|
||||
if let Some(tok) = self.tokens.next() {
|
||||
let selector = match tok.kind {
|
||||
TokenKind::Symbol(Symbol::Period) => match self
|
||||
.tokens
|
||||
.next()
|
||||
.expect("expected ident after `.`")
|
||||
.clone()
|
||||
.kind
|
||||
{
|
||||
TokenKind::Ident(tok) => Selector::Class(tok),
|
||||
_ => todo!("there should normally be an ident after `.`"),
|
||||
},
|
||||
TokenKind::Symbol(Symbol::Mul) => Selector::Universal,
|
||||
TokenKind::Symbol(Symbol::Hash) => match self
|
||||
.tokens
|
||||
.next()
|
||||
.expect("expected ident after `#`")
|
||||
.clone()
|
||||
.kind
|
||||
{
|
||||
TokenKind::Ident(tok) => Selector::Id(tok),
|
||||
_ => todo!("there should normally be an ident after `#`"),
|
||||
},
|
||||
TokenKind::Symbol(Symbol::Colon) => {
|
||||
match self
|
||||
.tokens
|
||||
.next()
|
||||
.expect("expected ident after `:`")
|
||||
.clone()
|
||||
.kind
|
||||
{
|
||||
TokenKind::Ident(tok) => Selector::Pseudo(tok),
|
||||
_ => todo!("there should normally be an ident after `:`"),
|
||||
}
|
||||
}
|
||||
TokenKind::Ident(ref tok) => Selector::Element(tok.clone()),
|
||||
TokenKind::Selector(ref sel) => sel.clone(),
|
||||
_ => todo!(),
|
||||
};
|
||||
Some(selector)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Selector {
|
||||
pub fn from_tokens(tokens: Peekable<Iter<Token>>) -> Selector {
|
||||
SelectorParser::new(tokens).all_selectors()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct Attribute {
|
||||
pub attr: String,
|
||||
pub value: String,
|
||||
pub case_sensitive: bool,
|
||||
pub kind: AttributeKind,
|
||||
}
|
||||
|
||||
impl Display for Attribute {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
if self.case_sensitive {
|
||||
match self.kind {
|
||||
AttributeKind::Any => write!(f, "[{}]", self.attr),
|
||||
AttributeKind::Equals => write!(f, "[{}={}]", self.attr, self.value),
|
||||
AttributeKind::InList => write!(f, "[{}~={}]", self.attr, self.value),
|
||||
AttributeKind::BeginsWithHyphenOrExact => {
|
||||
write!(f, "[{}|={}]", self.attr, self.value)
|
||||
}
|
||||
AttributeKind::StartsWith => write!(f, "[{}^={}]", self.attr, self.value),
|
||||
AttributeKind::EndsWith => write!(f, "[{}$={}]", self.attr, self.value),
|
||||
AttributeKind::Contains => write!(f, "[{}*={}]", self.attr, self.value),
|
||||
}
|
||||
} else {
|
||||
match self.kind {
|
||||
AttributeKind::Any => write!(f, "[{} i]", self.attr),
|
||||
AttributeKind::Equals => write!(f, "[{}={} i]", self.attr, self.value),
|
||||
AttributeKind::InList => write!(f, "[{}~={} i]", self.attr, self.value),
|
||||
AttributeKind::BeginsWithHyphenOrExact => {
|
||||
write!(f, "[{}|={} i]", self.attr, self.value)
|
||||
}
|
||||
AttributeKind::StartsWith => write!(f, "[{}^={} i]", self.attr, self.value),
|
||||
AttributeKind::EndsWith => write!(f, "[{}$={} i]", self.attr, self.value),
|
||||
AttributeKind::Contains => write!(f, "[{}*={} i]", self.attr, self.value),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
|
||||
pub enum AttributeKind {
|
||||
/// [attr]
|
||||
/// Represents elements with an attribute name of `attr`
|
||||
Any,
|
||||
/// [attr=value]
|
||||
/// Represents elements with an attribute name of `attr` whose value is exactly `value`
|
||||
Equals,
|
||||
/// [attr~=value]
|
||||
/// Represents elements with an attribute name of `attr` whose value is a whitespace-separated list of words, one of which is exactly `value`
|
||||
InList,
|
||||
/// [attr|=value]
|
||||
/// Represents elements with an attribute name of `attr` whose value can be exactly value or can begin with `value` immediately followed by a hyphen (`-`)
|
||||
BeginsWithHyphenOrExact,
|
||||
/// [attr^=value]
|
||||
StartsWith,
|
||||
/// [attr$=value]
|
||||
EndsWith,
|
||||
/// [attr*=value]
|
||||
/// Represents elements with an attribute name of `attr` whose value contains at least one occurrence of `value` within the string
|
||||
Contains,
|
||||
}
|
261
src/units.rs
Normal file
261
src/units.rs
Normal file
@ -0,0 +1,261 @@
|
||||
use std::convert::TryFrom;
|
||||
use std::fmt;
|
||||
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
|
||||
pub enum Unit {
|
||||
// Absolute units
|
||||
/// Pixels
|
||||
Px,
|
||||
/// Millimeters
|
||||
Mm,
|
||||
/// Inches
|
||||
In,
|
||||
/// Centimeters
|
||||
Cm,
|
||||
/// Quarter-millimeters
|
||||
Q,
|
||||
/// Points
|
||||
Pt,
|
||||
/// Picas
|
||||
Pc,
|
||||
// Font relative units
|
||||
/// Font size of the parent element
|
||||
Em,
|
||||
/// Font size of the root element
|
||||
Rem,
|
||||
/// Line height of the element
|
||||
Lh,
|
||||
Percent,
|
||||
/// x-height of the element's font
|
||||
Ex,
|
||||
/// The advance measure (width) of the glyph "0" of the element's font
|
||||
Ch,
|
||||
/// Represents the "cap height" (nominal height of capital letters) of the element’s font
|
||||
Cap,
|
||||
/// Equal to the used advance measure of the "水" (CJK water ideograph, U+6C34) glyph found in the font used to render it
|
||||
Ic,
|
||||
/// Equal to the computed value of the line-height property on the root element (typically <html>), converted to an absolute length
|
||||
Rlh,
|
||||
|
||||
// Viewport relative units
|
||||
/// 1% of the viewport's width
|
||||
Vw,
|
||||
/// 1% of the viewport's height
|
||||
Vh,
|
||||
/// 1% of the viewport's smaller dimension
|
||||
Vmin,
|
||||
/// 1% of the viewport's larger dimension
|
||||
Vmax,
|
||||
/// Equal to 1% of the size of the initial containing block, in the direction of the root element’s inline axis
|
||||
Vi,
|
||||
/// Equal to 1% of the size of the initial containing block, in the direction of the root element’s block axis
|
||||
Vb,
|
||||
|
||||
// Angle units
|
||||
/// Represents an angle in degrees. One full circle is 360deg
|
||||
Deg,
|
||||
/// Represents an angle in gradians. One full circle is 400grad
|
||||
Grad,
|
||||
/// Represents an angle in radians. One full circle is 2π radians which approximates to 6.2832rad
|
||||
Rad,
|
||||
/// Represents an angle in a number of turns. One full circle is 1turn
|
||||
Turn,
|
||||
|
||||
// Time units
|
||||
/// Represents a time in seconds
|
||||
S,
|
||||
/// Represents a time in milliseconds
|
||||
Ms,
|
||||
|
||||
// Frequency units
|
||||
/// Represents a frequency in hertz
|
||||
Hz,
|
||||
/// Represents a frequency in kilohertz
|
||||
Khz,
|
||||
|
||||
// Resolution units
|
||||
/// Represents the number of dots per inch
|
||||
Dpi,
|
||||
/// Represents the number of dots per centimeter
|
||||
Dpcm,
|
||||
/// Represents the number of dots per px unit
|
||||
Dppx,
|
||||
/// Alias for dppx
|
||||
X,
|
||||
|
||||
/// Represents a fraction of the available space in the grid container
|
||||
Fr,
|
||||
/// Unspecified unit
|
||||
None,
|
||||
}
|
||||
|
||||
impl TryFrom<&str> for Unit {
|
||||
type Error = &'static str;
|
||||
|
||||
fn try_from(unit: &str) -> Result<Self, Self::Error> {
|
||||
match unit.to_ascii_lowercase().as_bytes() {
|
||||
b"px" => Ok(Unit::Px),
|
||||
b"mm" => Ok(Unit::Mm),
|
||||
b"in" => Ok(Unit::In),
|
||||
b"cm" => Ok(Unit::Cm),
|
||||
b"Q" => Ok(Unit::Q),
|
||||
b"pt" => Ok(Unit::Pt),
|
||||
b"pc" => Ok(Unit::Pc),
|
||||
b"em" => Ok(Unit::Em),
|
||||
b"rem" => Ok(Unit::Rem),
|
||||
b"lh" => Ok(Unit::Lh),
|
||||
b"%" => Ok(Unit::Percent),
|
||||
b"ex" => Ok(Unit::Ex),
|
||||
b"ch" => Ok(Unit::Ch),
|
||||
b"cap" => Ok(Unit::Cap),
|
||||
b"ic" => Ok(Unit::Ic),
|
||||
b"rlh" => Ok(Unit::Rlh),
|
||||
b"vw" => Ok(Unit::Vw),
|
||||
b"vh" => Ok(Unit::Vh),
|
||||
b"vmin" => Ok(Unit::Vmin),
|
||||
b"vmax" => Ok(Unit::Vmax),
|
||||
b"vi" => Ok(Unit::Vi),
|
||||
b"vb" => Ok(Unit::Vb),
|
||||
b"deg" => Ok(Unit::Deg),
|
||||
b"grad" => Ok(Unit::Grad),
|
||||
b"rad" => Ok(Unit::Rad),
|
||||
b"turn" => Ok(Unit::Turn),
|
||||
b"s" => Ok(Unit::S),
|
||||
b"ms" => Ok(Unit::Ms),
|
||||
b"Hz" => Ok(Unit::Hz),
|
||||
b"kHz" => Ok(Unit::Khz),
|
||||
b"dpi" => Ok(Unit::Dpi),
|
||||
b"dpcm" => Ok(Unit::Dpcm),
|
||||
b"dppx" | b"x" => Ok(Unit::Dppx),
|
||||
b"fr" => Ok(Unit::Fr),
|
||||
_ => Err("invalid unit"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Into<String> for Unit {
|
||||
fn into(self) -> String {
|
||||
match self {
|
||||
Unit::Px => "px",
|
||||
Unit::Mm => "mm",
|
||||
Unit::In => "in",
|
||||
Unit::Cm => "cm",
|
||||
Unit::Q => "Q",
|
||||
Unit::Pt => "pt",
|
||||
Unit::Pc => "pc",
|
||||
Unit::Em => "em",
|
||||
Unit::Rem => "rem",
|
||||
Unit::Lh => "lh",
|
||||
Unit::Percent => "%",
|
||||
Unit::Ex => "ex",
|
||||
Unit::Ch => "ch",
|
||||
Unit::Cap => "cap",
|
||||
Unit::Ic => "ic",
|
||||
Unit::Rlh => "rlh",
|
||||
Unit::Vw => "vw",
|
||||
Unit::Vh => "vh",
|
||||
Unit::Vmin => "vmin",
|
||||
Unit::Vmax => "vmax",
|
||||
Unit::Vi => "vi",
|
||||
Unit::Vb => "vb",
|
||||
Unit::Deg => "deg",
|
||||
Unit::Grad => "grad",
|
||||
Unit::Rad => "rad",
|
||||
Unit::Turn => "turn",
|
||||
Unit::S => "s",
|
||||
Unit::Ms => "ms",
|
||||
Unit::Hz => "Hz",
|
||||
Unit::Khz => "kHz",
|
||||
Unit::Dpi => "dpi",
|
||||
Unit::Dpcm => "dpcm",
|
||||
Unit::Dppx | Unit::X => "dppx",
|
||||
Unit::Fr => "fr",
|
||||
Unit::None => "",
|
||||
}
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl Into<&'static str> for Unit {
|
||||
fn into(self) -> &'static str {
|
||||
match self {
|
||||
Unit::Px => "px",
|
||||
Unit::Mm => "mm",
|
||||
Unit::In => "in",
|
||||
Unit::Cm => "cm",
|
||||
Unit::Q => "Q",
|
||||
Unit::Pt => "pt",
|
||||
Unit::Pc => "pc",
|
||||
Unit::Em => "em",
|
||||
Unit::Rem => "rem",
|
||||
Unit::Lh => "lh",
|
||||
Unit::Percent => "%",
|
||||
Unit::Ex => "ex",
|
||||
Unit::Ch => "ch",
|
||||
Unit::Cap => "cap",
|
||||
Unit::Ic => "ic",
|
||||
Unit::Rlh => "rlh",
|
||||
Unit::Vw => "vw",
|
||||
Unit::Vh => "vh",
|
||||
Unit::Vmin => "vmin",
|
||||
Unit::Vmax => "vmax",
|
||||
Unit::Vi => "vi",
|
||||
Unit::Vb => "vb",
|
||||
Unit::Deg => "deg",
|
||||
Unit::Grad => "grad",
|
||||
Unit::Rad => "rad",
|
||||
Unit::Turn => "turn",
|
||||
Unit::S => "s",
|
||||
Unit::Ms => "ms",
|
||||
Unit::Hz => "Hz",
|
||||
Unit::Khz => "kHz",
|
||||
Unit::Dpi => "dpi",
|
||||
Unit::Dpcm => "dpcm",
|
||||
Unit::Dppx | Unit::X => "dppx",
|
||||
Unit::Fr => "fr",
|
||||
Unit::None => "",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Unit {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match self {
|
||||
Unit::Px => write!(f, "px"),
|
||||
Unit::Mm => write!(f, "mm"),
|
||||
Unit::In => write!(f, "in"),
|
||||
Unit::Cm => write!(f, "cm"),
|
||||
Unit::Q => write!(f, "Q"),
|
||||
Unit::Pt => write!(f, "pt"),
|
||||
Unit::Pc => write!(f, "pc"),
|
||||
Unit::Em => write!(f, "em"),
|
||||
Unit::Rem => write!(f, "rem"),
|
||||
Unit::Lh => write!(f, "lh"),
|
||||
Unit::Percent => write!(f, "%"),
|
||||
Unit::Ex => write!(f, "ex"),
|
||||
Unit::Ch => write!(f, "ch"),
|
||||
Unit::Cap => write!(f, "cap"),
|
||||
Unit::Ic => write!(f, "ic"),
|
||||
Unit::Rlh => write!(f, "rlh"),
|
||||
Unit::Vw => write!(f, "vw"),
|
||||
Unit::Vh => write!(f, "vh"),
|
||||
Unit::Vmin => write!(f, "vmin"),
|
||||
Unit::Vmax => write!(f, "vmax"),
|
||||
Unit::Vi => write!(f, "vi"),
|
||||
Unit::Vb => write!(f, "vb"),
|
||||
Unit::Deg => write!(f, "deg"),
|
||||
Unit::Grad => write!(f, "grad"),
|
||||
Unit::Rad => write!(f, "rad"),
|
||||
Unit::Turn => write!(f, "turn"),
|
||||
Unit::S => write!(f, "s"),
|
||||
Unit::Ms => write!(f, "ms"),
|
||||
Unit::Hz => write!(f, "Hz"),
|
||||
Unit::Khz => write!(f, "kHz"),
|
||||
Unit::Dpi => write!(f, "dpi"),
|
||||
Unit::Dpcm => write!(f, "dpcm"),
|
||||
Unit::Dppx | Unit::X => write!(f, "dppx"),
|
||||
Unit::Fr => write!(f, "fr"),
|
||||
Unit::None => write!(f, ""),
|
||||
}
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user