mirror of
https://github.com/storytold/wordcraft.git
synced 2026-10-26 19:37:03 -06:00
8a2a8bb60dfd46ddceee26a4aa91aeb5cbe14532
55
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
a5b17eb3a4 |
Enclose Characters: put a character in a circle, square, triangle or diamond (#301)
* Home › Font › Enclose Characters: circle, square, triangle or diamond around a character (#297) New command format.enclose {"shape": "circle|square|triangle|diamond" | null, "style"?: "shrink|enlarge", "text"?: string} puts one full-width or up to two half-width characters in an enclosure, changes its shape, or (null) takes the text back out. It is stored the way ECMA-376 describes it: an EQ field with the overstrike switch, eq \o\ac(○,字), with the text as its plain-text result. Layout now draws EQ \o fields: the arguments overstruck as one cluster (centred, \al or \ar). An enclosure symbol among them is drawn as a vector shape around the text in the text's colour, so it never depends on the font having ○ □ △ ◇; "shrink text" scales the text into a one-em shape, "enlarge symbol" grows the shape around text at its own size. Screen and PDF both get it through the display list. Imported Word documents with enclosed characters used to show nothing (the field has no result); they now render. .docx: written as Word stores enclosed characters (begin, the code split so the symbol and the text have their own run sizes, end, no result); the reader recovers the style from those sizes. The style lives in the model as a trailing \* enlarge on the code, never written to a file. Home › Font gets an Enclose Characters button that opens a dialog (Style: None / Shrink text / Enlarge symbol, Text, Enclosure list, a drawn preview); scripts with a shape never see it. Strings in every interface catalog. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Merge fix: keep both sides (enclosures and ruby; tests); regenerate parity checklist --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Brandon Thomas <echelon@gmail.com> |
||
|
|
3042e5251e |
Phonetic Guide: ruby text (furigana, pinyin) over base text (#296)
* Phonetic Guide: ruby text over base text (#288) Home › Font › Phonetic Guide (format.phonetic) sets small ruby text (furigana, pinyin) over the selected text. The ruby is one inline object anchored at U+FFFC whose plain text is the base, so selection and the caret treat it as one unit and exports read the base text. - Model: InlineObject::Ruby with base, ruby, alignment (ECMA-376 ST_RubyAlign), ruby size, raise, base size, language and the ruby's own formatting. - Layout: the base is drawn in its run's formatting, the ruby above it by alignment (centre, distribute letters/space, left, right); the wider of the two sets the width and the line grows to fit the ruby. Drawn in the shared display list, so screen and PDF both show it. - DOCX: w:ruby read and written with w:rubyPr (rubyAlign, hps, hpsRaise, hpsBaseText, lid), w:rt and w:rubyBase runs with their formatting. Previously only the base text was kept. - Command format.phonetic {"ruby", "align"?, "size"?, "offset"?}: sets, edits or (null/"") removes the ruby, undoable; without "ruby" it opens the dialog and returns the selection's phonetic guide. - UI: Phonetic Guide button in Home › Font and a dialog (base text, ruby text, alignment, offset, size, Remove), strings in every catalog. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Merge fix: keep both sides' tests --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Brandon Thomas <echelon@gmail.com> |
||
|
|
dba6e66d53 |
Choose which side of an object text wraps on (#364)
* Tight and Through wrapping follow the object's outline (#354) Text now wraps around a Tight or Through object's shape instead of its bounding box. The outline is the object's wrap polygon (wp:wrapPolygon, in the 21600-unit space of its frame) when it has one; otherwise it is derived: a shape's geometry (ellipse, triangle, star, heart... flattened), the convex hull of a picture's opaque pixels (alpha threshold on a coarse grid, at most 64 points), or the rectangle. The outline turns and flips with the object, keeps the left/right and top/bottom distances from text, and is built once per placed object; each line asks it which stretches of its band it covers. Through also lets text into gaps inside the outline that are wide enough. DOCX reads and writes wp:wrapPolygon (with edited) for wrapTight and wrapThrough; a shape without one gets its outline written, other objects their rectangle, since the schema requires a polygon. Square, top and bottom, behind and in front of text are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Choose which side of an object text wraps on (#354) Square, Tight and Through objects now have a wrap side, like Word's "Wrap text" choice: both sides (the default), left only, right only, or the largest side. Left keeps the band to the object's right empty on every line beside it, Right the band to its left, and Largest picks the wider side line by line. It works with contour wrapping and with turned objects, and right-to-left paragraphs mirror it. The setting is Float::wrap_text, read from and written to wrapText on wp:wrapSquare, wp:wrapTight and wp:wrapThrough (previously always written as bothSides). The new arrange.wrapText command {"side": "bothSides|left|right|largest"} sets it on the selected object as one undo step. The ribbon's Wrap Text button is now a menu with the wrap styles and a Wrap Text Sides submenu, translated in every interface language. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Brandon Thomas <echelon@gmail.com> |
||
|
|
860f4ca260 |
Tight and Through wrapping follow the object's outline (#354) (#360)
Text now wraps around a Tight or Through object's shape instead of its bounding box. The outline is the object's wrap polygon (wp:wrapPolygon, in the 21600-unit space of its frame) when it has one; otherwise it is derived: a shape's geometry (ellipse, triangle, star, heart... flattened), the convex hull of a picture's opaque pixels (alpha threshold on a coarse grid, at most 64 points), or the rectangle. The outline turns and flips with the object, keeps the left/right and top/bottom distances from text, and is built once per placed object; each line asks it which stretches of its band it covers. Through also lets text into gaps inside the outline that are wide enough. DOCX reads and writes wp:wrapPolygon (with edited) for wrapTight and wrapThrough; a shape without one gets its outline written, other objects their rectangle, since the schema requires a polygon. Square, top and bottom, behind and in front of text are unchanged. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Brandon Thomas <echelon@gmail.com> |
||
|
|
39ab3b5433 |
Field codes like Word's field keys (#339) (#349)
Show field codes instead of results with View > Field Codes (Alt+F9) or
per field with Toggle Field Codes (Shift+F9); a field showing its code is
drawn as { CODE } on light grey. Edit a field's code in a small popup
(double-click a field showing its code, right-click > Edit Field..., or
fields.setCode), and Ctrl+F9 asks for a new field's code. F9 updates
fields and shows the selected ones' results again.
New commands: fields.unlink (Ctrl+Shift+F9) replaces fields with their
text, fields.lock / fields.unlock (Ctrl+F11 / Ctrl+Shift+F11) keep a
result through updates (already saved as w:fldLock), fields.next /
fields.previous (F11 / Shift+F11) select the next or previous field, and
fields.selected reports the fields in the selection. Range fields
(citations) can be locked, unlinked, edited and stepped through too.
The right-click menu gains Update Field, Edit Field... and Toggle Field
Codes when the selection holds a field. Strings are translated in every
catalog; parity catalog, ui-parity, gaps and roadmap updated.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
|
||
|
|
972df1e0c7 |
Track Changes: formatting revisions (w:rPrChange/w:pPrChange) (#41) (#399)
Formatting changes are tracked like insertions and deletions: - Model: runs and paragraph marks carry the formatting before a tracked change (CharProps::fmt_change), paragraphs their old properties (ParaProps::fmt_change) and a list numbering change; tables, rows, cells and sections their old properties. - DOCX: w:rPrChange (runs and paragraph marks), w:pPrChange, w:numberingChange, w:tblPrChange, w:trPrChange, w:tcPrChange and w:sectPrChange are read and written back, each last in its parent (ECMA-376 17.13.5). Nested changes are ignored; ids are renumbered. - Recording: with Track Changes and Track Formatting on, Session::run compares the paragraphs a command changed with the document before it, so every formatting command records without per-command code. An author's later changes keep the formatting before the first one; formatting set back to the original stops being a change. - Accept/Reject (at the caret, selection, all) keep or restore the formatting; Next/Previous and the Reviewing Pane list format changes with a "Formatted: Bold, Size: 14" description. - Markup: change bars beside formatted lines and "Formatted: ..." balloons in the markup area, per Track Changes Options. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
144ddb7f2d |
Layout and render: extreme page numbers and wave borders no longer overflow or run away (#393)
- A page-number start near u32::MAX (.docx accepts up to 4294967295) or a line-number start near u32::MAX (open through .json and commands) overflowed when the next page or line was counted: a panic whenever overflow checks are on, silently wrapped numbers otherwise. The three additions now saturate. - The wave stroke used by wave borders and underlines looped forever, pushing curves until memory ran out, once its x coordinate passed about 1e16 and `x + step == x` (a wave page border with a 1e20 `space`, reachable through .json, the control channel and MCP). The path is built in `wave_path`, which stops as soon as a step makes no progress. Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com> |
||
|
|
da07540f31 |
Layout: a drop cap in a font over 1000 pt no longer panics (#389)
The drop-cap letter's size was `clamp(rc.size, 1000.0)`, and `f32::clamp` panics when the minimum is above the maximum, which happens as soon as the paragraph's text is larger than 1000 pt (Word allows up to 1638). Layout, PDF export and saving run outside `Session::run`'s panic guard, so the desktop app panicked on every frame, and `wordcraft-cli convert` failed. Use `.min(1000.0).max(rc.size)`, which never panics and keeps the letter at least as large as the text. Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com> |
||
|
|
e71e2d1c52 |
Draw tab: draw with pen, pencil and highlighter, erase ink, and hide ink (#315)
* Draw Table and Eraser: draw tables and split or merge cells with a pen (#303) Insert › Tables › Draw Table and Table Layout › Draw › Draw Table / Eraser. table.draw takes a pen stroke in page points: a rectangle outside a table inserts a one-cell table of that size at the paragraph under its start; a straight vertical or horizontal line inside a table splits the cells it crosses (new grid column or row; untouched cells span it). table.eraser merges the two cells beside the border nearest a point. Each stroke is one undo step. In the app the buttons toggle pen and eraser modes with a live preview, a crosshair with the tool's icon, and Esc to stop; canvas clicks don't move the caret while a tool is on. Splitting through part of a vertically merged cell and erasing beside one are refused with a clear error. Catalog 384/430 -> 387/430. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Draw tab: pen, pencil and highlighter ink, stroke eraser and Hide Ink (#307) Ink strokes are floating freeform shapes: a new Freeform geometry (paths in shape-local coordinates, opacity, and the pen that drew it) on InlineObject::Shape with ShapeKind::Freeform. draw.stroke inserts one anchored to the paragraph under the first point (one undo step; points capped per stroke, strokes capped per page, NaN dropped); draw.erase / draw.eraser with a point deletes the stroke under it; draw.pen, draw.pencil, draw.highlighter, draw.eraser and draw.select set the Draw tab's tool in the session view state; review.hideInk toggles showing ink on screen. The canvas draws a live stroke while a pen is dragged and commits it on release; the eraser deletes strokes it passes over (one undo step per drag); Esc or Select goes back to selecting. Draw tab buttons show the tool in use, with colour and thickness menus; Review gets an Ink group with Hide Ink. Ink renders with round caps and joins on screen and in PDF; highlighters are translucent and behind the text. DOCX writes freeforms as wps:wsp with a:custGeom (moveTo/lnTo) in a wp:anchor and reads them back, along with simple custom-geometry freeforms from other files. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * #315 review fixes: eraser undo test, sanitized tap dots, custGeom path cap, ink docs - draw.eraser with a point erases through draw.erase: test that it is one undo step (undo/redo) and marks the document dirty. - Render and PDF: a single-point stroke's dot is drawn at the sanitized point, not the raw first one. - DOCX custGeom reading stops once MAX_PATHS paths (subpaths included) are collected, across a:path elements. - docs/file-format-parity.md: ink is saved as custom geometry, not InkML, so Word shows it as a freeform. - widgets test: theme::apply takes an Appearance (#317). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fmt Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Draw tab: pen, pencil and highlighter ink, stroke eraser and Hide Ink (#307) Ink strokes are floating freeform shapes: a new Freeform geometry (paths in shape-local coordinates, opacity, and the pen that drew it) on InlineObject::Shape with ShapeKind::Freeform. draw.stroke inserts one anchored to the paragraph under the first point (one undo step; points capped per stroke, strokes capped per page, NaN dropped); draw.erase / draw.eraser with a point deletes the stroke under it; draw.pen, draw.pencil, draw.highlighter, draw.eraser and draw.select set the Draw tab's tool in the session view state; review.hideInk toggles showing ink on screen. The canvas draws a live stroke while a pen is dragged and commits it on release; the eraser deletes strokes it passes over (one undo step per drag); Esc or Select goes back to selecting. Draw tab buttons show the tool in use, with colour and thickness menus; Review gets an Ink group with Hide Ink. Ink renders with round caps and joins on screen and in PDF; highlighters are translucent and behind the text. DOCX writes freeforms as wps:wsp with a:custGeom (moveTo/lnTo) in a wp:anchor and reads them back, along with simple custom-geometry freeforms from other files. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Brandon Thomas <echelon@gmail.com> |
||
|
|
a7c809e63b |
Charts, SmartArt and OLE objects are no longer lost when you save a .docx (#333)
* Keep charts, SmartArt and OLE objects when saving DOCX (#319) Charts and SmartArt diagrams were drawn since #292 but the writer skipped them, and OLE objects kept only their picture, so opening and saving a document silently dropped them. The reader now keeps each object's own markup (the a:graphic of a chart or diagram, the whole w:object of an OLE object) on the model object, and every part it reaches through relationships (chart part, embedded workbook, colour and style parts; diagram data, layout, quick style, colours and drawing; OLE embedding and picture) as opaque bytes in Document::passthrough, with their content types and relationships in a manifest, as the VBA project already is. The writer emits the object with fresh relationship ids pointing at the re-added parts, keeping each part's own relationship ids and content types. A chart or diagram gets its frame from the object's current size and position; an OLE object gets its new size written into its VML shape. A part whose name the writer uses itself is renamed, and an object pasted more than once gets its own copies of every part (sharing past a size budget). Part counts, relationship counts, markup size and copy bytes are all capped. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Keep charts, SmartArt and OLE objects when saving DOCX (#319) Charts and SmartArt diagrams were drawn since #292 but the writer skipped them, and OLE objects kept only their picture, so opening and saving a document silently dropped them. The reader now keeps each object's own markup (the a:graphic of a chart or diagram, the whole w:object of an OLE object) on the model object, and every part it reaches through relationships (chart part, embedded workbook, colour and style parts; diagram data, layout, quick style, colours and drawing; OLE embedding and picture) as opaque bytes in Document::passthrough, with their content types and relationships in a manifest, as the VBA project already is. The writer emits the object with fresh relationship ids pointing at the re-added parts, keeping each part's own relationship ids and content types. A chart or diagram gets its frame from the object's current size and position; an OLE object gets its new size written into its VML shape. A part whose name the writer uses itself is renamed, and an object pasted more than once gets its own copies of every part (sharing past a size budget). Part counts, relationship counts, markup size and copy bytes are all capped. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Merge fix: #340's rotation round-trip test builds an Image with #333's ole field Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Brandon Thomas <echelon@gmail.com> |
||
|
|
79b6f33b03 |
Long footnotes continue onto the next page (#352) (#357)
When the footnotes referenced on a page don't fit below its text, the note that doesn't fit is split at a line boundary: its first line stays on the page of its reference (if even that doesn't fit, the reference line moves to the next page, as before), and the rest continues at the top of the next page's note area, before that page's own notes, under a full-width continuation separator. Notes still continuing after the last page get pages of their own. Endnotes are unchanged. The docx reader doesn't keep a document's own continuation separator or continuation notice, so the default line is drawn and no notice. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Brandon Thomas <echelon@gmail.com> |
||
|
|
1d7a54f883 |
Define New Multilevel List and Track Changes Options dialogs (#342)
* Define New Multilevel List and Track Changes Options dialogs (#328) Home > Paragraph > Multilevel List > Define New Multilevel List edits all nine levels with a live preview: number text and style, start, restart after a level, linked paragraph style, alignment, aligned-at and text indent, what follows the number with an optional tab stop, and the number's font. OK runs the new list.define command (list.get reads a definition back). List levels gain a restart-after level and a tab stop, which the counters and layout honour and .docx reads and writes (lvlRestart, num tab); number alignment now places the label. Review > Tracking's launcher opens Track Changes Options: show comments, ink, insertions and deletions, formatting; balloon mode; insertion and deletion marks and colours; changed-line bars and their colour; track formatting. They go through review.trackingOptions, are saved per user with the interface preferences, and the page drawing honours the marks, colours, hidden deletions, change bars and inline mode. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Parity checklist regenerated after merging main into #342 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Brandon Thomas <echelon@gmail.com> |
||
|
|
bd10795306 |
Free rotation and flips for pictures, shapes and text boxes (#332) (#340)
Drawings carry an angle and horizontal/vertical flips on their placement (Float::rot, flip_h, flip_v), read and written as DrawingML a:xfrm rot, flipH and flipV for pictures, shapes, text boxes and groups; the written effect extent covers the rotated bounds and reading takes that overhang back out. Layout draws them turned about their centre (screen and PDF), square wrap and inline lines keep clear of the rotated bounds, and hit testing follows the turned shape. arrange.rotate now turns the frame (90 degrees, flips) instead of rewriting pixels; arrange.rotation sets an exact angle. The canvas frame has a rotation handle (Shift snaps to 15 degrees) and turned frames resize along their own axes; Picture Format and Shape Format get a Rotation box and a Rotate menu. Text inside a rotated text box stays upright for now. Groups turn and flip as a whole (wpg:grpSpPr a:xfrm): members keep their own turn inside the group's, are carried round its centre, and a group's text boxes move with it. Shape effects are drawn inside the turn; a shadow's direction follows DrawingML rotWithShape (kept on the model, true when absent; Word's presets write 0, so their shadows keep their page direction). Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
2545b170ba |
Tests: the equation-wrap test accepts plain y where no math font is installed (FreeBSD CI) (#348)
The first glyph is math italic 𝑦 only when a font has it; FreeBSD CI has none, so it shapes as plain y. The test is about line breaks and indents, not the glyph. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
81bf9187f2 |
Wrap long display equations across lines like Word (#326) (#329)
A display equation wider than its column ran off the page. It now breaks at top-level binary operators and relations (never inside a fraction, radical, script or matrix), filling each line greedily; a term wider than the line overflows instead of breaking inside. - Break before the operator by default, or after it, or repeated on both lines, as the document's m:brkBin says; m:brkBinSub picks how a minus repeats. - Continuation lines are indented by m:wrapIndent (one inch by default) or set flush right with m:wrapRight. - Manual breaks (m:brk on a run) start a new line; m:alnAt aligns it at an operator of the first line. - DOCX: m:mathPr in settings.xml and m:brk in math runs are read and written back. Documents without them save as before. - Inline equations, numbered equations and m:jc alignment are unchanged. Tests: layout (a long sum wraps within the column with indented continuation lines; after/repeat modes; manual breaks and alnAt; short equations stay on one line; hostile break input) and DOCX round-trip. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Brandon Thomas <echelon@gmail.com> |
||
|
|
29e978d6d8 |
Re-land #264, #263, #270, #271, #267, #280: clipboard pane, table styles, Manage Styles, Asian Typography, Group, Shape Effects (#323)
The original squash merges of these six PRs were empty. Integrated and tested together on current main. Co-authored-by: Valentin Klein <vangel1976@googlemail.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
08e8d4ade0 |
Table sizing: Height/Width boxes, AutoFit to contents and a Table Properties dialog (#287)
* Table sizing: Height/Width boxes, real AutoFit Contents, Table Properties dialog (#44) - Table Layout > Cell Size gets Height and Width boxes (inches) for the caret cell's row and column; each edit runs table.rowHeight / table.columnWidth, a drag is one Undo. - AutoFit Contents now measures every column's narrowest (longest word) and widest (unwrapped text) content with the layout's text shaping, shares the text width the way Word does, and writes the widths into the table grid and cell widths. - Table Properties opens a dialog (table width, alignment, indent; row height and rule, break across pages, header row; column width; cell width and vertical alignment). OK applies only what changed as one table.properties step; with settings the command still works headlessly, and without them it reports the current values. - table.columnWidth clears the table's preferred width so the column keeps the new size; table.rowHeight takes an optional exact rule. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Progress docs: Table Properties dialog and AutoFit Contents landed (#44) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Table sizing: saturating column-span sum in sync_cell_widths Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Merge fix: one region_text (main's) after #287 met the landed table-style code Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Brandon Thomas <echelon@gmail.com> |
||
|
|
c0bbcbac24 |
Charts and SmartArt: draw them instead of dropping them (#292)
A chart or SmartArt diagram in a DOCX was dropped on import: it took no room, so the text after it moved up (five of the six chart documents in docxide-pdf's fixtures came out a page short), and nothing was drawn. They are now an InlineObject::Graphic: it takes its room, floats and wraps like a picture, and draws a list of neutral items (shapes, paths, one-line texts, pictures) that layout scales to the object's box. - Charts (read/chart.rs) are drawn from the values the chart part caches: clustered, stacked and 100% bar and column charts, line, area, pie, doughnut and scatter charts, with title, legend, gridlines, axis labels and Excel number and date formats. - SmartArt (read/diagram.rs) is drawn from the drawing Word stores beside the diagram: preset shapes, theme colours, text and pictures. - Theme colour handling is shared (read/drawing_color.rs). - Each chart or diagram part is read once, only through a relationship of its type, under a per-file budget of items. - Tagged PDF tags them as figures with their alternative text, and the accessibility check reports charts without one. Not yet: charts and diagrams aren't written back when saving a DOCX (they weren't before either), so the editor doesn't let you move or resize them. Radar, stock, 3-D and combination charts draw their first supported chart type only; SmartArt pictures aren't clipped to their shape. docxide-pdf bench (365 documents, vs main + #135): page counts right 320 -> 326 (all five chart documents and one SmartArt document), Jaccard passes 208 -> 215, SSIM passes 76 -> 79; eight documents better, none worse. Co-authored-by: Oliver Zieschang <4963183+naxvog@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Brandon Thomas <echelon@gmail.com> |
||
|
|
12a4da3be5 |
Make the floating header text box test independent of installed fonts (#284)
The test put a two-line header 36pt from the top of the page and checked that the body still starts at the 72pt top margin. How tall those two lines are depends on the font the default text resolves to: with the installed Carlito-style fonts they fit, but with only the bundled fallback fonts (as on the FreeBSD CI VM) they end at 72.6pt, so the header correctly pushed the body down and the control check failed. Start the header 18pt from the top instead, which leaves the two lines plenty of room above the margin with any font. The checks are unchanged: the control body starts at the top margin, the floating box changes neither the body start nor the page count, and the same box inline still pushes the body down. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
23395e7fc0 |
Tables: autofit by content, and floating tables inside cells (#291)
Word sizes the columns of a table that may follow its content (tblLayout
not fixed) from what is in them; we kept the stored grid as it was:
- A table with no preferred width anywhere (no tblW, no tcW: what
generators write) now gets its columns from their content: each its
widest line if all fit, else shared out between longest word and
widest line, as Word does (case51: a nested table no longer fills its
cell, and the document is two pages like Word's).
- In other autofit tables no column is narrower than its longest word;
the columns with room give up the difference ("ON" no longer breaks
into "O/N" in case91).
Floating tables (w:tblpPr) inside a table cell were laid out in the
text flow. They now stand at their position in the cell and take no
room; the text after them wraps around them, a table that may not
overlap goes below the ones in its way, and the cell grows to hold
them. A row whose lines all fit but whose nested floating table runs
past the page bottom splits there instead of moving to the next page.
Text now flows beside a floating object into gaps of 20pt or more
(was 36pt): Word puts a paragraph beside a floating table in a 21.3pt
gap but not in an 18.6pt one (docxide-pdf case139-148 vary just that).
docxide-pdf bench (365 documents, vs main + #135): Jaccard passes
208 -> 227, SSIM passes 76 -> 86, page counts right 320 -> 324; three
documents slightly worse (case149 -4 SSIM: Word moves the whole floating
table to the next page, we split it; two others -1 or less).
Co-authored-by: Oliver Zieschang <4963183+naxvog@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
|
||
|
|
a99dddcbdc |
Show/Hide ¶: paragraph mark no longer overlaps text in mixed-direction paragraphs (#278)
* Show section breaks with Show/Hide ¶ (#42) With formatting marks on, the last paragraph of a section now ends in a dotted double rule with the break's name centred in it (Section Break (Next Page), (Continuous), (Even Page), (Odd Page)) instead of a plain ¶. The type shown is the start type of the section that follows. The mark is screen-only: PDF export skips it, and print/PNG never turn marks on. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Show/Hide ¶: put the paragraph mark past mixed-direction text (#274) The paragraph mark (and the section-break mark) was anchored at the caret position after the paragraph's logically last character. When that text runs against the paragraph's direction (Hebrew ending a left-to-right paragraph, Latin ending a right-to-left one) that position is inside the line, so the mark was drawn on top of a letter. The mark now sits at the line's visual end for the paragraph's direction: right of the rightmost glyph in a left-to-right paragraph, left of the leftmost in a right-to-left one. Pure left-to-right and pure right-to-left lines are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
f6a6ded2cc |
Tests no longer depend on the installed fonts; PDF text of field results with ligatures (#261)
On a Linux machine with Carlito and Noto Naskh Arabic installed, four
layout and PDF tests failed on main:
- field_results_are_extractable_text found a real bug: a field result is
laid out as one cluster, and the display list gave each of its glyphs one
character. A ligature (Carlito, the Calibri substitute, joins "ti") took
only its first letter and shifted the rest, so the PDF text read
"Sectio 3.01 fir details3". The layout now keeps each glyph's text range
from shaping, so a ligature carries all its letters. A new test pins this
with bundled Source Sans 3 ("fi", "ffi"), whatever fonts are installed.
- fragmented_hidden_text_lays_out_like_the_visible_text compared visible
text in alternating bold runs with an all-regular paragraph, so it only
passed when the default font's bold happened to break lines the same.
The plain paragraph now has the same bold runs.
- persian_letters_join_and_brackets_mirror counted glyphs, but Noto Naskh
Arabic draws a dotted letter as two glyphs (the dotless form and the
dot). It now compares letters by shaping cluster.
- glyph_runs_carry_their_text_for_pdf_export expected لا as one ligature
glyph; Noto Naskh Arabic draws the lam and the alef separately. The
ligature check now applies when the font forms the ligature.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
|
||
|
|
15f5ad274e |
Column Selection: Alt+drag selects a block of text (#237)
select.column (Mod+Shift+F8) selects the same x range on a run of lines, from two positions, two page points (Alt+drag), or by toggling column mode where the arrow keys extend the block. Copy/Cut give one line per row, Delete/Backspace remove only the block, character formatting and Change Case apply per row, typing/paste replace the block. Any ordinary caret move drops it. F8 keys now reach their shortcuts in the UI. Co-authored-by: Dmitrii Khlapov <19991457+dkhlapov@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Brandon Thomas <echelon@gmail.com> |
||
|
|
d8f71959d3 |
Create and modify your own table styles, saved in the .docx (#146) (#256)
New Table Style and Modify Table Style (Table Design > Table Styles) make real table styles in the document's style sheet: name, based on, and formatting for the whole table, the header row and banded rows (borders, fill, bold, font colour). The table at the caret takes the new style, it shows in the gallery, and other tools that read table styles by name (pandoc's custom-style, for one) see it. - engine: table.newStyle / table.modifyStyle with documented params (regions take borders, fill, bold, italic, colour, size, font; bandSize; para); table.style only accepts table styles now. - doc/layout: table styles gain header-row and row-band cell borders, row-band character formatting and rows per band, applied by layout. - docx: tblStyleRowBandSize and firstRow/band1Horz tcBorders and rPr are read and written, so custom styles round-trip. - ui: a compact dialog from the Styles menu next to the gallery; table style tiles redraw when a style changes. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Brandon Thomas <echelon@gmail.com> |
||
|
|
8b49ea7546 |
Table Layout › Text Direction turns the cell's text (#226) (#245)
* Tests: shapes and pictures can be clicked, resized, dragged and nudged Fixed by #46 but only text boxes were covered. These walk the editor's path for shapes (rectangle, ellipse, star, arrow) and pictures: hit test, select, resize with arrange.bounds, drag to float with Square wrap, nudge, undo. Covers #142 and #82. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Font list offers every weight a family installs, named as Word names them (#219) Fonts were listed and matched only by their typographic family name (name ID 16, e.g. "Roboto"), so a face other than Regular, Bold or Italic (Light, Medium, SemiBold, Black, Condensed...) never appeared in the font list, and a document asking for "Roboto Light" fell back to a substitute. Each face now also carries its legacy family name, the one Word lists and stores in documents: name ID 1 for static fonts ("Source Sans 3 Semibold"), or the family plus the style words other than Regular, Bold and Italic for a variable font's named instances ("Inter Light"). The font list includes these names, has_family and face() resolve them (bold and italic still choose or synthesise within the faces the name covers), and asking for a legacy name loads the whole installed family. The system font scan now also reads fvar, so a variable font's instances are listed before it is loaded. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Absolutely positioned VML text boxes float instead of sitting inline (#149) A legacy VML shape (w:pict > v:shape / v:rect) with position:absolute in its style was read as an inline object, so a tall text box in a header made the header as tall as the box and pushed the body down the page. The reader now maps the VML style to the same float model DrawingML anchors use: left/margin-left and top/margin-top offsets, mso-position-*-relative reference areas, mso-position-horizontal/vertical alignment, mso-wrap-distance-* and w10:wrap (square, tight, through, topAndBottom; none or absent is in front of the text, or behind it at a negative z-index). Shapes without position:absolute stay inline. Saving writes the boxes as DrawingML anchors with the same placement. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Reject All removes paragraph breaks inserted with Track Changes on (#229) Enter under Track Changes already recorded the new paragraph mark as an insertion, but accepting or rejecting only cleared that flag, so Reject All left the paragraphs split. Rejecting an inserted mark (or accepting a deleted one) now joins the paragraph with the next one again; Accept All, Reject All, Accept/Reject at the caret or over a selection, Next/Previous Change and the Reviewing Pane all see paragraph marks. Backspace at a paragraph start and Delete at a paragraph end now record a tracked deletion of the mark (or remove a mark the same author inserted) instead of only moving the caret; tracked deletions across paragraphs mark the paragraph marks too. DOCX reads and writes paragraph-mark revisions (w:ins / w:del in the mark's w:rPr), and a tracked mark is drawn in its author's colour when formatting marks are shown. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Table Layout: Text Direction turns cell text (#226) The Text Direction button only flipped a flag that layout and rendering ignored (and docx always wrote it as btLr), so pressing it did nothing visible. Cells now carry a text direction (horizontal, top-to-bottom tbRl, bottom-to-top btLr). The button cycles through them like Word's, for every selected cell. Layout runs turned text along the cell's height: an exact row height sets the line length, otherwise the row grows to fit the unwrapped text. Turned lines are drawn in a turned frame on screen and in PDF, and clicks, caret, and selection follow the turn. docx reads and writes w:textDirection with the matching value. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Chinese interface: embed the Chinese UI font in releases, fall back to an installed one (#241) The release workflow pinned storytold/craft-fonts at abb8331, the commit that added the Japanese fonts. Noto Sans CJK SC came one commit later (8dcdacd), so release builds embedded only the Japanese faces and the Simplified Chinese interface fell back to BIZ UDPGothic, which lacks about 157 simplified-only hanzi (页, 选, 删 …) and drew them as boxes. Document text was unaffected because it uses the system font fallback. - Bump the craft-fonts pin in the release and FreeBSD workflows to 8dcdacd. - With CRAFT_FONTS_REQUIRED, a native build now fails when the craft-fonts manifest has no Jpan or Hans font, so an outdated pin can't ship silently again. - When no embedded face covers the interface language, the UI font families get an installed CJK font (desktop only): a short per-platform list of files in the system font folders (Windows Deng/msyh/simhei/simsun, macOS PingFang/STHeiti, Linux Noto Sans CJK/WenQuanYi), header-checked, size-capped, and the face chosen by coverage (a SC face in a pan-CJK collection), so egui never sees a file it can't parse. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Add a System interface theme that follows the OS appearance (#115) The interface theme is now a three-way choice: Use system setting, Light or Dark, in File > Options > General and in a new Interface Theme menu in View > Dark Mode. System follows the light/dark appearance egui reports (winit on macOS, Windows and Linux; prefers-color-scheme on the web) at launch and live, and falls back to light when the OS reports none. egui's own theme is now pinned to ours, so an OS change no longer swaps in egui's unstyled style. Saved preferences migrate: the old "dark" switch becomes Dark or Light and is never written again. Light stays the default for new installs. New ui.theme command (light/dark/system); ui.dark still sets Light/Dark. The dark page (View > Switch Modes) is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * #248: read the installed CJK UI font only when CJK text is shown A build without craft-fonts read a large system CJK font on the first frame even for an English interface. Now it is loaded only for a CJK interface language, or once File > Options shows the language list (names in their own scripts). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Brandon Thomas <echelon@gmail.com> |
||
|
|
302528d67e |
Web: use the browser clock everywhere on wasm (insert weekday, PDF dates, layout timing, UI timers) — follow-up to #214 (#230)
#214 gave now_iso the browser's clock on wasm; the other clock helpers were still stubs there: Insert Date's weekday was always Thursday, the PDF creation date was left out, and the layout and UI millisecond clocks returned 0, so browser AutoSave's timer never fired, the caret didn't blink and status messages never cleared. The engine's clock is now one now_unix() (std on native, js_sys::Date in the browser, as #214 does) that now_iso and the weekday share; the pdf, layout and ui-egui clocks read js_sys::Date the same way on wasm. Refs #188. Co-authored-by: Dmitrii Khlapov <19991457+dkhlapov@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
4c4dc14a54 |
Reject All removes paragraph breaks inserted with Track Changes on (#229) (#244)
Enter under Track Changes already recorded the new paragraph mark as an insertion, but accepting or rejecting only cleared that flag, so Reject All left the paragraphs split. Rejecting an inserted mark (or accepting a deleted one) now joins the paragraph with the next one again; Accept All, Reject All, Accept/Reject at the caret or over a selection, Next/Previous Change and the Reviewing Pane all see paragraph marks. Backspace at a paragraph start and Delete at a paragraph end now record a tracked deletion of the mark (or remove a mark the same author inserted) instead of only moving the caret; tracked deletions across paragraphs mark the paragraph marks too. DOCX reads and writes paragraph-mark revisions (w:ins / w:del in the mark's w:rPr), and a tracked mark is drawn in its author's colour when formatting marks are shown. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
e53b618584 |
Absolutely positioned VML text boxes float instead of sitting inline (#149) (#242)
A legacy VML shape (w:pict > v:shape / v:rect) with position:absolute in its style was read as an inline object, so a tall text box in a header made the header as tall as the box and pushed the body down the page. The reader now maps the VML style to the same float model DrawingML anchors use: left/margin-left and top/margin-top offsets, mso-position-*-relative reference areas, mso-position-horizontal/vertical alignment, mso-wrap-distance-* and w10:wrap (square, tight, through, topAndBottom; none or absent is in front of the text, or behind it at a negative z-index). Shapes without position:absolute stay inline. Saving writes the boxes as DrawingML anchors with the same placement. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
5d86dec250 |
Right-to-left and Persian text support (bidi, shaping, RTL paragraphs, DOCX) (#207)
* feat(doc,docx): complex-script run properties and RTL OOXML round-trip Model the formatting Word applies to Persian, Arabic and Hebrew text: cs, font_cs, size_cs, bold_cs, italic_cs and lang_bidi on CharProps, resolved with Word's fallbacks (ResolvedChar::complex). Size, bold and italic pair with their complex-script values so a run's size or bold also covers its Persian characters. Add doc::bidi script helpers, and keep ZWNJ and Arabic marks inside words for word selection. DOCX: read and write w:rFonts/@w:cs, w:szCs, w:bCs, w:iCs, w:cs and w:lang/@w:bidi in schema order; stop using the cs font as the Latin font. Tests round-trip Persian text with ZWNJ, digits and mixed Latin. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(fonts): directional shaping and Persian font fallback Add shape_run to shape a run in a known bidi direction (Arabic joining, mirrored brackets). Prefer Persian-capable fonts (Vazirmatn, Noto Naskh/ Sans Arabic…) for right-to-left fallback, pick bold/italic fallback faces, and map common Persian document fonts (B Nazanin, B Lotus, B Titr, IRANSans…) to open substitutes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(layout): Unicode Bidirectional Algorithm and right-to-left paragraphs Run UAX #9 per paragraph (unicode-bidi), shape each level run in its direction, keep marks and ZWNJ with their letter and a word in one fallback face. After line breaking, apply rules L1/L2 per line and give each cluster a visual box; right-to-left paragraphs are laid out from their start edge and mirrored, so indents, tabs, alignment, list labels, drop caps, floats and borders sit on the right. Caret, hit testing and selection use visual geometry (split selections, caret at bidi boundaries, DocLayout::visual_step for arrow keys). Display items carry glyph-to-text ranges, which the PDF writer now uses. Plain left-to-right lines are unchanged. No hyphenation of RTL words. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(engine,ui): paragraph direction commands and RTL editing Add para.ltr / para.rtl (Word's text direction buttons) that change only the reading order, and make the alignment buttons visual in right-to-left paragraphs (para.align also takes start/end). Arrow keys move visually in bidirectional text, Ctrl+arrows follow reading order, and Bold/Italic/ Size/Font cover complex-script text. Ribbon buttons with code-drawn icons, a mirrored ruler for RTL paragraphs, translations, catalog, parity and roadmap updates. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * chore(contributors): add Saeed Azarehgar to people.toml Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(engine): para.align with an explicit value sets it, without toggling Only the alignment buttons toggle back to the start edge when pressed again. para.align (used by the Paragraph dialog, scripts and agents) now sets the requested alignment, as seen on the page, in each selected paragraph's own direction. Previously pressing OK in the dialog with Center or Right selected reset the paragraph to the start edge. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(ui): Direction option in the Paragraph dialog Add Word's Direction (Right-to-left / Left-to-right) to the Paragraph dialog's General section. The dialog now shows alignment and Left/Right indents as seen on the page, mapping them to the paragraph's logical start/end values on OK, so an unchanged OK no longer flips a right-to-left paragraph. OK logic moved to apply_paragraph, with tests; ja/zh translations. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(layout): allow hanging trailing spaces in the RTL display test A wrapped right-to-left line's trailing spaces hang past its end edge (the left margin), as left-to-right lines hang them past the right. The test failed whenever the loaded fallback fonts made a line end there; it now checks only visible glyphs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
10eb8798cd |
Equations: Word (OMML) import, layout and save, plus an equation editor like Word's (#191)
* M5.5: Word equations (OMML) import, lay out and save like Word Equations were flattened to plain text on import. Now OMML is read into a math tree (fractions, scripts, radicals, n-ary operators, delimiters, functions, limits, accents, bars, boxes, equation arrays, matrices, phantoms), laid out with the OpenType MATH table (Cambria Math when installed, else STIX Two Math and other open math fonts), drawn to screen and PDF, and written back as the original OMML. Display equations sit on their own centred line. insert.equation parses the linear format into structure. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * M5.5: equation editor like Word's — Equation tab, in-place editing, # numbers Insert › Equation now opens an equation for in-place editing, with the Equation tab: ready-made equations, Unicode/LaTeX input and conversion (professional ↔ linear), Normal Text, eight symbol sets, and the eleven structure galleries (fraction, script, radical, integral, large operator, bracket, function, accent, limit and log, operator, matrix) with tiles rendered by the math engine. Typing builds up like Word (a/b, x^2, √, ∑… then space), Math AutoCorrect turns \alpha, \sqrt… into symbols, Tab moves between empty slots, arrows/Backspace work through structures, and the right-click menu has structure commands (fraction type, radical degree, limit position, matrix rows/columns, remove brackets…). A trailing #(n) numbers a display equation with the number at the right margin; # alone numbers it automatically. DOCX output keeps numbers Word-compatible. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * i18n: equation editor strings in Spanish, Brazilian Portuguese and Ukrainian The catalogs that landed on main after #191 was opened lacked the Equation tab and equation command labels. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Equations: our own names and order for the built-in equations Clean room: the ready-made equation list used another product's gallery entry names and order. Same formulas (and ids), with our own descriptive labels, grouped by subject (geometry, algebra, calculus, trigonometry, probability). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * ROADMAP: equations row matches the landed equation editor Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fmt after merge Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Brandon Thomas <echelon@gmail.com> |
||
|
|
2982e75544 |
Tables: cell text takes its table style's formatting; derived table styles keep their base (#104)
Word's Table Grid style sets single spacing and no space after (<w:pPr><w:spacing w:after="0" w:line="240" w:lineRule="auto"/></w:pPr>). Cell text ignored it and used the document defaults (8 pt after, 1.08 lines in Word's default template), so rows came out ~60% taller than in Word: an 80-row table took 3 pages instead of 2. A table style based on Table Grid that adds red bold text showed no red, no bold and no borders. - Style resolution has a table-style level (resolve_para_in, resolve_char_in), in ECMA-376 §17.7.2 order: document defaults, table style, paragraph style chain, character style, direct formatting. Any property the paragraph style chain sets, Normal included, beats the table style; the table style only replaces the defaults. (So a document whose Normal sets its own spacing keeps tall rows, as in Word.) - StyleSheet::table_style merges a table style's based-on chain: pPr, rPr and the conditional parts, borders edge by edge. Layout used the first style in the chain with any table parts and dropped the rest. - Layout passes the table style's paragraph and run formatting, with the cell's header row / total row / first column formatting on top, to each cell paragraph. Conditional run formatting now sits at the table-style level too, under the paragraph style, instead of over it. - DOCX: the wholeTable region's pPr/rPr go to the style's own formatting and its cell shading to a new whole-table fill; tblPr shd and tblCellMar of a table style are read and written (as wholeTable shading and tblCellMar). New tables use Table Grid, so tables made in WordCraft get Word's compact rows too. Co-authored-by: lloyd <lloyd@stackworx.io> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Brandon Thomas <echelon@gmail.com> |
||
|
|
e4456efb84 |
Text boxes and floating objects: editing, drag/resize, wrapping, overflow, footnotes (#46)
* fix text boxes: type into a new box, click in and out, Escape leaves it Insert › Text Box left the caret in the body after the box and clicks inside a box never reached its story, so its text could not be edited. - insert.textBox puts the caret in the new box (when anchored in a top-level body paragraph, the only place layout shows box text today). - Layout emits a hit-test-only Placed::TextBox area; story_at prefers a box under the point (body text wins over boxes behind it) and ignores box lines outside their box. - Canvas: click enters a box, clicking outside goes back to the body (not while editing a header/footer); double-click in a box selects a word there. - Escape in a box puts the caret just after it (Document::text_box_anchor). - Editing a box doesn't dim the body or show "Editing header/footer". - Tests: engine (caret in box, typing, undo, table cell unchanged), layout (story_at inside/outside/behind), doc (anchor lookup). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix pasted text boxes sharing the original's text A copied fragment only carried the text box's part id, so a pasted box showed the original's story: editing either changed both, edits made after copying leaked into the paste, and pasting into another document (or Insert › Text from File) pointed at whatever part had that id there, e.g. a header. - Fragment carries copies of the text box stories it shows (nested boxes too). - insert_fragment gives every pasted box a new story of its own; a box whose story can't be found keeps none rather than showing an unrelated part. Nesting is depth-limited and one paste creates at most 4096 stories. - Insert › Text from File uses Document::body_fragment, which brings the file's text boxes along. - Tests: doc (own story, copy-time snapshot, other document with clashing ids, box in a table, fragment without parts, self-nested box), engine (copy/paste/type/undo through commands). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * word count includes text boxes, footnotes and endnotes (Word's default) The status bar and Word Count dialog counted only the body, so words typed into text boxes (and notes) were missing. - Document::counted_stories / word_count_including_notes: the body plus the text boxes, footnotes and endnotes it actually shows (nested too); orphaned stories (deleted or cut boxes), headers, footers and comments don't count. - Session::count_notes (default on) with Session::word_count for the status bar; review.wordCount takes {"includeTextBoxes"?: bool} and reports it. - Word Count dialog: "Include text boxes, footnotes and endnotes" checkbox. - Document::word_count stays body-only (docx Words property, NUMWORDS, stats). - Tests: doc (used vs orphaned stories, nesting, self-nested box), engine (default on, toggle sticks, deleting a box drops its words). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * canvas: select, drag to move and resize pictures, shapes and text boxes Click a picture or shape (or a text box's border) to select it: a frame with eight handles. Drag it to move it, drag a handle to resize it (pictures keep their proportions from a corner, Shift toggles), arrows nudge a floating one (Ctrl/Alt: 1 pt), Escape cancels a drag. While a text box's text is edited its frame shows dashed with handles. Typing or Enter with a text box selected goes into it. Reusable pieces, no per-kind code: - layout: Placed::Object, one hit area for every picture/shape/text box (replaces the text-box-only item); DocLayout::object_at / object / text_box / find_object (page-hinted, no whole-document scans per frame). - engine: arrange.bounds {width,height,x,y,page} (floating objects keep their anchors on the same page; inline ones float where dropped; another page re-anchors to the nearest line) and arrange.nudge; objects::min_size shared with the UI. - ui: frame.rs, a document-agnostic transform frame (handles, hit testing, drag/resize maths, painting); objects.rs wires it to document objects. - geom: Rect::expand. control: ui.press / ui.release for half-way drags. Rendering: nothing is laid out or rasterised while dragging. The preview is the object's own pixels from the cached page texture (moves, picture resizes) or a light outline (text box/shape resizes); the drop is one command, one relayout, one undo step. Tests: frame maths, layout hit areas, engine move/resize/nudge across anchors and pages, hostile values. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * moving an object anchors it to the paragraph it's dropped on, so text wraps A floating object only pushes aside text from its anchor paragraph down, and a move kept the old anchor: drag a text box above its paragraph and the text there ran straight under it. - arrange.bounds now anchors the object to the paragraph under its top edge (the last one starting at or above it on that page) and positions it relative to that paragraph's column and top, as Word does: the text it lands on wraps, and it moves with that text. On a page where no paragraph starts it anchors to the nearest line, positioned on the page. - layout: Placed::Object / ObjectHit carry the paragraph origin that column- and paragraph-relative offsets start from. - engine: edit_obj / edit_float change an object at a position (with_obj uses it). - Test: a box dropped over an earlier paragraph anchors there and the lines beside it stop short of it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * text boxes hide text that doesn't fit, as in Word Text past a text box's bottom spilled out over the page below it. - layout: fit_box keeps what fits in a box: whole lines that fit (a line that only partly fits is hidden too) and other items starting inside; the first line always stays so even a tiny box shows something. Done in layout, so the canvas, PNG and PDF agree and hidden text can't be clicked; no renderer changes. Reusable for anything laid out into a fixed height. - BOX_INSET_X / BOX_INSET_Y name the text box internal margins (were 7.2/3.6 literals). - Test: overflow hidden, every shown line inside the box, a tall box shows everything, a minimum-size box shows its first line. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * square wrap flows text on both sides of a floating object Text only used the wider side of a floating picture, shape or text box; Word's Square wrap fills both sides when there's room. - para: row_spans gives the spans of a row around the objects in its way (each at least 0.5" wide, else the line goes below); a row cut in two is two Lines sharing top and height, the second marked `beside`. Everything per line (alignment and justification per side, hit testing, caret, selection, drawing, PDF) works unchanged; a row is as tall as its tallest part. - layout: a row stays on one page; line numbers count rows, not parts. - Exclusions that don't overlap a line horizontally no longer narrow it. - Test: text both sides of a mid-column box (same row, left then right, hittable), a too-narrow gap stays empty. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * drop a deleted or cut text box's text from the document Deleting or cutting a text box left its story in Document::parts: never shown or saved to .docx, but listed by document.inspect and kept in memory. - doc: prune_text_boxes removes the text box stories no box shows (nested ones too); free without text boxes, one paragraph pass otherwise. Session runs it after every editing command. Undo snapshots and the clipboard hold their own copies, so undo and cut/paste are unaffected. - doc: reachable() walks the stories objects lead to; counted_stories (word count) and prune_text_boxes share it, and it walks blocks directly instead of building paragraph paths. - Tests: orphaned and nested-orphan boxes go, shown ones (body, nested, in a header) stay; delete then undo, cut then paste. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * word count's include-text-boxes setting survives a restart The Word Count dialog's "Include text boxes, footnotes and endnotes" was a session field, so it reset to on every launch. - engine: Prefs, the editing preferences the front end saves between runs (count_notes moves there; Session::prefs is the one source of truth). - ui: SavedPrefs pairs UiState with the engine Prefs; WordApp::saved_prefs / restore_prefs. UI fields stay at the top level of ui.json, so existing files still load (the new `editing` section defaults). - wordcraft app: saves and restores SavedPrefs instead of UiState alone. - Tests: an old prefs file loads with Word's default; the setting round-trips. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * text boxes and floating objects outside the body; footnotes in text boxes Text boxes in table cells, headers, footers, footnotes and other text boxes showed an empty frame (or nothing), and floating pictures and shapes there weren't drawn. Footnotes inserted inside a text box were never numbered or placed. - layout: place_objects, one routine for the pictures, shapes and text boxes in a paragraph's lines (floating ones drawn, every one's area, text boxes' text), used by body paragraphs and by layout_box (cells, headers, footers, notes, box contents). Floats are positioned against an AnchorFrame (column, paragraph, margins, page); headers and footers pass where the page is, so page/margin-anchored objects land right (a footer is laid out again only when it has floating objects); elsewhere they're placed in their container. Nested boxes show MAX_TEXT_BOX_DEPTH (doc) levels deep. - Placed::Object / ObjectHit carry their story; DocLayout::object(&Pos), object lookups include headers and footers, header_footer_text_box_at. A part's lines are looked up in headers and footers too (caret, typing). - Footnotes: numbered in reading order with text boxes where they are (Document::objects_in_reading_order); a box's footnotes go on the page of its line (Document::notes_in_text_box), sharing the body's reservation. - engine: insert.textBox always puts the caret in the new box; disabled inside a text box (as in Word). - canvas: click into a header/footer's text boxes while editing it; leaving a box returns to where it is; body dimming computed once per frame and used for both the render and its cache key. objects: only body objects are selectable, only body paragraphs' objects movable; frames off the body show without handles. - Tests: boxes in header (inline and page-placed), footer, cell and note show text and take the caret; footnotes in a box numbered and placed in order; a box in a table cell takes the caret and its text. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * word count: a selected object counts the whole document, in the dialog too With a picture, shape or text box selected the status bar showed the document's total but Review > Word Count counted the selection (0 words). - engine: cmd::objects::object_selection, "the selection is exactly one object"; review.wordCount counts the document then. The UI's objects::selected now uses it (one definition). - Test: an object selection counts the document; a text selection itself. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * review: bound nested text box expansion; tidy-ups Blocker: laying out a text box lays out the boxes inside it, which could fan out unboundedly (a box whose shapes show itself, or chains of boxes each showing the next many times) - 30 shapes made 1.7M layout items. Footnote numbering's walk fanned out the same way. - doc: BoxBudget, one bound for both: a box isn't expanded inside itself, nesting stops at MAX_TEXT_BOX_DEPTH, an outermost box expands at most 64 boxes inside it (so one hostile box can't starve the next page's header boxes) and a pass at most 20,000. - layout: Ctx carries a BoxBudget for the whole layout; place_objects asks it before laying out a box's text. DocLayout::text_boxes reports how many were. - doc: walk_objects (objects_in_reading_order, notes_in_text_box) uses it too. - Tests (crafted documents): a box showing itself 30 times lays out once; a five-level chain fanning out 30 ways stays within the budget, for layout and for the walk; a box inside a box still shows its text. Weakening the budget makes them fail (20,000 boxes / 600,001 visits). Tidy-ups: - para: row spans in a VecDeque (pop_front, not remove(0)). - fit_box: a text box's text area excludes both the top and bottom margins. - theme: Tokens::handle / handle_shadow for the frame handles (light, dark); the drag preview's opacity is a named constant. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * box budget: no overall cap; bound the .docx writer too; tests An audit of the previous commit found: - The 20,000-box overall cap starved ordinary long documents: one budget per layout, spent by body boxes first, then one per page for header boxes (two for footers with floats), so a long document's header boxes went blank on every page. It wasn't needed: only nesting grows exponentially, and MAX_NESTED caps that per outermost box, keeping the work linear. Removed. - The .docx writer still expanded a shape's story only depth-limited (a box showing itself 30 times saved as ~28,000 nested copies, ~7 MB zipped). It now uses the same BoxBudget, and only for text box parts. - Layout expanded a shape's story whatever its part kind (a shape pointing at a header showed the header's text); now text box parts only, as the walk does. The story's blocks are cloned only once the budget agrees. - Translations of "Count words in text boxes and notes" said footnotes only; now footnotes and endnotes, in each catalog's own terms. - Tests: no timing assert or leftovers; a long document with 400 boxes and a header box shows all of them on every page; notes_in_text_box follows nested boxes once; saving a self-showing box stays small (fails at 7 MB without the bound). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Arrange: nudging or dragging an aligned float keeps it in place first main (#136) reads w:align into Float.h_align/v_align, which overrides the offset; the PR's nudge only changed the offset and drag only reset the anchors. Nudge now converts an aligned axis into a column/paragraph offset that keeps the object where it is, and a drag clears the alignment. Test included. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Fonts: a lookup that races another thread's load of the same family no longer falls back FontDb::face fell back to Source Sans 3 when its load_cataloged added nothing, which happens when another thread loaded the family between the miss and the load. word::resolve memoises that, so the wrong font stuck for the process; it made layout tests on main flaky (justified_lines_shrink_spaces_in_word_2013_mode, space_shrinking_is_only_for_justified_text failed 6 of 15 runs on origin/main). Look again after loading whatever it returns. Regression test races 8 threads on fresh databases. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Layout tests: text box and two-sided wrap tests measure what they mean square_wrap_flows_text_on_both_sides compared xs.last() (the end of a trailing space, which may hang past the edge) with the edge; compare the last non-space cluster's right edge. text_box_hides_text_that_does_not_fit compared a tall box's lines with another document's line count; compare with its own. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Weeb Labs <weeblabs@VFIEVOX3.Router> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Brandon Thomas <echelon@gmail.com> |
||
|
|
6c152055b9 |
Hidden text: no line breaks, hyphens, wrap areas or bookmark text from text that isn't shown (builds on #152) (#173)
* Tables: stop repeating a header row that leaves no room for the next row A repeating header row plus a row that could not fit below it on an empty page made place_table loop: after each page break the header was placed again, so the page was never "at top" when the row was retried, split_row had nothing to split, and the loop ran its 1,000 iterations per row. A 20-row table with a 640 pt header came out as 19,002 pages. After a page break the header now repeats only when the row (or its first split) still fits below it; otherwise the row starts at the top of the page, as Word does when a header row would fill the page. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Layout: give runs with different links or decorations their own style Builder::style memoised StyleRuns by a key that folded strike, double strike and "has a link" into one boolean and left out the underline colour and baseline position. The display list reads all of these from the shared StyleRun, so in a paragraph of like-formatted links every link pointed at the first one's URL in the PDF (and an external link after an internal one became a bookmark jump), double strike drew as single strike after a struck run, and underline colours were copied. The key is now the whole resolved format, so runs share a StyleRun only when nothing the display list reads differs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * PDF and No Markup: leave tracked deletions out of the final text PDF export defaults to no markup, but it laid the document out with every deleted run in place and only switched off the strike and colour, so the PDF printed deleted text as ordinary text ("Keep DELETEDTEXT INSERTED"): neither the original nor the final document, and a leak of text the author removed. The No Markup view did the same on screen. A new LayoutOptions::hide_deleted lays deleted runs out like hidden text (zero-width markers, so offsets, carets and hit testing keep working). PDF export sets it whenever markup is off, the session layout sets it when markup is hidden, and the markup toggles relayout as Show/Hide marks does. The display list also never draws a deletion as plain text when markup is off, whatever layout it is given. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Tables: repeat the header on every page and put the next row under it, as Word does The earlier fix for the runaway loop dropped the repeated header row on any page where the header and the next row did not fit together. Word never does that: measured on Word for Mac, it moves the row to a new page, repeats the header there and places the row under it even when the row then runs into the bottom margin (a 300 pt header with 400 pt rows gives one header + row per page; a 640 pt header with 100 pt rows does the same). place_table now remembers when the page holds only the repeated header rows. A row that cannot split is placed there anyway instead of breaking the page again, which is both Word's behaviour and what keeps the loop from spinning: every pass now places something. Rows of text taller than the room under the header still split across pages. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * PDF: keep tracked deletions out of bookmarks and heading tags Without markup the PDF pages show the final text, but bookmark titles and the titles of heading structure tags were still built from plain_text(), which includes deleted runs. A heading "Intro SECRET Part" with "SECRET " deleted printed as "Intro Part" yet bookmarked as "Intro SECRET Part", and a fully deleted heading still got a bookmark: text the author removed leaked into the outline and the tag tree. Paragraph::final_text() gives the text with deletions left out, and the exporter titles headings with it unless markup is included. A heading whose text is all deleted has an empty title and so no bookmark. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * No Markup: deleted note references take no number and print no endnote With deletions hidden, a tracked-deleted footnote or endnote reference disappears from the text, but note_numbers() still counted it. Deleting the first of two footnote references left the surviving one numbered 2, and a deleted endnote reference still printed its endnote after the last paragraph, because the endnote list is taken from the same map. note_numbers() now skips references inside tracked deletions when the layout hides them, so the remaining notes number from 1 and endnotes whose reference was deleted are not emitted. With markup shown nothing changes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * No Markup: a deleted floating object leaves no wrap area behind With deletions hidden, a tracked-deleted floating picture or shape was not drawn, but place_para had already added its wrap exclusion from p.objects without looking at the anchor, so the final text still flowed around an invisible object (indented beside a square-wrapped shape). Floating objects whose anchor is a tracked deletion are now skipped when the layout hides deletions; deleted_objects() is shared with note numbering, which needs the same test. With markup shown nothing changes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * No Markup: a deleted soft hyphen no longer breaks the word with a hyphen With deletions hidden, a tracked-deleted soft hyphen became a zero-width marker, but hyphenation_points() reads the paragraph's raw text and accepted marker clusters, so the line could still break there and draw "ab-" in a narrow column although the final text has no soft hyphen. When the layout hides deletions, clusters inside a tracked deletion are no longer hyphenation points (soft hyphens or dictionary points alike). With markup shown nothing changes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * No Markup: tracked deletions add no line-break opportunities Break opportunities came from running UAX #14 over the paragraph's raw text, deleted runs included. With deletions hidden, a deleted soft hyphen or space still let the line break there: "xx ab<del>SHY</del>cdef" in a narrow column came out as "xx ab" / "cdef" instead of moving the word "abcdef" to the next line whole. When the layout hides deletions, the opportunities are computed over the text without them and mapped back to offsets in the paragraph. With markup shown, or with no deletions, the computation is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Hidden text: no hyphenation points or line breaks in text that is not shown Hidden text that is not shown (the default, and always in print and PDF) is laid out as zero-width markers, but break opportunities came from UAX #14 over the paragraph's raw text and hyphenation_points() accepted marker clusters, so a hidden space or soft hyphen still let the line break there: "xx ab<hidden SHY>cdef" in a narrow column came out "xx ab-" / "cdef". Measured in Word for Mac, the hidden soft hyphen and the hidden space are no place to break: "xx" / "abcdef". left_out() is now the one test for text the layout leaves out (hidden text not shown, tracked deletions without markup). The run loop records the ranges it leaves out, and both the break opportunities and the hyphenation points come from that, so deletions behave exactly as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Hidden text: a floating object anchored in it leaves no wrap area behind With hidden text not shown, a floating picture or shape anchored in a hidden run was not drawn (the run is a zero-width marker, so no object cluster places it), but place_para had already added its wrap exclusion, so the text still flowed around an invisible object. Measured in Word for Mac: a square-wrapped picture anchored in hidden text is not printed and the text runs the full width. deleted_objects() becomes left_out_objects(), which resolves each anchor's formatting through the styles (hidden text often comes from a character style) and applies the same left_out() test as the paragraph layout. Note numbering keeps counting hidden reference marks, as Word does, so it asks only about deletions; its behaviour is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Hidden text: print the footnote of a hidden reference mark, as Word does A footnote whose reference mark is hidden text was numbered but never printed: the hidden run became zero-width markers without recording the note, so place_para never put the footnote at the foot of the page. Measured in Word for Mac with hidden text not printed, a hidden footnote or endnote reference still takes its number (the next visible footnote is 2) and both notes are printed. Endnotes already were, and the numbering already matched. A hidden reference mark now records its note like a visible one, so the footnote goes to the foot of the page holding the line it sits on. A deleted reference in the final text still records nothing, as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * PDF: keep hidden text out of bookmarks and heading tags The PDF never prints hidden text (its layout does not show it), but the bookmark and heading-tag titles were built from final_text(), which drops tracked deletions only. A heading "Intro SECRET Part" with "SECRET " hidden printed as "Intro Part" yet bookmarked as "Intro SECRET Part", and a heading that is all hidden text still got a bookmark for a heading nobody can see. Paragraph::text_where() generalises final_text() to any per-run test, and the exporter titles headings with the runs that are neither hidden (resolved through the styles, since a character style can hide text) nor, without markup, deleted. A heading that is all hidden has an empty title and so no bookmark. Word's PDF export from AppleScript writes no outline, so there is no Word answer to match; this follows what the pages show, as for deletions. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * PDF: titles leave out what the layout left out, table formatting included Bookmark and heading-tag titles resolved hidden text from the paragraph and run formatting alone, but the layout also applies the table style's conditional formatting (header row, first column, total row) beneath the runs. A Heading 1 in a table header row whose style hides it printed nothing yet still got a bookmark, and a partly hidden heading leaked the hidden part into its tag. ParaLayout now records the byte ranges it laid out as nothing (left_out), resolved exactly as laid out, and the exporter titles a heading from the text outside them (plus tracked deletions without markup, as before), so the titles cannot disagree with the pages. text_where() gives way to text_without() over byte ranges; final_text() is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Hyphenation: hyphenate words as laid out, without hidden or deleted text Automatic hyphenation looked up the words of the paragraph's raw text and only dropped points that fell inside text the layout leaves out. Words and the minimum-letters limits still came from the raw text: with "accep" hidden in "acceptable", the visible "table" could break as "t-able" (the point after "accept"), and a hidden space split "foot<space>ball" into two words, so the visible "football" never hyphenated. Soft hyphens and dictionary points are now found in the same text as laid out that the UAX #14 break opportunities already use, and mapped back to paragraph offsets. With hidden text shown and markup on, that text is the paragraph's own and nothing changes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Layout, PDF: skip left-out text in one pass, not a scan per char Building the text as laid out tested every char against every left-out range, and Paragraph::text_without did the same for PDF titles. Hidden text in many runs (formatting that alternates, a common result of imports) made both quadratic: a 100,000-char hidden heading in one-char runs took 13.9 s to convert with the release CLI, 400,000 chars minutes. The layout now merges adjacent left-out runs as it records them and walks the ordered ranges with a cursor; text_without sorts its ranges by start (they may come in any order and overlap) and walks them the same way. Both are linear: 0.20 s for that heading, 0.81 s at 400,000 chars, with the same output. No left-out text still means no filtering at all. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Hyphenation: check the limits of each point in constant time hyphen_points() found each candidate point's letter run by scanning out from the point and counted the letters on each side by rescanning the run, and the syllable heuristic rebuilt the rest of the word as a string for every position. Both are quadratic in the word's length. Ordinary words are short, but with hidden separators the layout now hyphenates the visible word, so "hyphenation " repeated 8,192 times with every space hidden is one 90,112-letter word: 2.0 s to convert with the release CLI. Letter and uppercase counts before each char, the letter run of each char and the heuristic's next vowel and byte offsets are now computed once, so every point is checked in constant time: that document converts in 0.09 s. The output is unchanged: the old functions stay in the tests as the reference, compared over awkward words and every combination of limits, and a hyphenated document's PDF text is identical. No maximum word length is imposed: the work is linear, so a cap would only change output. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Layout: find the formatting of every object anchor in one walk left_out_objects() asked props_of_char() for each floating object's anchor, and props_of_char() scans the runs from the start, so a paragraph with many anchors in their own runs cost a scan per anchor, quadratic, and paid it on every layout because it runs before the paragraph cache is consulted: 40,000 hidden anchors took 0.69 s, against 0.05 s for 10,000. The runs and the object offsets are both in order, so one walk now finds each anchor's run, resolving a run's formatting once; past the last run it is the paragraph mark, as props_of_char() gives. With hidden text shown and markup on nothing can be left out and no work is done at all. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Layout: find a run's style without formatting it as a string #152 keyed each run's style by its whole format's Debug string, built for every shaped piece; on a long document with many runs that doubled layout time (3000 paragraphs x 40 linked runs: 12.7 s vs 6.4 s for the old key). Key by a hash of the commonly varying fields and confirm by comparing formats: 3.8 s. A capped bucket scan keeps formats that never compare equal (NaN) linear. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Layout: floats hidden in table cells take no room, table style included Test the layout_box path the merge extended (a float anchored in hidden text in a table cell is not placed and leaves no wrap area), and resolve the anchors' formatting with the table style's character formatting under the runs' own, as the cell's text is laid out. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Tests: linear-time checks compare 8x the input against a 24x limit At 4x with an 8x limit a busy machine could push a linear layout over the line (8.8x seen once in a full workspace run); at 8x a linear layout takes about 8x and a quadratic one about 64x, so 24x stays far from both. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Brandon Thomas <echelon@gmail.com> |
||
|
|
b64153f604 |
Layout: shrink spaces in justified lines like Word 2013+ (compatibility mode 15) (#109)
Word 2013 and later fit more text on a justified line by shrinking its spaces by up to 20% of their width, so long justified paragraphs broke differently in WordCraft (a one-page test document's summary took 8 lines instead of Word's 7). Lines that only fit with shrunk spaces now get them, the last line of a paragraph included; lines with tabs never shrink. The behaviour depends on the document's compatibility mode, which is now read from settings.xml (w:compatSetting compatibilityMode), kept in Settings::compat_mode and written back, so saving an older file no longer silently upgrades it to mode 15. A .docx that names no mode is treated as Word 2007 (12); new documents are 15. Co-authored-by: ssamstudio <267867645+ssamstudio@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Brandon Thomas <echelon@gmail.com> |
||
|
|
7c951a5490 |
Layout: picture line heights, body top below the header, table row heights (#138)
* Layout: picture line heights, body top below the header, effect extents Headers with a logo or rule picture put the body in the wrong place in Word documents, for three reasons: - A line holding only a picture was exactly as tall as the picture. Word keeps the run's font ascent and descent: the picture stands on the baseline and the line is the taller of the font's line and the picture plus the font's descent. Multiple line spacing scales the font's line height only, never the picture (a 144pt picture at 1.15 made a 165.6pt line; Word's is 146.4pt). - The body started 6pt below a header that reaches past the top margin. Word starts it right where the header ends. - wp:effectExtent (room Word keeps around a picture for its shadow or glow) was dropped. It is now kept on Float::effect, read and written, and inline pictures reserve it in their line and are drawn inside it. The reader also takes w:lineRule (and w:hRule) case-insensitively, as Word does: `atleast` was read as multiple spacing. Measured against the Word reference PDFs of docxide-pdf's corpus (365 documents, same open fonts for every run): mean SSIM 39.3% -> 42.1%, documents passing SSIM 37 -> 51, mean ink overlap 19.5% -> 22.3%, page count right 301 -> 304; 52 documents better, 6 worse (one header built from nested tables had its own height error hidden by the old 6pt gap). Tests: picture lines are the picture plus the font's descent under 1.15 spacing; the body starts where a tall header ends; effect extents grow the picture's line box and the picture is drawn inside them; effect extents round-trip through .docx; `atleast` and `EXACT` line rules read. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Layout: table rows keep their cell margins and border bands like Word Word sizes a table row from the inside of its borders out: the cell's text starts below the top border and the top margin, and an at-least row height gets the cells' top and bottom margins and border widths on top (a 30pt row with 5pt margins and 0.5pt borders steps 40.5pt). We drew the text at the top margin and took an at-least height as the whole row, so rows came out short and tables, and headers built from tables, ended early: a form's header table ended 6pt above where Word starts the body. Each row now reserves its cells' top border width above their text (the last row also the bottom border below it), and an at-least height adds the tallest margins plus border bands in the row. Exact heights are unchanged. Measured against the Word reference PDFs of docxide-pdf's corpus (365 documents, same open fonts), on top of the previous commit: mean SSIM 42.1% -> 42.8%, documents passing SSIM 51 -> 54, mean ink overlap 22.3% -> 23.3%, mean line-break agreement 63.0% -> 63.5%; 42 documents better, 28 worse, mostly by a few points where an earlier offset had cancelled the short rows. Test: two 30pt at-least rows with 5pt margins and 0.5pt borders step 40.5pt (was 36.3), with the first cell's text 5.5pt below the table top. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Layout: note the known limit of the picture-line rule The space Word leaves below a line of only pictures is less than the font's full descent we give it; two rules tried against docxide-pdf's Word references (no descent, only the text's descent) each fixed some documents and broke others. A shortcut: comment records the measured cases so the rule isn't changed again without Word probes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Oliver Zieschang <4963183+naxvog@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Brandon Thomas <echelon@gmail.com> |
||
|
|
da37b7987e |
Character border (Home > Font > Character Border) (#60)
* Character border (format.border): w:bdr run property, toggle command, boxed rendering * Character border: Home > Font ribbon button and icon * Character border: address review (plain notes), translate ribbon label, fix flaky layout test and clippy nonminimal_bool * Tests: merge the fixture imports after merging main Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * i18n: Spanish and Brazilian Portuguese labels for Character Border Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: WordCraft agent <agent@localhost> Co-authored-by: 999sian <999sian@users.noreply.github.com> Co-authored-by: Brandon Thomas <echelon@gmail.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
6e73fe40e3 |
Tables: Word 2013 table edge, and floating tables (w:tblpPr) (#137)
* Layout: place floating pictures and shapes the way Word does Pictures and shapes anchored in a .docx often landed in the wrong place: the reader folded every margin-relative position into "margin", ignored <wp:align>, and kept only distL as the distance from text. Floats in headers, footers and table cells were not drawn at all. Document model (doc): - Anchor gains the left/right/top/bottom/inside/outside margin areas, the anchor character and the anchor line - Float gains h_align/v_align (FloatAlign: start, center, end, inside, outside), which take precedence over the offsets, and separate dist_top/dist_bottom (dist stays the left/right distance) DOCX (docx): read and write wp:align, every relativeFrom value and distT/distB/distL/distR. Layout (layout): - float_rect positions against each reference area, with alignment - floats in headers, footers, table cells and text boxes are placed, drawn (behind-text ones under the text) and wrapped around; headers and footers get a page frame so page-relative floats land in page coordinates, also when the header's height is measured for the body top - a paragraph's first line pushed below a top-and-bottom float is drawn (and hit-tested) where it was laid out - floats follow their paragraph to a new column or page, and are placed at the paragraph's actual top (space before counted once, contextual spacing respected) - wrap areas use each distance as given; the old 9pt minimum is gone, so a float with distance 0 now touches its text as in Word - a list item's label is counted once (Ctx::next_label) and passed into every layout of its paragraph (Ctx::para_labelled), so laying it out again around floats doesn't advance the list counters twice; a paragraph whose top moved is only laid out again when the page has something to wrap around Engine: switching an inline object to a wrapping style gives it Word's default 9pt side distance; the position presets clear any alignment. Approximations: inside/outside margins and alignments ignore mirrored margins (treated as left/right); character- and line-relative positions fall back to the column and the paragraph top. Tests: docx fixture and round trip for alignment, reference areas and distances; layout tests for alignment, the pushed-down first line, floats moving to page two (with space before), header and cell floats, header floats not changing the body top, hostile distances, and list items counted once when laid out again (contextual spacing, page break before, beside a float in a table cell). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Tables: place a table's edge by the document's Word compatibility mode Word 2013 and later (compatibilityMode 15) put a left-aligned table's border at the margin plus its indent, moved in by half the first cell's left border; earlier modes line the first cell's text up with the margin instead, the border one cell margin further out. We always did the latter, so in Word 2013+ documents (43% of docxide-pdf's Word reference corpus) every table and its text sat a cell margin (usually 5.4pt) too far left. Settings::compat_mode keeps the document's mode: read from w:compat/w:compatSetting, 12 when a document has no settings or doesn't state one (as Word treats it), 15 for new documents, and written back instead of a fixed 15. The legacy shift now uses the first cell's own left margin. Measured against docxide-pdf's corpus (365 documents, same open fonts): mean SSIM 40.4% -> 42.1%, documents passing SSIM 40 -> 50, passing ink overlap 142 -> 154; 71 documents better, 6 worse (tables nested in a cell whose text Word puts at the outer cell's edge, which docxide-pdf doesn't match either). Tests: a mode-15 table's text sits a cell margin and half a border inside the margin, a mode-12 table's at it; the mode round-trips through .docx and defaults to 12 without settings. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Layout: floating tables (w:tblpPr) stand at their position, text wraps Floating tables were read as ordinary tables: laid out in the text flow with no regard to their position, and the text after them waited below instead of running beside them. 71 of the 382 documents in docxide-pdf's Word reference corpus have them, typically forms anchored to the text and the margin. TableProps::float (TableFloat) keeps w:tblpPr and w:tblOverlap: what x and y are measured from (column, margin or page; text, margin or page), offsets or alignments, distances from text and whether it may overlap other floating tables. The .docx reader and writer carry it. A floating table at the top level of the body is placed at its position (tblpY below the text where it stands, by default) and takes no room in the flow; its area plus its distances from text joins the page's wrap areas, so the following text runs beside it or below it. One that may not overlap moves below the floating tables in its way, keeping (in Word 2013+ layout) its distance from the text's left edge; in earlier modes an offset places the first cell's text, as an indent does. A table in the flow starts below the floating objects in its way. Floating tables inside table cells and headers are still laid out in their flow. Measured against docxide-pdf's corpus (365 documents, same open fonts), on top of the compatibility-mode commit: mean SSIM 42.1% -> 43.9%, mean line-break agreement 63.5% -> 66.6%, documents passing SSIM 50 -> 55, page count right 303 -> 305; 33 documents better, 4 worse by at most 3.1 points. Tests: a full-width and a narrow floating table that may not overlap stack, the narrow one keeping 9pt from the text edge, and the next paragraph runs beside it; floating table positions round-trip through .docx. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Layout: a floating table taller than a page runs across pages A floating table was always placed whole at its position, so one taller than the page's body ran past the bottom margin and its rows were lost: a Bulgarian programme whose whole body is one floating table came out as 3 pages instead of Word's 6. Such a table now flows across pages like a table in the text (rows splitting and header rows repeating as usual), from the floating table's own left edge; the position is worked out by float_table_x, which the whole-table placement shares. In docxide-pdf's Word reference corpus that document has Word's page count again with all its rows; nothing else changes. Test: an 80-row floating table runs onto a second page, every row drawn above the bottom margin from the table's left edge (was one page). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Tidy floating-table code: one alignment helper, named steps, shared mappings No change in behaviour; makes the floating-table code easier to read. - FloatAlign::from_ooxml/ooxml (added with floating pictures) replace the copies of the same mapping in the w:tblpPr reader and writer, and TableFloat::dist_from_text the copies of its clamping. - place_floating_table and float_table_x place by offset or alignment with align_in, like floating pictures do, instead of their own copies. - The overlap rule uses Rect::inset/intersects and names its condition (keeps_text_distance) instead of a one-line chain of comparisons. - below_floats names the step that starts a table in the flow below the floating objects in its way. - The floating-table functions move next to place_table, which calls them, from after `mod tests;` at the end of the file. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Oliver Zieschang <4963183+naxvog@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Brandon Thomas <echelon@gmail.com> |
||
|
|
c7efead47b |
Layout: place floating pictures and shapes the way Word does (#136)
Pictures and shapes anchored in a .docx often landed in the wrong place: the reader folded every margin-relative position into "margin", ignored <wp:align>, and kept only distL as the distance from text. Floats in headers, footers and table cells were not drawn at all. Document model (doc): - Anchor gains the left/right/top/bottom/inside/outside margin areas, the anchor character and the anchor line - Float gains h_align/v_align (FloatAlign: start, center, end, inside, outside), which take precedence over the offsets, and separate dist_top/dist_bottom (dist stays the left/right distance) DOCX (docx): read and write wp:align, every relativeFrom value and distT/distB/distL/distR. Layout (layout): - float_rect positions against each reference area, with alignment - floats in headers, footers, table cells and text boxes are placed, drawn (behind-text ones under the text) and wrapped around; headers and footers get a page frame so page-relative floats land in page coordinates, also when the header's height is measured for the body top - a paragraph's first line pushed below a top-and-bottom float is drawn (and hit-tested) where it was laid out - floats follow their paragraph to a new column or page, and are placed at the paragraph's actual top (space before counted once, contextual spacing respected) - wrap areas use each distance as given; the old 9pt minimum is gone, so a float with distance 0 now touches its text as in Word - a list item's label is counted once (Ctx::next_label) and passed into every layout of its paragraph (Ctx::para_labelled), so laying it out again around floats doesn't advance the list counters twice; a paragraph whose top moved is only laid out again when the page has something to wrap around Engine: switching an inline object to a wrapping style gives it Word's default 9pt side distance; the position presets clear any alignment. Approximations: inside/outside margins and alignments ignore mirrored margins (treated as left/right); character- and line-relative positions fall back to the column and the paragraph top. Tests: docx fixture and round trip for alignment, reference areas and distances; layout tests for alignment, the pushed-down first line, floats moving to page two (with space before), header and cell floats, header floats not changing the body top, hostile distances, and list items counted once when laid out again (contextual spacing, page break before, beside a float in a table cell). Co-authored-by: Oliver Zieschang <4963183+naxvog@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Brandon Thomas <echelon@gmail.com> |
||
|
|
bb88278b98 |
PDF: put the shown text of fields and note numbers in the text layer (#133)
* PDF: fields and note numbers put their shown text in the text layer A field, a note reference or an equation is shaped as one cluster that stands for the object's placeholder character (U+FFFC) in the paragraph text. The display list took a glyph run's text from the paragraph, so a footer's "Page 1 of 5" went into the PDF's text layer as "Page  of ": copying, search and screen readers got the placeholder. 76 of the 365 PDFs written from docxide-pdf's Word reference corpus had it; Word's have none. ParaLayout::shown keeps, by cluster index, the text such a cluster draws (filled by Builder::shape_atomic), and the display list takes that text for those clusters. The drawing doesn't change. Now none of the 365 PDFs carries the placeholder; mean line-break agreement with Word 62.9% -> 63.4% (the scorer reads the text layer). Test: a footer's PAGE and NUMPAGES fields read "Page 1 of 2" and "Page 2 of 2" in the exported PDF's text (was "Page  of "). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Tests: a REF field's result is extractable PDF text (from #112 by @LloydNicholson) Co-Authored-By: LloydNicholson <LloydNicholson@users.noreply.github.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Oliver Zieschang <4963183+naxvog@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Brandon Thomas <echelon@gmail.com> |
||
|
|
8907b589ac |
Tables: draw style borders when w:tblBorders is empty (#143) (#148)
* Ignore the .superpowers/ agent scratch directory * doc: Borders::overlay merges border sets per side (#143) * Tables: draw style borders when w:tblBorders is empty (#143) * docx: fixture pinning empty tblBorders/tcBorders parse contract (#143) * Tables: border presets mask cleared sides with nil over the style (#143) * Drop a stray tool entry from .gitignore Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Brandon Thomas <echelon@gmail.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
869ac16110 |
Typing parity with Word: lists, AutoFormat and AutoCorrect as you type (#204)
* Typing parity with Word: lists, AutoFormat and AutoCorrect as you type
Typed side by side with Word (same keystrokes, results read back) and matched it:
- Enter after a list no longer gets stuck: the paragraph that leaves a list goes back to
Normal, and later Enters make blank paragraphs (the `num: 0` marker counted as a list item,
so every Enter re-ran "leave the list" and did nothing; the list indent also stuck to every
following paragraph).
- Enter on an empty nested item moves it up a level before leaving the list.
- Second Backspace at the start of an un-numbered list paragraph returns it to Normal
instead of joining it to the paragraph above.
- "1. " always starts numbering at 1; "a. " and "I. " start lettered/roman lists; "- " makes
a dash bullet.
- Undo right after an AutoFormat/AutoCorrect change undoes only that change.
- AutoCorrect: sentence capitals (with abbreviation/initial exceptions), runs on Enter too,
"word -- word" en dash, "wait..." ellipsis, 1/2 1/4 3/4, ordinal superscripts, web
addresses become links.
- Border lines: Enter after ---, ===, ***, ___, ~~~, ### draws a bottom border.
- Layout: no line break right after a slash ("and/or", URLs), as in Word.
- ui_shot: UI_SHOT_FULL=1 prints whole replies for scripted checks.
Regression tests in crates/engine/src/tests_typing.rs and the layout tests.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Typing parity: record Word's observed typing rules, two more tests
docs/typing-parity.md lists what Word does for each key sequence (lists, Backspace, Tab,
AutoCorrect, AutoFormat, slash line breaks) with the test that pins it, the known gaps, and how
to compare without taking over the shared machine. CLAUDE.md/AGENTS.md point at it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
|
||
|
|
0edb87e3da | draw Symbol and Wingdings bullets as Unicode when the font is missing (#29) (#37) | ||
|
|
daf9a99eb2 |
Layout: first-page header test no longer depends on installed fonts (#65)
tall_first_page_header_pushes_body_down compared page 1's first line with the last paragraph on the last page. Where Aptos falls back to Liberation Sans (Linux without Carlito), the last page is nearly full and that paragraph sits below page 1's first line, so the test failed although layout was correct. Compare with the first paragraph on page 2 instead, which is what the test means: later pages don't reserve the tall first-page header's space. Co-authored-by: Dmitrii Khlapov <19991457+dkhlapov@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
a9ce05d2db |
fix first-page header layout and tiff header images (#1)
- body on a titlePg first page now starts below the first-page header - decode tiff pictures - fade images through an opacity layer; vello_cpu panics on image sampler alpha |
||
|
|
f38710fcb6 |
Layout: automatic hyphenation (dictionary + patterns, 0.25" zone), soft hyphens show a hyphen at line end
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
4c63bb0230 |
Layout: drop caps (dropped letter sized to N lines, text indented beside it)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
e8c50e9dde |
Layout: table rows split across pages between lines (cantSplit honoured, header rows repeat)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
6379a25ce6 |
Layout: text wrap around floating objects, text box contents, page borders (from page edge), column separators, vertical alignment, line numbers; cli run --page/--scale
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
f3dbc31a57 |
README with screenshots, Crafting Apps table, community links; proofing ignores deletions/objects; comment shading; hhea line metrics; watermark tuning
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
d95e09ba25 |
Clippy/fmt clean workspace, ROADMAP with honest parity estimate; xtask ci green
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |