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
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,13 @@ The release run heads these entries with the version and opens a fresh
- An editable ods or xlsx sheet page carries the formula module, where the
build has one: the npm package does, and another build names it with
`ODR_FORMULA_MODULE`. The page is about 540 KB larger.
- A text file, a pdf and a sheet page let the browser lay out only the
lines, pages and rows near the viewport. A text file of 1 M lines shows
its first frame after 0.85 s, not 154 s. The find of the browser does not
reach a far part, but `odr.search` does. `HtmlConfig::lazy_layout` (on by
default, also in the bindings) turns this off.
- The line numbers of a text file are in each line, so a wrapped line keeps
its number beside it.

### Spreadsheets

Expand Down
2 changes: 2 additions & 0 deletions apple/include/OdrCoreObjC/ODRHtml.h
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,8 @@ NS_SWIFT_NAME(HtmlConfig)
/// The zoom the view opens at, 1 being actual size; `nil` follows the fit.
@property(nonatomic, strong, nullable)
NSNumber *initialZoom NS_REFINED_FOR_SWIFT;
/// Lay out only the lines, rows and pages near the viewport.
@property(nonatomic) BOOL lazyLayout;

/// The least distance the generated content keeps from the view's border. A
/// set side raises the inset the view already has, never lowers it.
Expand Down
2 changes: 2 additions & 0 deletions apple/src/ODRHtml.mm
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ - (instancetype)initWithNativeConfig:(const odr::HtmlConfig &)config {
: nil;
_initialZoom =
config.initial_zoom.has_value() ? @(*config.initial_zoom) : nil;
_lazyLayout = config.lazy_layout ? YES : NO;
_formatHtml = config.format_html ? YES : NO;
_htmlIndent = config.html_indent;
_htmlIndentString = to_nsstring(config.html_indent_string);
Expand Down Expand Up @@ -223,6 +224,7 @@ - (instancetype)initWithNativeConfig:(const odr::HtmlConfig &)config {
} else {
config.initial_zoom.reset();
}
config.lazy_layout = _lazyLayout == YES;
if (_minContentMargin != nil) {
config.min_content_margin = _minContentMargin.handle;
} else {
Expand Down
7 changes: 7 additions & 0 deletions apple/tests/OdrCoreTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,13 @@ final class HtmlTests: XCTestCase {
XCTAssertNil(config.spreadsheetLimit)
}

func testLazyLayoutRoundTrips() throws {
let config = HtmlConfig()
XCTAssertTrue(config.lazyLayout)
config.lazyLayout = false
XCTAssertFalse(config.lazyLayout)
}

func testBoxedNumbersRoundTrip() throws {
let config = HtmlConfig()
config.spreadsheetCellLimit = 1234
Expand Down
5 changes: 5 additions & 0 deletions docs/design/lazy-loading.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,11 @@ hidden chunk. Level 2 removes the DOM cost too, but takes more away
a host. A view takes the lowest level that is enough
for the size of its file.

`HtmlConfig::lazy_layout` turns level 1 off. Then no view writes a chunk,
and the find of the browser and a screen reader reach all of the content.
The size of a chunk stays inside the views, because the measurements were
flat around it.

`content-visibility` is in Chrome from 85, Firefox from 125 and Safari from
18. A browser without it ignores the property and lays out every chunk, as
it lays out every line today.
Expand Down
2 changes: 2 additions & 0 deletions jni/java/app/opendocument/core/HtmlConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@ public final class HtmlConfig {
public Integer viewportWidth;
/** The zoom the view opens at, 1 being actual size; {@code null} follows the fit. */
public Double initialZoom;
/** Lay out only the lines, rows and pages near the viewport. */
public boolean lazyLayout = true;

/**
* The least distance the generated content keeps from the view's border. A set side raises the
Expand Down
2 changes: 2 additions & 0 deletions jni/src/jni_style.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -714,6 +714,7 @@ jobject html_config_to_java(JNIEnv *env, const odr::HtmlConfig &config) {
box_integer(env, config.viewport_width));
set_object("initialZoom", "Ljava/lang/Double;",
box_double(env, config.initial_zoom));
set_boolean("lazyLayout", config.lazy_layout);
set_object("minContentMargin", "Lapp/opendocument/core/DirectionalMeasure;",
make_directional_measure(env, config.min_content_margin));
set_boolean("formatHtml", config.format_html);
Expand Down Expand Up @@ -913,6 +914,7 @@ odr::HtmlConfig html_config_from_java(JNIEnv *env, jobject config) {
}
env->DeleteLocalRef(zoom);
}
result.lazy_layout = get_boolean("lazyLayout");
{
jobject margin = get_object("minContentMargin",
"Lapp/opendocument/core/DirectionalMeasure;");
Expand Down
4 changes: 4 additions & 0 deletions jni/tests/app/opendocument/core/HtmlTest.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package app.opendocument.core;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
Expand Down Expand Up @@ -56,6 +57,7 @@ void htmlConfigDefaults() {
assertNull(config.viewportContent);
assertNull(config.viewportWidth);
assertNull(config.initialZoom);
assertTrue(config.lazyLayout);
}

@Test
Expand Down Expand Up @@ -97,6 +99,7 @@ void viewportConfigRoundTrips() throws IOException {
config.viewportContent = "width=420";
config.viewportWidth = 420;
config.initialZoom = 1.5;
config.lazyLayout = false;
config.sheetEditOnClick = false;
config.pdfAnnotationMarkOnSelection = true;
config.hostMessageHandler = "reader.postMessage";
Expand All @@ -109,6 +112,7 @@ void viewportConfigRoundTrips() throws IOException {
assertEquals("width=420", readBack.viewportContent);
assertEquals(Integer.valueOf(420), readBack.viewportWidth);
assertEquals(Double.valueOf(1.5), readBack.initialZoom);
assertFalse(readBack.lazyLayout);
assertEquals(Boolean.FALSE, readBack.sheetEditOnClick);
assertTrue(readBack.pdfAnnotationMarkOnSelection);
assertEquals("reader.postMessage", readBack.hostMessageHandler);
Expand Down
1 change: 1 addition & 0 deletions python/src/bind_html.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ void odr_python::bind_html(py::module_ &m) {
.def_readwrite("viewport_content", &odr::HtmlConfig::viewport_content)
.def_readwrite("viewport_width", &odr::HtmlConfig::viewport_width)
.def_readwrite("initial_zoom", &odr::HtmlConfig::initial_zoom)
.def_readwrite("lazy_layout", &odr::HtmlConfig::lazy_layout)
.def_readwrite("min_content_margin", &odr::HtmlConfig::min_content_margin)
.def_readwrite("format_html", &odr::HtmlConfig::format_html)
.def_readwrite("html_indent", &odr::HtmlConfig::html_indent)
Expand Down
3 changes: 3 additions & 0 deletions python/tests/test_html.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,17 +76,20 @@ def test_html_config_viewport_defaults():
assert config.viewport_content is None
assert config.viewport_width is None
assert config.initial_zoom is None
assert config.lazy_layout is True

config.viewport_mode = pyodr.HtmlViewportMode.fit_width
config.spreadsheet_viewport_mode = pyodr.HtmlViewportMode.actual_size
config.viewport_content = "width=420"
config.viewport_width = 420
config.initial_zoom = 1.5
config.lazy_layout = False
assert config.viewport_mode == pyodr.HtmlViewportMode.fit_width
assert config.spreadsheet_viewport_mode == pyodr.HtmlViewportMode.actual_size
assert config.viewport_content == "width=420"
assert config.viewport_width == 420
assert config.initial_zoom == 1.5
assert config.lazy_layout is False


def test_viewport_mode_reaches_the_html(odt_path):
Expand Down
4 changes: 4 additions & 0 deletions src/odr/html.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,10 @@ struct HtmlConfig {
std::optional<std::uint32_t> viewport_width;
/// The zoom the view opens at, 1 being actual size; unset follows the fit.
std::optional<double> initial_zoom;
/// Lay out only the lines, rows and pages near the viewport. Off, the
/// browser lays out all of them, which a large file may not survive, but its
/// own find and a screen reader reach every one.
bool lazy_layout{true};

/// The least distance the generated content keeps from the view's border. A
/// set side raises the inset the view already has, never lowers it; a unit
Expand Down
28 changes: 18 additions & 10 deletions src/odr/internal/html/document_element.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -758,32 +758,40 @@ void html::translate_sheet(const Sheet &sheet, const WritingState &state) {
state.out().write_element_end("table");
}

// a chunk ends where no merge is open, so no `rowspan` crosses two tables
const std::uint32_t chunk_rows = std::clamp<std::uint32_t>(
chunk_cells / std::max<std::uint32_t>(end_column, 1), 8, 256);
// a chunk ends where no merge is open, so no `rowspan` crosses two tables;
// without lazy layout, all the rows are one table
const bool lazy = state.config().lazy_layout;
const std::uint32_t chunk_rows =
lazy ? std::clamp<std::uint32_t>(
chunk_cells / std::max<std::uint32_t>(end_column, 1), 8, 256)
: std::max<std::uint32_t>(end_row, 1);
std::uint32_t rows_in_chunk = 0;
bool chunk_open = false;
const auto begin_chunk = [&](const std::uint32_t first_row) {
double height = 0;
for (std::uint32_t row_index = first_row;
row_index < std::min(end_row, first_row + chunk_rows); ++row_index) {
lazy && row_index < std::min(end_row, first_row + chunk_rows);
++row_index) {
height += css_pixels(sheet.row_style(row_index).height)
.value_or(estimated_row_pixels);
}
state.out().write_element_begin(
"table",
HtmlElementOptions()
.set_class(first_row == 0 ? "odr-sheet-rows odr-chunk "
"odr-chunk-near"
: "odr-sheet-rows odr-chunk")
.set_class("odr-sheet-rows" +
chunk_classes(state.config(), first_row == 0))
.set_attributes([&](const HtmlAttributeWriterCallback &clb) {
// a shape hangs from A1 and may reach past the first chunk
if (first_row == 0 && has_shapes) {
if (lazy && first_row == 0 && has_shapes) {
clb("data-odr-chunk-held", "");
}
})
.set_style("contain-intrinsic-height:auto " +
util::number::to_string_significant(height, 7) + "px"));
.set_style(
lazy
? std::optional<HtmlWritable>(
"contain-intrinsic-height:auto " +
util::number::to_string_significant(height, 7) + "px")
: std::nullopt));
write_columns();
state.out().write_element_begin("tbody");
chunk_open = true;
Expand Down
10 changes: 10 additions & 0 deletions src/odr/internal/html/frontend.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -315,11 +315,21 @@ void html::write_search_style(const WritingState &state) {
write_style(search_css_asset, state);
}

std::string html::chunk_classes(const HtmlConfig &config, const bool near) {
if (!config.lazy_layout) {
return {};
}
return near ? " odr-chunk odr-chunk-near" : " odr-chunk";
}

void html::write_search_dark_style(const WritingState &state) {
write_dark_style(search_dark_css_asset, state);
}

void html::write_chunk_style(const WritingState &state) {
if (!state.config().lazy_layout) {
return;
}
HtmlWriter &out = state.out();
out.write_header_style_begin();
// WebKit prints a chunk that it skips blank
Expand Down
5 changes: 5 additions & 0 deletions src/odr/internal/html/frontend.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,12 @@ void write_search_style(const WritingState &state);
void write_search_dark_style(const WritingState &state);
/// The browser lays out an `odr-chunk` only where `viewport.js` marks it near
/// the viewport. Inline, because a chunk must start hidden on the first frame.
/// Nothing where `HtmlConfig::lazy_layout` is off.
void write_chunk_style(const WritingState &state);
/// The classes that make an element a chunk, each with a space before it, and
/// @p near where it is near the viewport before the script runs. Empty where
/// `HtmlConfig::lazy_layout` is off.
[[nodiscard]] std::string chunk_classes(const HtmlConfig &config, bool near);

/// For a view writing its style inline: whether to write a dark one, and the
/// `media` gating it. A print stays light, so the query names `screen`.
Expand Down
8 changes: 4 additions & 4 deletions src/odr/internal/html/frontend/text.css
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,10 @@ body{margin:0;background:#fff}
.odr-text-wrap{white-space:break-spaces;word-break:break-word;overflow-wrap:anywhere}
/* A chunk far from the viewport keeps the height it had when it was last
shown. */
.odr-chunk{display:flex;flex-direction:column;contain-intrinsic-height:auto calc(var(--odr-text-lines,256) * 20px)}
.odr-chunk>div{position:relative;padding-left:calc(var(--odr-text-gutter-width) + 17px);counter-increment:odr-line}
.odr-chunk>div:hover{background:var(--odr-text-wash)}
.odr-text-lines{display:flex;flex-direction:column;contain-intrinsic-height:auto calc(var(--odr-text-lines,256) * 20px)}
.odr-text-lines>div{position:relative;padding-left:calc(var(--odr-text-gutter-width) + 17px);counter-increment:odr-line}
.odr-text-lines>div:hover{background:var(--odr-text-wash)}
/* The numbers are ours, not the file's: generated content stays out of a
selection, and the empty alternative text keeps it from a screen reader. */
.odr-chunk>div::before{position:absolute;left:var(--odr-text-gutter-left);width:calc(var(--odr-text-digits,1) * 1ch);text-align:right;color:var(--odr-text-muted);font-variant-numeric:tabular-nums;user-select:none;-webkit-user-select:none;content:counter(odr-line);content:counter(odr-line) / ""}
.odr-text-lines>div::before{position:absolute;left:var(--odr-text-gutter-left);width:calc(var(--odr-text-digits,1) * 1ch);text-align:right;color:var(--odr-text-muted);font-variant-numeric:tabular-nums;user-select:none;-webkit-user-select:none;content:counter(odr-line);content:counter(odr-line) / ""}
[contenteditable]:focus{outline:none}
16 changes: 7 additions & 9 deletions src/odr/internal/html/pdf_file.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1979,10 +1979,9 @@ class HtmlServiceImpl final : public HtmlService {
std::size_t page_number = first_page_number;
for (const DualPageOut &page : pages_out) {
// the first page is near the viewport before the script says so
std::string classes = page.classes;
if (page_number == first_page_number) {
classes += " odr-chunk-near";
}
std::string classes =
page.classes +
chunk_classes(config(), page_number == first_page_number);
out.write_element_begin(
"div", HtmlElementOptions()
.set_class(std::move(classes))
Expand Down Expand Up @@ -2487,10 +2486,9 @@ class HtmlServiceImpl final : public HtmlService {
std::size_t page_number = first_page_number;
for (const SinglePageOut &page : pages_out) {
// the first page is near the viewport before the script says so
std::string classes = page.classes;
if (page_number == first_page_number) {
classes += " odr-chunk-near";
}
std::string classes =
page.classes +
chunk_classes(config(), page_number == first_page_number);
out.write_element_begin(
"div", HtmlElementOptions()
.set_class(std::move(classes))
Expand Down Expand Up @@ -2675,7 +2673,7 @@ class HtmlServiceImpl final : public HtmlService {
const double width = quarter_turn ? box_height : box_width;
const double height = quarter_turn ? box_width : box_height;

std::string classes = "p odr-chunk";
std::string classes = "p";
{
std::ostringstream w;
w << "width:" << width * pt_to_in << "in";
Expand Down
11 changes: 7 additions & 4 deletions src/odr/internal/html/text_file.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -143,18 +143,21 @@ class HtmlServiceImpl final : public HtmlService {
out.write_element_begin(
"div", HtmlElementOptions().set_class("odr-text-body odr-text-wrap"));
in = std::istringstream(text);
// the browser lays out only the chunks near the viewport; without that,
// all the lines are one block
const std::uint32_t lines_per_block =
config().lazy_layout ? chunk_lines : line_count;
for (std::uint32_t line = 0; line < line_count; ++line) {
// the browser lays out only the chunks near the viewport
const bool first = line % chunk_lines == 0;
const bool first = line % lines_per_block == 0;
if (first) {
if (line > 0) {
out.write_element_end("div");
}
// the first chunk is near the viewport before the script says so
HtmlElementOptions chunk;
chunk.set_class(line == 0 ? "odr-chunk odr-chunk-near" : "odr-chunk");
chunk.set_class("odr-text-lines" + chunk_classes(config(), line == 0));
if (const std::uint32_t lines = line_count - line;
lines < chunk_lines) {
config().lazy_layout && lines < chunk_lines) {
chunk.set_style("--odr-text-lines:" + std::to_string(lines));
}
out.write_element_begin("div", chunk);
Expand Down
6 changes: 3 additions & 3 deletions test/browser/plaintext/tests.html
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,11 @@
two lines here put the chunk boundary inside the checks. -->
<div class="odr-text" style="--odr-text-digits:1">
<div class="odr-text-body odr-text-wrap">
<div class="odr-chunk odr-chunk-near">
<div class="odr-text-lines odr-chunk odr-chunk-near">
<div style="counter-reset:odr-line 0">first line</div>
<div>second line</div>
</div>
<div class="odr-chunk" style="--odr-text-lines:1">
<div class="odr-text-lines odr-chunk" style="--odr-text-lines:1">
<div style="counter-reset:odr-line 2">third line</div>
</div>
</div>
Expand All @@ -43,7 +43,7 @@
var body = document.querySelector(".odr-text-body");

function lineNodes() {
return body.querySelectorAll(".odr-chunk > div");
return body.querySelectorAll(".odr-text-lines > div");
}
function lines() {
return Array.prototype.map
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 "fc8d7fb7e148811fd8d3db46f2087148db950d7b")
REVISION "1ff01470c6b43a1962d6f934a9d6a46600e3b72e")

odr_test_data(
PATH "reference-output/odr-private"
URL "https://github.com/opendocument-app/OpenDocument.test-private.output.git"
REVISION "434d4109e59de2843d7fd5a297469980cf1496f9")
REVISION "07c9ef2bb3b2397249ef0ff0dec15baa9fc5d887")
Loading
Loading