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

## Unreleased

- `HtmlConfig::editing_scope` narrows the document view's editor to edits
inside one paragraph (`paragraph`); the page refuses the rest with the new
`ErrorCode::edit_out_of_scope` (1010, `outOfScope`). Bound in every binding.

- Writing a cell of an `.ods` takes the cached result of every formula reading
it away, keeping the formula itself, so the saved file states no number that
is now wrong. Such a cell renders empty until a reader computes it.
Expand Down
12 changes: 12 additions & 0 deletions apple/include/OdrCoreObjC/ODRHtml.h
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,15 @@ typedef NS_ENUM(NSInteger, ODRHtmlViewportMode) {
ODRHtmlViewportModeFitWidthByView,
} NS_SWIFT_NAME(HtmlViewportMode);

/// How far an edit in a document view may reach. Host policy, not an engine
/// fact.
typedef NS_ENUM(NSInteger, ODRHtmlEditingScope) {
/// Inside one paragraph: no split and no merge.
ODRHtmlEditingScopeParagraph = 0,
/// Across paragraphs: every operation.
ODRHtmlEditingScopeDocument,
} NS_SWIFT_NAME(HtmlEditingScope);

/// How text is emitted in PDF鈫扝TML output.
typedef NS_ENUM(NSInteger, ODRPdfTextMode) {
/// A visual layer plus a transparent selection layer, like pdf.js.
Expand Down Expand Up @@ -83,6 +92,9 @@ NS_SWIFT_NAME(HtmlConfig)
@property(nonatomic) BOOL relativeResourcePaths;

@property(nonatomic) BOOL editable;
/// How much of the document editor the page offers; it refuses the rest with
/// `ODRErrorCode` 1010, `outOfScope`.
@property(nonatomic) ODRHtmlEditingScope editingScope;
/// Whether the view's scripts take the keys that move the selection.
@property(nonatomic) BOOL keyboardNavigation;
/// Whether the view's scripts take the editing chords: undo and redo.
Expand Down
5 changes: 5 additions & 0 deletions apple/src/ODRHtml.mm
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@
ODR_SAME_ENUM(ODRHtmlViewportModeFitWidthByView,
odr::HtmlViewportMode::fit_width_by_view);

ODR_SAME_ENUM(ODRHtmlEditingScopeParagraph, odr::HtmlEditingScope::paragraph);
ODR_SAME_ENUM(ODRHtmlEditingScopeDocument, odr::HtmlEditingScope::document);

ODR_SAME_ENUM(ODRPdfTextModeDualLayer, odr::PdfTextMode::dual_layer);
ODR_SAME_ENUM(ODRPdfTextModeSingleLayer, odr::PdfTextMode::single_layer);

Expand Down Expand Up @@ -89,6 +92,7 @@ - (instancetype)initWithNativeConfig:(const odr::HtmlConfig &)config {
_resourcePath = to_nsstring(config.resource_path);
_relativeResourcePaths = config.relative_resource_paths ? YES : NO;
_editable = config.editable ? YES : NO;
_editingScope = static_cast<ODRHtmlEditingScope>(config.editing_scope);
_keyboardNavigation = config.keyboard_navigation ? YES : NO;
_keyboardShortcuts = config.keyboard_shortcuts ? YES : NO;
_textDocumentMargin = config.text_document_margin ? YES : NO;
Expand Down Expand Up @@ -158,6 +162,7 @@ - (instancetype)initWithNativeConfig:(const odr::HtmlConfig &)config {
}
config.relative_resource_paths = _relativeResourcePaths == YES;
config.editable = _editable == YES;
config.editing_scope = static_cast<odr::HtmlEditingScope>(_editingScope);
config.keyboard_navigation = _keyboardNavigation == YES;
config.keyboard_shortcuts = _keyboardShortcuts == YES;
config.text_document_margin = _textDocumentMargin == YES;
Expand Down
17 changes: 17 additions & 0 deletions apple/tests/OdrCoreTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,23 @@ final class HtmlTests: XCTestCase {
XCTAssertTrue(html.contains("<style"), "the html has no stylesheet")
}

/// Proves the scope crosses the binding; the C++ suite covers the rest.
func testEditingScopeReachesTheHtml() throws {
XCTAssertEqual(HtmlConfig().editingScope, .document)

let config = HtmlConfig()
config.editable = true
config.editingScope = .paragraph

let file = try DecodedFile.decode(path: try Fixture.odt())
let service = try HtmlTranslator.translate(file: file, config: config)
var resources: NSArray?
let html = try XCTUnwrap(service.views.first).writeHtml(resources: &resources)

XCTAssertTrue(
html.contains("data-odr-editing-scope=\"paragraph\""), "the scope did not reach the html")
}

/// The C++ suite covers where the floor lands; this only proves the margin
/// crosses the binding, `nil` sides and all.
func testMinContentMarginReachesTheHtml() throws {
Expand Down
29 changes: 29 additions & 0 deletions docs/design/editing.md
Original file line number Diff line number Diff line change
Expand Up @@ -397,6 +397,35 @@ code 9 rather than being dropped. Android WebView's incomplete `beforeinput`
range the browser did not state is extended by one character rather than
refused; verify both on a device.

### 14. The scope is host policy, and the page refuses past it

`Document::is_editable` is an engine fact; what a host offers of it is the
host's call. The core carries no policy, only one seam,
`HtmlConfig::editing_scope`, and one signal back,
`ErrorCode::edit_out_of_scope` (1010, `outOfScope`).

| Scope | What the document editor takes |
|---|---|
| `document` (default) | everything in decision 13 |
| `paragraph` | an edit that starts and ends in one paragraph |

Scope `paragraph` refuses Enter, Backspace at a paragraph start, a paste
holding a line break, and a selection over two paragraphs. A paragraph is a
unit the reader sees, where a run is not: Word splits runs by revision session,
so a wall at a run would stand in the middle of uniform text.

**Why on the config:** the document did not change, the host's offer did. The
scope joins the other host policy on `<body>` (decision 12) as
`data-odr-editing-scope`.

**Why the editor reads it per edit:** a host widens it by setting the
attribute, with no second render.

**Why its own code:** a host maps 1010 to what the wider scope offers.

**Why replay does not check it:** the page cannot produce an operation past its
scope, and a check in `Document::edit` would refuse the save.

## What landed, and what did not

The plan this document carried ran in five steps, and the first four are in.
Expand Down
1 change: 1 addition & 0 deletions jni/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ add_jar(odr_java
"java/app/opendocument/core/Html.java"
"java/app/opendocument/core/HtmlColorScheme.java"
"java/app/opendocument/core/HtmlConfig.java"
"java/app/opendocument/core/HtmlEditingScope.java"
"java/app/opendocument/core/HtmlPage.java"
"java/app/opendocument/core/HtmlResource.java"
"java/app/opendocument/core/HtmlResourceType.java"
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 @@ -20,6 +20,8 @@ public final class HtmlConfig {
public boolean relativeResourcePaths = true;

public boolean editable = false;
/** How far an edit in a document view may reach; the page refuses the rest. */
public HtmlEditingScope editingScope = HtmlEditingScope.DOCUMENT;

/** Whether the view's scripts take the keys that move the selection. */
public boolean keyboardNavigation = true;
Expand Down
14 changes: 14 additions & 0 deletions jni/java/app/opendocument/core/HtmlEditingScope.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package app.opendocument.core;

/** Mirrors {@code odr::HtmlEditingScope}; constant order must match the C++ declaration. */
public enum HtmlEditingScope {
PARAGRAPH, DOCUMENT;

static HtmlEditingScope fromNative(int code) {
return code < 0 ? null : values()[code];
}

int toNative() {
return ordinal();
}
}
11 changes: 11 additions & 0 deletions jni/src/jni_style.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -453,6 +453,9 @@ jobject html_config_to_java(JNIEnv *env, const odr::HtmlConfig &config) {
set_string("resourcePath", config.resource_path);
set_boolean("relativeResourcePaths", config.relative_resource_paths);
set_boolean("editable", config.editable);
set_object("editingScope", "Lapp/opendocument/core/HtmlEditingScope;",
enum_from_code(env, "app/opendocument/core/HtmlEditingScope",
static_cast<jint>(config.editing_scope)));
set_boolean("keyboardNavigation", config.keyboard_navigation);
set_boolean("keyboardShortcuts", config.keyboard_shortcuts);
set_boolean("textDocumentMargin", config.text_document_margin);
Expand Down Expand Up @@ -575,6 +578,14 @@ odr::HtmlConfig html_config_from_java(JNIEnv *env, jobject config) {
}
result.relative_resource_paths = get_boolean("relativeResourcePaths");
result.editable = get_boolean("editable");
{
const jint code = enum_ordinal(
env,
get_object("editingScope", "Lapp/opendocument/core/HtmlEditingScope;"));
if (code >= 0) {
result.editing_scope = static_cast<odr::HtmlEditingScope>(code);
}
}
result.keyboard_navigation = get_boolean("keyboardNavigation");
result.keyboard_shortcuts = get_boolean("keyboardShortcuts");
result.text_document_margin = get_boolean("textDocumentMargin");
Expand Down
21 changes: 21 additions & 0 deletions jni/tests/app/opendocument/core/HtmlTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ void htmlConfigDefaults() {
HtmlConfig config = new HtmlConfig();
assertTrue(config.embedImages);
assertTrue(!config.editable);
assertEquals(HtmlEditingScope.DOCUMENT, config.editingScope);
assertEquals(HtmlTableGridlines.SOFT, config.spreadsheetGridlines);
assertEquals(HtmlViewportMode.AUTOMATIC, config.viewportMode);
assertNull(config.spreadsheetViewportMode);
Expand Down Expand Up @@ -60,6 +61,26 @@ void viewportConfigRoundTrips() throws IOException {
assertEquals(Double.valueOf(1.5), readBack.initialZoom);
}

/** Proves the scope crosses JNI; the C++ suite covers the rest. */
@Test
void editingScopeReachesTheHtml() throws IOException {
// the attribute, not the name: the script names it too
assertTrue(!renderOdt(new HtmlConfig()).contains("data-odr-editing-scope=\""));

HtmlConfig editable = new HtmlConfig();
editable.editable = true;
assertTrue(renderOdt(editable).contains("data-odr-editing-scope=\"document\""));

HtmlConfig narrowed = new HtmlConfig();
narrowed.editable = true;
narrowed.editingScope = HtmlEditingScope.PARAGRAPH;
assertTrue(renderOdt(narrowed).contains("data-odr-editing-scope=\"paragraph\""));

DecodedFile file = Odr.open(TestFiles.odtFile(tempDir).toString());
HtmlConfig readBack = Html.translate(file, narrowed).config();
assertEquals(HtmlEditingScope.PARAGRAPH, readBack.editingScope);
}

/** The C++ suite covers where the floor lands; this only proves it crosses JNI. */
@Test
void minContentMarginReachesTheHtml() throws IOException {
Expand Down
5 changes: 5 additions & 0 deletions python/src/bind_html.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ void odr_python::bind_html(py::module_ &m) {
.value("none", odr::HtmlViewportMode::none)
.value("fit_width_by_view", odr::HtmlViewportMode::fit_width_by_view);

py::enum_<odr::HtmlEditingScope>(m, "HtmlEditingScope")
.value("paragraph", odr::HtmlEditingScope::paragraph)
.value("document", odr::HtmlEditingScope::document);

py::enum_<odr::PdfTextMode>(m, "PdfTextMode")
.value("dual_layer", odr::PdfTextMode::dual_layer)
.value("single_layer", odr::PdfTextMode::single_layer);
Expand Down Expand Up @@ -79,6 +83,7 @@ void odr_python::bind_html(py::module_ &m) {
.def_readwrite("relative_resource_paths",
&odr::HtmlConfig::relative_resource_paths)
.def_readwrite("editable", &odr::HtmlConfig::editable)
.def_readwrite("editing_scope", &odr::HtmlConfig::editing_scope)
.def_readwrite("keyboard_navigation",
&odr::HtmlConfig::keyboard_navigation)
.def_readwrite("keyboard_shortcuts", &odr::HtmlConfig::keyboard_shortcuts)
Expand Down
18 changes: 18 additions & 0 deletions python/tests/test_html.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ def test_html_config_defaults():
config = pyodr.HtmlConfig()
assert config.embed_images
assert not config.editable
assert config.editing_scope == pyodr.HtmlEditingScope.document
assert config.keyboard_navigation
assert config.keyboard_shortcuts
assert config.spreadsheet_gridlines == pyodr.HtmlTableGridlines.soft
Expand Down Expand Up @@ -111,6 +112,23 @@ def render(config):
assert '<meta name="viewport" content="width=420"/>' in render(raw)


def test_editing_scope_reaches_the_html(odt_path):
# proves the scope crosses the binding; the C++ suite covers the rest
def render(config):
file = pyodr.open(str(odt_path))
service = pyodr.html.translate(file, config)
content, _ = service.list_views()[0].write_html()
return content

# the attribute, not the name: the script names it too
assert 'data-odr-editing-scope="' not in render(pyodr.HtmlConfig())

config = pyodr.HtmlConfig()
config.editable = True
config.editing_scope = pyodr.HtmlEditingScope.paragraph
assert 'data-odr-editing-scope="paragraph"' in render(config)


def test_min_content_margin_reaches_the_html(odt_path):
# The C++ suite covers where the floor lands; this only proves it crosses
# the binding, unset sides and all.
Expand Down
3 changes: 2 additions & 1 deletion src/odr/error_code.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ struct Row final {

using odr::ErrorCode;

constexpr std::array<Row, 61> rows{{
constexpr std::array<Row, 62> rows{{
{ErrorCode::unknown, "Unknown"},
{ErrorCode::unsupported_operation, "UnsupportedOperation"},
{ErrorCode::file_not_found, "FileNotFound"},
Expand Down Expand Up @@ -77,6 +77,7 @@ constexpr std::array<Row, 61> rows{{
{ErrorCode::edit_unsupported, "unsupportedEdit"},
{ErrorCode::edit_range, "range"},
{ErrorCode::edit_unnameable, "unnameableEdit"},
{ErrorCode::edit_out_of_scope, "outOfScope"},
}};

} // namespace
Expand Down
2 changes: 2 additions & 0 deletions src/odr/error_code.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,8 @@ enum class ErrorCode : std::int32_t {
edit_unsupported = 1007,
edit_range = 1008, ///< The range reaches over a picture or a table.
edit_unnameable = 1009, ///< The edit landed where no operation names it.
/// The edit reaches past @ref HtmlConfig::editing_scope.
edit_out_of_scope = 1010,
};

/// @brief The code's name, as the bindings already spell it.
Expand Down
10 changes: 10 additions & 0 deletions src/odr/html.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,13 @@ enum class HtmlViewportMode {
fit_width_by_view,
};

/// How far an edit in a document view may reach. Host policy, not an engine
/// fact.
enum class HtmlEditingScope {
paragraph, ///< inside one paragraph: no split and no merge
document, ///< across paragraphs: every operation
};

/// How text is emitted in PDF鈫扝TML output. Neither mode needs JavaScript.
enum class PdfTextMode {
dual_layer, ///< a visual layer (paint order, embedded PUA glyphs) plus a
Expand Down Expand Up @@ -135,6 +142,9 @@ struct HtmlConfig {
/// edit operation names, and the editor script. The mode itself starts off -
/// the host turns it on with `odr.editing.enable()`.
bool editable{false};
/// How much of the document editor the page offers; it refuses the rest
/// with @ref ErrorCode::edit_out_of_scope. Only the document view reads it.
HtmlEditingScope editing_scope{HtmlEditingScope::document};

/// Whether the view's scripts take the keys that move the selection: the
/// arrows, Tab, Escape, and the keys that open an editor over it.
Expand Down
10 changes: 10 additions & 0 deletions src/odr/internal/html/common.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -359,4 +359,14 @@ std::string html::keyboard_classes(const HtmlConfig &config) {
return classes;
}

std::string_view html::editing_scope_name(const HtmlEditingScope scope) {
switch (scope) {
case HtmlEditingScope::paragraph:
return "paragraph";
case HtmlEditingScope::document:
default:
return "document";
}
}

} // namespace odr::internal
3 changes: 3 additions & 0 deletions src/odr/internal/html/common.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -160,4 +160,7 @@ std::string file_to_url(const abstract::File &file,
/// The key classes a view's scripts may take; `editing.md` decision 12.
[[nodiscard]] std::string keyboard_classes(const HtmlConfig &config);

/// The scope as the page states it; `editing.md` decision 14.
[[nodiscard]] std::string_view editing_scope_name(HtmlEditingScope scope);

} // namespace odr::internal::html
2 changes: 2 additions & 0 deletions src/odr/internal/html/document.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,8 @@ void write_body_begin(const Document &document, const WritingState &state) {
if (state.config().editable) {
clb("data-odr-editable",
state.document_editable() ? "true" : "readOnly");
clb("data-odr-editing-scope", std::string(editing_scope_name(
state.config().editing_scope)));
}
// not an editing fact: a read-only sheet has a pin to clear
clb("data-odr-keyboard", keyboard_classes(state.config()));
Expand Down
Loading
Loading