Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion src/odr/internal/html/document.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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));

Expand Down
5 changes: 3 additions & 2 deletions src/odr/internal/html/html_writer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -121,8 +121,9 @@ HtmlElementOptions::set_style(std::optional<HtmlWritable> _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;
}
}
Expand Down
46 changes: 5 additions & 41 deletions src/odr/internal/html/pdf_file.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
#include <odr/internal/html/frontend.hpp>
#include <odr/internal/html/html_service.hpp>
#include <odr/internal/html/html_writer.hpp>
#include <odr/internal/html/style_registry.hpp>
#include <odr/internal/pdf/pdf_color.hpp>
#include <odr/internal/pdf/pdf_document.hpp>
#include <odr/internal/pdf/pdf_document_element.hpp>
Expand Down Expand Up @@ -1121,43 +1122,6 @@ std::vector<pdf::PageElement> page_elements(const pdf::Page &page,
return elements;
}

/// Deduplicates CSS declarations into atomic, single-property classes named
/// `<prefix><n>` in first-seen order, emitted once in `<head>`. 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<std::string, std::string> m_class_by_declaration;
std::unordered_map<std::string, int> m_count_by_prefix;
std::vector<const std::pair<const std::string, std::string> *> m_order;
};

class HtmlServiceImpl final : public HtmlService {
public:
HtmlServiceImpl(PdfFile pdf_file, HtmlConfig config, const Logger &logger)
Expand Down Expand Up @@ -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<DualPageOut> pages_out;
pages_out.reserve(pages.size());

Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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 {};
}
Expand Down Expand Up @@ -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<double> content,
WriteModeCss &&write_mode_css) const {
HtmlWriter &out = state.out();
Expand Down
48 changes: 28 additions & 20 deletions src/odr/internal/html/style_registry.cpp
Original file line number Diff line number Diff line change
@@ -1,54 +1,62 @@
#include <odr/internal/html/style_registry.hpp>

#include <ostream>
#include <string_view>

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)
<< '}';
}
}

Expand Down
37 changes: 30 additions & 7 deletions src/odr/internal/html/style_registry.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,40 @@
#include <cstddef>
#include <iosfwd>
#include <string>
#include <string_view>
#include <unordered_map>
#include <vector>

namespace odr::internal::html {

/// Names each distinct style block a class, defined once in `<head>`. 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 `<head>`. 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, `<prefix><n>`. 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(); }
Expand All @@ -28,6 +48,9 @@ class StyleRegistry {
/// Node-based: the pointers in `m_order` outlive every insertion.
std::unordered_map<std::string, std::string> m_entries;
std::vector<const std::pair<const std::string, std::string> *> m_order;
std::unordered_map<std::string, std::size_t> m_count_by_prefix;
Rank m_rank{Rank::plain};
Digits m_digits{Digits::decimal};
bool m_closed{false};
};

Expand Down
4 changes: 2 additions & 2 deletions test/data.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -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")
4 changes: 2 additions & 2 deletions test/src/internal/pdf/pdf_file.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -255,8 +255,8 @@ TEST(PdfFile, matrix_runs_flow_into_one_selection_block) {
EXPECT_TRUE(contains(html, ">Hi</span>"));
EXPECT_TRUE(contains(html, ">to</span>"));
// Both words and the space between them carry a PDF-derived width.
EXPECT_EQ(count(html, R"(<span class="sr f1 w)"), 2u);
EXPECT_EQ(count(html, R"(<span class="sg f1 w)"), 1u);
EXPECT_EQ(count(html, R"(<span class="sr f0 w)"), 2u);
EXPECT_EQ(count(html, R"(<span class="sg f0 w)"), 1u);
}

// A standalone page view (`page{index}.html`) resolves internal links to the
Expand Down
Loading