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
3 changes: 2 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,8 @@ bytes ─▢ magic/open_strategy ─▢ DecodedFile ─▢ Document ─▢ Eleme
| `src/odr/internal/markdown/` | Markdown (CommonMark + GFM via md4c), decoded to a text document; see [`markdown/AGENTS.md`](src/odr/internal/markdown/AGENTS.md) + [`markdown/PLAN.md`](src/odr/internal/markdown/PLAN.md). |
| `src/odr/internal/xml/` | XML, rendered as a source view; see [`xml/AGENTS.md`](src/odr/internal/xml/AGENTS.md). |
| `src/odr/internal/svg/` | SVG, detected by reading it as xml; see [`svg/AGENTS.md`](src/odr/internal/svg/AGENTS.md). |
| `src/odr/internal/{csv,json,text,svm}/` | Smaller formats. |
| `src/odr/internal/svm/` | StarView metafile, the vector image odf/ooxml packages carry for charts and OLE objects; translated to svg. See [`svm/AGENTS.md`](src/odr/internal/svm/AGENTS.md) + [`svm/PLAN.md`](src/odr/internal/svm/PLAN.md). |
| `src/odr/internal/{csv,json,text}/` | Smaller formats. |
| `cli/src/` | CLI tools: `translate`, `back_translate`, `meta`, `server`. |
| `python/` | Python bindings (`pyodr`, pybind11); see [`python/AGENTS.md`](python/AGENTS.md). |
| `jni/` | JNI bindings (Java package `app.opendocument.core`); see [`jni/AGENTS.md`](jni/AGENTS.md). |
Expand Down
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,16 @@ The release run heads these entries with the version and opens a fresh
- A spreadsheet decodes in less memory: 626 MB peak instead of 914 MB on a
297 MB `content.xml`. Rendered output is unchanged.

- Text in a StarView metafile is escaped into the svg it renders as. An `&`,
`<` or `>` in a label made the svg malformed, and a malformed svg renders as
nothing.

- A StarView metafile translation logs what it drops: the actions it does not
implement, and a translation failure it used to fall back from silently.

- An html attribute value drops the control characters xml forbids, rather
than carrying them through. One escaper writes both html and svg now.

## v6.12.0 - 2026-08-30

- New `Document::save(std::ostream &)` and `Document::save_to_memory()`, which
Expand Down
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,7 @@ set(ODR_SOURCE_FILES
"src/odr/internal/rtf/rtf_tokenizer.cpp"

"src/odr/internal/svg/svg_file.cpp"
"src/odr/internal/svg/svg_writer.cpp"

"src/odr/internal/svm/svm_file.cpp"
"src/odr/internal/svm/svm_format.cpp"
Expand Down
4 changes: 3 additions & 1 deletion src/odr/internal/abstract/html_service.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@

namespace odr {
class File;
}
class Logger;
} // namespace odr

namespace odr::internal::html {
class HtmlWriter;
Expand All @@ -20,6 +21,7 @@ class HtmlService {
virtual ~HtmlService() = default;

[[nodiscard]] virtual const HtmlConfig &config() const = 0;
[[nodiscard]] virtual const Logger &logger() const = 0;
[[nodiscard]] virtual const HtmlViews &list_views() const = 0;

virtual void warmup() const = 0;
Expand Down
15 changes: 3 additions & 12 deletions src/odr/internal/html/common.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
#include <odr/internal/html/html_writer.hpp>
#include <odr/internal/util/stream_util.hpp>
#include <odr/internal/util/string_util.hpp>
#include <odr/internal/util/xml_util.hpp>

#include <odr/html.hpp>
#include <odr/quantity.hpp>
Expand All @@ -31,7 +32,7 @@ void html::write_viewport_meta(
const std::optional<HtmlViewportMode> mode_override) {
if (config.viewport_content.has_value()) {
out.write_header_viewport(
escape_attribute(config.viewport_content.value()));
util::xml::escape_attribute(config.viewport_content.value()));
return;
}

Expand Down Expand Up @@ -256,9 +257,7 @@ std::string html::escape_text(std::string text) {
return text;
}

util::string::replace_all(text, "&", "&amp;");
util::string::replace_all(text, "<", "&lt;");
util::string::replace_all(text, ">", "&gt;");
text = util::xml::escape_text(text);

if (text.front() == ' ') {
text = "&nbsp;" + text.substr(1);
Expand All @@ -274,14 +273,6 @@ std::string html::escape_text(std::string text) {
return text;
}

std::string html::escape_attribute(std::string value) {
util::string::replace_all(value, "&", "&amp;");
util::string::replace_all(value, "\"", "&quot;");
util::string::replace_all(value, "<", "&lt;");
util::string::replace_all(value, ">", "&gt;");
return value;
}

html::UriKind html::uri_kind(const std::string_view uri) {
std::string scheme;
for (const char ch : uri) {
Expand Down
15 changes: 9 additions & 6 deletions src/odr/internal/html/common.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ namespace odr {
struct Color;
struct HtmlConfig;
class Html;
class Logger;
} // namespace odr

namespace odr::internal::abstract {
Expand All @@ -24,17 +25,20 @@ namespace odr::internal::html {

struct WritingState {
WritingState(HtmlWriter &out, const HtmlConfig &config,
HtmlResources &resources)
: m_out{&out}, m_config{&config}, m_resources(&resources) {}
HtmlResources &resources, const Logger &logger)
: m_out{&out}, m_config{&config}, m_resources(&resources),
m_logger{&logger} {}

[[nodiscard]] HtmlWriter &out() const { return *m_out; }
[[nodiscard]] const HtmlConfig &config() const { return *m_config; }
[[nodiscard]] HtmlResources &resources() const { return *m_resources; }
[[nodiscard]] const Logger &logger() const { return *m_logger; }

private:
HtmlWriter *m_out;
const HtmlConfig *m_config;
HtmlResources *m_resources;
const Logger *m_logger;
};

/// Writes the viewport meta tag. Precedence: `config.viewport_content` (raw,
Expand Down Expand Up @@ -78,12 +82,11 @@ void write_zoom_style(HtmlWriter &out, const HtmlConfig &config, WidthFit fits,
/// length, which leaves those insets as shipped.
void write_content_margin_style(HtmlWriter &out, const HtmlConfig &config);

/// @ref util::xml::escape_text, plus the `&nbsp;` and `&emsp;` that keep html
/// from collapsing the run's own whitespace. An attribute value wants
/// @ref util::xml::escape_attribute instead, which leaves spaces intact.
std::string escape_text(std::string text);

/// Escape a string for use as an HTML double-quoted attribute value (`&`, `"`,
/// `<`, `>`). Unlike `escape_text`, it leaves leading/trailing spaces intact.
std::string escape_attribute(std::string value);

/// What a target is, as an `href` would be dispatched. Whitespace and control
/// bytes are skipped while reading the scheme, as browsers strip them first.
enum class UriKind {
Expand Down
4 changes: 2 additions & 2 deletions src/odr/internal/html/document.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -279,7 +279,7 @@ class HtmlFragmentView final : public abstract::HtmlView {

HtmlResources write_html(HtmlWriter &out) const override {
HtmlResources resources;
WritingState state(out, service().config(), resources);
WritingState state(out, service().config(), resources, service().logger());
m_fragment->write_document(out, state);
return resources;
}
Expand Down Expand Up @@ -415,7 +415,7 @@ class HtmlServiceImpl final : public HtmlService {
HtmlResources write_document(HtmlWriter &out) const {
HtmlResources resources;

WritingState state(out, config(), resources);
WritingState state(out, config(), resources, logger());

// every page in one file, so the column is as wide as the widest of them
const std::optional<double> content =
Expand Down
13 changes: 8 additions & 5 deletions src/odr/internal/html/document_element.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
#include <odr/internal/html/html_service.hpp>
#include <odr/internal/html/html_writer.hpp>
#include <odr/internal/html/image_file.hpp>
#include <odr/internal/util/xml_util.hpp>

namespace odr::internal {

Expand Down Expand Up @@ -413,7 +414,7 @@ void html::translate_link(const Element &element, const WritingState &state) {
// A refused target loses the attribute, not the element.
HtmlAttributesVector attributes;
if (kind != UriKind::refused) {
attributes.emplace_back("href", escape_attribute(href));
attributes.emplace_back("href", util::xml::escape_attribute(href));
}

HtmlElementOptions options =
Expand All @@ -435,8 +436,8 @@ void html::translate_bookmark(const Element &element,

state.out().write_element_begin(
"a",
HtmlElementOptions().set_inline(true).set_attributes(
HtmlAttributesVector{{"id", escape_attribute(bookmark.name())}}));
HtmlElementOptions().set_inline(true).set_attributes(HtmlAttributesVector{
{"id", util::xml::escape_attribute(bookmark.name())}}));
state.out().write_element_end("a");
}

Expand Down Expand Up @@ -565,12 +566,14 @@ void html::translate_image(const Element &element, const WritingState &state) {
.set_attributes([&](const HtmlAttributeWriterCallback &clb) {
clb("alt", "Error: image not found or unsupported");
if (resource_location.has_value()) {
clb("src", escape_attribute(resource_location.value()));
clb("src",
util::xml::escape_attribute(resource_location.value()));
} else {
clb("src", [&](std::ostream &o) {
// reached only for internal images, which have a file
// NOLINTNEXTLINE(bugprone-unchecked-optional-access)
translate_image_src(image.file().value(), o, state.config());
translate_image_src(image.file().value(), o, state.config(),
state.logger());
});
}
})
Expand Down
15 changes: 8 additions & 7 deletions src/odr/internal/html/document_style.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
#include <odr/style.hpp>

#include <odr/internal/html/common.hpp>
#include <odr/internal/util/xml_util.hpp>

namespace odr::internal {

Expand Down Expand Up @@ -150,7 +151,7 @@ std::string html::translate_text_style(const TextStyle &text_style) {
if (const std::optional<std::string_view> font_name = text_style.font_name;
font_name.has_value()) {
result.append("font-family:")
.append(escape_attribute(std::string(*font_name)))
.append(util::xml::escape_attribute(std::string(*font_name)))
.append(";");
}
if (const std::optional<Measure> font_size = text_style.font_size;
Expand Down Expand Up @@ -178,7 +179,7 @@ std::string html::translate_text_style(const TextStyle &text_style) {
if (const std::optional<std::string> font_shadow = text_style.font_shadow;
font_shadow.has_value()) {
result.append("text-shadow:")
.append(escape_attribute(*font_shadow))
.append(util::xml::escape_attribute(*font_shadow))
.append(";");
}
if (const std::optional<Color> font_color = text_style.font_color;
Expand Down Expand Up @@ -206,7 +207,7 @@ std::string html::translate_block_font_style(const TextStyle &text_style) {
if (const std::optional<std::string_view> font_name = text_style.font_name;
font_name.has_value()) {
result.append("font-family:")
.append(escape_attribute(std::string(*font_name)))
.append(util::xml::escape_attribute(std::string(*font_name)))
.append(";");
}
if (const std::optional<Measure> font_size = text_style.font_size;
Expand Down Expand Up @@ -356,27 +357,27 @@ html::translate_table_cell_style(const TableCellStyle &table_cell_style) {
table_cell_style.border.right;
border_right.has_value()) {
result.append("border-right:")
.append(escape_attribute(*border_right))
.append(util::xml::escape_attribute(*border_right))
.append(";");
}
if (const std::optional<std::string> border_top = table_cell_style.border.top;
border_top.has_value()) {
result.append("border-top:")
.append(escape_attribute(*border_top))
.append(util::xml::escape_attribute(*border_top))
.append(";");
}
if (const std::optional<std::string> border_left =
table_cell_style.border.left;
border_left.has_value()) {
result.append("border-left:")
.append(escape_attribute(*border_left))
.append(util::xml::escape_attribute(*border_left))
.append(";");
}
if (const std::optional<std::string> border_bottom =
table_cell_style.border.bottom;
border_bottom.has_value()) {
result.append("border-bottom:")
.append(escape_attribute(*border_bottom))
.append(util::xml::escape_attribute(*border_bottom))
.append(";");
}
if (const std::optional<double> text_rotation =
Expand Down
17 changes: 10 additions & 7 deletions src/odr/internal/html/filesystem.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
#include <odr/internal/html/frontend.hpp>
#include <odr/internal/html/html_service.hpp>
#include <odr/internal/html/html_writer.hpp>
#include <odr/internal/util/xml_util.hpp>

#include <array>
#include <iomanip>
Expand Down Expand Up @@ -177,7 +178,7 @@ class HtmlServiceImpl final : public HtmlService {

HtmlResources write_filesystem(HtmlWriter &out) const {
HtmlResources resources;
const WritingState state(out, config(), resources);
const WritingState state(out, config(), resources, logger());

const FileWalker file_walker = m_filesystem.file_walker("/");

Expand Down Expand Up @@ -226,8 +227,9 @@ class HtmlServiceImpl final : public HtmlService {
if (location.has_value()) {
out.write_element_begin(
"a", HtmlElementOptions().set_inline(true).set_attributes(
HtmlAttributesVector{{"href", escape_attribute(*location)},
{"title", escape_attribute(name)}}));
HtmlAttributesVector{
{"href", util::xml::escape_attribute(*location)},
{"title", util::xml::escape_attribute(name)}}));
out.write_raw(escape_text(file_path.string()));
out.write_element_end("a");
} else {
Expand All @@ -248,14 +250,15 @@ class HtmlServiceImpl final : public HtmlService {
HtmlElementOptions().set_inline(true).set_class("odr-files-action"));
if (const std::optional<std::string> href =
location.has_value()
? std::optional(escape_attribute(*location))
? std::optional(util::xml::escape_attribute(*location))
: entry_data_url(file, mime_type_of(file_path));
href.has_value()) {
out.write_element_begin(
"a", HtmlElementOptions().set_inline(true).set_attributes(
HtmlAttributesVector{{"href", *href},
{"download", escape_attribute(name)},
{"title", escape_attribute(name)}}));
HtmlAttributesVector{
{"href", *href},
{"download", util::xml::escape_attribute(name)},
{"title", util::xml::escape_attribute(name)}}));
out.write_raw("\u2193");
out.write_element_end("a");
}
Expand Down
6 changes: 4 additions & 2 deletions src/odr/internal/html/frontend.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
#include <odr/internal/html/common.hpp>
#include <odr/internal/html/html_service.hpp>
#include <odr/internal/html/html_writer.hpp>
#include <odr/internal/util/xml_util.hpp>

#include <array>
#include <span>
Expand Down Expand Up @@ -1560,7 +1561,8 @@ void write_style(const Asset &asset, const WritingState &state,
if (const HtmlResourceLocation location =
locate(asset, state.config(), state.resources());
location.has_value()) {
state.out().write_header_style(escape_attribute(*location), media);
state.out().write_header_style(util::xml::escape_attribute(*location),
media);
return;
}

Expand All @@ -1579,7 +1581,7 @@ void write_script(const Asset &asset, const WritingState &state) {
if (const HtmlResourceLocation location =
locate(asset, state.config(), state.resources());
location.has_value()) {
state.out().write_script(escape_attribute(*location));
state.out().write_script(util::xml::escape_attribute(*location));
return;
}

Expand Down
4 changes: 4 additions & 0 deletions src/odr/internal/html/html_service.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ HtmlService::HtmlService(HtmlConfig config, const Logger &logger)

const HtmlConfig &HtmlService::config() const { return m_config; }

const Logger &HtmlService::logger() const { return m_logger; }

HtmlView::HtmlView(const abstract::HtmlService &service, std::string name,
std::size_t index, std::string path)
: m_service{&service}, m_name{std::move(name)}, m_index{index},
Expand All @@ -32,6 +34,8 @@ const std::optional<HtmlSheetCut> &HtmlView::sheet_cut() const {

const abstract::HtmlService &HtmlView::service() const { return *m_service; }

const Logger &HtmlView::logger() const { return m_service->logger(); }

HtmlResources HtmlView::write_html(HtmlWriter &out) const {
return m_service->write_html(path(), out);
}
Expand Down
2 changes: 2 additions & 0 deletions src/odr/internal/html/html_service.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ class HtmlService : public abstract::HtmlService {
HtmlService(HtmlConfig config, const Logger &logger);

[[nodiscard]] const HtmlConfig &config() const override;
[[nodiscard]] const Logger &logger() const override;

private:
HtmlConfig m_config;
Expand All @@ -31,6 +32,7 @@ class HtmlView : public abstract::HtmlView {
[[nodiscard]] const HtmlConfig &config() const override;
[[nodiscard]] const std::optional<HtmlSheetCut> &sheet_cut() const override;
[[nodiscard]] const abstract::HtmlService &service() const;
[[nodiscard]] const Logger &logger() const;

HtmlResources write_html(HtmlWriter &out) const override;

Expand Down
Loading
Loading