From f81a201bd123968e4c58fede02d399f7501556dd Mon Sep 17 00:00:00 2001 From: "Lou (loha)" Date: Wed, 8 May 2024 11:21:02 +0200 Subject: [PATCH] =?UTF-8?q?web-css:=20cssom=20builder=20and=20parser=20imp?= =?UTF-8?q?rovements=20=F0=9F=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/web/web-css/ast.h | 35 ++++++++ src/web/web-css/builder.h | 79 ++++++++++++++++++ src/web/web-css/cli/main.cpp | 2 +- src/web/web-css/lexer.cpp | 2 +- src/web/web-css/parser.h | 101 +++++++++++------------ src/web/web-css/tests/test-lexer.cpp | 115 +++++++++++++++++++++++++++ src/web/web-cssom/stylesheet.h | 8 +- 7 files changed, 285 insertions(+), 57 deletions(-) create mode 100644 src/web/web-css/ast.h create mode 100644 src/web/web-css/builder.h create mode 100644 src/web/web-css/tests/test-lexer.cpp diff --git a/src/web/web-css/ast.h b/src/web/web-css/ast.h new file mode 100644 index 0000000000..f3fc9ac039 --- /dev/null +++ b/src/web/web-css/ast.h @@ -0,0 +1,35 @@ +#include + +#include "lexer.h" + +namespace Web::Css { + +struct Ast; + +using Content = Vec; + +#define FOREACH_AST(AST) \ + AST(QUALIFIED_RULE) \ + AST(FUNC) \ + AST(DECL) \ + AST(LIST) \ + AST(TOKEN) \ + AST(BLOCK) + +struct Ast { + enum struct _Type { +#define ITER(NAME) NAME, + FOREACH_AST(ITER) +#undef ITER + }; + + using enum _Type; + + _Type type; + Opt token{}; + Opt> prefix{}; + Content content{}; + + Ast(_Type type) : type(type) {} +}; +} // namespace Web::Css \ No newline at end of file diff --git a/src/web/web-css/builder.h b/src/web/web-css/builder.h new file mode 100644 index 0000000000..8de892c722 --- /dev/null +++ b/src/web/web-css/builder.h @@ -0,0 +1,79 @@ +#pragma once + +#include + +#include "ast.h" + +namespace Web::Css { + +static CSSOM::CSSStyleRule parseQualifiedRule(Ast rule) { + CSSOM::CSSStyleRule parsed; + + auto &prefix = rule.prefix.unwrap()->content; + for (usize i = 0; i < prefix.len(); i++) { + switch (prefix[i].type) { + case Ast::QUALIFIED_RULE: + case Ast::FUNC: + case Ast::DECL: + case Ast::LIST: + case Ast::BLOCK: + break; + case Ast::TOKEN: + parsed.selector.add(prefix[i].token.unwrap()); + break; + } + } + + auto block = rule.content[0].content; + bool parsingContent = false; + for (usize i = 0; i < block.len(); i++) { + switch (block[i].type) { + case Ast::QUALIFIED_RULE: + case Ast::FUNC: + case Ast::DECL: + case Ast::LIST: + case Ast::BLOCK: + break; + case Ast::TOKEN: + if (!parsingContent) { + if (block[i].token->type != Token::WHITESPACE) { + if (block[i].token->type != Token::COLON) { + parsed.declarations.add(CSSOM::CSSStyleDeclaration(block[i].token.unwrap())); + } else { + parsingContent = true; + } + } + } else { + if (block[i].token->type != Token::SEMICOLON) { + last(parsed.declarations).value.add(block[i].token.unwrap()); + } else { + parsingContent = false; + } + } + break; + } + } + return parsed; +} + +// No spec, we take the AST we built and convert it to a usable list of rules +Vec parseAST(Ast ast) { + Vec rules; + for (usize i = 0; i < ast.content.len(); i++) { + switch (ast.content[i].type) { + + case Ast::_Type::QUALIFIED_RULE: + rules.add(parseQualifiedRule(ast.content[i])); + break; + case Ast::_Type::FUNC: + case Ast::_Type::DECL: + case Ast::_Type::LIST: + case Ast::_Type::TOKEN: + case Ast::_Type::BLOCK: + break; + } + } + return rules; +} + +} // namespace Web::Css \ No newline at end of file diff --git a/src/web/web-css/cli/main.cpp b/src/web/web-css/cli/main.cpp index 480dccd5f5..da0c218683 100644 --- a/src/web/web-css/cli/main.cpp +++ b/src/web/web-css/cli/main.cpp @@ -7,7 +7,7 @@ Res<> entryPoint(Sys::Ctx &) { Sys::println("Parsing:"); - try$(Web::Css::parseStylesheet("bundle://web-css-cli/exemple_2.css")); + try$(Web::Css::parseStylesheet("bundle://web-css-cli/exemple_1.css")); Sys::println("Result:"); Io::Emit emit{Sys::out()}; diff --git a/src/web/web-css/lexer.cpp b/src/web/web-css/lexer.cpp index 150f4a7b1d..42b38c3154 100644 --- a/src/web/web-css/lexer.cpp +++ b/src/web/web-css/lexer.cpp @@ -211,7 +211,7 @@ Res nextToken(Io::SScan &s) { return Ok(Token{Token::Type::HASH, s.end()}); } - // https://www.w3.org/TR/css-syntax-3/#consume-numeric-token + // https://www.w3.org/TR/css-syntax-3/#consume-comment if (s.skip("/*")) { s.skip(Re::untilAndConsume(Re::word("*/"))); diff --git a/src/web/web-css/parser.h b/src/web/web-css/parser.h index b765d68501..bafa2edb8f 100644 --- a/src/web/web-css/parser.h +++ b/src/web/web-css/parser.h @@ -6,51 +6,31 @@ #include #include +#include "builder.h" #include "lexer.h" namespace Web::Css { - -struct Ast; - -using Block = Vec>; - -struct Ast { - enum struct Type { - QUALIFIED_RULE, - FUNC, - DECL, - LIST, - BLOCK, - TOKEN, - }; - - using enum Type; - - Type type; - Opt token; - Opt> pre; - Block block; -}; - Res _consumeComponentValue(Io::SScan &s, Token token); // https://www.w3.org/TR/css-syntax-3/#consume-a-simple-block Res _consumeBlock(Io::SScan &s, Token::Type term) { - Ast block; - block.type = Ast::Type::BLOCK; + Ast block = Ast::BLOCK; while (true) { auto token = nextToken(s).unwrap(); switch (token.type) { - case Token::Type::END_OF_FILE: + case Token::END_OF_FILE: // this is a parse error - return Ok(std::move(block)); + return Ok(block); default: if (token.type == term) { - return Ok(std::move(block)); + logDebug("closing the curent block"); + return Ok(block); } else { - block.block.emplaceBack(try$(_consumeComponentValue(s, token))); + auto a = try$(_consumeComponentValue(s, token)); + logDebug("adding {#} to the curent block", a); + block.content.emplaceBack(a); } } } @@ -58,8 +38,7 @@ Res _consumeBlock(Io::SScan &s, Token::Type term) { // https://www.w3.org/TR/css-syntax-3/#consume-function Res _consumeFunc(Io::SScan &s) { - Ast fn; - fn.type = Ast::Type::FUNC; + Ast fn = Ast::FUNC; while (true) { auto token = nextToken(s).unwrap(); @@ -71,7 +50,7 @@ Res _consumeFunc(Io::SScan &s) { // this is a parse error return Ok(std::move(fn)); default: - fn.block.emplaceBack(try$(_consumeComponentValue(s, token))); + fn.content.emplaceBack(try$(_consumeComponentValue(s, token))); } } } @@ -92,8 +71,7 @@ Res _consumeComponentValue(Io::SScan &s, Token token) { return _consumeFunc(s); default: - Ast tok; - tok.type = Ast::TOKEN; + Ast tok = Ast::TOKEN; tok.token = token; return Ok(std::move(tok)); } @@ -101,64 +79,79 @@ Res _consumeComponentValue(Io::SScan &s, Token token) { // https://www.w3.org/TR/css-syntax-3/#consume-qualified-rule Res _consumeQualifiedRule(Io::SScan &s, Token token) { - Ast ast; - ast.type = Ast::QUALIFIED_RULE; + Ast ast{Ast::QUALIFIED_RULE}; + Ast pre{Ast::LIST}; while (true) { switch (token.type) { - case Token::Type::END_OF_FILE: + case Token::END_OF_FILE: return Error(); - case Token::Type::LEFT_CURLY_BRACKET: - ast.block.emplaceBack(try$(_consumeBlock(s, Token::RIGHT_CURLY_BRACKET))); + case Token::LEFT_CURLY_BRACKET: + logDebug("creating a block for curent rule"); + ast.prefix = Box{pre}; + ast.content.emplaceBack(try$(_consumeBlock(s, Token::RIGHT_CURLY_BRACKET))); return Ok(ast); default: - ast.pre.emplace(try$(_consumeComponentValue(s, token))); + auto a = try$(_consumeComponentValue(s, token)); + logDebug("adding a new component value to the prefix {#}", a); + pre.content.pushBack(a); break; } token = nextToken(s).unwrap(); } - - return Ok(ast); } // https://www.w3.org/TR/css-syntax-3/#consume-list-of-rules Res _consumeRuleList(Str str) { auto s = Io::SScan(str); - Ast ast; - ast.type = Ast::LIST; + Ast ast = Ast::LIST; while (true) { auto token = try$(nextToken(s)); switch (token.type) { case Token::END_OF_FILE: return Ok(ast); + case Token::COMMENT: case Token::WHITESPACE: break; default: - ast.block.pushBack(try$(_consumeQualifiedRule(s, token))); + ast.content.pushBack(try$(_consumeQualifiedRule(s, token))); break; } } } -// No spec, we take the AST we built and convert it to a usable list of rules -Vec parseAST(Ast) { - Vec rules; - - return rules; -} - // https://www.w3.org/TR/css-syntax-3/#parse-stylesheet Res<> parseStylesheet(Str source) { CSSOM::StyleSheet stylesheet = CSSOM::StyleSheet(source); auto file = try$(Sys::File::open(Mime::parseUrlOrPath(source).unwrap())); auto buf = try$(Io::readAllUtf8(file)); Ast ast = try$(_consumeRuleList(buf)); + logDebug("AST : {#}", ast); stylesheet.cssRules = parseAST(ast); - logDebug("AST : {}", ast); + logDebug("RULES : {#}", stylesheet.cssRules); return Ok(); } +static inline Str toStr(Ast::_Type type) { + switch (type) { +#define ITER(NAME) \ + case Ast::NAME: \ + return #NAME; + FOREACH_AST(ITER) +#undef ITER + default: + panic("invalid ast type"); + } +} + } // namespace Web::Css -Reflectable$(Web::Css::Ast, token, pre, block); \ No newline at end of file +Reflectable$(Web::Css::Ast, type, token, prefix, content); + +template <> +struct Karm::Io::Formatter { + Res format(Io::TextWriter &writer, Web::Css::Ast::_Type val) { + return (writer.writeStr(try$(Io::toParamCase(toStr(val))))); + } +}; diff --git a/src/web/web-css/tests/test-lexer.cpp b/src/web/web-css/tests/test-lexer.cpp new file mode 100644 index 0000000000..a88645d5b7 --- /dev/null +++ b/src/web/web-css/tests/test-lexer.cpp @@ -0,0 +1,115 @@ +#include +#include +#include +#include + +namespace Web::Css::Tests { + +test$(comments) { + auto s = Io::SScan("/* comment */"); + auto t = try$(nextToken(s)); + expect$(t.type == Token::Type::COMMENT); + expectEq$(t.data, "/* comment */"); + + s = Io::SScan("/* unterminated comment"); + auto t2 = nextToken(s); + expect$(not t2.has()); + expectEq$(t2.none(), Error::invalidInput("unterminated comment")); + return Ok(); +} + +test$(numbers) { + auto s = Io::SScan("123"); + auto t = try$(nextToken(s)); + expect$(t.type == Token::Type::NUMBER); + expectEq$(t.data, "123"); + + s = Io::SScan("123%"); + t = try$(nextToken(s)); + expect$(t.type == Token::Type::PERCENTAGE); + expectEq$(t.data, "123%"); + + s = Io::SScan("123.456"); + t = try$(nextToken(s)); + expect$(t.type == Token::Type::NUMBER); + expectEq$(t.data, "123.456"); + + s = Io::SScan("123.456e7"); + t = try$(nextToken(s)); + expect$(t.type == Token::Type::NUMBER); + expectEq$(t.data, "123.456e7"); + + s = Io::SScan("123.456E7"); + t = try$(nextToken(s)); + expect$(t.type == Token::Type::NUMBER); + expectEq$(t.data, "123.456E7"); + + s = Io::SScan("123.456E7"); + t = try$(nextToken(s)); + expect$(t.type == Token::Type::NUMBER); + expectEq$(t.data, "123.456E7"); + + s = Io::SScan("-123.456E7"); + t = try$(nextToken(s)); + expect$(t.type == Token::Type::NUMBER); + expectEq$(t.data, "-123.456E7"); + + s = Io::SScan("123.456E7"); + t = try$(nextToken(s)); + expect$(t.type == Token::Type::NUMBER); + expectEq$(t.data, "123.456E7"); + + s = Io::SScan("123.456E-7"); + t = try$(nextToken(s)); + expect$(t.type == Token::Type::NUMBER); + expectEq$(t.data, "123.456E-7"); + + s = Io::SScan("123px"); + t = try$(nextToken(s)); + expect$(t.type == Token::Type::DIMENSION); + expectEq$(t.data, "123px"); + + s = Io::SScan("123.456px"); + t = try$(nextToken(s)); + expect$(t.type == Token::Type::DIMENSION); + expectEq$(t.data, "123.456px"); + + s = Io::SScan("123.456e7px"); + t = try$(nextToken(s)); + expect$(t.type == Token::Type::DIMENSION); + + s = Io::SScan("123.456E7px"); + t = try$(nextToken(s)); + expect$(t.type == Token::Type::DIMENSION); + + s = Io::SScan("+123.456E7px"); + t = try$(nextToken(s)); + expect$(t.type == Token::Type::DIMENSION); + + return Ok(); +} + +test$(strings) { + auto s = Io::SScan("''"); + auto t = try$(nextToken(s)); + expect$(t.type == Token::Type::STRING); + + s = Io::SScan("\"\""); + t = try$(nextToken(s)); + expect$(t.type == Token::Type::STRING); + + s = Io::SScan("\"abc\""); + t = try$(nextToken(s)); + expect$(t.type == Token::Type::STRING); + + s = Io::SScan("'abc'"); + t = try$(nextToken(s)); + expect$(t.type == Token::Type::STRING); + + s = Io::SScan("' Hello World !'"); + t = try$(nextToken(s)); + expect$(t.type == Token::Type::STRING); + + return Ok(); +} +}; // namespace Web::Css::Tests diff --git a/src/web/web-cssom/stylesheet.h b/src/web/web-cssom/stylesheet.h index d45afd6c73..2d7db4f167 100644 --- a/src/web/web-cssom/stylesheet.h +++ b/src/web/web-cssom/stylesheet.h @@ -3,7 +3,6 @@ #include #include #include - #include // SPEC COMPLIANCE : everything related to parent/scope is nospec as well as everything concerning dynamic modification @@ -15,6 +14,10 @@ struct CSSStyleDeclaration { Css::Token propertyName; Vec value; + CSSStyleDeclaration(Css::Token name) + : propertyName(name) { + } + CSSStyleDeclaration(Css::Token name, Vec val) : propertyName(name), value(val) { } @@ -80,3 +83,6 @@ struct Karm::Io::Formatter { return Ok(written); } }; + +Reflectable$(Web::CSSOM::CSSStyleDeclaration, propertyName, value); +Reflectable$(Web::CSSOM::CSSStyleRule, selector, declarations);