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