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"(