diff --git a/samples/page-margins.xhtml b/samples/page-margins.xhtml new file mode 100644 index 0000000..790fea8 --- /dev/null +++ b/samples/page-margins.xhtml @@ -0,0 +1,105 @@ + + + + + +

Lorem ipsum dolor, sit amet...

+

+ ...consectetur adipisicing elit. Ab sunt aliquid voluptates fuga, autem earum minus + debitis temporibus ut facilis reprehenderit voluptatum maiores ipsum quis totam esse nemo sint minima. +

+ + + diff --git a/src/libs/karm-io/traits.h b/src/libs/karm-io/traits.h index 0465415..a0b0f46 100644 --- a/src/libs/karm-io/traits.h +++ b/src/libs/karm-io/traits.h @@ -41,7 +41,6 @@ template concept SeekableDuplexable = Duplexable and Seekable; struct Writer { - virtual ~Writer() = default; virtual Res write(Bytes) = 0; @@ -89,7 +88,9 @@ struct TextWriter : virtual Res writeRune(Rune rune) = 0; - Res flush() override { return Ok(0uz); } + Res flush() override { + return Ok(0uz); + } }; template diff --git a/src/web/vaev-base/page.h b/src/web/vaev-base/page.h new file mode 100644 index 0000000..06b743e --- /dev/null +++ b/src/web/vaev-base/page.h @@ -0,0 +1,5 @@ +#pragma once + +namespace Vaev { + +} // namespace Vaev diff --git a/src/web/vaev-driver/render.cpp b/src/web/vaev-driver/render.cpp index 4ea561e..4ca70db 100644 --- a/src/web/vaev-driver/render.cpp +++ b/src/web/vaev-driver/render.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include @@ -13,7 +14,7 @@ namespace Vaev::Driver { -static constexpr bool DEBUG_RENDER = false; +static constexpr bool DEBUG_RENDER = true; static void _collectStyle(Markup::Node const &node, Style::StyleBook &sb) { auto el = node.is(); @@ -79,7 +80,6 @@ RenderResult render(Markup::Document const &dom, Style::Media const &media, Layo Layout::layout( tree, - tree.root, { .commit = Layout::Commit::YES, .knownSize = {viewport.small.width, NONE}, @@ -88,8 +88,6 @@ RenderResult render(Markup::Document const &dom, Style::Media const &media, Layo } ); - Layout::layoutPositioned(tree, tree.root, {viewport.small.width, viewport.small.height}); - auto sceneRoot = makeStrong(); elapsed = Sys::now() - start; @@ -110,6 +108,215 @@ RenderResult render(Markup::Document const &dom, Style::Media const &media, Layo }; } +static void _paintMargins(Style::PageComputedStyle &pageStyle, RectPx pageRect, RectPx pageContent, Scene::Stack &stack) { + // MARK: Top Left Corner --------------------------------------------------- + + auto topLeftMarginCornerRect = RectPx::fromTwoPoint( + pageRect.topStart(), + pageContent.topStart() + ); + Layout::Tree topLeftMarginCornerTree{ + .root = Layout::buildForPseudoElement(pageStyle.area(Style::PageArea::TOP_LEFT_CORNER)), + .viewport = Layout::Viewport{.small = topLeftMarginCornerRect.size()} + }; + Layout::layout( + topLeftMarginCornerTree, + { + .commit = Layout::Commit::YES, + .knownSize = topLeftMarginCornerRect.size().cast>(), + .position = topLeftMarginCornerRect.topStart(), + .availableSpace = topLeftMarginCornerRect.size(), + .containingBlock = topLeftMarginCornerRect.size(), + } + ); + Layout::paint(topLeftMarginCornerTree.root, stack); + + // MARK: Top Right Corner -------------------------------------------------- + + auto topRightMarginCornerRect = RectPx::fromTwoPoint( + pageRect.topEnd(), + pageContent.topEnd() + ); + Layout::Tree topRightMarginCornerTree{ + .root = Layout::buildForPseudoElement(pageStyle.area(Style::PageArea::TOP_RIGHT_CORNER)), + .viewport = Layout::Viewport{.small = topRightMarginCornerRect.size()} + }; + Layout::layout( + topRightMarginCornerTree, + { + .commit = Layout::Commit::YES, + .knownSize = topRightMarginCornerRect.size().cast>(), + .position = topRightMarginCornerRect.topStart(), + .availableSpace = topRightMarginCornerRect.size(), + .containingBlock = topRightMarginCornerRect.size(), + } + ); + Layout::paint(topRightMarginCornerTree.root, stack); + + // MARK: Bottom Left Corner ------------------------------------------------ + + auto bottomLeftMarginCornerRect = RectPx::fromTwoPoint( + pageRect.bottomStart(), + pageContent.bottomStart() + ); + Layout::Tree bottomLeftMarginCornerTree{ + .root = Layout::buildForPseudoElement(pageStyle.area(Style::PageArea::BOTTOM_LEFT_CORNER)), + .viewport = Layout::Viewport{.small = bottomLeftMarginCornerRect.size()} + }; + Layout::layout( + bottomLeftMarginCornerTree, + bottomLeftMarginCornerTree.root, + { + .commit = Layout::Commit::YES, + .knownSize = bottomLeftMarginCornerRect.size().cast>(), + .position = bottomLeftMarginCornerRect.topStart(), + .availableSpace = bottomLeftMarginCornerRect.size(), + .containingBlock = bottomLeftMarginCornerRect.size(), + } + ); + Layout::paint(bottomLeftMarginCornerTree.root, stack); + + // MARK: Bottom Right Corner ----------------------------------------------- + + auto bottomRightMarginCornerRect = RectPx::fromTwoPoint( + pageRect.bottomEnd(), + pageContent.bottomEnd() + ); + Layout::Tree bottomRightMarginCornerTree{ + .root = Layout::buildForPseudoElement(pageStyle.area(Style::PageArea::BOTTOM_RIGHT_CORNER)), + .viewport = Layout::Viewport{.small = bottomRightMarginCornerRect.size()} + }; + Layout::layout( + bottomRightMarginCornerTree, + { + .commit = Layout::Commit::YES, + .knownSize = bottomRightMarginCornerRect.size().cast>(), + .position = bottomRightMarginCornerRect.topStart(), + .availableSpace = bottomRightMarginCornerRect.size(), + .containingBlock = bottomRightMarginCornerRect.size(), + } + ); + Layout::paint(bottomRightMarginCornerTree.root, stack); + + // MARK: Top --------------------------------------------------------------- + + auto topRect = RectPx::fromTwoPoint( + topLeftMarginCornerRect.topEnd(), + topRightMarginCornerRect.bottomStart() + ); + + auto topBox = Layout::buildForPseudoElement(pageStyle.area(Style::PageArea::TOP)); + topBox.add(Layout::buildForPseudoElement(pageStyle.area(Style::PageArea::TOP_LEFT))); + topBox.add(Layout::buildForPseudoElement(pageStyle.area(Style::PageArea::TOP_CENTER))); + topBox.add(Layout::buildForPseudoElement(pageStyle.area(Style::PageArea::TOP_RIGHT))); + + Layout::Tree topTree{ + .root = std::move(topBox), + .viewport = Layout::Viewport{.small = topRect.size()} + }; + + Layout::layout( + topTree, + { + .commit = Layout::Commit::YES, + .knownSize = topRect.size().cast>(), + .position = topRect.topStart(), + .availableSpace = topRect.size(), + .containingBlock = topRect.size(), + } + ); + Layout::paint(topTree.root, stack); + + // MARK: Bottom ------------------------------------------------------------ + + auto bottomRect = RectPx::fromTwoPoint( + bottomLeftMarginCornerRect.topEnd(), + bottomRightMarginCornerRect.bottomStart() + ); + + auto bottomBox = Layout::buildForPseudoElement(pageStyle.area(Style::PageArea::BOTTOM)); + bottomBox.add(Layout::buildForPseudoElement(pageStyle.area(Style::PageArea::BOTTOM_LEFT))); + bottomBox.add(Layout::buildForPseudoElement(pageStyle.area(Style::PageArea::BOTTOM_CENTER))); + bottomBox.add(Layout::buildForPseudoElement(pageStyle.area(Style::PageArea::BOTTOM_RIGHT))); + + Layout::Tree bottomTree{ + .root = std::move(bottomBox), + .viewport = Layout::Viewport{.small = bottomRect.size()} + }; + + Layout::layout( + bottomTree, + { + .commit = Layout::Commit::YES, + .knownSize = bottomRect.size().cast>(), + .position = bottomRect.topStart(), + .availableSpace = bottomRect.size(), + .containingBlock = bottomRect.size(), + } + ); + + Layout::paint(bottomTree.root, stack); + + // MARK: Left -------------------------------------------------------------- + auto leftRect = RectPx::fromTwoPoint( + topLeftMarginCornerRect.bottomEnd(), + bottomLeftMarginCornerRect.topStart() + ); + + auto leftBox = Layout::buildForPseudoElement(pageStyle.area(Style::PageArea::LEFT)); + leftBox.add(Layout::buildForPseudoElement(pageStyle.area(Style::PageArea::LEFT_TOP))); + leftBox.add(Layout::buildForPseudoElement(pageStyle.area(Style::PageArea::LEFT_MIDDLE))); + leftBox.add(Layout::buildForPseudoElement(pageStyle.area(Style::PageArea::LEFT_BOTTOM))); + + Layout::Tree leftTree{ + .root = std::move(leftBox), + .viewport = Layout::Viewport{.small = leftRect.size()} + }; + + Layout::layout( + leftTree, + { + .commit = Layout::Commit::YES, + .knownSize = leftRect.size().cast>(), + .position = leftRect.topStart(), + .availableSpace = leftRect.size(), + .containingBlock = leftRect.size(), + } + ); + + Layout::paint(leftTree.root, stack); + + // MARK: Right ------------------------------------------------------------- + + auto rightRect = RectPx::fromTwoPoint( + topRightMarginCornerRect.bottomEnd(), + bottomRightMarginCornerRect.topStart() + ); + + auto rightBox = Layout::buildForPseudoElement(pageStyle.area(Style::PageArea::RIGHT)); + rightBox.add(Layout::buildForPseudoElement(pageStyle.area(Style::PageArea::RIGHT_TOP))); + rightBox.add(Layout::buildForPseudoElement(pageStyle.area(Style::PageArea::RIGHT_MIDDLE))); + rightBox.add(Layout::buildForPseudoElement(pageStyle.area(Style::PageArea::RIGHT_BOTTOM))); + + Layout::Tree rightTree{ + .root = std::move(rightBox), + .viewport = Layout::Viewport{.small = rightRect.size()} + }; + + Layout::layout( + rightTree, + { + .commit = Layout::Commit::YES, + .knownSize = rightRect.size().cast>(), + .position = rightRect.topStart(), + .availableSpace = rightRect.size(), + .containingBlock = rightRect.size(), + } + ); + + Layout::paint(rightTree.root, stack); +} + Vec> print(Markup::Document const &dom, Style::Media const &media) { Style::StyleBook stylebook; stylebook.add( @@ -121,43 +328,63 @@ Vec> print(Markup::Document const &dom, Style::Media const & .take("print stylesheet not available") ); - auto start = Sys::now(); _collectStyle(dom, stylebook); - auto elapsed = Sys::now() - start; - logDebugIf(DEBUG_RENDER, "style collection time: {}", elapsed); Style::Computer computer{media, stylebook}; + // MARK: Page and Margins -------------------------------------------------- + + Vec> pages; + + Layout::Resolver resolver{}; + Style::Page page{.name = ""s, .number = pages.len(), .blank = false}; + auto pageStyle = computer.computeFor(page); + RectPx pageRect{ + media.width / Px{media.resolution.toDppx()}, + media.height / Px{media.resolution.toDppx()} + }; + + auto scaleMatrix = Math::Trans2f::makeScale(media.resolution.toDppx()); + auto pageScene = makeStrong(pageRect.size().cast(), scaleMatrix); + + InsetsPx pageMargin = { + resolver.resolve(pageStyle->style->margin->top, pageRect.height), + resolver.resolve(pageStyle->style->margin->end, pageRect.width), + resolver.resolve(pageStyle->style->margin->bottom, pageRect.height), + resolver.resolve(pageStyle->style->margin->start, pageRect.width), + }; + + RectPx pageContent = pageRect.shrink(pageMargin); + + _paintMargins(*pageStyle, pageRect, pageContent, *pageScene); + + // MARK: Page Content ------------------------------------------------------ + Layout::Viewport vp{ - .small = { - media.width / Px{media.resolution.toDppx()}, - media.height / Px{media.resolution.toDppx()}, - }, + .small = pageContent.size(), + .large = pageRect.size(), + .dynamic = pageRect.size(), }; - Layout::Tree tree = { + Layout::Tree contentTree = { Layout::build(computer, dom), vp, }; Layout::layout( - tree, - tree.root, + contentTree, { .commit = Layout::Commit::YES, - .knownSize = {vp.small.width, NONE}, - .availableSpace = {vp.small.width, 0_px}, - .containingBlock = {vp.small.width, vp.small.height}, + .knownSize = {pageContent.width, NONE}, + .position = pageContent.topStart(), + .availableSpace = pageContent.size(), + .containingBlock = pageContent.size(), } ); - auto scaleMatrix = Math::Trans2f::makeScale(media.resolution.toDppx()); - - Vec> pages; - auto page = makeStrong(vp.small.size().cast(), scaleMatrix); - Layout::paint(tree.root, *page); - page->prepare(); - pages.pushBack(page); + Layout::paint(contentTree.root, *pageScene); + pageScene->prepare(); + pages.pushBack(pageScene); return pages; } diff --git a/src/web/vaev-driver/res/print.css b/src/web/vaev-driver/res/print.css index d7cd41e..0ffaeff 100644 --- a/src/web/vaev-driver/res/print.css +++ b/src/web/vaev-driver/res/print.css @@ -1,87 +1,128 @@ @page { - margin: 1in; + margin: 0.5in; @footnote { - margin-top: 1em + margin-top: 10px; + } + + /* MARK: Top ------------------------------------------------------------ */ + + @-vaev-top { + display: flex; } @top-left-corner { text-align: right; - vertical-align: middle + vertical-align: middle; } @top-left { text-align: left; - vertical-align: middle + vertical-align: middle; + flex-grow: 1; } @top-center { text-align: center; - vertical-align: middle + vertical-align: middle; + flex-grow: 1; } @top-right { text-align: right; - vertical-align: middle + vertical-align: middle; + flex-grow: 1; } @top-right-corner { text-align: left; - vertical-align: middle + vertical-align: middle; + } + + /* MARK: Left ----------------------------------------------------------- */ + + @-vaev-left { + display: flex; + flex-direction: column; } @left-top { text-align: center; - vertical-align: top + vertical-align: top; + flex-grow: 1; } + @left-middle { text-align: center; - vertical-align: middle + vertical-align: middle; + flex-grow: 1; } @left-bottom { text-align: center; - vertical-align: bottom + vertical-align: bottom; + flex-grow: 1; + } + + /* MARK: Right ---------------------------------------------------------- */ + + @-vaev-right { + display: flex; + flex-direction: column; } @right-top { text-align: center; - vertical-align: top + vertical-align: top; + flex-grow: 1; } @right-middle { text-align: center; - vertical-align: middle + vertical-align: middle; + flex-grow: 1; } @right-bottom { text-align: center; - vertical-align: bottom + vertical-align: bottom; + flex-grow: 1; + } + + /* MARK: Bottom --------------------------------------------------------- */ + + @-vaev-bottom { + display: flex; } @bottom-left-corner { text-align: right; - vertical-align: middle + vertical-align: middle; } + @bottom-left { text-align: left; - vertical-align: middle + vertical-align: middle; + flex-grow: 1; } @bottom-center { text-align: center; - vertical-align: middle + vertical-align: middle; + flex-grow: 1; } @bottom-right { text-align: right; - vertical-align: middle + vertical-align: middle; + flex-grow: 1; + } @bottom-right-corner { text-align: left; - vertical-align: middle + vertical-align: middle; } } diff --git a/src/web/vaev-layout/builder.cpp b/src/web/vaev-layout/builder.cpp index 918d31a..0c90ca1 100644 --- a/src/web/vaev-layout/builder.cpp +++ b/src/web/vaev-layout/builder.cpp @@ -302,4 +302,30 @@ Box build(Style::Computer &c, Markup::Document const &doc) { return root; } +Box buildForPseudoElement(Strong style) { + auto fontFace = _lookupFontface(*style); + + // FIXME: We should pass this around from the top in order to properly resolve rems + Resolver resolver{ + .rootFont = Text::Font{fontFace, 16}, + .boxFont = Text::Font{fontFace, 16}, + }; + Text::ProseStyle proseStyle{ + .font = { + fontFace, + resolver.resolve(style->font->size).cast(), + }, + .multiline = true, + }; + + auto prose = makeStrong(proseStyle); + if (style->content) { + logDebug("content: '{}'", style->content); + prose->append(style->content.str()); + return {style, fontFace, prose}; + } + + return {style, fontFace}; +} + } // namespace Vaev::Layout diff --git a/src/web/vaev-layout/builder.h b/src/web/vaev-layout/builder.h index ebcd6d2..915fa90 100644 --- a/src/web/vaev-layout/builder.h +++ b/src/web/vaev-layout/builder.h @@ -6,4 +6,6 @@ namespace Vaev::Layout { Box build(Style::Computer &c, Markup::Document const &doc); +Box buildForPseudoElement(Strong style); + } // namespace Vaev::Layout diff --git a/src/web/vaev-layout/flex.cpp b/src/web/vaev-layout/flex.cpp index 9ebd554..6e08842 100644 --- a/src/web/vaev-layout/flex.cpp +++ b/src/web/vaev-layout/flex.cpp @@ -349,15 +349,22 @@ struct FlexItem { ? containerSize.x : containerSize.y ); + case Size::MIN_CONTENT: return isWidth ? minContentSize.x : minContentSize.y; + case Size::MAX_CONTENT: return isWidth ? maxContentSize.x : maxContentSize.y; + case Size::FIT_CONTENT: logWarn("not implemented"); + return 0_px; + case Size::AUTO: - if (not isMin) + if (not isMin) { + logDebug("{}", box->style->sizing); panic("AUTO is an invalid value for max-width"); + } // used cross min sizes are resolved to 0 whereas main sizes have specific method return isWidth == fa.isRowOriented diff --git a/src/web/vaev-layout/layout.cpp b/src/web/vaev-layout/layout.cpp index 8a4a1e6..7710382 100644 --- a/src/web/vaev-layout/layout.cpp +++ b/src/web/vaev-layout/layout.cpp @@ -5,6 +5,7 @@ #include "flex.h" #include "grid.h" #include "inline.h" +#include "positioned.h" #include "table.h" #include "values.h" @@ -191,4 +192,10 @@ Output layout(Tree &tree, Box &box, Input input) { return Output::fromSize(size); } +Output layout(Tree &tree, Input input) { + auto out = layout(tree, tree.root, input); + layoutPositioned(tree, tree.root, input.containingBlock); + return out; +} + } // namespace Vaev::Layout diff --git a/src/web/vaev-layout/layout.h b/src/web/vaev-layout/layout.h index d274638..7c0479d 100644 --- a/src/web/vaev-layout/layout.h +++ b/src/web/vaev-layout/layout.h @@ -12,4 +12,6 @@ Vec2Px computeIntrinsicSize(Tree &tree, Box &box, IntrinsicSize intrinsic, Vec2P Output layout(Tree &tree, Box &box, Input input); +Output layout(Tree &tree, Input input); + } // namespace Vaev::Layout diff --git a/src/web/vaev-style/computed.h b/src/web/vaev-style/computed.h index eda4860..bb9e4bf 100644 --- a/src/web/vaev-style/computed.h +++ b/src/web/vaev-style/computed.h @@ -28,6 +28,7 @@ struct Computed { Color color; Number opacity; + String content = ""s; AlignProps aligns; Math::Vec2>> gaps; diff --git a/src/web/vaev-style/computer.cpp b/src/web/vaev-style/computer.cpp index fe1f4e3..679592f 100644 --- a/src/web/vaev-style/computer.cpp +++ b/src/web/vaev-style/computer.cpp @@ -32,17 +32,24 @@ void Computer::_evalRule(Rule const &rule, Markup::Element const &el, MatchingRu }); } -// https://drafts.csswg.org/css-cascade/#cascade-origin -Strong Computer::computeFor(Computed const &parent, Markup::Element const &el) { - MatchingRules matchingRules; - - // Collect matching styles rules - for (auto const &sheet : _styleBook.styleSheets) { - for (auto const &rule : sheet.rules) { - _evalRule(rule, el, matchingRules); +void Computer::_evalRule(Rule const &rule, Page const &page, PageComputedStyle &c) { + rule.visit(Visitor{ + [&](PageRule const &r) { + if (r.match(page)) + r.apply(c); + }, + [&](MediaRule const &r) { + if (r.match(_media)) + for (auto const &subRule : r.rules) + _evalRule(subRule, page, c); + }, + [&](auto const &) { + // Ignore other rule types } - } + }); +} +Strong Computer::_evalCascade(Computed const &parent, MatchingRules &matchingRules) { // Sort origin and specificity stableSort( matchingRules, @@ -53,14 +60,6 @@ Strong Computer::computeFor(Computed const &parent, Markup::Element co } ); - // Get the style attribute if any - auto styleAttr = el.getAttribute(Html::STYLE_ATTR); - - StyleRule styleRule{ - .props = parseDeclarations(styleAttr ? *styleAttr : ""), - }; - matchingRules.pushBack(&styleRule); - // Compute computed style auto computed = makeStrong(Computed::initial()); computed->inherit(parent); @@ -91,4 +90,35 @@ Strong Computer::computeFor(Computed const &parent, Markup::Element co return computed; } +// https://drafts.csswg.org/css-cascade/#cascade-origin +Strong Computer::computeFor(Computed const &parent, Markup::Element const &el) { + MatchingRules matchingRules; + + // Collect matching styles rules + for (auto const &sheet : _styleBook.styleSheets) + for (auto const &rule : sheet.rules) + _evalRule(rule, el, matchingRules); + + // Get the style attribute if any + auto styleAttr = el.getAttribute(Html::STYLE_ATTR); + + StyleRule styleRule{ + .props = parseDeclarations(styleAttr ? *styleAttr : ""), + .origin = Origin::INLINE, + }; + matchingRules.pushBack(&styleRule); + + return _evalCascade(parent, matchingRules); +} + +Strong Computer::computeFor(Page const &page) { + auto computed = makeStrong(); + + for (auto const &sheet : _styleBook.styleSheets) + for (auto const &rule : sheet.rules) + _evalRule(rule, page, *computed); + + return computed; +} + } // namespace Vaev::Style diff --git a/src/web/vaev-style/computer.h b/src/web/vaev-style/computer.h index 5ea944b..83e0449 100644 --- a/src/web/vaev-style/computer.h +++ b/src/web/vaev-style/computer.h @@ -15,7 +15,13 @@ struct Computer { void _evalRule(Rule const &rule, Markup::Element const &el, MatchingRules &matches); + void _evalRule(Rule const &rule, Page const &page, PageComputedStyle &c); + + Strong _evalCascade(Computed const &parent, MatchingRules &matches); + Strong computeFor(Computed const &parent, Markup::Element const &el); + + Strong computeFor(Page const &page); }; } // namespace Vaev::Style diff --git a/src/web/vaev-style/origin.h b/src/web/vaev-style/origin.h index c5725b5..aa0033d 100644 --- a/src/web/vaev-style/origin.h +++ b/src/web/vaev-style/origin.h @@ -10,6 +10,7 @@ enum struct Origin { USER_AGENT, USER, AUTHOR, + INLINE, //< Declarations from style attributes }; static inline std::strong_ordering operator<=>(Origin a, Origin b) { diff --git a/src/web/vaev-style/page.cpp b/src/web/vaev-style/page.cpp new file mode 100644 index 0000000..54fa04f --- /dev/null +++ b/src/web/vaev-style/page.cpp @@ -0,0 +1,171 @@ +#include "page.h" + +#include "decls.h" + +namespace Vaev::Style { + +static bool DEBUG_PAGE = true; + +// MARK: Page Selector ---------------------------------------------------------- + +bool PageSelector::match(Page const &page) const { + if (name and page.name != name) + return false; + + for (auto const &pseudo : pseudos) { + switch (pseudo) { + case PagePseudo::FIRST: + if (not page.number) + return false; + break; + case PagePseudo::BLANK: + if (page.blank) + return false; + break; + case PagePseudo::LEFT: + if (page.number % 2 == 0) + return false; + break; + case PagePseudo::RIGHT: + if (page.number % 2 == 1) + return false; + break; + } + } + + return true; +} + +void PageSelector::repr(Io::Emit &e) const { + e("({} pseudos: {})", name, pseudos); +} + +PageSelector PageSelector::parse(Cursor &c) { + PageSelector res; + + if (c.peek() == Css::Token::IDENT) { + res.name = c.next().token.data; + } + + return res; +} + +Vec PageSelector::parseList(Cursor &c) { + Vec res; + + eatWhitespace(c); + while (not c.ended()) { + res.pushBack(parse(c)); + eatWhitespace(c); + } + + return res; +} + +// MARK: Page Margin Rule ------------------------------------------------------ + +void PageAreaRule::apply(Style::Computed &c) const { + for (auto const &prop : props) { + prop.apply(c, c); + } +} + +void PageAreaRule::repr(Io::Emit &e) const { + e("(page-margin-rule\nmargin: {}\nprops: {})", area, props); +} + +static Opt _parsePageArea(Css::Token tok) { + Str name = next(tok.data); + +#define ITER(ID, NAME) \ + if (name == NAME) \ + return PageArea::ID; + FOREACH_PAGE_AREA(ITER) +#undef ITER + + logWarn("unknown page area: {}", name); + + return NONE; +} + +Opt PageAreaRule::parse(Css::Sst const &sst) { + PageAreaRule res; + + res.area = try$(_parsePageArea(sst.token)); + + for (auto const &item : sst.content) { + if (item == Css::Sst::DECL) { + auto prop = parseDeclaration(item); + if (prop) + res.props.pushBack(prop.take()); + } else { + logWarnIf(DEBUG_PAGE, "unexpected item in style rule: {}", item); + } + } + + return res; +} + +// MARK: Page Rule ------------------------------------------------------------- + +bool PageRule::match(Page const &page) const { + if (selectors.len() == 0) + return true; + + for (auto &s : selectors) { + if (s.match(page)) + return true; + } + + return false; +} + +void PageRule::apply(PageComputedStyle &c) const { + for (auto const &prop : props) { + prop.apply(*c.style, *c.style); + } + + for (auto const &area : areas) { + auto computed = c.area(area.area); + area.apply(*computed); + } +} + +void PageRule::repr(Io::Emit &e) const { + e("(page-rule\nselectors: {}\nprops: {}\nmargins: {})", selectors, props, areas); +} + +PageRule PageRule::parse(Css::Sst const &sst) { + if (sst != Css::Sst::RULE) + panic("expected rule"); + + if (sst.prefix != Css::Sst::LIST) + panic("expected list"); + + PageRule res; + + // Parse the selector + auto &prefix = sst.prefix.unwrap(); + Cursor prefixContent = prefix->content; + res.selectors = PageSelector::parseList(prefixContent); + + // Parse the properties. + for (auto const &item : sst.content) { + if (item == Css::Sst::DECL) { + auto prop = parseDeclaration(item); + if (prop) + res.props.pushBack(prop.take()); + } else if (item == Css::Sst::RULE and + item.token == Css::Token::AT_KEYWORD) { + auto rule = PageAreaRule::parse(item); + if (rule) + res.areas.pushBack(*rule); + } else { + logWarnIf(DEBUG_PAGE, "unexpected item in style rule: {}", item); + } + } + + return res; +} + +} // namespace Vaev::Style diff --git a/src/web/vaev-style/page.h b/src/web/vaev-style/page.h new file mode 100644 index 0000000..2492826 --- /dev/null +++ b/src/web/vaev-style/page.h @@ -0,0 +1,110 @@ +#pragma once + +#include "origin.h" +#include "styles.h" + +namespace Vaev::Style { + +// https://drafts.csswg.org/css-page-3/#margin-at-rules +#define FOREACH_PAGE_AREA(ITER) \ + ITER(FOOTNOTE, "footnote") \ + ITER(TOP, "-vaev-top") \ + ITER(TOP_LEFT_CORNER, "top-left-corner") \ + ITER(TOP_LEFT, "top-left") \ + ITER(TOP_CENTER, "top-center") \ + ITER(TOP_RIGHT, "top-right") \ + ITER(TOP_RIGHT_CORNER, "top-right-corner") \ + ITER(RIGHT, "-vaev-right") \ + ITER(RIGHT_TOP, "right-top") \ + ITER(RIGHT_MIDDLE, "right-middle") \ + ITER(RIGHT_BOTTOM, "right-bottom") \ + ITER(BOTTOM, "-vaev-bottom") \ + ITER(BOTTOM_RIGHT_CORNER, "bottom-right-corner") \ + ITER(BOTTOM_RIGHT, "bottom-right") \ + ITER(BOTTOM_CENTER, "bottom-center") \ + ITER(BOTTOM_LEFT, "bottom-left") \ + ITER(BOTTOM_LEFT_CORNER, "bottom-left-corner") \ + ITER(LEFT, "-vaev-left") \ + ITER(LEFT_BOTTOM, "left-bottom") \ + ITER(LEFT_MIDDLE, "left-middle") \ + ITER(LEFT_TOP, "left-top") + +enum struct PageArea { +#define ITER(ID, ...) ID, + FOREACH_PAGE_AREA(ITER) +#undef ITER + _LEN, +}; + +struct Page { + String name = ""s; + usize number; + bool blank; +}; + +struct PageComputedStyle { + Strong style = makeStrong(Computed::initial()); + Array, static_cast(PageArea::_LEN)> _areas = { +#define ITER(...) makeStrong(Computed::initial()), + FOREACH_PAGE_AREA(ITER) +#undef ITER + }; + + Strong area(PageArea margin) const { + return _areas[static_cast(margin)]; + } +}; + +// https://drafts.csswg.org/css-page-3/#at-page-rule + +#define FOREACH_PAGE_PSEUDO(ITER) \ + ITER(FIRST, "first") \ + ITER(BLANK, "blank") \ + ITER(LEFT, "left") \ + ITER(RIGHT, "right") + +enum struct PagePseudo { +#define ITER(ID, ...) ID, + FOREACH_PAGE_PSEUDO(ITER) +#undef ITER +}; + +struct PageSelector { + String name = ""s; + Vec pseudos; + + static PageSelector parse(Cursor &c); + + static Vec parseList(Cursor &c); + + bool match(Page const &page) const; + + void repr(Io::Emit &e) const; +}; + +struct PageAreaRule { + PageArea area; + Vec props; + + static Opt parse(Css::Sst const &sst); + + void apply(Style::Computed &c) const; + + void repr(Io::Emit &e) const; +}; + +struct PageRule { + Vec selectors; + Vec props; + Vec areas; + + static PageRule parse(Css::Sst const &sst); + + bool match(Page const &page) const; + + void apply(PageComputedStyle &c) const; + + void repr(Io::Emit &e) const; +}; + +} // namespace Vaev::Style diff --git a/src/web/vaev-style/rules.cpp b/src/web/vaev-style/rules.cpp index 2bfaf9c..be1132c 100644 --- a/src/web/vaev-style/rules.cpp +++ b/src/web/vaev-style/rules.cpp @@ -8,6 +8,10 @@ static bool DEBUG_RULE = false; // MARK: StyleRule ------------------------------------------------------------- +bool StyleRule::match(Markup::Element const &el) const { + return selector.match(el); +} + void StyleRule::repr(Io::Emit &e) const { e("(style-rule"); e.indent(); @@ -142,6 +146,8 @@ Rule Rule::parse(Css::Sst const &sst, Origin origin) { return ImportRule::parse(sst); else if (tok.data == "@font-face") return FontFaceRule::parse(sst); + else if (tok.data == "@page") + return PageRule::parse(sst); else return StyleRule::parse(sst, origin); } diff --git a/src/web/vaev-style/rules.h b/src/web/vaev-style/rules.h index 9e559e2..060cdf5 100644 --- a/src/web/vaev-style/rules.h +++ b/src/web/vaev-style/rules.h @@ -3,6 +3,7 @@ #include "fonts.h" #include "media.h" #include "origin.h" +#include "page.h" #include "select.h" #include "styles.h" @@ -18,9 +19,7 @@ struct StyleRule { void repr(Io::Emit &e) const; - bool match(Markup::Element const &el) const { - return selector.match(el); - } + bool match(Markup::Element const &el) const; static StyleRule parse(Css::Sst const &sst, Origin origin = Origin::AUTHOR); }; @@ -60,7 +59,8 @@ using _Rule = Union< StyleRule, FontFaceRule, MediaRule, - ImportRule>; + ImportRule, + PageRule>; struct Rule : public _Rule { using _Rule::_Rule; diff --git a/src/web/vaev-style/styles.cpp b/src/web/vaev-style/styles.cpp index 17097ad..f25327d 100644 --- a/src/web/vaev-style/styles.cpp +++ b/src/web/vaev-style/styles.cpp @@ -118,4 +118,39 @@ void DefaultedProp::repr(Io::Emit &e) const { e("(Defaulted {#} = {})", propName, value); } +// MARK: Style Property ------------------------------------------------------- + +Str StyleProp::name() const { + return visit([](auto const &p) { + return p.name(); + }); +} + +void StyleProp::inherit(Computed const &parent, Computed &child) const { + visit([&](auto const &p) { + if constexpr (requires { p.inherit(parent, child); }) + p.inherit(parent, child); + }); +} + +void StyleProp::apply(Computed const &parent, Computed &c) const { + visit([&](auto const &p) { + if constexpr (requires { p.apply(c); }) + p.apply(c); + + if constexpr (requires { p.apply(parent, c); }) + p.apply(parent, c); + }); +} + +void StyleProp::repr(Io::Emit &e) const { + e("({}", name()); + visit([&](auto const &p) { + e(" {#}", p.value); + if (important == Important::YES) + e(" !important"); + }); + e(")"); +} + } // namespace Vaev::Style diff --git a/src/web/vaev-style/styles.h b/src/web/vaev-style/styles.h index a01ce74..bb34ecf 100644 --- a/src/web/vaev-style/styles.h +++ b/src/web/vaev-style/styles.h @@ -1149,7 +1149,31 @@ struct BorderWidthProp { } }; -// MARK: Borders - Table --------------------------------------------------------------- +// MARK: Content --------------------------------------------------------------- + +// https://drafts.csswg.org/css-content/#content-property +struct ContentProp { + String value = initial(); + + static constexpr Str name() { return "content"; } + + static String initial() { return ""s; } + + void apply(Computed &c) const { + c.content = value; + } + + static String load(Computed const &c) { + return c.content; + } + + Res<> parse(Cursor &c) { + value = try$(parseValue(c)); + return Ok(); + } +}; + +// MARK: Borders - Table ------------------------------------------------------- // https://www.w3.org/TR/CSS22/tables.html#propdef-border-collapse struct BorderCollapseProp { @@ -2733,6 +2757,9 @@ using _StyleProp = Union< BorderCollapseProp, BorderSpacingProp, + // Content + ContentProp, + // Flex FlexBasisProp, FlexDirectionProp, @@ -2834,38 +2861,13 @@ struct StyleProp : public _StyleProp { Cons{"grid-gap", "gap"}, }; - Str name() const { - return visit([](auto const &p) { - return p.name(); - }); - } + Str name() const; - void inherit(Computed const &parent, Computed &child) const { - visit([&](auto const &p) { - if constexpr (requires { p.inherit(parent, child); }) - p.inherit(parent, child); - }); - } + void inherit(Computed const &parent, Computed &child) const; - void apply(Computed const &parent, Computed &c) const { - visit([&](auto const &p) { - if constexpr (requires { p.apply(c); }) - p.apply(c); - - if constexpr (requires { p.apply(parent, c); }) - p.apply(parent, c); - }); - } + void apply(Computed const &parent, Computed &c) const; - void repr(Io::Emit &e) const { - e("({}", name()); - visit([&](auto const &p) { - e(" {}", p.value); - if (important == Important::YES) - e(" !important"); - }); - e(")"); - } + void repr(Io::Emit &e) const; }; } // namespace Vaev::Style