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
1 change: 1 addition & 0 deletions apple/include/OdrCoreObjC/ODRHtml.h
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ typedef NS_ENUM(NSInteger, ODRHtmlResourceType) {
ODRHtmlResourceTypeImage,
ODRHtmlResourceTypeFont,
ODRHtmlResourceTypeMedia,
ODRHtmlResourceTypeFile,
} NS_SWIFT_NAME(HtmlResourceType);

typedef NS_ENUM(NSInteger, ODRHtmlTableGridlines) {
Expand Down
1 change: 1 addition & 0 deletions apple/src/ODRHtml.mm
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
ODR_SAME_ENUM(ODRHtmlResourceTypeImage, odr::HtmlResourceType::image);
ODR_SAME_ENUM(ODRHtmlResourceTypeFont, odr::HtmlResourceType::font);
ODR_SAME_ENUM(ODRHtmlResourceTypeMedia, odr::HtmlResourceType::media);
ODR_SAME_ENUM(ODRHtmlResourceTypeFile, odr::HtmlResourceType::file);

ODR_SAME_ENUM(ODRHtmlTableGridlinesNone, odr::HtmlTableGridlines::none);
ODR_SAME_ENUM(ODRHtmlTableGridlinesSoft, odr::HtmlTableGridlines::soft);
Expand Down
2 changes: 1 addition & 1 deletion jni/java/app/opendocument/core/HtmlResourceType.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

/** Mirrors {@code odr::HtmlResourceType}; constant order must match the C++ declaration. */
public enum HtmlResourceType {
HTML_FRAGMENT, CSS, JS, IMAGE, FONT, MEDIA;
HTML_FRAGMENT, CSS, JS, IMAGE, FONT, MEDIA, FILE;

static HtmlResourceType fromNative(int code) {
return code < 0 ? null : values()[code];
Expand Down
3 changes: 2 additions & 1 deletion python/src/bind_html.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ void odr_python::bind_html(py::module_ &m) {
.value("js", odr::HtmlResourceType::js)
.value("image", odr::HtmlResourceType::image)
.value("font", odr::HtmlResourceType::font)
.value("media", odr::HtmlResourceType::media);
.value("media", odr::HtmlResourceType::media)
.value("file", odr::HtmlResourceType::file);

py::enum_<odr::HtmlTableGridlines>(m, "HtmlTableGridlines")
.value("none", odr::HtmlTableGridlines::none)
Expand Down
2 changes: 2 additions & 0 deletions src/odr/html.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ enum class HtmlResourceType {
// appended rather than sorted in: the bindings mirror this enum by ordinal
/// Audio or video, never embedded — see @ref HtmlConfig::embed_images.
media,
/// An entry of an archive, offered as itself rather than rendered.
file,
Comment thread
andiwand marked this conversation as resolved.
};

class HtmlResource final {
Expand Down
176 changes: 135 additions & 41 deletions src/odr/internal/html/filesystem.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@
#include <odr/exceptions.hpp>
#include <odr/filesystem.hpp>
#include <odr/html.hpp>
#include <odr/odr.hpp>

#include <odr/internal/abstract/file.hpp>
#include <odr/internal/common/null_stream.hpp>
#include <odr/internal/common/path.hpp>
#include <odr/internal/html/common.hpp>
#include <odr/internal/html/frontend.hpp>
Expand All @@ -13,11 +15,14 @@

#include <array>
#include <iomanip>
#include <mutex>
#include <sstream>

namespace odr::internal::html {
namespace {

constexpr std::string_view listing_path = "files.html";

/// Binary multiples, named as such — nothing to guess the base of.
std::string human_size(const std::size_t size) {
static constexpr std::array<const char *, 5> units{"B", "KiB", "MiB", "GiB",
Expand All @@ -36,29 +41,82 @@ std::string human_size(const std::size_t size) {
return result.str();
}

/// From the extension, not the bytes: sniffing every entry would read the whole
/// archive.
std::string mime_type_of(const Path &path) {
const FileType type = file_type_by_file_extension(path.extension());
if (type == FileType::unknown) {
return "application/octet-stream";
}
return std::string(mimetype_by_file_type(type));
}

/// The whole entry inline, for a listing that has to stand alone.
std::optional<std::string> entry_data_url(const File &file,
const std::string &mime_type) {
const std::unique_ptr<std::istream> stream = file.stream();
if (stream == nullptr) {
return std::nullopt;
}
return file_to_url(*stream, mime_type);
}

/// Where an entry is written, relative to the listing. The archive names it, so
/// a path that escapes the output directory or collides with the listing gets
/// none, and stays inline instead.
std::optional<RelPath> entry_location(const Path &path) {
const RelPath relative = path.make_relative();
if (relative.escaping() || relative.empty() ||
relative.string() == listing_path) {
return std::nullopt;
}
return relative;
}

class HtmlServiceImpl final : public HtmlService {
public:
HtmlServiceImpl(Filesystem filesystem, HtmlConfig config,
const Logger &logger)
: HtmlService(std::move(config), logger),
m_filesystem{std::move(filesystem)},
m_resources{locate_filesystem_resources(this->config())} {
m_views.emplace_back(
std::make_shared<HtmlView>(*this, "files", 0, "files.html"));
m_filesystem{std::move(filesystem)} {
m_views.emplace_back(std::make_shared<HtmlView>(*this, "files", 0,
std::string(listing_path)));
}

void warmup() const override {}
/// Walking the archive is what turns up the entries, so the resources a host
/// may ask for are only known once the listing has been written.
void warmup() const override {
std::lock_guard lock(m_mutex);

if (m_warm) {
return;
}

NullStream null;
HtmlWriter out(null, config());
m_resources = write_filesystem(out);

m_warm = true;
}

[[nodiscard]] const HtmlViews &list_views() const override { return m_views; }

[[nodiscard]] bool exists(const std::string &path) const override {
return path == "files.html" || resource_at(m_resources, path) != nullptr;
if (path == listing_path) {
return true;
}

warmup();

return resource_at(m_resources, path) != nullptr;
}

[[nodiscard]] std::string mimetype(const std::string &path) const override {
if (path == "files.html") {
if (path == listing_path) {
return "text/html";
}
warmup();

if (const odr::HtmlResource *resource = resource_at(m_resources, path);
resource != nullptr) {
return resource->mime_type();
Expand All @@ -68,11 +126,13 @@ class HtmlServiceImpl final : public HtmlService {
}

void write(const std::string &path, std::ostream &out) const override {
if (path == "files.html") {
if (path == listing_path) {
HtmlWriter writer(out, config());
write_filesystem(writer);
return;
}
warmup();

if (const odr::HtmlResource *resource = resource_at(m_resources, path);
resource != nullptr) {
resource->write_resource(out);
Expand All @@ -84,13 +144,37 @@ class HtmlServiceImpl final : public HtmlService {

HtmlResources write_html(const std::string &path,
HtmlWriter &out) const override {
if (path == "files.html") {
if (path == listing_path) {
return write_filesystem(out);
}

throw FileNotFound("Unknown path: " + path);
}

/// `nullopt` when the entry is to be written into the listing instead.
HtmlResourceLocation locate_entry(const WritingState &state, const Path &path,
const File &file) const {
const std::optional<RelPath> entry_path = entry_location(path);
if (!entry_path.has_value()) {
return std::nullopt;
}

const odr::HtmlResource resource = HtmlResource::create(
HtmlResourceType::file, mime_type_of(path), path.basename(),
entry_path->string(), file, false, false, true);
HtmlResourceLocation location =
config().resource_locator(resource, config());
// Two resources at one location write over each other; the stylesheet is
// registered first, so an entry landing on it yields.
if (location.has_value() &&
resource_at(state.resources(), *location) != nullptr) {
return std::nullopt;
}

state.resources().emplace_back(resource, location);
Comment thread
andiwand marked this conversation as resolved.
return location;
}

HtmlResources write_filesystem(HtmlWriter &out) const {
HtmlResources resources;
const WritingState state(out, config(), resources);
Expand All @@ -115,53 +199,61 @@ class HtmlServiceImpl final : public HtmlService {
// No header row: labels would be the only words in the page to translate.
out.write_element_begin("tbody");

// No directory rows: every entry names its whole path, and a directory is
// the one row with nothing to open, save or measure.
for (; !file_walker.end(); file_walker.next()) {
if (!file_walker.is_file()) {
continue;
}

const Path file_path(file_walker.path());
const bool is_file = file_walker.is_file();
const File file = m_filesystem.open(file_path.string());
const std::string name = file_path.basename();
const HtmlResourceLocation location =
locate_entry(state, file_path, file);

out.write_element_begin("tr", HtmlElementOptions().set_class(
is_file ? std::nullopt
: std::optional<HtmlWritable>(
"odr-files-directory")));
out.write_element_begin("tr");

// A trailing separator says "directory" without a column saying so.
// An embedded entry is a `data:` URL, which no browser navigates to at
// the top level, so there the path is text rather than a link.
out.write_element_begin(
"td",
HtmlElementOptions().set_inline(true).set_class("odr-files-name"));
out.write_raw(
escape_text(is_file ? file_path.string() : file_path.string() + "/"));
if (location.has_value()) {
out.write_element_begin(
"a", HtmlElementOptions().set_inline(true).set_attributes(
HtmlAttributesVector{{"href", escape_attribute(*location)},
{"title", escape_attribute(name)}}));
out.write_raw(escape_text(file_path.string()));
out.write_element_end("a");
} else {
out.write_raw(escape_text(file_path.string()));
}
out.write_element_end("td");

const File file =
is_file ? m_filesystem.open(file_path.string()) : File();

out.write_element_begin(
"td",
HtmlElementOptions().set_inline(true).set_class("odr-files-size"));
if (is_file) {
out.write_raw(human_size(file.size()));
}
out.write_raw(human_size(file.size()));
out.write_element_end("td");

// The glyph has no name of its own; the file's is what a tooltip and a
// screen reader read.
out.write_element_begin(
"td",
HtmlElementOptions().set_inline(true).set_class("odr-files-action"));
if (is_file) {
if (const std::unique_ptr<std::istream> stream = file.stream();
stream != nullptr) {
const std::string name = file_path.basename();
// The glyph has no name of its own; the file's is what a tooltip and
// a screen reader read.
out.write_element_begin(
"a", HtmlElementOptions().set_inline(true).set_attributes(
HtmlAttributesVector{
{"href",
file_to_url(*stream, "application/octet-stream")},
{"download", escape_attribute(name)},
{"title", escape_attribute(name)}}));
out.write_raw("\u2193");
out.write_element_end("a");
}
if (const std::optional<std::string> href =
location.has_value()
? std::optional(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)}}));
out.write_raw("\u2193");
out.write_element_end("a");
}
out.write_element_end("td");

Expand All @@ -180,10 +272,12 @@ class HtmlServiceImpl final : public HtmlService {

protected:
Filesystem m_filesystem;
/// The css this view links; empty of locations when the config embeds it.
HtmlResources m_resources;

HtmlViews m_views;

mutable std::mutex m_mutex;
mutable bool m_warm = false;
mutable HtmlResources m_resources;
};

} // namespace
Expand Down
10 changes: 3 additions & 7 deletions src/odr/internal/html/frontend.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -97,9 +97,10 @@ body{background:#fff;color:#1f2328;font:13px/1.5 var(--odr-files-font)}
.odr-files td{padding:5px 12px;border-top:1px solid var(--odr-files-line)}
.odr-files tbody tr:hover>*{background-image:linear-gradient(rgba(0,0,0,.04),rgba(0,0,0,.04))}
.odr-files-name{font-family:var(--odr-files-mono);word-break:break-all}
.odr-files-directory .odr-files-name{color:var(--odr-files-muted)}
.odr-files-name a{color:inherit;text-decoration:none}
.odr-files-name a:hover{color:var(--odr-files-link);text-decoration:underline}
.odr-files-size{width:1%;text-align:right;white-space:nowrap;font-variant-numeric:tabular-nums;color:var(--odr-files-muted)}
.odr-files-action{width:1%;white-space:nowrap}
.odr-files-action{width:1%;white-space:nowrap;text-align:right}
/* U+2193: the download glyphs are missing from enough system fonts to tofu. */
.odr-files-action a{display:inline-flex;align-items:center;justify-content:center;width:24px;height:24px;border-radius:4px;color:var(--odr-files-muted);font-size:15px;line-height:1;text-decoration:none}
.odr-files-action a:hover{background:rgba(0,0,0,.07);color:var(--odr-files-link)}
Expand Down Expand Up @@ -988,11 +989,6 @@ HtmlResources html::locate_text_resources(const HtmlConfig &config) {
return locate_all(assets, config);
}

HtmlResources html::locate_filesystem_resources(const HtmlConfig &config) {
static constexpr std::array assets{filesystem_css_asset};
return locate_all(assets, config);
}

HtmlResources html::locate_media_resources(const HtmlConfig &config) {
static constexpr std::array assets{media_css_asset};
return locate_all(assets, config);
Expand Down
1 change: 0 additions & 1 deletion src/odr/internal/html/frontend.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ void write_text_script(const WritingState &state);
/// 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_text_resources(const HtmlConfig &config);
HtmlResources locate_filesystem_resources(const HtmlConfig &config);
HtmlResources locate_media_resources(const HtmlConfig &config);

} // namespace odr::internal::html
2 changes: 1 addition & 1 deletion test/data.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ odr_test_data(
odr_test_data(
PATH "reference-output/odr-public"
URL "https://github.com/opendocument-app/OpenDocument.test.output.git"
REVISION "e8413d37cfcd943f88bc05b325ee3bb91912212d")
REVISION "9e2f4c30b31d38aa4cf4bfae0b6920fd05f8563b")

odr_test_data(
PATH "reference-output/odr-private"
Expand Down
Loading
Loading