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: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ The release run heads these entries with the version and opens a fresh

## Unreleased

- Document images cannot replace viewer scripts or stylesheets at shared output
paths; colliding images are embedded instead.

- Document HTML escapes locale metadata and keeps individual views for sheets,
slides and pages named `document`.

Expand Down
3 changes: 2 additions & 1 deletion src/odr/internal/html/document.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -292,7 +292,8 @@ HtmlResources
render(const Document &document, const HtmlConfig &config, const Logger &logger,
HtmlWriter &out, const std::string &name,
const std::optional<double> content_pixels, Write &&write) {
HtmlResources resources;
HtmlResources resources = locate_document_resources(
config, document.document_type() == DocumentType::spreadsheet);

const auto body = [&](const WritingState &state) {
write_body_begin(document, state);
Expand Down
12 changes: 12 additions & 0 deletions src/odr/internal/html/document_element.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1163,6 +1163,18 @@ void html::translate_image(const Element &element, const WritingState &state) {
path, image.file(), false, false, true);
resource_location =
state.config().resource_locator(resource, state.config());
if (resource_location) {
// a location taken by anything but this same image embeds instead
const odr::HtmlResource *existing =
resource_at(state.resources(), *resource_location);
const bool same_image = existing != nullptr && !existing->is_shipped() &&
existing->path() == resource.path();
if (uri_kind(*resource_location) == UriKind::refused ||
(!same_image &&
resource_location_taken(state.resources(), *resource_location))) {
resource_location.reset();
}
}
} else if (loads_external_source(image.href(), state.config())) {
resource =
HtmlResource::create(HtmlResourceType::image, "image/jpg", "image",
Expand Down
5 changes: 1 addition & 4 deletions src/odr/internal/html/filesystem.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -168,10 +168,7 @@ class HtmlServiceImpl final : public HtmlService {
if (location.has_value() &&
(uri_kind(*location) == UriKind::refused ||
util::string::equals_ignore_case(*location, listing_path) ||
std::ranges::any_of(state.resources(), [&](const auto &entry) {
return entry.second &&
util::string::equals_ignore_case(*entry.second, *location);
}))) {
resource_location_taken(state.resources(), *location))) {
return std::nullopt;
}

Expand Down
25 changes: 25 additions & 0 deletions src/odr/internal/html/frontend.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,31 @@ void html::write_host_bridge_script(const WritingState &state) {
write_script(host_bridge_js_asset, state);
}

HtmlResources html::locate_document_resources(const HtmlConfig &config,
const bool spreadsheet) {
static constexpr std::array assets{document_css_asset, search_css_asset,
search_js_asset, editing_js_asset,
viewport_js_asset};
static constexpr std::array dark{document_dark_css_asset,
search_dark_css_asset};
HtmlResources resources = locate_all(assets, dark, config);
if (config.editable) {
locate(document_js_asset, config, resources);
}
if (spreadsheet) {
locate(spreadsheet_css_asset, config, resources);
if (writes_dark_style(config)) {
locate(spreadsheet_dark_css_asset, config, resources);
}
locate(spreadsheet_js_asset, config, resources);
if (config.editable) {
locate(sheet_editing_js_asset, config, resources);
}
}
locate_host_bridge(config, resources);
return resources;
}

HtmlResources html::locate_filesystem_resources(const HtmlConfig &config) {
static constexpr std::array assets{filesystem_css_asset, search_css_asset,
search_js_asset, viewport_js_asset};
Expand Down
2 changes: 2 additions & 0 deletions src/odr/internal/html/frontend.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ void write_host_bridge_script(const WritingState &state);
/// What the corresponding `write_*` calls would link, without writing anything:
/// a service has to answer for these paths as well as for its views. Every
/// entry is located `nullopt` when the config embeds them.
HtmlResources locate_document_resources(const HtmlConfig &config,
bool spreadsheet);
HtmlResources locate_filesystem_resources(const HtmlConfig &config);
HtmlResources locate_text_resources(const HtmlConfig &config);
HtmlResources locate_xml_resources(const HtmlConfig &config);
Expand Down
11 changes: 11 additions & 0 deletions src/odr/internal/html/html_service.cpp
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
#include <odr/internal/html/html_service.hpp>

#include <odr/internal/util/stream_util.hpp>
#include <odr/internal/util/string_util.hpp>

#include <odr/exceptions.hpp>

#include <algorithm>

namespace odr::internal::html {

HtmlService::HtmlService(HtmlConfig config, const Logger &logger)
Expand Down Expand Up @@ -89,6 +92,14 @@ void HtmlResource::write_resource(std::ostream &os) const {

namespace odr::internal {

bool html::resource_location_taken(const HtmlResources &resources,
const std::string_view location) {
return std::ranges::any_of(resources, [&](const auto &entry) {
return entry.second &&
util::string::equals_ignore_case(*entry.second, location);
});
}

const odr::HtmlResource *html::resource_at(const HtmlResources &resources,
const std::string &path) {
for (const auto &[resource, location] : resources) {
Expand Down
6 changes: 6 additions & 0 deletions src/odr/internal/html/html_service.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@

#include <odr/internal/abstract/html_service.hpp>

#include <string_view>

namespace odr::internal::html {

class HtmlService : public abstract::HtmlService {
Expand Down Expand Up @@ -48,6 +50,10 @@ class HtmlView : public abstract::HtmlView {
const odr::HtmlResource *resource_at(const HtmlResources &resources,
const std::string &path);

/// Whether a location is already used, including case-insensitive filesystems.
bool resource_location_taken(const HtmlResources &resources,
std::string_view location);

class HtmlResource : public abstract::HtmlResource {
public:
static odr::HtmlResource create(HtmlResourceType type, std::string mime_type,
Expand Down
42 changes: 42 additions & 0 deletions test/src/html_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,48 @@ TEST(html, linked_images_are_served) {
check("odr-public/xlsx/sample.xlsx");
}

TEST(html, document_images_yield_to_shipped_resources) {
for (const auto &[path, target] : std::array{
std::pair{"odr-public/odt/image-text-wrap.odt", "search.js"},
std::pair{"odr-public/odt/image-text-wrap.odt", "Search.JS"},
std::pair{"odr-public/docx/file-sample_100kB.docx",
"host-bridge.js"},
std::pair{"odr-public/xlsx/sample.xlsx", "spreadsheet.css"}}) {
SCOPED_TRACE(path);
SCOPED_TRACE(target);
HtmlConfig config;
config.embed_images = false;
config.embed_shipped_resources = false;
config.host_message_handler = "host";
config.resource_locator = [target](const HtmlResource &resource,
const HtmlConfig &options) {
return resource.type() == HtmlResourceType::image
? HtmlResourceLocation(target)
: html::standard_resource_locator()(resource, options);
};
const HtmlService service =
html::translate(open(TestData::test_file_path(path)), config);
std::ostringstream page;
const HtmlResources resources = service.list_views().at(0).write_html(page);
std::size_t images = 0;
for (const auto &[resource, location] : resources) {
if (resource.type() == HtmlResourceType::image) {
++images;
EXPECT_FALSE(location.has_value());
} else if (resource.is_shipped() && location) {
std::ostringstream expected;
resource.write_resource(expected);
std::ostringstream served;
service.write(*location, served);
EXPECT_EQ(served.str(), expected.str()) << *location;
EXPECT_EQ(service.mimetype(*location), resource.mime_type());
}
}
EXPECT_GT(images, 0u);
EXPECT_NE(page.str().find(R"(src="data:image/)"), std::string::npos);
}
}

TEST(html, archive_entry_yields_to_each_shipped_resource) {
const DecodedFile file =
open(TestData::test_file_path("odr-public/odt/about.odt"),
Expand Down
Loading