1111
1212#include < optional>
1313#include < string>
14+ #include < string_view>
1415
1516namespace aniparse ::text {
1617namespace {
@@ -27,6 +28,57 @@ std::optional<TextStyle::Kind> style_of(std::string_view tag) {
2728 return std::nullopt ;
2829}
2930
31+ std::string_view trim (std::string_view value) {
32+ while (!value.empty () && (value.front () == ' ' || value.front () == ' \t ' || value.front () == ' \n ' || value.front () == ' \r ' ))
33+ value.remove_prefix (1 );
34+ while (!value.empty () && (value.back () == ' ' || value.back () == ' \t ' || value.back () == ' \n ' || value.back () == ' \r ' ))
35+ value.remove_suffix (1 );
36+ return value;
37+ }
38+
39+ int hex_digit (char character) {
40+ if (character >= ' 0' && character <= ' 9' ) return character - ' 0' ;
41+ if (character >= ' a' && character <= ' f' ) return character - ' a' + 10 ;
42+ if (character >= ' A' && character <= ' F' ) return character - ' A' + 10 ;
43+ return -1 ;
44+ }
45+
46+ std::optional<TextColor> color_value (std::string_view value) {
47+ value = trim (value);
48+ if (value.size () < 4 || value.front () != ' #' ) return std::nullopt ;
49+ value.remove_prefix (1 );
50+ if (value.size () != 3 && value.size () != 4 && value.size () != 6 && value.size () != 8 ) return std::nullopt ;
51+ auto component = [&](std::size_t offset) -> std::optional<std::uint8_t > {
52+ const int high = hex_digit (value[offset]);
53+ const int low = value.size () <= 4 ? high : hex_digit (value[offset + 1 ]);
54+ if (high < 0 || low < 0 ) return std::nullopt ;
55+ return static_cast <std::uint8_t >(high * 16 + low);
56+ };
57+ const std::size_t step = value.size () <= 4 ? 1 : 2 ;
58+ const auto red = component (0 ), green = component (step), blue = component (step * 2 );
59+ if (!red || !green || !blue) return std::nullopt ;
60+ const auto alpha = value.size () == 4 || value.size () == 8 ? component (step * 3 ) : std::optional<std::uint8_t >{ 255 };
61+ if (!alpha) return std::nullopt ;
62+ return TextColor{ *red, *green, *blue, *alpha };
63+ }
64+
65+ std::optional<TextColor> color_of (const DOMElementView& element) {
66+ if (const auto value = element.get_attr (" color" ))
67+ if (const auto color = color_value (*value)) return color;
68+ if (const auto style = element.get_attr (" style" )) {
69+ std::string_view declarations = *style;
70+ while (!declarations.empty ()) {
71+ const std::size_t separator = declarations.find (' ;' );
72+ const std::string_view declaration = trim (declarations.substr (0 , separator));
73+ declarations = separator == std::string_view::npos ? std::string_view{} : declarations.substr (separator + 1 );
74+ const std::size_t colon = declaration.find (' :' );
75+ if (colon == std::string_view::npos || trim (declaration.substr (0 , colon)) != " color" ) continue ;
76+ if (const auto color = color_value (declaration.substr (colon + 1 ))) return color;
77+ }
78+ }
79+ return std::nullopt ;
80+ }
81+
3082// Emit a line break only when the text does not already end in one, so successive block
3183// tags and <br>s collapse instead of stacking blank lines.
3284void break_line (std::string& text) {
@@ -55,26 +107,26 @@ void walk(DOMNodeView node, AttributedText& out) {
55107 const std::size_t n = out.text .size ();
56108 if (n < 2 || out.text [n - 1 ] != ' \n ' || out.text [n - 2 ] != ' \n ' )
57109 out.text += ' \n ' ;
58- } else if (tag == " p" || tag == " div" || tag == " li" ) {
110+ continue ;
111+ }
112+ if (tag == " p" || tag == " div" || tag == " li" ) {
59113 break_line (out.text );
60114 walk (child, out);
61115 break_line (out.text );
62- } else if (tag == " a" ) {
63- const int start = static_cast <int >(out.text .size ());
64- walk (child, out);
65- const int end = static_cast <int >(out.text .size ());
66- if (const auto href = element.get_attr (" href" ); href && !href->empty () && end > start)
67- out.attributes .push_back ({ start, end, Hyperlink{ std::string (*href) } });
68- } else if (const auto kind = style_of (tag)) {
69- const int start = static_cast <int >(out.text .size ());
70- walk (child, out);
71- const int end = static_cast <int >(out.text .size ());
72- if (end > start)
73- out.attributes .push_back ({ start, end, TextStyle{ *kind } });
74- } else {
75- // Unknown/transparent tag (span, wbr, ...): keep its text, drop the tag.
76- walk (child, out);
116+ continue ;
77117 }
118+
119+ const int start = static_cast <int >(out.text .size ());
120+ walk (child, out);
121+ const int end = static_cast <int >(out.text .size ());
122+ if (end <= start) continue ;
123+ if (tag == " a" )
124+ if (const auto href = element.get_attr (" href" ); href && !href->empty ())
125+ out.attributes .push_back ({ start, end, Hyperlink{ std::string (*href) } });
126+ if (const auto kind = style_of (tag))
127+ out.attributes .push_back ({ start, end, TextStyle{ *kind } });
128+ if (const auto color = color_of (element))
129+ out.attributes .push_back ({ start, end, *color });
78130 }
79131}
80132} // namespace
0 commit comments