Skip to content
Open
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

- A paragraph of an odt, odp, odg, docx or pptx file takes an alignment
(`Paragraph::set_style`, `setParagraphStyle`, `odr.editing.format({align})`,
and the bindings). The docx reader now reads `w:jc="both"` as justified.

- The python, Java, Objective-C and npm bindings style a sheet cell
(`Sheet::set_cell_style`). In Java and Objective-C, a `TableCellStyle` can
now be built and written.
Expand Down
4 changes: 4 additions & 0 deletions apple/include/OdrCoreObjC/ODRDocumentElement.h
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,10 @@ NS_SWIFT_NAME(Paragraph)
@interface ODRParagraph : ODRElement
@property(nonatomic, readonly) ODRParagraphStyle *style;
@property(nonatomic, readonly) ODRTextStyle *textStyle;
/// States the non-nil properties of `style` on the paragraph and leaves the
/// rest. Only `textAlign` is written; any other refuses with
/// `ODRErrorUnsupportedOperation`.
- (BOOL)setStyle:(ODRParagraphStyle *)style error:(NSError **)error;
@end

/// `odr::Span`.
Expand Down
16 changes: 8 additions & 8 deletions apple/include/OdrCoreObjC/ODRStyle.h
Original file line number Diff line number Diff line change
Expand Up @@ -173,19 +173,19 @@ NS_SWIFT_NAME(TextStyle)
NS_SWIFT_NAME(ParagraphStyle)
@interface ODRParagraphStyle : NSObject
/// `ODRTextAlign`, boxed.
@property(nonatomic, readonly, nullable) NSNumber *textAlign;
@property(nonatomic, nullable) NSNumber *textAlign;
/// `ODRTextDirection`, boxed; `nil` where the style says nothing.
@property(nonatomic, readonly, nullable) NSNumber *direction;
@property(nonatomic, nullable) NSNumber *direction;
@property(nonatomic, readonly) ODRDirectionalMeasure *margin;
@property(nonatomic, readonly, nullable) ODRMeasure *lineHeight;
@property(nonatomic, readonly, nullable) ODRMeasure *textIndent;
@property(nonatomic, nullable) ODRMeasure *lineHeight;
@property(nonatomic, nullable) ODRMeasure *textIndent;
/// `ODRBreakType`, boxed; `nil` where the style says nothing.
@property(nonatomic, readonly, nullable) NSNumber *breakBefore;
@property(nonatomic, nullable) NSNumber *breakBefore;
/// `ODRBreakType`, boxed; `nil` where the style says nothing.
@property(nonatomic, readonly, nullable) NSNumber *breakAfter;
@property(nonatomic, nullable) NSNumber *breakAfter;

- (instancetype)init NS_UNAVAILABLE;
+ (instancetype)new NS_UNAVAILABLE;
/// Every property `nil`, and no side of `margin` stated.
- (instancetype)init;
@end

/// Table style — `odr::TableStyle`.
Expand Down
7 changes: 7 additions & 0 deletions apple/src/ODRDocumentElement.mm
Original file line number Diff line number Diff line change
Expand Up @@ -510,6 +510,13 @@ - (ODRTextStyle *)textStyle {
nil);
}

- (BOOL)setStyle:(ODRParagraphStyle *)style error:(NSError **)error {
return guarded(error, [&] {
self.handle.as_paragraph().set_style([style handle]);
return YES;
});
}

@end

@implementation ODRSpan
Expand Down
3 changes: 3 additions & 0 deletions apple/src/ODRPrivate.h
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,9 @@ NS_ASSUME_NONNULL_BEGIN

@interface ODRParagraphStyle (Private)
+ (instancetype)styleWithHandle:(const odr::ParagraphStyle &)handle;
/// The set properties as a `ParagraphStyle`; throws `UnsupportedOperation`
/// for any property but `textAlign`, since no engine writes one.
- (odr::ParagraphStyle)handle;
@end

@interface ODRTableStyle (Private)
Expand Down
21 changes: 21 additions & 0 deletions apple/src/ODRStyle.mm
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,27 @@ + (instancetype)styleWithHandle:(const odr::TextStyle &)handle {

@implementation ODRParagraphStyle

- (instancetype)init {
if ((self = [super init]) != nil) {
_margin = [ODRDirectionalMeasure
directionalWithHandle:odr::DirectionalStyle<odr::Measure>()];
}
return self;
}

- (odr::ParagraphStyle)handle {
if (_direction != nil || _margin.right != nil || _margin.top != nil ||
_margin.left != nil || _margin.bottom != nil || _lineHeight != nil ||
_textIndent != nil || _breakBefore != nil || _breakAfter != nil) {
throw odr::UnsupportedOperation();
}
odr::ParagraphStyle result;
if (_textAlign != nil) {
result.text_align = static_cast<odr::TextAlign>(_textAlign.integerValue);
}
return result;
}

+ (instancetype)styleWithHandle:(const odr::ParagraphStyle &)handle {
ODRParagraphStyle *const result = [[ODRParagraphStyle alloc] init];
result->_textAlign = box_enum(handle.text_align);
Expand Down
33 changes: 33 additions & 0 deletions apple/tests/OdrCoreTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -420,6 +420,39 @@ final class DocumentSaveTests: XCTestCase {
}
}

func testSetStyleAlignsAParagraph() throws {
let document = try self.document()
let root = try XCTUnwrap(try document.rootElement())
let paragraph = try XCTUnwrap(root.firstDescendant(ofType: Paragraph.self))

let style = ParagraphStyle()
style.textAlign = NSNumber(value: TextAlign.center.rawValue)
try paragraph.setStyle(style)

let saved = try XCTUnwrap(try document.saveToMemory())
let path = URL(fileURLWithPath: try temporaryDirectory())
.appendingPathComponent("aligned.odt")
try saved.write(to: path)

let reloaded = try DecodedFile.decode(path: path.path)
.asDocumentFile().document()
let reloadedRoot = try XCTUnwrap(try reloaded.rootElement())
let aligned = try XCTUnwrap(reloadedRoot.firstDescendant(ofType: Paragraph.self)).style
XCTAssertEqual(aligned.textAlign?.intValue, TextAlign.center.rawValue)
}

func testSetParagraphStyleRefusesALineHeight() throws {
let document = try self.document()
let root = try XCTUnwrap(try document.rootElement())
let paragraph = try XCTUnwrap(root.firstDescendant(ofType: Paragraph.self))

let style = ParagraphStyle()
style.lineHeight = Measure(string: "12pt")
XCTAssertThrowsError(try paragraph.setStyle(style)) { error in
XCTAssertEqual((error as NSError).code, ODRError.unsupportedOperation.rawValue)
}
}

func testSetCellStyleFillsACell() throws {
let document = try DecodedFile.decode(path: try Fixture.ods())
.asDocumentFile().document()
Expand Down
56 changes: 55 additions & 1 deletion docs/design/document-editing.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ Version 2. Version 1 addressed by path, and replay refuses it.
| `mergeParagraph` | `paragraph` | takes the children of the next sibling paragraph and removes it |
| `insertParagraph` | `after`, `id` | a fresh empty paragraph after the named one, copying its style |
| `setTextStyle` | `id`, `style` | states the listed properties on one run; see [Inline formatting](#inline-formatting) |
| `setParagraphStyle` | `id`, `style` | states the alignment of one paragraph; see [Paragraph alignment](#paragraph-alignment) |
| `setCell` | `sheet`, `column`, `row`, `value` | see [`spreadsheet-editing.md`](spreadsheet-editing.md) |

Every `id` on an op that creates an element is negative (decision 4). Every
Expand Down Expand Up @@ -154,7 +155,7 @@ call refuses an element of another document.

The adapter hooks, all defaulting to `UnsupportedOperation` (decision 7):
`element_remove`, `text_insert`, `text_set_style`, `paragraph_split`,
`paragraph_merge_next` and `paragraph_insert_after`. Each engine resolves the
`paragraph_merge_next`, `paragraph_insert_after` and `paragraph_set_style`. Each engine resolves the
id to its registry entry, splices the pugixml subtree and fixes the registry
links. Only the tag names differ: `text:p` and `text:span` against `w:p`,
`w:r`, `a:p` and `a:r`. `internal::ElementRegistry` has `unlink_child`,
Expand Down Expand Up @@ -333,6 +334,59 @@ run stops the fold.
`background_color` with alpha 0. The bindings expose it in python, Java,
Objective-C and the npm package.

## Paragraph alignment

```json
{"op": "setParagraphStyle", "id": 9, "style": {"align": "center"}}
```

`align` is `left`, `center`, `right` or `justify`. It is the only key.

| On the wire | `ParagraphStyle` | ODF `style:paragraph-properties` | docx `w:pPr` | pptx `a:pPr` |
|---|---|---|---|---|
| `left` | `TextAlign::left` | `fo:text-align="left"` | `<w:jc w:val="left"/>` | `algn="l"` |
| `center` | `TextAlign::center` | `fo:text-align="center"` | `<w:jc w:val="center"/>` | `algn="ctr"` |
| `right` | `TextAlign::right` | `fo:text-align="right"` | `<w:jc w:val="right"/>` | `algn="r"` |
| `justify` | `TextAlign::justify` | `fo:text-align="justify"` | `<w:jc w:val="both"/>` | `algn="just"` |

`Paragraph::set_style(delta)` reaches `ParagraphAdapter::paragraph_set_style`.
It writes `text_align` only, and any other field of the delta refuses with
`UnsupportedOperation`. The C++ call also takes `start` and `end`. ODF and
docx write them as they are, and pptx refuses them, because
`ST_TextAlignType` has no value for either.

### 17. ODF aligns through a fresh automatic paragraph style

This is decision 12 for a paragraph. The writer copies the automatic style
that the paragraph shows, or makes a child of a named one, under a fresh
`P<n>`. It then sets `fo:text-align` in the copy. The same base and the same
delta give one style for the length of a replay, so a selection over twenty
paragraphs of one style adds one style.

### 18. docx and pptx write into the paragraph's own properties

`w:pPr` and `a:pPr` belong to one paragraph, so no cut is needed. The writer
makes the element as the first child where it is missing. `CT_PPr` is a
sequence, so `w:jc` goes to its rank, as the run properties of decision 13
do.

### 19. The editor aligns every paragraph that the selection reaches

`odr.editing.format({align: "center"})` sends one `setParagraphStyle` per
paragraph from the start of the selection to its end. A collapsed caret
aligns its own paragraph. A style can hold `align` and run keys together. The
paragraphs are checked first and aligned last, so a refused mark leaves no
paragraph aligned. The editor writes `text-align` on the `x-p`, which is the
declaration that `translate_paragraph_style` writes. `onSelectionChange`
reports `align` where the paragraphs agree, and it resolves `start` and `end`
against the direction of the paragraph. Two alignments of one paragraph fold
into one op, unless an op that names the paragraph lies between them.

`formatJustifyLeft`, `formatJustifyCenter`, `formatJustifyRight` and
`formatJustifyFull` are chords, as `formatBold` is. Under scope `paragraph`
the host's `format` aligns, because an alignment moves no range, but a chord
refuses, as every chord does.

## Open items

- A list item is a paragraph in a list. Enter at the end of one makes a bare
Expand Down
8 changes: 5 additions & 3 deletions docs/design/editing.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,9 @@ no live connection between the browser and C++.
- The `back_translate` CLI replays an envelope onto a source document and
saves it.
- Inline formatting is `setTextStyle` on the wire and `odr.editing.format` in
the page. [`document-editing.md`](document-editing.md#inline-formatting)
holds its decisions.
the page, and a paragraph alignment is `setParagraphStyle`.
[`document-editing.md`](document-editing.md#inline-formatting) holds their
decisions.

## Decisions

Expand Down Expand Up @@ -189,6 +190,7 @@ the markup.
| Backspace at the start of a paragraph | taken: the paragraph merges into the one before |
| a paste of plain text | taken: each line after the first opens a paragraph |
| a mark (ctrl/cmd+B, I, U, or `odr.editing.format`) under scope `document` | taken: a run covered in part is cut, and the covered runs are restyled |
| an alignment (`formatJustify*`, or `odr.editing.format({align})`) | taken: every paragraph the selection reaches is aligned |
| a composition (CJK, autocorrect, dictation) | let through, and each change recorded after its `input` |
| a soft line break (`insertLineBreak`) | refused, reason `newLine` |
| a range over a picture | taken: the frame carries an address |
Expand Down Expand Up @@ -217,7 +219,7 @@ each `input`. A run the browser took out of the page raises `unnameableEdit`.
| Scope | What the document editor takes |
|---|---|
| `document` (default) | everything in decision 13 |
| `paragraph` | an edit that starts and ends in one paragraph, and no formatting |
| `paragraph` | an edit or a host's `format` that starts and ends in one paragraph, and no formatting chord |

Why a paragraph and not a run: Word splits runs by revision session, so a wall
at a run would stand in the middle of uniform text. The editor reads the
Expand Down
10 changes: 10 additions & 0 deletions jni/java/app/opendocument/core/Paragraph.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,21 @@ public ParagraphStyle style() {
return styleNative(handle());
}

/**
* States the non-null fields of {@code style} on the paragraph and leaves the rest. Only {@code
* textAlign} is written; any other field is refused.
*/
public void setStyle(ParagraphStyle style) {
setStyleNative(handle(), style);
}

public TextStyle textStyle() {
return textStyleNative(handle());
}

private native ParagraphStyle styleNative(long handle);

private native void setStyleNative(long handle, ParagraphStyle style);

private native TextStyle textStyleNative(long handle);
}
23 changes: 15 additions & 8 deletions jni/java/app/opendocument/core/ParagraphStyle.java
Original file line number Diff line number Diff line change
@@ -1,17 +1,24 @@
package app.opendocument.core;

/** Style of a paragraph. Mirrors {@code odr::ParagraphStyle}; fields may be {@code null}. */
/**
* Style of a paragraph. Mirrors {@code odr::ParagraphStyle}; a {@code null} field is one the
* document does not state. A caller builds one for {@link Paragraph#setStyle}: every field left
* {@code null} is left alone on the paragraph.
*/
public final class ParagraphStyle {
public final TextAlign textAlign;
public TextAlign textAlign;
/** The base direction the paragraph's text runs in; {@code null} if the style says nothing. */
public final TextDirection direction;
public final DirectionalMeasure margin;
public final Measure lineHeight;
public final Measure textIndent;
public TextDirection direction;
public DirectionalMeasure margin;
public Measure lineHeight;
public Measure textIndent;
/** A break the author put before the paragraph; {@code null} if the style says nothing. */
public final BreakType breakBefore;
public BreakType breakBefore;
/** A break the author put after the paragraph; {@code null} if the style says nothing. */
public final BreakType breakAfter;
public BreakType breakAfter;

/** Every field {@code null}. */
public ParagraphStyle() {}

ParagraphStyle(
int textAlign,
Expand Down
3 changes: 3 additions & 0 deletions jni/src/jni_convert.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,9 @@ odr::TextStyle text_style_from_java(JNIEnv *env, jobject style);
/// The fields a Java `TableCellStyle` states; a padding or a border is
/// refused, since no engine writes one.
odr::TableCellStyle table_cell_style_from_java(JNIEnv *env, jobject style);
/// The fields a Java `ParagraphStyle` states; any but `textAlign` is refused,
/// since no engine writes one.
odr::ParagraphStyle paragraph_style_from_java(JNIEnv *env, jobject style);

/// Optional enum to a Java-side code; -1 encodes absent.
template <typename E> jint enum_code(const std::optional<E> &value) {
Expand Down
10 changes: 10 additions & 0 deletions jni/src/jni_document.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -611,6 +611,16 @@ Java_app_opendocument_core_Paragraph_styleNative(JNIEnv *env, jobject,
});
}

extern "C" JNIEXPORT void JNICALL
Java_app_opendocument_core_Paragraph_setStyleNative(JNIEnv *env, jobject,
jlong handle,
jobject style) {
guarded(env, [&] {
element(handle).as_paragraph().set_style(
odr_jni::paragraph_style_from_java(env, style));
});
}

extern "C" JNIEXPORT jobject JNICALL
Java_app_opendocument_core_Paragraph_textStyleNative(JNIEnv *env, jobject,
jlong handle) {
Expand Down
35 changes: 35 additions & 0 deletions jni/src/jni_style.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -402,6 +402,41 @@ odr::TableCellStyle table_cell_style_from_java(JNIEnv *env,
return result;
}

odr::ParagraphStyle paragraph_style_from_java(JNIEnv *env,
const jobject style) {
odr::ParagraphStyle result;
if (style == nullptr) {
return result;
}
jclass cls = env->GetObjectClass(style);
const auto field = [&](const char *name, const char *signature) {
return env->GetObjectField(style, env->GetFieldID(cls, name, signature));
};

for (const auto &[name, signature] :
{std::pair{"direction", "Lapp/opendocument/core/TextDirection;"},
std::pair{"margin", "Lapp/opendocument/core/DirectionalMeasure;"},
std::pair{"lineHeight", "Lapp/opendocument/core/Measure;"},
std::pair{"textIndent", "Lapp/opendocument/core/Measure;"},
std::pair{"breakBefore", "Lapp/opendocument/core/BreakType;"},
std::pair{"breakAfter", "Lapp/opendocument/core/BreakType;"}}) {
if (const jobject value = field(name, signature); value != nullptr) {
env->DeleteLocalRef(value);
env->DeleteLocalRef(cls);
throw odr::UnsupportedOperation();
}
}
const jobject text_align =
field("textAlign", "Lapp/opendocument/core/TextAlign;");
result.text_align = enum_from_java<odr::TextAlign>(env, text_align);
if (text_align != nullptr) {
env->DeleteLocalRef(text_align);
}

env->DeleteLocalRef(cls);
return result;
}

odr::DirectionalStyle<odr::Measure>
directional_measure_from_java(JNIEnv *env, jobject value) {
odr::DirectionalStyle<odr::Measure> result;
Expand Down
Loading
Loading