NovaSVG 0.10.0
A header-only SVG library
Loading...
Searching...
No Matches
svgparser.hpp
Go to the documentation of this file.
1#include <cassert>
2#include <unordered_map>
3
4#ifndef NOVASVG_DISABLE_EMBEDDED_FONT
5#include "default_font.h"
6#endif
7
8namespace novasvg {
9
10struct SimpleSelector;
11
12using Selector = std::vector<SimpleSelector>;
13using SelectorList = std::vector<Selector>;
14
30
49
51 enum class Combinator {
52 None,
54 Child,
57 };
58
60
63 std::string tag; // lower-case tag name as written; empty for `*`. For matching HTML (no ElementID for it)
64 std::vector<AttributeSelector> attributeSelectors;
65 std::vector<PseudoClassSelector> pseudoClassSelectors;
66};
67
70 PropertyID id; // Unknown for a property no SVG element has (only the HTML in a foreignObject reads those)
71 std::string value;
72 std::string name; // lower-case property name as written
73};
74
75using DeclarationList = std::vector<Declaration>;
76
81
82class RuleData {
83public:
85 : m_selector(selector), m_declarations(declarations), m_specificity(specificity), m_position(position)
86 {}
87
88 bool isLessThan(const RuleData& rule) const { return std::tie(m_specificity, m_position) < std::tie(rule.m_specificity, rule.m_position); }
89
90 const Selector& selector() const { return m_selector; }
91 const DeclarationList& declarations() const { return m_declarations; }
92 size_t specificity() const { return m_specificity; }
93 size_t position() const { return m_position; }
94
95 bool match(const SVGElement* element) const;
96
97private:
98 Selector m_selector;
99 DeclarationList m_declarations;
100 size_t m_specificity;
101 size_t m_position;
102};
103
104inline bool operator<(const RuleData& a, const RuleData& b) { return a.isLessThan(b); }
105
106using RuleDataList = std::vector<RuleData>;
107
108NOVASVG_INLINE constexpr bool equals(std::string_view value, std::string_view subvalue)
109{
110 return value.compare(subvalue) == 0;
111}
112
113NOVASVG_INLINE constexpr bool contains(std::string_view value, std::string_view subvalue)
114{
115 return value.find(subvalue) != std::string_view::npos;
116}
117
118NOVASVG_INLINE constexpr bool includes(std::string_view value, std::string_view subvalue)
119{
120 if(subvalue.empty() || subvalue.length() > value.length())
121 return false;
122 std::string_view input(value);
123 while(!input.empty()) {
124 skipOptionalSpaces(input);
125 std::string_view start(input);
126 while(!input.empty() && !IS_WS(input.front()))
127 input.remove_prefix(1);
128 if(subvalue == start.substr(0, start.length() - input.length())) {
129 return true;
130 }
131 }
132
133 return false;
134}
135
136NOVASVG_INLINE constexpr bool startswith(std::string_view value, std::string_view subvalue)
137{
138 if(subvalue.empty() || subvalue.length() > value.length())
139 return false;
140 return subvalue == value.substr(0, subvalue.size());
141}
142
143NOVASVG_INLINE constexpr bool endswith(std::string_view value, std::string_view subvalue)
144{
145 if(subvalue.empty() || subvalue.length() > value.length())
146 return false;
147 return subvalue == value.substr(value.size() - subvalue.size(), subvalue.size());
148}
149
150NOVASVG_INLINE constexpr bool dashequals(std::string_view value, std::string_view subvalue)
151{
152 if(startswith(value, subvalue))
153 return (value.length() == subvalue.length() || value.at(subvalue.length()) == '-');
154 return false;
155}
156
157static bool matchAttributeValue(const AttributeSelector& selector, std::string_view value)
158{
159 if(selector.matchType == AttributeSelector::MatchType::None)
160 return !value.empty();
161 if(selector.matchType == AttributeSelector::MatchType::Equals)
162 return equals(value, selector.value);
163 if(selector.matchType == AttributeSelector::MatchType::Contains)
164 return contains(value, selector.value);
165 if(selector.matchType == AttributeSelector::MatchType::Includes)
166 return includes(value, selector.value);
167 if(selector.matchType == AttributeSelector::MatchType::StartsWith)
168 return startswith(value, selector.value);
169 if(selector.matchType == AttributeSelector::MatchType::EndsWith)
170 return endswith(value, selector.value);
171 if(selector.matchType == AttributeSelector::MatchType::DashEquals)
172 return dashequals(value, selector.value);
173 return false;
174}
175
176static bool matchAttributeSelector(const AttributeSelector& selector, const SVGElement* element)
177{
178 return matchAttributeValue(selector, element->getAttribute(selector.id));
179}
180
181static bool matchSimpleSelector(const SimpleSelector& selector, const SVGElement* element);
182
183static bool matchPseudoClassSelector(const PseudoClassSelector& selector, const SVGElement* element)
184{
185 if(selector.type == PseudoClassSelector::Type::Empty)
186 return element->children().empty();
187 if(selector.type == PseudoClassSelector::Type::Root)
188 return element->isRootElement();
189 if(selector.type == PseudoClassSelector::Type::Is) {
190 for(const auto& subSelector : selector.subSelectors) {
191 for(const auto& simpleSelector : subSelector) {
192 if(!matchSimpleSelector(simpleSelector, element)) {
193 return false;
194 }
195 }
196 }
197
198 return true;
199 }
200
201 if(selector.type == PseudoClassSelector::Type::Not) {
202 for(const auto& subSelector : selector.subSelectors) {
203 for(const auto& simpleSelector : subSelector) {
204 if(matchSimpleSelector(simpleSelector, element)) {
205 return false;
206 }
207 }
208 }
209
210 return true;
211 }
212
213 if(selector.type == PseudoClassSelector::Type::FirstChild)
214 return !element->previousElement();
215 if(selector.type == PseudoClassSelector::Type::LastChild)
216 return !element->nextElement();
217 if(selector.type == PseudoClassSelector::Type::OnlyChild)
218 return !(element->previousElement() || element->nextElement());
219 if(selector.type == PseudoClassSelector::Type::FirstOfType) {
220 auto sibling = element->previousElement();
221 while(sibling) {
222 if(sibling->id() == element->id())
223 return false;
224 sibling = sibling->previousElement();
225 }
226
227 return true;
228 }
229
230 if(selector.type == PseudoClassSelector::Type::LastOfType) {
231 auto sibling = element->nextElement();
232 while(sibling) {
233 if(sibling->id() == element->id())
234 return false;
235 sibling = sibling->nextElement();
236 }
237
238 return true;
239 }
240
241 return false;
242}
243
244static bool matchSimpleSelector(const SimpleSelector& selector, const SVGElement* element)
245{
246 if(selector.id != ElementID::Star && selector.id != element->id())
247 return false;
248 for(const auto& sel : selector.attributeSelectors) {
249 if(!matchAttributeSelector(sel, element)) {
250 return false;
251 }
252 }
253
254 for(const auto& sel : selector.pseudoClassSelectors) {
255 if(!matchPseudoClassSelector(sel, element)) {
256 return false;
257 }
258 }
259
260 return true;
261}
262
263static bool matchSelector(const Selector& selector, const SVGElement* element)
264{
265 if(selector.empty())
266 return false;
267 auto it = selector.rbegin();
268 auto end = selector.rend();
269 if(!matchSimpleSelector(*it, element)) {
270 return false;
271 }
272
273 auto combinator = it->combinator;
274 ++it;
275
276 while(it != end) {
277 switch(combinator) {
280 element = element->parentElement();
281 break;
284 element = element->previousElement();
285 break;
287 assert(false);
288 }
289
290 if(element == nullptr)
291 return false;
292 if(matchSimpleSelector(*it, element)) {
293 combinator = it->combinator;
294 ++it;
295 } else if(combinator != SimpleSelector::Combinator::Descendant
297 return false;
298 }
299 }
300
301 return true;
302}
303
304NOVASVG_INLINE bool RuleData::match(const SVGElement* element) const
305{
306 return matchSelector(m_selector, element);
307}
308
309constexpr bool IS_CSS_STARTNAMECHAR(int c) { return IS_ALPHA(c) || c == '_' || c == '-'; }
310constexpr bool IS_CSS_NAMECHAR(int c) { return IS_CSS_STARTNAMECHAR(c) || IS_NUM(c); }
311
312inline bool readCSSIdentifier(std::string_view& input, std::string& output)
313{
314 if(input.empty() || !IS_CSS_STARTNAMECHAR(input.front()))
315 return false;
316 output.clear();
317 do {
318 output.push_back(input.front());
319 input.remove_prefix(1);
320 } while(!input.empty() && IS_CSS_NAMECHAR(input.front()));
321 return true;
322}
323
324static bool parseTagSelector(std::string_view& input, SimpleSelector& simpleSelector)
325{
326 std::string name;
327 if(skipDelimiter(input, '*'))
328 simpleSelector.id = ElementID::Star;
329 else if(readCSSIdentifier(input, name)) {
330 simpleSelector.id = elementid(name);
331 for(auto& ch : name)
332 ch = char(std::tolower(static_cast<unsigned char>(ch)));
333 simpleSelector.tag = name;
334 } else {
335 return false;
336 }
337 return true;
338}
339
340static bool parseIdSelector(std::string_view& input, SimpleSelector& simpleSelector)
341{
342 AttributeSelector a;
343 a.id = PropertyID::Id;
345 if(!readCSSIdentifier(input, a.value))
346 return false;
347 simpleSelector.attributeSelectors.push_back(std::move(a));
348 return true;
349}
350
351static bool parseClassSelector(std::string_view& input, SimpleSelector& simpleSelector)
352{
353 AttributeSelector a;
354 a.id = PropertyID::Class;
356 if(!readCSSIdentifier(input, a.value))
357 return false;
358 simpleSelector.attributeSelectors.push_back(std::move(a));
359 return true;
360}
361
362static bool parseAttributeSelector(std::string_view& input, SimpleSelector& simpleSelector)
363{
364 std::string name;
365 skipOptionalSpaces(input);
366 if(!readCSSIdentifier(input, name))
367 return false;
368 AttributeSelector a;
369 a.id = propertyid(name);
371 if(skipDelimiter(input, '='))
373 else if(skipString(input, "*="))
375 else if(skipString(input, "~="))
377 else if(skipString(input, "^="))
379 else if(skipString(input, "$="))
381 else if(skipString(input, "|="))
383 if(a.matchType != AttributeSelector::MatchType::None) {
384 skipOptionalSpaces(input);
385 if(!readCSSIdentifier(input, a.value)) {
386 if(input.empty() || !(input.front() == '\"' || input.front() == '\''))
387 return false;
388 auto quote = input.front();
389 input.remove_prefix(1);
390 auto n = input.find(quote);
391 if(n == std::string_view::npos)
392 return false;
393 a.value.assign(input.substr(0, n));
394 input.remove_prefix(n + 1);
395 }
396 }
397
398 skipOptionalSpaces(input);
399 if(!skipDelimiter(input, ']'))
400 return false;
401 simpleSelector.attributeSelectors.push_back(std::move(a));
402 return true;
403}
404
405static bool parseSelectors(std::string_view& input, SelectorList& selectors);
406
407static bool parsePseudoClassSelector(std::string_view& input, SimpleSelector& simpleSelector)
408{
409 std::string name;
410 if(!readCSSIdentifier(input, name))
411 return false;
412 PseudoClassSelector selector;
413 if(name.compare("empty") == 0)
414 selector.type = PseudoClassSelector::Type::Empty;
415 else if(name.compare("root") == 0)
416 selector.type = PseudoClassSelector::Type::Root;
417 else if(name.compare("not") == 0)
418 selector.type = PseudoClassSelector::Type::Not;
419 else if(name.compare("first-child") == 0)
421 else if(name.compare("last-child") == 0)
423 else if(name.compare("only-child") == 0)
425 else if(name.compare("first-of-type") == 0)
427 else if(name.compare("last-of-type") == 0)
429 else if(name.compare("only-of-type") == 0)
431 if(selector.type == PseudoClassSelector::Type::Is || selector.type == PseudoClassSelector::Type::Not) {
432 skipOptionalSpaces(input);
433 if(!skipDelimiter(input, '('))
434 return false;
435 skipOptionalSpaces(input);
436 if(!parseSelectors(input, selector.subSelectors))
437 return false;
438 skipOptionalSpaces(input);
439 if(!skipDelimiter(input, ')')) {
440 return false;
441 }
442 }
443
444 simpleSelector.pseudoClassSelectors.push_back(std::move(selector));
445 return true;
446}
447
448static bool parseSimpleSelector(std::string_view& input, SimpleSelector& simpleSelector, bool& failed)
449{
450 auto consumed = parseTagSelector(input, simpleSelector);
451 do {
452 if(skipDelimiter(input, '#'))
453 failed = !parseIdSelector(input, simpleSelector);
454 else if(skipDelimiter(input, '.'))
455 failed = !parseClassSelector(input, simpleSelector);
456 else if(skipDelimiter(input, '['))
457 failed = !parseAttributeSelector(input, simpleSelector);
458 else if(skipDelimiter(input, ':'))
459 failed = !parsePseudoClassSelector(input, simpleSelector);
460 else
461 break;
462 consumed = true;
463 } while(!failed);
464 return consumed && !failed;
465}
466
467static bool parseCombinator(std::string_view& input, SimpleSelector::Combinator& combinator)
468{
470 while(!input.empty() && IS_WS(input.front())) {
472 input.remove_prefix(1);
473 }
474
475 if(skipDelimiterAndOptionalSpaces(input, '>'))
477 else if(skipDelimiterAndOptionalSpaces(input, '+'))
479 else if(skipDelimiterAndOptionalSpaces(input, '~'))
481 return combinator != SimpleSelector::Combinator::None;
482}
483
484static bool parseSelector(std::string_view& input, Selector& selector)
485{
486 auto combinator = SimpleSelector::Combinator::None;
487 do {
488 bool failed = false;
489 SimpleSelector simpleSelector(combinator);
490 if(!parseSimpleSelector(input, simpleSelector, failed))
491 return !failed && (combinator == SimpleSelector::Combinator::Descendant);
492 selector.push_back(std::move(simpleSelector));
493 } while(parseCombinator(input, combinator));
494 return true;
495}
496
497static bool parseSelectors(std::string_view& input, SelectorList& selectors)
498{
499 do {
500 Selector selector;
501 if(!parseSelector(input, selector))
502 return false;
503 selectors.push_back(std::move(selector));
504 } while(skipDelimiterAndOptionalSpaces(input, ','));
505 return true;
506}
507
508static bool parseDeclarations(std::string_view& input, DeclarationList& declarations)
509{
510 if(!skipDelimiter(input, '{'))
511 return false;
512 skipOptionalSpaces(input);
513 do {
514 std::string name;
515 if(!readCSSIdentifier(input, name))
516 return false;
517 skipOptionalSpaces(input);
518 if(!skipDelimiter(input, ':'))
519 return false;
520 skipOptionalSpaces(input);
521 std::string_view value(input);
522 while(!input.empty() && !(input.front() == '!' || input.front() == ';' || input.front() == '}'))
523 input.remove_prefix(1);
524 value.remove_suffix(input.length());
525 stripTrailingSpaces(value);
526
527 Declaration declaration;
528 declaration.specificity = Specificity::Stylesheet;
529 declaration.id = propertyid(name);
530 declaration.value.assign(value);
531 for(auto& ch : name)
532 ch = char(std::tolower(static_cast<unsigned char>(ch)));
533 declaration.name = name;
534 if(skipDelimiter(input, '!')) {
535 skipOptionalSpaces(input);
536 if(!skipString(input, "important"))
537 return false;
538 declaration.specificity = Specificity::StylesheetImportant;
539 }
540
541 // `background-color` is no SVG property, but the HTML inside a foreignObject paints it
542 if(declaration.id != PropertyID::Unknown || declaration.name == "background-color")
543 declarations.push_back(std::move(declaration));
545 } while(!input.empty() && input.front() != '}');
546 return skipDelimiter(input, '}');
547}
548
549static bool parseRule(std::string_view& input, Rule& rule)
550{
551 if(!parseSelectors(input, rule.selectors))
552 return false;
553 return parseDeclarations(input, rule.declarations);
554}
555
556// Splits a `src:` value on its *top-level* commas (each a
557// `url(...)` optionally followed by `format(...)`) -- paren-depth-aware
558// so a comma inside url()/format() (none occur in valid CSS, but
559// malformed input shouldn't split incorrectly) doesn't split the list.
560static std::vector<std::string_view> splitTopLevelCommas(std::string_view value)
561{
562 std::vector<std::string_view> parts;
563 size_t start = 0;
564 int depth = 0;
565 for(size_t i = 0; i < value.size(); ++i) {
566 auto ch = value[i];
567 if(ch == '(')
568 ++depth;
569 else if(ch == ')') {
570 if(depth > 0)
571 --depth;
572 } else if(ch == ',' && depth == 0) {
573 parts.push_back(value.substr(start, i - start));
574 start = i + 1;
575 }
576 }
577 parts.push_back(value.substr(start));
578 return parts;
579}
580
581static std::string_view stripQuotes(std::string_view value)
582{
584 if(!value.empty() && (value.front() == '\'' || value.front() == '"')) {
585 auto quote = value.front();
586 value.remove_prefix(1);
587 if(!value.empty() && value.back() == quote)
588 value.remove_suffix(1);
589 }
590 return value;
591}
592
593// Decodes a base64 payload holding raw TrueType/OpenType (SFNT) data into a
594// FontFace; a null face when the payload is malformed or isn't raw SFNT
595// (e.g. WOFF/WOFF2). Shared by `@font-face` data URIs and the embedded
596// fallback font.
597static FontFace fontFaceFromBase64(std::string_view payload)
598{
599 size_t decodedLength = 0;
600 auto* decoded = base64_decode(payload.data(), int(payload.size()), &decodedLength);
601 if(decoded == nullptr)
602 return FontFace();
603 return FontFace(decoded, decodedLength, [](void* closure) { free(closure); }, decoded);
604}
605
606// fontFaceFromBase64() for a payload that was zlib-compressed before it was base64-encoded. The inflater is
607// the one stb_image already contains for PNG, so this needs nothing the library doesn't have anyway.
608static FontFace fontFaceFromZlibBase64(std::string_view payload)
609{
610 size_t compressedLength = 0;
611 auto* compressed = base64_decode(payload.data(), int(payload.size()), &compressedLength);
612 if(compressed == nullptr)
613 return FontFace();
614 int length = 0;
615 char* inflated = stbi_zlib_decode_malloc(reinterpret_cast<const char*>(compressed), int(compressedLength), &length);
616 free(compressed);
617 if(inflated == nullptr || length <= 0) {
618 free(inflated);
619 return FontFace();
620 }
621 return FontFace(inflated, size_t(length), [](void* closure) { free(closure); }, inflated);
622}
623
625{
626#ifdef NOVASVG_DISABLE_EMBEDDED_FONT
627 (void)bold;
628 (void)italic;
629 return FontFace();
630#else
631 // Each face is built on first use only: a machine with fonts never pays for the decode, and a
632 // program that never asks for bold never decodes the bold face.
633 auto build = [](const char* const* chunks) {
634 std::string payload;
635 for(auto chunk = chunks; *chunk; ++chunk)
636 payload += *chunk;
637 return fontFaceFromZlibBase64(payload);
638 };
639 if(bold && italic) {
640 static const FontFace face = build(detail::kEmbeddedFontBoldItalicZlibBase64);
641 return face;
642 }
643 if(bold) {
644 static const FontFace face = build(detail::kEmbeddedFontBoldZlibBase64);
645 return face;
646 }
647 if(italic) {
648 static const FontFace face = build(detail::kEmbeddedFontItalicZlibBase64);
649 return face;
650 }
651 static const FontFace face = build(detail::kEmbeddedFontZlibBase64);
652 return face;
653#endif
654}
655
656// Loads an `@font-face` rule's embedded font (a `src: url(data:...)`
657// entry) and registers it, matching what the CSS itself declares:
658// `font-family`, `font-weight`/`font-style` (bold/italic only -- numeric
659// weights other than "bold" all collapse to regular, matching the rest
660// of this codebase's Font handling elsewhere). Only *embedded* fonts
661// (data: URIs) are in scope -- a `url("external.ttf")` reference isn't
662// something we can resolve without filesystem/network context the SVG
663// doesn't provide, and is silently skipped, same as before this existed.
664//
665// Only raw TrueType/OpenType (SFNT) data is understood (via the same
666// stb_truetype loader every other font already goes through) -- WOFF/
667// WOFF2 aren't decoded. Rather than inspect each src alternative's
668// `format()` hint to guess which might work, this just tries loading
669// each in turn and keeps the first one that actually parses as valid
670// SFNT, exactly matching how real `@font-face` `src` fallback lists are
671// meant to be read (most-preferred format first, older formats after).
672static void parseFontFaceRule(std::string_view block)
673{
674 auto familyValue = findRawDeclarationValue(block, "font-family");
675 if(!familyValue)
676 return;
677 auto family = stripQuotes(*familyValue);
678 if(family.empty())
679 return;
680
681 auto srcValue = findRawDeclarationValue(block, "src");
682 if(!srcValue)
683 return;
684
685 // The src value is where the megabytes are (the font itself, base64). font-weight/font-style are never
686 // inside it, so look for them in what lies before and after it.
687 const auto srcOffset = size_t(srcValue->data() - block.data());
688 const auto before = block.substr(0, srcOffset);
689 const auto after = block.substr(srcOffset + srcValue->size());
690 auto findOutsideSrc = [&](std::string_view property) {
691 auto value = findRawDeclarationValue(before, property);
692 return value ? value : findRawDeclarationValue(after, property);
693 };
694
695 auto bold = false;
696 if(auto weightValue = findOutsideSrc("font-weight")) {
697 std::string weight(*weightValue);
698 for(auto& ch : weight)
699 ch = char(std::tolower(static_cast<unsigned char>(ch)));
700 bold = weight.find("bold") != std::string::npos || std::strtof(weight.c_str(), nullptr) >= 600.f;
701 }
702
703 auto italic = false;
704 if(auto styleValue = findOutsideSrc("font-style")) {
705 std::string style(*styleValue);
706 for(auto& ch : style)
707 ch = char(std::tolower(static_cast<unsigned char>(ch)));
708 italic = style.find("italic") != std::string::npos || style.find("oblique") != std::string::npos;
709 }
710
711 for(auto alternative : splitTopLevelCommas(*srcValue)) {
712 auto urlPos = alternative.find("url(");
713 if(urlPos == std::string_view::npos)
714 continue;
715 auto contentStart = urlPos + 4;
716 auto closeParen = alternative.find(')', contentStart);
717 if(closeParen == std::string_view::npos)
718 continue;
719 auto url = stripQuotes(alternative.substr(contentStart, closeParen - contentStart));
720
721 if(url.compare(0, 5, "data:") != 0)
722 continue; // not embedded -- out of scope, see comment above
723 auto base64Pos = url.find("base64,");
724 if(base64Pos == std::string_view::npos)
725 continue;
726 auto payload = url.substr(base64Pos + 7);
727
728 auto face = fontFaceFromBase64(payload);
729 if(face.isNull())
730 continue; // malformed, or not raw SFNT (e.g. still WOFF/WOFF2) -- try the next alternative
731
732 fontFaceCache()->addFontFace(std::string(family), bold, italic, face);
733 return; // first successfully-loaded alternative wins
734 }
735}
736
737static RuleDataList parseStyleSheet(std::string_view input)
738{
739 RuleDataList rules;
740 while(!input.empty()) {
741 skipOptionalSpaces(input);
742 if(skipDelimiter(input, '@')) {
743 std::string atKeyword;
744 readCSSIdentifier(input, atKeyword);
745 for(auto& ch : atKeyword)
746 ch = char(std::tolower(static_cast<unsigned char>(ch)));
747
748 if(atKeyword == "font-face") {
749 skipOptionalSpaces(input);
750 if(skipDelimiter(input, '{')) {
751 int depth = 1;
752 size_t blockLength = 0;
753 auto block = input;
754 // An embedded font makes this block hundreds of kilobytes, with no brace inside it but the
755 // last: look for that one with memchr, and only walk character by character if a `{`
756 // shows up before it.
757 if(auto* close = static_cast<const char*>(std::memchr(input.data(), '}', input.size()))) {
758 const size_t n = size_t(close - input.data());
759 if(std::memchr(input.data(), '{', n) == nullptr) {
760 blockLength = n + 1;
761 depth = 0;
762 }
763 }
764
765 while(blockLength < input.size() && depth > 0) {
766 auto ch = input[blockLength];
767 if(ch == '{') ++depth;
768 else if(ch == '}') --depth;
769 ++blockLength;
770 }
771 parseFontFaceRule(block.substr(0, depth == 0 ? blockLength - 1 : blockLength));
772 input.remove_prefix(blockLength);
773 }
774 continue;
775 }
776
777 int depth = 0;
778 while(!input.empty()) {
779 auto ch = input.front();
780 input.remove_prefix(1);
781 if(ch == ';' && depth == 0)
782 break;
783 if(ch == '{') ++depth;
784 else if(ch == '}' && depth > 0) {
785 if(depth == 1)
786 break;
787 --depth;
788 }
789 }
790
791 continue;
792 }
793
794 Rule rule;
795 if(!parseRule(input, rule))
796 break;
797 for(const auto& selector : rule.selectors) {
798 size_t specificity = 0;
799 for(const auto& simpleSelector : selector) {
800 specificity += (simpleSelector.id == ElementID::Star) ? 0x0 : 0x1;
801 for(const auto& attributeSelector : simpleSelector.attributeSelectors) {
802 specificity += (attributeSelector.id == PropertyID::Id) ? 0x10000 : 0x100;
803 }
804 for(const auto& pseudoClassSelector : simpleSelector.pseudoClassSelectors) {
805 specificity += 0x100;
806 }
807 }
808
809 rules.emplace_back(selector, rule.declarations, specificity, rules.size());
810 }
811 }
812
813 return rules;
814}
815
816static SelectorList parseQuerySelectors(std::string_view input)
817{
818 SelectorList selectors;
820 if(!parseSelectors(input, selectors)
821 || !input.empty()) {
822 return SelectorList();
823 }
824
825 return selectors;
826}
827
828// Removes a trailing `!important` (any case, any spacing around the bang) from a declaration value and
829// reports whether there was one. `fill:#f9f !important` is the colour #f9f, flagged important -- it is
830// not a colour called "#f9f !important".
831inline bool stripImportantFlag(std::string& value)
832{
833 auto bang = value.rfind('!');
834 if(bang == std::string::npos)
835 return false;
836 std::string_view flag(value);
837 flag.remove_prefix(bang + 1);
838 skipOptionalSpaces(flag);
840 if(flag.size() != 9)
841 return false;
842 for(size_t i = 0; i < flag.size(); ++i) {
843 if(std::tolower(static_cast<unsigned char>(flag[i])) != "important"[i])
844 return false;
845 }
846
847 value.erase(bang);
848 while(!value.empty() && IS_WS(value.back()))
849 value.pop_back();
850 return true;
851}
852
853inline void parseInlineStyle(std::string_view input, SVGElement* element)
854{
855 std::string name;
856 skipOptionalSpaces(input);
857 while(readCSSIdentifier(input, name)) {
858 skipOptionalSpaces(input);
859 if(!skipDelimiter(input, ':'))
860 return;
861 std::string value;
862 while(!input.empty() && input.front() != ';') {
863 value.push_back(input.front());
864 input.remove_prefix(1);
865 }
866
867 auto id = csspropertyid(name);
868 if(id != PropertyID::Unknown) {
869 const bool important = stripImportantFlag(value);
870 element->setAttribute(important ? Specificity::InlineImportant : Specificity::InlineStyle, id, value);
871 }
873 }
874}
875
876inline void removeStyleComments(std::string& value)
877{
878 auto start = value.find("/*");
879 while(start != std::string::npos) {
880 auto end = value.find("*/", start + 2);
881 value.erase(start, end - start + 2);
882 start = value.find("/*");
883 }
884}
885
886inline bool decodeText(std::string_view input, std::string& output)
887{
888 output.clear();
889 while(!input.empty()) {
890 // Copy the plain text up to the next `&` in one go; a style sheet with an embedded font is hundreds
891 // of kilobytes with no entity in it at all.
892 auto amp = input.find('&');
893 if(amp == std::string_view::npos) {
894 output.append(input.data(), input.size());
895 return true;
896 }
897
898 output.append(input.data(), amp);
899 input.remove_prefix(amp + 1);
900
901 if(skipDelimiter(input, '#')) {
902 int base = 10;
903 if(skipDelimiter(input, 'x'))
904 base = 16;
905 unsigned int cp;
906 if(!parseInteger(input, cp, base))
907 return false;
908 char c[5] = {0, 0, 0, 0, 0};
909 if(cp < 0x80) {
910 c[1] = 0;
911 c[0] = char(cp);
912 } else if(cp < 0x800) {
913 c[2] = 0;
914 c[1] = char((cp & 0x3F) | 0x80);
915 cp >>= 6;
916 c[0] = char(cp | 0xC0);
917 } else if(cp < 0x10000) {
918 c[3] = 0;
919 c[2] = char((cp & 0x3F) | 0x80);
920 cp >>= 6;
921 c[1] = char((cp & 0x3F) | 0x80);
922 cp >>= 6;
923 c[0] = char(cp | 0xE0);
924 } else if(cp < 0x200000) {
925 c[4] = 0;
926 c[3] = char((cp & 0x3F) | 0x80);
927 cp >>= 6;
928 c[2] = char((cp & 0x3F) | 0x80);
929 cp >>= 6;
930 c[1] = char((cp & 0x3F) | 0x80);
931 cp >>= 6;
932 c[0] = char(cp | 0xF0);
933 }
934
935 output.append(c);
936 } else {
937 if(skipString(input, "amp")) {
938 output.push_back('&');
939 } else if(skipString(input, "lt")) {
940 output.push_back('<');
941 } else if(skipString(input, "gt")) {
942 output.push_back('>');
943 } else if(skipString(input, "quot")) {
944 output.push_back('\"');
945 } else if(skipString(input, "apos")) {
946 output.push_back('\'');
947 } else {
948 return false;
949 }
950 }
951
952 if(!skipDelimiter(input, ';')) {
953 return false;
954 }
955 }
956
957 return true;
958}
959
960constexpr bool IS_STARTNAMECHAR(int c) { return IS_ALPHA(c) || c == '_' || c == ':'; }
961constexpr bool IS_NAMECHAR(int c) { return IS_STARTNAMECHAR(c) || IS_NUM(c) || c == '-' || c == '.'; }
962
963inline bool readIdentifier(std::string_view& input, std::string& output)
964{
965 if(input.empty() || !IS_STARTNAMECHAR(input.front()))
966 return false;
967 output.clear();
968 do {
969 output.push_back(input.front());
970 input.remove_prefix(1);
971 } while(!input.empty() && IS_NAMECHAR(input.front()));
972 return true;
973}
974
975NOVASVG_INLINE bool Document::parse(const char* data, size_t length)
976{
977 std::string buffer;
978 std::string styleSheet;
979 SVGElement* currentElement = nullptr;
980 int ignoring = 0;
981 auto handleText = [&](std::string_view text, bool in_cdata) {
982 if(text.empty() || currentElement == nullptr || ignoring > 0)
983 return;
985 return;
986 }
987
988 if(in_cdata) {
989 buffer.assign(text);
990 } else {
991 decodeText(text, buffer);
992 }
993
994 if(currentElement->id() == ElementID::Style) {
995 removeStyleComments(buffer);
996 styleSheet.append(buffer);
997 } else {
998 auto node = std::make_unique<SVGTextNode>(this);
999 node->setData(buffer);
1000 currentElement->addChild(std::move(node));
1001 }
1002 };
1003
1004 std::string_view input(data, length);
1005 if(length >= 3) {
1006 auto buffer = (const uint8_t*)(data);
1007
1008 const auto c1 = buffer[0];
1009 const auto c2 = buffer[1];
1010 const auto c3 = buffer[2];
1011 if(c1 == 0xEF && c2 == 0xBB && c3 == 0xBF) {
1012 input.remove_prefix(3);
1013 }
1014 }
1015
1016 while(!input.empty()) {
1017 if(currentElement) {
1018 auto text = input.substr(0, input.find('<'));
1019 handleText(text, false);
1020 input.remove_prefix(text.length());
1021 } else {
1023 break;
1024 }
1025 }
1026
1027 if(!skipDelimiter(input, '<'))
1028 return false;
1029 if(skipDelimiter(input, '?')) {
1030 if(!readIdentifier(input, buffer))
1031 return false;
1032 auto n = input.find("?>");
1033 if(n == std::string_view::npos)
1034 return false;
1035 input.remove_prefix(n + 2);
1036 continue;
1037 }
1038
1039 if(skipDelimiter(input, '!')) {
1040 if(skipString(input, "--")) {
1041 auto n = input.find("-->");
1042 if(n == std::string_view::npos)
1043 return false;
1044 handleText(input.substr(0, n), false);
1045 input.remove_prefix(n + 3);
1046 continue;
1047 }
1048
1049 if(skipString(input, "[CDATA[")) {
1050 auto n = input.find("]]>");
1051 if(n == std::string_view::npos)
1052 return false;
1053 handleText(input.substr(0, n), true);
1054 input.remove_prefix(n + 3);
1055 continue;
1056 }
1057
1058 if(skipString(input, "DOCTYPE")) {
1059 while(!input.empty() && input.front() != '>') {
1060 if(input.front() == '[') {
1061 int depth = 1;
1062 input.remove_prefix(1);
1063 while(!input.empty() && depth > 0) {
1064 if(input.front() == '[') ++depth;
1065 else if(input.front() == ']') --depth;
1066 input.remove_prefix(1);
1067 }
1068 } else {
1069 input.remove_prefix(1);
1070 }
1071 }
1072
1073 if(!skipDelimiter(input, '>'))
1074 return false;
1075 continue;
1076 }
1077
1078 return false;
1079 }
1080
1081 if(skipDelimiter(input, '/')) {
1082 if(currentElement == nullptr && ignoring == 0)
1083 return false;
1084 if(!readIdentifier(input, buffer))
1085 return false;
1086 if(ignoring == 0) {
1087 auto id = elementid(buffer);
1088 if(id != currentElement->id())
1089 return false;
1090 currentElement = currentElement->parentElement();
1091 } else {
1092 --ignoring;
1093 }
1094
1096 if(!skipDelimiter(input, '>'))
1097 return false;
1098 continue;
1099 }
1100
1101 if(!readIdentifier(input, buffer))
1102 return false;
1103 SVGElement* element = nullptr;
1104 if(ignoring > 0) {
1105 ++ignoring;
1106 } else {
1107 auto id = elementid(buffer);
1108 if(id == ElementID::Unknown) {
1109 ignoring = 1;
1110 } else {
1111 if(m_rootElement && currentElement == nullptr)
1112 return false;
1113 if(m_rootElement == nullptr) {
1114 if(id != ElementID::Svg)
1115 return false;
1116 m_rootElement = std::make_unique<SVGRootElement>(this);
1117 element = m_rootElement.get();
1118 } else {
1119 auto child = SVGElement::create(this, id);
1120 element = child.get();
1121 currentElement->addChild(std::move(child));
1122 }
1123 }
1124 }
1125
1127 while(readIdentifier(input, buffer)) {
1129 if(!skipDelimiter(input, '='))
1130 return false;
1132 if(input.empty() || !(input.front() == '\"' || input.front() == '\''))
1133 return false;
1134 auto quote = input.front();
1135 input.remove_prefix(1);
1136 auto n = input.find(quote);
1137 if(n == std::string_view::npos)
1138 return false;
1139 auto id = PropertyID::Unknown;
1140 if(element != nullptr)
1141 id = propertyid(buffer);
1142 if(id != PropertyID::Unknown) {
1143 decodeText(input.substr(0, n), buffer);
1144 if(id == PropertyID::Style) {
1145 removeStyleComments(buffer);
1146 parseInlineStyle(buffer, element);
1147 } else {
1148 if(id == PropertyID::Id)
1149 m_rootElement->addElementById(buffer, element);
1150 element->setAttribute(Specificity::PresentationAttribute, id, buffer);
1151 }
1152 }
1153
1154 input.remove_prefix(n + 1);
1156 }
1157
1158 if(skipDelimiter(input, '>')) {
1159 if(element != nullptr) {
1160 if(element->id() == ElementID::ForeignObject) {
1161 // foreignObject content is arbitrary HTML/XML, not SVG --
1162 // capture it verbatim instead of trying (and failing) to
1163 // parse div/span/p/etc. as SVG elements. Mirrors the
1164 // CDATA/comment handling above: find the matching close
1165 // tag and skip straight past it.
1166 static constexpr std::string_view closeTag = "</foreignObject>";
1167 auto n = input.find(closeTag);
1168 if(n == std::string_view::npos)
1169 return false;
1170 static_cast<SVGForeignObjectElement*>(element)->setRawContent(std::string(input.substr(0, n)));
1171 input.remove_prefix(n + closeTag.length());
1172 } else {
1173 currentElement = element;
1174 }
1175 }
1176 continue;
1177 }
1178
1179 if(skipDelimiter(input, '/')) {
1180 if(!skipDelimiter(input, '>'))
1181 return false;
1182 if(ignoring > 0)
1183 --ignoring;
1184 continue;
1185 }
1186
1187 return false;
1188 }
1189
1190 if(m_rootElement == nullptr || ignoring > 0 || !input.empty())
1191 return false;
1193 m_rootElement->build();
1194 return true;
1195}
1196
1197// ---------------------------------------------------------------------------------------------------
1198// The HTML inside a <foreignObject>, styled by the document's CSS.
1199//
1200// ForeignObjectSimple paints HTML as plain text in one colour on one optional box, so all the CSS has to
1201// answer is: which `color` does the text end up with, and which `background-color` box sits behind it.
1202// A browser answers that with the cascade, and the part that matters in practice (mermaid) is a selector
1203// that reaches *through* the foreignObject into the SVG around it: `.section-0 span{color:black}` styles a
1204// <span> because the <g class="section-0"> it lives in matches. So the HTML tags are modelled as nodes
1205// that have a parent chain -- first the HTML ancestors, then the foreignObject, then the SVG elements
1206// above it -- and the very same selector matcher the SVG elements use walks that chain.
1207// ---------------------------------------------------------------------------------------------------
1208
1209struct HtmlNode {
1210 std::string tag; // lower case
1211 std::string id;
1212 std::string className;
1213 std::string style; // the style="" attribute, verbatim
1214 int parent{-1}; // index in the node list; -1 = a direct child of the foreignObject
1215 bool hasText{false}; // has non-blank text of its own
1216};
1217
1218static bool isVoidHtmlTag(const std::string& tag)
1219{
1220 static constexpr std::string_view names[] = {"area", "base", "br", "col", "embed", "hr", "img",
1221 "input", "link", "meta", "source", "track", "wbr"};
1222 for(auto name : names) {
1223 if(tag == name)
1224 return true;
1225 }
1226
1227 return false;
1228}
1229
1230static std::string lowerCase(std::string_view text)
1231{
1232 std::string out(text);
1233 for(auto& ch : out)
1234 ch = char(std::tolower(static_cast<unsigned char>(ch)));
1235 return out;
1236}
1237
1238// The opening/closing tags of a piece of HTML as a flat list in document order, each with the index of
1239// the element it sits in. Only what the cascade needs is read: tag name, id, class, style.
1240static std::vector<HtmlNode> parseHtmlNodes(std::string_view html)
1241{
1242 std::vector<HtmlNode> nodes;
1243 std::vector<int> open;
1244 size_t pos = 0;
1245 while(pos < html.size()) {
1246 auto lt = html.find('<', pos);
1247 auto text = html.substr(pos, lt == std::string_view::npos ? std::string_view::npos : lt - pos);
1248 if(!open.empty()) {
1249 for(char ch : text) {
1250 if(!IS_WS(ch)) {
1251 nodes[open.back()].hasText = true;
1252 break;
1253 }
1254 }
1255 }
1256
1257 if(lt == std::string_view::npos)
1258 break;
1259 if(html.compare(lt, 4, "<!--") == 0) {
1260 auto end = html.find("-->", lt + 4);
1261 pos = end == std::string_view::npos ? html.size() : end + 3;
1262 continue;
1263 }
1264
1265 // the end of the tag: the first '>' that is not inside a quoted attribute value
1266 size_t gt = lt + 1;
1267 char quote = 0;
1268 while(gt < html.size() && (quote || html[gt] != '>')) {
1269 if(quote) {
1270 if(html[gt] == quote)
1271 quote = 0;
1272 } else if(html[gt] == '"' || html[gt] == '\'') {
1273 quote = html[gt];
1274 }
1275 ++gt;
1276 }
1277
1278 auto inside = html.substr(lt + 1, gt - lt - 1);
1279 pos = NOVASVG_MIN(gt + 1, html.size());
1280 if(inside.empty() || inside.front() == '!' || inside.front() == '?')
1281 continue;
1282
1283 if(inside.front() == '/') {
1284 inside.remove_prefix(1);
1285 size_t n = 0;
1286 while(n < inside.size() && !IS_WS(inside[n]))
1287 ++n;
1288 auto name = lowerCase(inside.substr(0, n));
1289 for(size_t i = open.size(); i-- > 0;) {
1290 if(nodes[open[i]].tag == name) {
1291 open.resize(i);
1292 break;
1293 }
1294 }
1295
1296 continue;
1297 }
1298
1299 HtmlNode node;
1300 size_t n = 0;
1301 while(n < inside.size() && !IS_WS(inside[n]) && inside[n] != '/')
1302 ++n;
1303 node.tag = lowerCase(inside.substr(0, n));
1304 inside.remove_prefix(n);
1305 bool selfClosing = false;
1306 while(!inside.empty()) {
1307 skipOptionalSpaces(inside);
1308 if(inside.empty())
1309 break;
1310 if(inside.front() == '/') {
1311 selfClosing = true;
1312 inside.remove_prefix(1);
1313 continue;
1314 }
1315
1316 selfClosing = false;
1317 size_t m = 0;
1318 while(m < inside.size() && !IS_WS(inside[m]) && inside[m] != '=' && inside[m] != '/')
1319 ++m;
1320 auto name = lowerCase(inside.substr(0, m));
1321 inside.remove_prefix(m);
1322 skipOptionalSpaces(inside);
1323 std::string value;
1324 if(!inside.empty() && inside.front() == '=') {
1325 inside.remove_prefix(1);
1326 skipOptionalSpaces(inside);
1327 if(!inside.empty() && (inside.front() == '"' || inside.front() == '\'')) {
1328 auto q = inside.front();
1329 inside.remove_prefix(1);
1330 auto end = inside.find(q);
1331 value.assign(inside.substr(0, end));
1332 inside.remove_prefix(end == std::string_view::npos ? inside.size() : end + 1);
1333 } else {
1334 size_t v = 0;
1335 while(v < inside.size() && !IS_WS(inside[v]))
1336 ++v;
1337 value.assign(inside.substr(0, v));
1338 inside.remove_prefix(v);
1339 }
1340 }
1341
1342 if(name == "class")
1343 node.className = value;
1344 else if(name == "id")
1345 node.id = value;
1346 else if(name == "style")
1347 node.style = value;
1348 }
1349
1350 node.parent = open.empty() ? -1 : open.back();
1351 nodes.push_back(std::move(node));
1352 if(!selfClosing && !isVoidHtmlTag(nodes.back().tag))
1353 open.push_back(int(nodes.size()) - 1);
1354 }
1355
1356 return nodes;
1357}
1358
1359// A step along the ancestor chain: an HTML node, or -- once the HTML runs out -- an SVG element.
1361 const HtmlNode* html{nullptr};
1362 const SVGElement* svg{nullptr};
1363 explicit operator bool() const { return html || svg; }
1364};
1365
1366static bool matchHtmlSimpleSelector(const SimpleSelector& selector, const HtmlNode& node)
1367{
1368 if(selector.id != ElementID::Star && (selector.tag.empty() || selector.tag != node.tag))
1369 return false;
1370 if(!selector.pseudoClassSelectors.empty())
1371 return false; // :first-child, :not(...) & co are not modelled for HTML
1372 for(const auto& attribute : selector.attributeSelectors) {
1373 std::string_view value;
1374 if(attribute.id == PropertyID::Class)
1375 value = node.className;
1376 else if(attribute.id == PropertyID::Id)
1377 value = node.id;
1378 else if(attribute.id == PropertyID::Style)
1379 value = node.style;
1380 if(!matchAttributeValue(attribute, value))
1381 return false;
1382 }
1383
1384 return true;
1385}
1386
1387// matchSelector() for an HTML node: the rightmost compound must match the node, the ones before it its
1388// ancestors -- HTML elements first, then `owner` (the foreignObject) and the SVG elements above it.
1389// Sibling combinators (+ ~) are not modelled for HTML, so a selector using one never matches.
1390static bool matchHtmlSelector(const Selector& selector, const std::vector<HtmlNode>& nodes, int index, const SVGElement* owner)
1391{
1392 if(selector.empty())
1393 return false;
1394 auto parentOf = [&](const StyleNode& node) {
1395 if(node.html) {
1396 if(node.html->parent >= 0)
1397 return StyleNode{&nodes[node.html->parent], nullptr};
1398 return StyleNode{nullptr, owner};
1399 }
1400
1401 return StyleNode{nullptr, node.svg->parentElement()};
1402 };
1403 auto matches = [](const SimpleSelector& simple, const StyleNode& node) {
1404 return node.html ? matchHtmlSimpleSelector(simple, *node.html) : matchSimpleSelector(simple, node.svg);
1405 };
1406
1407 StyleNode node{&nodes[index], nullptr};
1408 auto it = selector.rbegin();
1409 auto end = selector.rend();
1410 if(!matches(*it, node))
1411 return false;
1412 auto combinator = it->combinator;
1413 ++it;
1414 while(it != end) {
1416 return false;
1417 node = parentOf(node);
1418 if(!node)
1419 return false;
1420 if(matches(*it, node)) {
1421 combinator = it->combinator;
1422 ++it;
1423 } else if(combinator != SimpleSelector::Combinator::Descendant) {
1424 return false;
1425 }
1426 }
1427
1428 return true;
1429}
1430
1431// The declaration of one property that currently wins, by cascade priority (a later one wins a tie).
1434 std::string value;
1435 void offer(int priority, const std::string& candidate)
1436 {
1437 if(priority >= specificity) {
1438 specificity = priority;
1439 value = candidate;
1440 }
1441 }
1442};
1443
1444// Offers every `color` / `background-color` declaration of a style="" attribute.
1445static void offerInlineStyle(std::string_view style, CascadedValue& color, CascadedValue& background)
1446{
1447 while(!style.empty()) {
1448 auto end = style.find(';');
1449 auto declaration = style.substr(0, end);
1450 style.remove_prefix(end == std::string_view::npos ? style.size() : end + 1);
1451 auto colon = declaration.find(':');
1452 if(colon == std::string_view::npos)
1453 continue;
1454 auto nameText = declaration.substr(0, colon);
1456 auto name = lowerCase(nameText);
1457 std::string value(declaration.substr(colon + 1));
1458 const int priority = stripImportantFlag(value) ? Specificity::InlineImportant : Specificity::InlineStyle;
1459 std::string_view trimmed(value);
1461 if(name == "color")
1462 color.offer(priority, std::string(trimmed));
1463 else if(name == "background-color")
1464 background.offer(priority, std::string(trimmed));
1465 }
1466}
1467
1468// Works out the text colour and the box colour the CSS gives this foreignObject's HTML and stores them on
1469// it. `rules` must be sorted (least to most specific, then source order), as applyStyleSheet() leaves them.
1470// * text colour: `color` inherits, so every node has the colour of its own rule or else its parent's; the
1471// text is painted in the colour of the last element that holds text itself (black if nothing sets one).
1472// This is the HTML's CSS `color` -- deliberately not the SVG `fill` that happens to be in effect around
1473// the foreignObject: a classDef like `.green>*{fill:#9f6}` also matches the label's <g>, and must not
1474// turn the label text the same shade as its box.
1475// * box colour: `background-color` does not inherit. Checked against a real WebKit render: mermaid's edge
1476// labels have an opaque box on an inner <span> stacked over a translucent one on the outer <div>, and
1477// the opaque one is what shows -- so the last element in document order that has a visible box wins,
1478// like paint order (later/nested elements paint over earlier ones). `transparent` is no box.
1479static void resolveForeignObjectStyle(SVGForeignObjectElement* foreignObject, const RuleDataList& rules)
1480{
1481 const auto nodes = parseHtmlNodes(foreignObject->rawContent());
1482 std::vector<const RuleData*> relevant;
1483 for(const auto& rule : rules) {
1484 for(const auto& declaration : rule.declarations()) {
1485 if(declaration.name == "color" || declaration.name == "background-color") {
1486 relevant.push_back(&rule);
1487 break;
1488 }
1489 }
1490 }
1491
1492 std::vector<std::optional<Color>> colorOf(nodes.size());
1493 std::optional<Color> textColor, textColorOfLastNode, background;
1494 bool anyText = false;
1495 for(size_t i = 0; i < nodes.size(); ++i) {
1496 CascadedValue color, box;
1497 for(const auto* rule : relevant) {
1498 if(!matchHtmlSelector(rule->selector(), nodes, int(i), foreignObject))
1499 continue;
1500 for(const auto& declaration : rule->declarations()) {
1501 if(declaration.name == "color")
1502 color.offer(declaration.specificity, declaration.value);
1503 else if(declaration.name == "background-color")
1504 box.offer(declaration.specificity, declaration.value);
1505 }
1506 }
1507
1508 offerInlineStyle(nodes[i].style, color, box);
1509
1510 std::optional<Color> declared;
1511 if(color.specificity >= 0)
1512 declared = parseCssColor(color.value);
1513 colorOf[i] = declared ? declared : (nodes[i].parent >= 0 ? colorOf[nodes[i].parent] : std::nullopt);
1514 textColorOfLastNode = colorOf[i];
1515 if(nodes[i].hasText) {
1516 textColor = colorOf[i];
1517 anyText = true;
1518 }
1519
1520 if(box.specificity >= 0) {
1521 if(auto fill = parseCssColor(box.value); fill && fill->isVisible())
1522 background = fill;
1523 }
1524 }
1525
1526 foreignObject->setHtmlStyle(anyText ? textColor : textColorOfLastNode, background);
1527}
1528
1529// One RuleDataList out of several stylesheets applied one after another, each parsed on its own (a rule
1530// the parser can't read ends only its own sheet) and numbered on from the sheets before it, so a later
1531// sheet wins a tie. Sorted the way applyStyleSheet() sorts.
1532static RuleDataList parseStyleSheets(const std::vector<std::string>& sheets)
1533{
1534 RuleDataList all;
1535 for(const auto& sheet : sheets) {
1536 for(auto& rule : parseStyleSheet(sheet))
1537 all.emplace_back(rule.selector(), rule.declarations(), rule.specificity(), all.size());
1538 }
1539
1540 std::sort(all.begin(), all.end());
1541 return all;
1542}
1543
1544// Which rules can possibly match a given element, so that applyStyleSheet() does not have to try every rule on
1545// every element. A rule is filed under what its *rightmost* compound selector demands of the element itself:
1546// a tag name (`rect`), else a class (`.node`), else nothing in particular. An element then only has to try the
1547// rules filed under its own tag, under each of its own classes, and the unfiled ones -- which are the only
1548// rules that could match it. The index only prunes; each candidate still goes through RuleData::match(), and
1549// candidates are visited in rule order, so the result is the same as trying every rule.
1551public:
1552 explicit RuleIndex(const RuleDataList& rules) : m_byTag(256)
1553 {
1554 for(size_t i = 0; i < rules.size(); ++i) {
1555 const auto& selector = rules[i].selector();
1556 if(selector.empty())
1557 continue; // can never match
1558 const auto& last = selector.back();
1559 if(last.id != ElementID::Star) {
1560 m_byTag[size_t(last.id)].push_back(uint32_t(i));
1561 continue;
1562 }
1563
1564 const std::string* className = nullptr;
1565 for(const auto& attribute : last.attributeSelectors) {
1566 if(attribute.id == PropertyID::Class && attribute.matchType == AttributeSelector::MatchType::Includes
1567 && !attribute.value.empty()) {
1568 className = &attribute.value;
1569 break;
1570 }
1571 }
1572
1573 if(className)
1574 m_byClass[*className].push_back(uint32_t(i));
1575 else
1576 m_other.push_back(uint32_t(i));
1577 }
1578 }
1579
1580 // Indices (ascending) of the rules that might match `element`.
1581 void candidates(const SVGElement* element, std::vector<uint32_t>& out) const
1582 {
1583 out.clear();
1584 const auto& tagged = m_byTag[size_t(element->id())];
1585 out.insert(out.end(), tagged.begin(), tagged.end());
1586 out.insert(out.end(), m_other.begin(), m_other.end());
1587 if(!m_byClass.empty()) {
1588 std::string_view classes(element->getAttribute(PropertyID::Class));
1589 while(!classes.empty()) {
1590 skipOptionalSpaces(classes);
1591 size_t n = 0;
1592 while(n < classes.size() && !IS_WS(classes[n]))
1593 ++n;
1594 if(n > 0) {
1595 auto it = m_byClass.find(std::string(classes.substr(0, n)));
1596 if(it != m_byClass.end())
1597 out.insert(out.end(), it->second.begin(), it->second.end());
1598 }
1599 classes.remove_prefix(n);
1600 }
1601 }
1602
1603 std::sort(out.begin(), out.end());
1604 out.erase(std::unique(out.begin(), out.end()), out.end());
1605 }
1606
1607private:
1608 std::vector<std::vector<uint32_t>> m_byTag;
1609 std::unordered_map<std::string, std::vector<uint32_t>> m_byClass;
1610 std::vector<uint32_t> m_other;
1611};
1612
1613NOVASVG_INLINE void Document::applyStyleSheet(const std::string& content)
1614{
1615 auto rules = parseStyleSheet(content);
1616 std::sort(rules.begin(), rules.end());
1617 if(!rules.empty()) {
1618 const RuleIndex index(rules);
1619 std::vector<uint32_t> candidates;
1620 m_rootElement->transverse([&](SVGElement* element) {
1621 index.candidates(element, candidates);
1622 for(auto i : candidates) {
1623 const auto& rule = rules[i];
1624 if(rule.match(element)) {
1625 for(const auto& declaration : rule.declarations()) {
1627 element->setAttribute(declaration.specificity, declaration.id, declaration.value);
1628 }
1629 }
1630 }
1631 });
1632 }
1633
1634 if(!content.empty())
1635 m_rootElement->addStyleSheet(content);
1636
1637 // The HTML in a foreignObject is not part of the element tree above, so it is styled here, from every
1638 // sheet applied so far (a stylesheet given later than the one in the file sits after it in the cascade).
1639 // Built only if there is a foreignObject to style; and when `content` is the only sheet so far those
1640 // rules are exactly the ones just parsed and sorted, so they are not parsed a second time.
1642 bool haveHtmlRules = false;
1643 m_rootElement->transverse([&](SVGElement* element) {
1644 if(element->id() != ElementID::ForeignObject)
1645 return;
1646 if(!haveHtmlRules) {
1647 const auto& sheets = m_rootElement->styleSheets();
1648 if(!content.empty() && sheets.size() == 1)
1649 htmlRules = std::move(rules);
1650 else
1651 htmlRules = parseStyleSheets(sheets);
1652 haveHtmlRules = true;
1653 }
1654
1655 resolveForeignObjectStyle(static_cast<SVGForeignObjectElement*>(element), htmlRules);
1656 });
1657}
1658
1660{
1661 auto selectors = parseQuerySelectors(content);
1662 if(selectors.empty())
1663 return ElementList();
1664 ElementList elements;
1665 m_rootElement->transverse([&](SVGElement* element) {
1666 for(const auto& selector : selectors) {
1667 if(matchSelector(selector, element)) {
1668 elements.push_back(element);
1669 break;
1670 }
1671 }
1672 });
1673
1674 return elements;
1675}
1676
1677} // namespace novasvg
void applyStyleSheet(const std::string &content)
Applies a CSS stylesheet to the document.
float width() const
Returns the intrinsic width of the document in pixels.
Definition novasvg.h:1041
ElementList querySelectorAll(const std::string &content) const
Selects all elements that match the given CSS selector(s).
bool addFontFace(const std::string &family, bool bold, bool italic, const FontFace &face)
Definition font.h:322
size_t position() const
Definition svgparser.hpp:93
size_t specificity() const
Definition svgparser.hpp:92
const DeclarationList & declarations() const
Definition svgparser.hpp:91
bool match(const SVGElement *element) const
const Selector & selector() const
Definition svgparser.hpp:90
bool isLessThan(const RuleData &rule) const
Definition svgparser.hpp:88
RuleData(const Selector &selector, const DeclarationList &declarations, size_t specificity, size_t position)
Definition svgparser.hpp:84
RuleIndex(const RuleDataList &rules)
void candidates(const SVGElement *element, std::vector< uint32_t > &out) const
const std::string & getAttribute(std::string_view name) const
ElementID id() const
Definition svgelement.h:847
static std::unique_ptr< SVGElement > create(Document *document, ElementID id)
bool setAttribute(std::string_view name, const std::string &value)
#define NOVASVG_MIN(a, b)
Definition color.h:14
constexpr const char *const kEmbeddedFontBoldItalicZlibBase64[]
constexpr const char *const kEmbeddedFontZlibBase64[]
constexpr const char *const kEmbeddedFontBoldZlibBase64[]
constexpr const char *const kEmbeddedFontItalicZlibBase64[]
NOVASVG_INLINE uint8_t * base64_decode(const char *data, int length, size_t *out_length)
Definition surface.h:7822
Main namespace for the NovaSVG library.
Definition bitmap.h:23
constexpr bool IS_WS(int cc)
bool readCSSIdentifier(std::string_view &input, std::string &output)
NOVASVG_INLINE constexpr bool equals(std::string_view value, std::string_view subvalue)
bool stripImportantFlag(std::string &value)
bool readIdentifier(std::string_view &input, std::string &output)
FontFace embeddedFallbackFont(bool bold=false, bool italic=false)
bool parseInteger(std::string_view &input, T &integer, int base=10)
FontFaceCache * fontFaceCache()
Definition font.h:449
NOVASVG_INLINE constexpr bool endswith(std::string_view value, std::string_view subvalue)
std::vector< Element > ElementList
Definition novasvg.h:547
constexpr bool skipDelimiter(std::string_view &input, char delimiter)
std::vector< RuleData > RuleDataList
PropertyID propertyid(std::string_view name)
constexpr bool IS_CSS_NAMECHAR(int c)
constexpr bool skipString(std::string_view &input, std::string_view value)
constexpr bool skipDelimiterAndOptionalSpaces(std::string_view &input, char delimiter)
ElementID elementid(std::string_view name)
constexpr bool IS_NUM(int cc)
constexpr bool IS_STARTNAMECHAR(int c)
constexpr void stripLeadingAndTrailingSpaces(std::string_view &input)
PropertyID csspropertyid(std::string_view name)
NOVASVG_INLINE constexpr bool includes(std::string_view value, std::string_view subvalue)
constexpr bool IS_ALPHA(int cc)
constexpr bool IS_NAMECHAR(int c)
constexpr bool IS_CSS_STARTNAMECHAR(int c)
NOVASVG_INLINE constexpr bool dashequals(std::string_view value, std::string_view subvalue)
std::vector< Selector > SelectorList
Definition svgparser.hpp:13
std::vector< SimpleSelector > Selector
Definition svgparser.hpp:12
bool operator<(const RuleData &a, const RuleData &b)
NOVASVG_INLINE constexpr bool contains(std::string_view value, std::string_view subvalue)
constexpr bool skipOptionalSpaces(std::string_view &input)
NOVASVG_INLINE constexpr bool startswith(std::string_view value, std::string_view subvalue)
bool decodeText(std::string_view input, std::string &output)
std::vector< Declaration > DeclarationList
Definition svgparser.hpp:75
constexpr bool skipOptionalSpacesOrDelimiter(std::string_view &input, char delimiter)
constexpr void stripTrailingSpaces(std::string_view &input)
void parseInlineStyle(std::string_view input, SVGElement *element)
void removeStyleComments(std::string &value)
#define NOVASVG_INLINE
Definition novasvg.h:42
void offer(int priority, const std::string &candidate)
std::string style
std::string className
SelectorList selectors
Definition svgparser.hpp:78
DeclarationList declarations
Definition svgparser.hpp:79
SimpleSelector(Combinator combinator)
Definition svgparser.hpp:59
std::vector< AttributeSelector > attributeSelectors
Definition svgparser.hpp:64
std::vector< PseudoClassSelector > pseudoClassSelectors
Definition svgparser.hpp:65
const SVGElement * svg
const HtmlNode * html
unsigned char style
std::string text
UTF-8; ' ' marks a forced line break ( ,.