diff --git a/src/odr/internal/html/document.cpp b/src/odr/internal/html/document.cpp index 1850f7c77..c9c07ee70 100644 --- a/src/odr/internal/html/document.cpp +++ b/src/odr/internal/html/document.cpp @@ -270,7 +270,9 @@ render(const Document &document, const HtmlConfig &config, const Logger &logger, return resources; } - StyleRegistry styles; + // One prefix, so no name can be read as another's. + StyleRegistry styles(StyleRegistry::Rank::replaces_inline, + StyleRegistry::Digits::base36); WritingState head_state(out, config, resources, logger, &styles); head_state.set_direction(document_direction(document)); diff --git a/src/odr/internal/html/html_writer.cpp b/src/odr/internal/html/html_writer.cpp index 79c821bb4..860802deb 100644 --- a/src/odr/internal/html/html_writer.cpp +++ b/src/odr/internal/html/html_writer.cpp @@ -121,8 +121,9 @@ HtmlElementOptions::set_style(std::optional _style) { HtmlElementOptions &HtmlElementOptions::set_style(std::string _style, StyleRegistry *registry) { if (registry != nullptr) { - if (const std::string *name = registry->use(_style); name != nullptr) { - style_class = *name; + if (const std::string &name = registry->intern("c", _style); + !name.empty()) { + style_class = name; return *this; } } diff --git a/src/odr/internal/html/pdf_file.cpp b/src/odr/internal/html/pdf_file.cpp index 761e29a3b..b5b759435 100644 --- a/src/odr/internal/html/pdf_file.cpp +++ b/src/odr/internal/html/pdf_file.cpp @@ -18,6 +18,7 @@ #include #include #include +#include #include #include #include @@ -1121,43 +1122,6 @@ std::vector page_elements(const pdf::Page &page, return elements; } -/// Deduplicates CSS declarations into atomic, single-property classes named -/// `` in first-seen order, emitted once in ``. The same font -/// sizes, offsets and spacings recur across up to millions of positioned -/// elements, and inline declarations bloat the document. Representation-only: -/// no element's computed style changes. -class AtomicStyles { -public: - /// `prefix` selects the property family; `declaration` is a full CSS - /// declaration without trailing ';' (e.g. "font-size:9.96pt"). Returns the - /// class name to add to the element. - const std::string &intern(const std::string &prefix, - std::string declaration) { - const auto [it, inserted] = - m_class_by_declaration.try_emplace(std::move(declaration)); - if (inserted) { - it->second = prefix + std::to_string(++m_count_by_prefix[prefix]); - m_order.push_back(&*it); - } - return it->second; - } - - /// One rule per line (`.f1{font-size:9.96pt}`) so regeneration diffs stay - /// legible; each is preceded by a newline. - void write_rules(std::ostream &o) const { - for (const auto *entry : m_order) { - o << "\n." << entry->second << '{' << entry->first << '}'; - } - } - -private: - /// Node-based map: pointers stored in `m_order` stay valid across - /// insertions. - std::unordered_map m_class_by_declaration; - std::unordered_map m_count_by_prefix; - std::vector *> m_order; -}; - class HtmlServiceImpl final : public HtmlService { public: HtmlServiceImpl(PdfFile pdf_file, HtmlConfig config, const Logger &logger) @@ -1383,7 +1347,7 @@ class HtmlServiceImpl final : public HtmlService { pdf::DocumentParser &parser = *m_parser; LinkResolver &link_resolver = *m_link_resolver; - AtomicStyles styles; + StyleRegistry styles(StyleRegistry::Rank::plain); std::vector pages_out; pages_out.reserve(pages.size()); @@ -2000,7 +1964,7 @@ class HtmlServiceImpl final : public HtmlService { }); }; - AtomicStyles styles; + StyleRegistry styles(StyleRegistry::Rank::plain); const auto add_class = [&styles](std::string &classes, const std::string &prefix, std::string declaration) { @@ -2489,7 +2453,7 @@ class HtmlServiceImpl final : public HtmlService { /// The colour class suffix (with a leading space) for a run's paint colour, /// or "" for black / invisible. static std::string color_class(const pdf::TextElement &text, - const bool invisible, AtomicStyles &styles) { + const bool invisible, StyleRegistry &styles) { if (invisible) { return {}; } @@ -2666,7 +2630,7 @@ class HtmlServiceImpl final : public HtmlService { void write_header_common(const WritingState &state, const std::string &font_faces, const std::string &font_styles, - const AtomicStyles &styles, + const StyleRegistry &styles, const std::optional content, WriteModeCss &&write_mode_css) const { HtmlWriter &out = state.out(); diff --git a/src/odr/internal/html/style_registry.cpp b/src/odr/internal/html/style_registry.cpp index 658822dd5..dd4dde846 100644 --- a/src/odr/internal/html/style_registry.cpp +++ b/src/odr/internal/html/style_registry.cpp @@ -1,54 +1,62 @@ #include #include -#include namespace odr::internal::html { namespace { -/// Base 36, so the first 36 blocks fit in two characters. Each name is written -/// once per element carrying the block. -std::string name(std::size_t index) { +/// Each name is written once per element carrying the declaration, so a +/// character costs more than it looks. +std::string name(std::string_view prefix, std::size_t index, + const std::size_t base) { static constexpr std::string_view digits = "0123456789abcdefghijklmnopqrstuvwxyz"; std::string suffix; do { - suffix.insert(suffix.begin(), digits[index % digits.size()]); - index /= digits.size(); + suffix.insert(suffix.begin(), digits[index % base]); + index /= base; } while (index != 0); - return 'c' + suffix; + return std::string(prefix) + suffix; } +const std::string unnamed; + } // namespace -const std::string *StyleRegistry::use(const std::string &style) { - if (style.empty()) { - return nullptr; +const std::string &StyleRegistry::intern(const std::string_view prefix, + std::string declaration) { + if (declaration.empty()) { + return unnamed; } if (m_closed) { - const auto it = m_entries.find(style); - return it == m_entries.end() ? nullptr : &it->second; + const auto it = m_entries.find(declaration); + return it == m_entries.end() ? unnamed : it->second; } - const auto [it, inserted] = m_entries.try_emplace(style); + const auto [it, inserted] = m_entries.try_emplace(std::move(declaration)); if (inserted) { - it->second = name(m_order.size()); + it->second = name(prefix, m_count_by_prefix[std::string(prefix)]++, + m_digits == Digits::base36 ? 36 : 10); m_order.push_back(&*it); } - return &it->second; + return it->second; } void StyleRegistry::write_rules(std::ostream &out) const { for (const auto *entry : m_order) { - // Named three times for the specificity an inline `style` had. Still under - // `!important`, which the dark sheet needs. const std::string &name = entry->second; - out << "\n." << name << '.' << name << '.' << name << '{'; + out << "\n." << name; + if (m_rank == Rank::replaces_inline) { + // The specificity the inline `style` it stands in for had. Still under + // `!important`, which the dark sheet needs. + out << '.' << name << '.' << name; + } const std::string_view block = entry->first; - out << (block.back() == ';' ? block.substr(0, block.size() - 1) : block); - out << '}'; + out << '{' + << (block.back() == ';' ? block.substr(0, block.size() - 1) : block) + << '}'; } } diff --git a/src/odr/internal/html/style_registry.hpp b/src/odr/internal/html/style_registry.hpp index 8d43c36ee..9f5d46fe8 100644 --- a/src/odr/internal/html/style_registry.hpp +++ b/src/odr/internal/html/style_registry.hpp @@ -3,20 +3,40 @@ #include #include #include +#include #include #include namespace odr::internal::html { -/// Names each distinct style block a class, defined once in ``. An -/// inline `style` is the one shape a browser cannot share across the cells of -/// a sheet. +/// Names each distinct declaration a class, written once in ``. The same +/// declarations recur across the cells of a sheet and the positioned elements +/// of a pdf page, and an inline `style` is the one shape a browser cannot +/// share: each is parsed into its own block, and each element gets its own +/// computed style. class StyleRegistry { public: - /// The class @p style is written as; `nullptr` leaves it inline. - const std::string *use(const std::string &style); - - /// Names nothing further, so a block first seen after this stays inline. + /// What a rule has to outrank. + enum class Rank { + plain, ///< nothing else styles the elements carrying it + replaces_inline, ///< the class three times, for the specificity it took + ///< over + }; + + /// How the number after a prefix is written. `base36` names the first 36 of a + /// prefix in one character, but only a caller whose prefixes never extend one + /// another may ask for it: `w` at 1008 spells `ws0`, which is also `ws` at 0. + enum class Digits { decimal, base36 }; + + explicit StyleRegistry(const Rank rank, const Digits digits = Digits::decimal) + : m_rank{rank}, m_digits{digits} {} + + /// The class @p declaration is written as, ``. Empty where it + /// cannot be named: an empty declaration, or one first seen after `close`. + const std::string &intern(std::string_view prefix, std::string declaration); + + /// Names nothing further, so a declaration first seen after this stays + /// inline. void close() { m_closed = true; } [[nodiscard]] bool has_rules() const { return !m_order.empty(); } @@ -28,6 +48,9 @@ class StyleRegistry { /// Node-based: the pointers in `m_order` outlive every insertion. std::unordered_map m_entries; std::vector *> m_order; + std::unordered_map m_count_by_prefix; + Rank m_rank{Rank::plain}; + Digits m_digits{Digits::decimal}; bool m_closed{false}; }; diff --git a/test/data.cmake b/test/data.cmake index 2b923d3c3..5dacfb5f6 100644 --- a/test/data.cmake +++ b/test/data.cmake @@ -17,9 +17,9 @@ odr_test_data( odr_test_data( PATH "reference-output/odr-public" URL "https://github.com/opendocument-app/OpenDocument.test.output.git" - REVISION "8d9a5459118e25eb44db8b86564b7848cb5a4b66") + REVISION "29fc1949468ed7aeba800a74db5025a7f8b61105") odr_test_data( PATH "reference-output/odr-private" URL "https://github.com/opendocument-app/OpenDocument.test-private.output.git" - REVISION "a83bd65792fba59ad2555c9e56a54bcb8f4605f0") + REVISION "d7aa0dc9c9e54450df9822322bff5d517ff520ba") diff --git a/test/src/internal/pdf/pdf_file.cpp b/test/src/internal/pdf/pdf_file.cpp index 699945f56..d95968e12 100644 --- a/test/src/internal/pdf/pdf_file.cpp +++ b/test/src/internal/pdf/pdf_file.cpp @@ -255,8 +255,8 @@ TEST(PdfFile, matrix_runs_flow_into_one_selection_block) { EXPECT_TRUE(contains(html, ">Hi")); EXPECT_TRUE(contains(html, ">to")); // Both words and the space between them carry a PDF-derived width. - EXPECT_EQ(count(html, R"(