Repository navigation
Releases: jandira-tech/jubarte-redlines
Release list
jubarte v0.12.0
jubarte FILE prints an agent view of a document or (jubarte A B) of its redline; edit and add take flag operations; text diffs in github, word, normal, context and side-by-side views; PDFs can list comments after the last page (--move-comments) and keep only changed pages (--changed-only). Redlines no longer hang Word 16.115 on an empty deleted PAGE field or a one-sided header table, and drop unused headers and footers. PDF text no font covers stays searchable, and math letters read back as math letters. rdocx-opc 0.2 fixes RUSTSEC-2026-0194. On two 600-item samples scored against Word's own output: redlines mean 71.89 (Docxodus 64.95), counting as 0 the 23 redlines Word 16.115 crashes opening (KNOWN_ISSUES #8); PDFs mean 79.19 (LibreOffice 54.61), no failed document.
Summary. jubarte FILE and jubarte read print an agent view (a YAML header, then the document as Markdown with paragraph ids, CriticMarkup changes and comments), and jubarte A B prints its redline's view instead of writing a file (-o writes it; compare A B is unchanged). jubarte edit and the new jubarte add take their operations from flags. Text diffs come as github, word, normal, context and side-by-side views. convert --move-comments lists comments after the last page with [JR1] markers, --changed-only keeps only the pages a change touches, and fields update writes Roman and letter NUMPAGES, PAGEREF and SEQ results as Word does. Redlines no longer hang Word 16.115 on a deleted empty PAGE field or a header table only one version has, and drop headers and footers no section uses. PDF text no font has glyphs for stays searchable, and math letters painted from their ASCII italic read back as math letters. rdocx-opc 0.2 fixes RUSTSEC-2026-0194. On two 600-item samples scored against Word's own output: redlines mean 71.89 (Docxodus 64.95), counting as 0 the 23 redlines Word 16.115 crashes opening (KNOWN_ISSUES #8); PDFs mean 79.19 (LibreOffice 54.61), no failed document.
Docs. docs/rust.md, docs/python.md and docs/javascript.md carry the regenerated CLI and API references; docs/TEXT_DIFF.md is new; examples/cli/help-cli.md and examples/cli/visualize/ show the agent view and every command's help; the library READMEs and their fragments document read, edit, add and the new PDF options; KNOWN_ISSUES.md records #9 (WASM fonts, text layer fixed) and #10 (float re-layout); docs/WORD_DIFFERENCES.md lists the open Word differences; the root plan files moved under docs/.
Added
-
jubarte FILEprints the agent view (read FILE; a lone name without
an extension needsread);jubarte A Bprints
the agent view of their redline, with thereadoptions (-o FILEwrites
it instead;compare A Bstill writes<A>_v_<B>.docx). Same on the
Python and npm CLIs. -
jubarte edit FILE -p WHERE --anchor TEXT --content TEXT(also
--delete,--resolve,--style) and the newjubarte add(a paragraph,
a comment, or a reply toc5) take their operations from flags: each-p
starts one, so one command carries several.--author,--datetime,
--existing-revisions(defaultauto: keep a file's tracked changes) and
--editing-mode(clean copy and report only) apply to all of them;
--out-dirdefaults to<stem>.editbeside the file. After applying, both
print the changed blocks as the agent view (the patch stays in
patch.diff). Same flags on the Python and npm CLIs; library:
jubarte::edit::flags::plan_from_flags,jubarte::markdown::changed_view,
Python_native.flag_plan/changed_view, WASMflagPlan/changedView. -
Document text diff views:
github(unified,text),word,normal,
contextandside-by-side, across Rust, Python and WASM. Line views
preserve existing CriticMarkup; word diff accepts all changes in both
documents before making fresh marks.--accept-changes,-U/--context
and--full-linescontrol the view. Long lines use a 70-character window
around the first change. Text output creates no implicit Word file. -
convert --move-comments(also ondiff; library:
PdfOptions::comments = CommentPlacement::End): the comments leave the
balloon pane and are listed after the last page, each with its marker,
author and page. The page keeps its own width, the commented text keeps
its tint, and a superscript[JR1]in the author's ink marks where each
comment is anchored (a reply rides on its thread's marker). The default
stays Word's balloons beside the text. -
convert --changed-only(also ondiff; library:
PdfOptions::changed_only): the whole document is laid out, then only
the pages a change bar marks are kept, so page numbers and "Page N of M"
stay the document's. A document without body changes keeps its first
page. With--move-comments, only the kept pages' comments are listed. -
PdfOptionshas two more public fields (comments,changed_only): a
struct literal that names every field needs..PdfOptions::default(). -
Python and JavaScript take both options. In Python they are
PdfOptions(move_comments=…, changed_only=…)forDocument.to_pdf,
to_pngandrender, keyword arguments ofdocx_to_pdf, and
python -m jubarte_redlines convert --move-comments / --changed-only.
In JavaScript they aredocxToPdf's two trailing arguments and
npx jubarte-redlines convert --move-comments / --changed-only. -
fields updatewritesNUMPAGES,PAGEREFandSEQresults in
\* romanand\* alphabeticas Word does: the switch's first letter
picks the case, letters past z repeat (aa, bb, ...), the last\*
switch wins, and past 780 in letters or 32767 in Roman numerals the
result is Word's "Error! Number cannot be represented in specified
format." ASEQidentifier with either format keeps counting. Other
formats still keep the cached result.
Security
- Upgrade rdocx-opc to 0.2 with quick-xml 0.41, fixing RUSTSEC-2026-0194
and RUSTSEC-2026-0195 across the native, Python, WASM and in-process
graphs, including the publicRelationships::from_xmlparser. Keep
the 256-attribute OPC admission budget and remove the advisory exceptions
and obsolete WASM-only dependency patch. Relationship/content-type parser
behavior is preserved; consumers sharing dependency types must also use
rdocx-opc 0.2 (the re-exported XML errors now use quick-xml 0.41). - Add direct relationship-parser regression tests and fuzzing. Fuzz checks
now block CI; native runs instrument mimalloc's C code and enable its
AddressSanitizer allocation tracking alongside Rust instrumentation. - Add contribution and private vulnerability-reporting policies, daily
dependency/history-secret checks, mandatory release security checks and
an OpenSSF criterion evidence ledger.
Changed
-
jubarte A Bno longer writes a file unless-ois given: it prints the
redline's agent view.jubarte compare A Bis unchanged. -
editprints the redline's changed blocks as the agent view instead of
the patch (patch.diffis still written);--planand--out-dirare
optional. -
jubarte read(aliastext) prints the agent view: a YAML header
(authors with handles and timestamps, owner, page setup, styles,
headers, footers, sections), an<!-- pN -->id line before every
paragraph (same numbering asedit), tracked changes as CriticMarkup
followed by a note with theirw:idand author handle, comments with
their ids and thread parents, page markers from the layout pass, and
-p/--head/--tail/--changed(only the blocks with marks;
--by AUTHORfor one author's) /--comments none/--dates/
--no-page-markers.editselectors take the view's short ids (p3,
header1.p1,t0.r1.c2). The previous[body:p:N]projection remains
available asinspect::markdownin the library bindings. -
CLI commands, help, aliases, defaults and usage validation now share a
clap-derived schema across native, Python and WASM.compare(alias
redline) is discoverable alongside the existing two-file shorthand. -
Python extensions avoid symbol stripping that can corrupt macOS
Mach-O metadata and prevent startup. -
The desktop app is no longer tracked here: it is its own repository
(arthrod/jubarte-app), cloned untracked atjubarte-app/or named by
JUBARTE_APP_DIR.scripts/release.shno longer bumps or commits the
app's files. It runs the app'sscripts/release-engine.shinstead,
with--preflightat step 0 and in full as step 13: jubarte.pro, the
app's facts, and its version files on arelease/vX.Y.Zpull request.
Step 14 is folded into it.release_downstream.sh,check_site_live.py
andcheck_release_facts.pymoved to the app with their tests. The
app's CI builds the app, and the app has its own Dependabot config.
Fixed
- Compare: a deleted or inserted field with no shown result
(<w:fldSimple w:instr="PAGE"/>) stayed live in the redline, so
rejecting or accepting kept the wrong page numbers, and Word 16.115 hung
on such a footer. It is now tracked the way Word's own redline does. - Compare: a header or footer that only one document has now marks its
table rows deleted or inserted along with their text; Word 16.115 hung
on a footer table whose cells were deleted but whose rows were not. - Compare: a redline no longer keeps the original's headers and footers
that no section uses once the revised document's section breaks bring
their own. Word's own redlines carry none. Their half-matched page
number fields madejubarte validatereport them as Word-fatal. - Removing a header, footer or other related part (redline, Accept, Reject,
markdown export) also removes the images and other parts that only it
used, instead of leaving them in the package with nothing pointing at
them. jubarte validateno longer calls two shapes Word opens Word-fatal: a
table cell that ends in a content control holding its last paragraph
(its repair ...
jubarte v0.11.3
jubarte convert reads Word 97-2003 .doc files (text, headings, lists, bold and italic, tables), writes markers into Markdown, and stops at --timeout with exit 124. PDFs set OMML math in Cambria Math with Word's delimiters and centring. The font report counts a face guessed from a name's class as substituted. A package repeating an identical relationship Id opens repaired. MSRV is Rust 1.94. On two 600-item samples scored against Word's own output: redlines mean 72.06 (Docxodus 65.15), counting as 0 the 23 redlines Word 16.115 crashes opening (KNOWN_ISSUES #8); PDFs mean 79.14 (LibreOffice 54.61), no failed document. The evidence (samples with a sha256 beside every file, per-document scores, paired intervals) is in release_info/; the 23 Word crashes are KNOWN_ISSUES #8.
Summary. jubarte convert reads a Word 97-2003 .doc (text, headings, lists, bold and italic, tables) and marks each page in Markdown output (); --timeout bounds a conversion. PDFs set OMML math in Cambria Math as Word does. The font report counts a face guessed from a name's class as substituted, and a repeated identical relationship Id opens repaired. MSRV is Rust 1.94. On two 600-item samples scored against Word's own output: redlines mean 72.06 (Docxodus 65.15), counting as 0 the 23 redlines Word 16.115 crashes opening (KNOWN_ISSUES #8); PDFs mean 79.14 (LibreOffice 54.61), no failed document.
Docs. README.md, docs/rust.md and the four registry READMEs state MSRV 1.94. docs/rust.md, docs/python.md and docs/javascript.md carry generated CLI and API references. docs/adoption/ and examples/adoption/ are new: one page per agent skill jubarte replaces, each row with runnable evidence; the .doc rows read released. KNOWN_ISSUES.md #8 records the Word 16.115 crash.
Added
jubarte convert old.docreads a Word 97-2003.doc: it reads the OLE
compound file and the Word 97 piece table into text, paragraphs, Title
and Heading 1-9, one-level tables (header rows kept), field results,
bold and italic runs, and bulleted and numbered lists with their
nesting. The default output is.docx;-t md, PDF and PNG go through
the converted package. Encrypted, Word 6/95 and non-Word compound files
stayLEGACY_DOC, and malformed structure is refused, never guessed: a
reversed, gapped or truncated piece table, a table outside its stream,
a repeated FAT sector, an invalid bold/italic toggle. The other commands
still refuse a.doc, now with the convert command in the hint.jubarte convert FILE -t mdwrites<!-- page N of M -->before the
first block of each page. The page comes from laying the document out
and matching each block's opening text on the painted pages.
--no-page-markersopts out. The markers are HTML comments, so the
Markdown converts back unchanged. Block keys come from pulldown-cmark
(images, link reference definitions, autolinks, escaped pipes, literal
angle brackets), and a marker never splits a loose list or a fenced
block.jubarte convert --timeout SECONDSexits 124 (timeout(1)'s status)
once the limit passes.- docs/rust.md, docs/python.md and docs/javascript.md carry generated CLI
and API references (scripts/gen_docs.sh). A docs CI job fails on
drift, andrelease.shregenerates them. - docs/adoption/ and examples/adoption/: one page per agent skill (Anthropic
docx, OpenAIdoc, MCP) that jubarte replaces. Each row has
side-by-side evidence with arun.sh, a test intests/adoption.rs, a
plan for every gap, and the measured disk each skill's container drops.
Theadoptionworkflow runs it all.
Fixed
- Convert, math: OMML text is set in Cambria Math with letters from the
mathematical italic block (m:sty p/bupright,m:norordinary text).
A display equation is centred unless itsm:jcsays otherwise.m:d
draws its delimiters and separators, and they follow the script
placement. Math is found under any namespace prefix. A face that lacks
a letter keeps the whole run in ASCII italic. Math text drops the
paragraph's hAnsi face. Deleted text counts toward a display
paragraph. On the 29 corpus documents with a display equation, against
Word's PDFs: jaccard +0.030 [+0.0005, +0.070], text_boundary +0.056
[+0.008, +0.116] against 0.11.2; the 116-document control without
OMML is byte-identical. - Font report: a family placed on an installed face by its name's class
("Fake Serif Pro" on Times, Helvetica on Arial) was reported as an
explicit match, so--fail-on-substitutionpassed. It is nowgeneric
and counts as substituted; metric twins (Carlito, Caladea, Liberation,
Arimo, Tinos, Cousine) still count as their family, and a trailing
"PS MT" is not a substitution. Layout is unchanged. - Package: a relationship Id repeated with an identical relationship is
kept once on open and reported as a repairedDUPLICATE_RID. Two
different relationships under one Id are refused. Word refuses both. - Command line: converting to
.docxor Markdown refuses--report,
--font-reportand--fail-on-substitutioninstead of ignoring them.
A page-marker warning names the step that failed, and--timeoutmust
be finite.
Changed
- MSRV is Rust 1.94 (was 1.88), for the library, the CLI and the Python
binding. The MSRV-aware resolver had been holdingaesat 0.9.2 and
rdocx-opcat 0.1.0 (their next releases need 1.89 and 1.93; the WASM
build already patched rdocx-opc 0.1.2). 1.94 also brings
slice::array_windowsandstr::floor_char_boundary, which replace
hand-rolled pair indexing and char-boundary loops. The CI MSRV job runs
the all-feature suite on 1.94. - The code is clippy-clean on 1.94 as well as on current stable: six
boolean expressions 1.94'snonminimal_boolflagged are rewritten
(is_none_or, a named pair test, a named toggle closure), and the lint
suppressions left in examples, tests and the Python binding are
#[expect(..., reason = ...)], which fails once a suppression is no
longer needed.tests/commonkeepsallow(dead_code): each test binary
uses part of it. - Every CLI command but
convertrefuses a.doc(and any other
admission refusal) leading with its code,LEGACY_DOC: old.doc is …, as
the library and Python already report it; the convert hint follows. validate --jsonis described as JSON Lines in its help: one object per
finding, nothing when there is none.
Docs
- The adoption pages say what is true today. Python's
convertis a
subset of the CLI's (no--timeout,--fail-on-substitutionor-t),
and exit 2 also meansvalidatefindings. The npm CLI needs Node 18.3+,
and crates.io builds need Rust 1.94+ from 0.11.3. Thebody:rev:
limitation is that id-less revisions share the id, not that--id
cannot select it. The MCP feature-gate note no longer reads as pending,
and Gemini CLI issue #20298 is closed as not planned. Two claims now
match what is tested: folder 16 alone holds Word's own answer, and
tests/adoption.rsruns the rows that have a jubarte command. - Example evidence: 14's table matches its committed page sizes (soffice
painted the size-less pandoc file at Letter, not A4). 00 measures the
14 MB download it quotes (download_jubarte.tsv). 07 names its validate
output.jsonl, and finds the validator inside the checkout, not at a
path on one machine. 04 cites the commit on this branch, and 16 says its
Word script logs were not kept. - The adoption workflow also runs on changes to
tests/common/,
Cargo.tomlandCargo.lock.
Known issues
- Microsoft Word 16.115 (installed on the bench 2026-10-05) crashes opening
23 of this release's 600 benchmark redlines, so they score 0 in
release_info/. 22 are byte-identical to the 0.11.2 redlines the previous
Word opened on 2026-10-03; the cause is not found yet (KNOWN_ISSUES.md #8).
Full Changelog: v0.11.2...v0.11.3
jubarte v0.11.2
Redlines follow Word's own verdict: a changed paragraph is marked word by word, or replaced whole, by the rule measured in 1,178 Word comparisons. PDFs paint comments as Word's balloons and follow Word's layout more closely (headers, table rows in content controls, footnotes, justification, embedded fonts). The CLI no longer overflows the Windows debug stack. On two 600-item samples scored against Word's own output: redlines mean 76.71 (Docxodus 69.24), PDFs mean 78.77 (LibreOffice 54.06), no failed item. The evidence (samples with a sha256 beside every file, per-document scores, paired intervals) is in release_info/.
Summary. A changed paragraph is marked word by word or replaced whole by Word's own rule (the kept characters against 15 % of the longer side), measured in 1,178 Word comparisons. jubarte convert paints comments as Word's balloons (resolved ones faded, ranges bracketed) and follows Word's PDF more closely in headers, table rows inside content controls, footnotes, justified lines and embedded fonts. The CLI runs on Windows debug builds. Every release now ships its benchmark evidence in release_info/. On two 600-item samples scored against Word's own output: redlines mean 76.71 (Docxodus 69.24), PDFs mean 78.77 (LibreOffice 54.06), no failed item.
Docs. README.md says how a PDF shows comments (Word's balloons in the markup pane) and that uvx/npx run the CLI from 0.11.0; the four registry READMEs are cut from it. docs/adoption/install-matrix.md lists the wheels and binaries every release since 0.11.0 carries, and adoption rows 0.11.0 shipped read released. docs/WORD_DIFFERENCES.md records comment balloons and sticky notes (10a). New release_info/README.md specifies the six evidence files; VERSIONING.md and AGENTS.md describe the evidence step. docs/python.md and docs/javascript.md are regenerated by the gates.
Added
jubarte convertpaints Word's comment balloons in the markup pane. Word's
Save as PDF draws each comment as "Commented [initials n]: " bold plus the
comment's text, in a rounded box filled with the author's tint and stroked
in the author's ink, a dotted connector from the end of the commented
range, and the range itself tinted. jubarte left the pane empty and wrote
a non-printed sticky note, which the raster scorer painted as an icon over
the text. The painter (paint_comment_balloons) is built from the geometry
of all 466 balloons of 152 corpus documents: chrome in page units (box
16.67pt right of the pane, 3.4pt short of its right edge, inset 3.4,
stroke 0.358, connector 0.179 dotted, stack gap 0.717, elbow 4.6 under the
box top), text at the Balloon Text style's size (9pt unless the document
sets it), first line 2.67 under the box top, pitch 1.236 × size. Word mode
writes no comment annotation, as Word does; the other revision styles keep
the sticky note, now bound where its range ends and anchored at the run
end when a paragraph's close binds it. Two bugs fell out: a note on a run
the line builder merged into its predecessor was dropped, and notes were
bound at the range start instead of the end. Author colours cannot be
copied (the same author gets up to six palette colours across the
oracle's documents); the order-of-appearance palette stays. On the 537
corpus documents with comments: mean 71.98 → 75.35, ≥90 from 25 to 58.- A resolved comment (
w15:donein commentsExtended.xml) paints faded, as
Word paints it: text in Word's BFBFBF grey, the box stroked in the
author's tint and filled 16 % of it toward white, the commented words under
the pale fill. Aw:commentRangeStartat body level opens the range for
the paragraphs that follow. Balloon numbers count through the document
whatever the author, and a reply takes its thread's number with "R" and its
rank, the parent counting as 1. - An empty comment range gets its balloon: a range end with its own start
before it in the paragraph is live (an empty range followed by its
reference is a reference alone), where the live-end rule read it dead. The
151-document balloon survey is exact at 151/151, and a paragraph holding
only the reference mark gets an empty strut run to carry the note. - A commented range gets Word's brackets at its ends: a 0.179pt stroke down
each end of the tinted text with 0.18pt serifs pointing in, the dotted
connector leaving the closing one.
Changed
- Compare, Word mode: a changed paragraph is marked word by word or replaced
whole by Word's own rule. The kept span is the characters of the words the
alignment keeps, with the blank inside each kept run and the one on either
side of it, plus the paragraph mark on both sides; at 0.15 or more of the
longer side's characters the paragraph is word-level, under it the words
are replaced, and the rule runs again on each gap between the anchors it
keeps (1178 single-paragraph Word probes of 6 to 4800 words: wave7 and
edge1 1.000, paragraphs of 40 words or fewer 0.992, short1 0.988, asym1
0.983, denom1 0.924). The engine used to void each common run shorter than
2 % of the window, which replaced paragraphs whose kept runs are short:
on 807 Word-labelled paragraphs its verdict agreed with Word's 41 % of the
time, now 89 %. Only whole paragraphs are judged; a fragment the run
resolvers cut out of a multi-paragraph region keeps their arrangement. A
field's code counts as text, so a field whose code is kept stays paired
at either end of a replaced window.weighted_lcs_pairs(Hirschberg,
linear space) returns the alignment andkept_spansums it. On a
2000-pair control scored against Word's redlines: +0.09 mean, interval
above zero.
Fixed
- Compare:
- A wholly deleted hyperlink, or a deletion holding a complete HYPERLINK
field, swaps behind the insertion that replaces it, as Word writes a
replacement (insertion first). The writer's swap only saw a barew:del
followed byw:ins(corpus a098bd5ad9, the one real loss of the
2000-pair control, −15.3). A wrapper holding part of a field stays put. - The resolver's replaced branch emits the deletion first. Ahead of
content that is itself a tracked insertion on the revised side (a table
pasted with Track Changes on, after a cell replaced across a hyperlink
field) the insertion-first order made the region re-streamer delete and
re-insert that content whole: 101 replaced paragraphs Word keeps in the
corpus pair docx_lots_of_comments_addition. The written order is still
Word's.
- A wholly deleted hyperlink, or a deletion holding a complete HYPERLINK
- Package:
- Relationships and content types written with explicit closes are read: a
<Relationship …></Relationship>(and aDefault/Overridein
[Content_Types].xml) was skipped, and with it the part it named.
Corpus 37c6c62345 lost its footer and scored 49.PartFs::openre-reads
every.relsand the content types with each explicit close folded in.
1 of 2925 corpus documents; Word reads both forms alike.
- Relationships and content types written with explicit closes are read: a
- Tables, grids and borders:
- A row a content control wraps is painted, as Word paints it: a
w:tr
insidew:sdt(a repeating section's rows) was skipped on purpose to
hold a page count the packing has since caught up with;
wrapped_children(dom, table, "tr")gathers rows through content
controls and custom XML alike. 38 of 2925 corpus documents carry row- or
cell-level controls; the five in the 300-document sample averaged 64
against 83. - A short row's cell wider than its grid columns spans the columns its
width covers:stretch_stale_spansextends a lone widetcWcell over
the following columns (25f1d311bd's tcW 10080 over a four-column grid,
no gridSpan, was given the first 84pt column, wrapping its paragraph
into 15 lines and a page more than Word). 91 corpus documents carry such
a row. - A row's
gridBefore/gridAfterskip is as wide as the grid columns it
leaves empty, not a cell's margins: ed36b607e8's 5-twip first column
keeps Word's 0.25pt, where a margin-fitted placeholder pushed the row's
text 10.5pt in and wrapped its lines a word short. A placeholder is no
cell: it sets no minimum in the column fitters, whatever it spans, and no
preference for its column. - A double cell or table border takes room for three strokes, two lines
and the gap between them:parse_border_edgesizes a doubleszat
3 × sz/8 (Word probes: a single sz=6 top border puts the first row's
text 0.72 under its no-border place, a double 2.16 to 2.25, a single
sz=24 2.88), and row pitch and the split rule follow. The painter still
fills that room as one band where Word strokes two lines. 4910ce2060's
double-rimmed rows sat 1.5pt high.
- A row a content control wraps is painted, as Word paints it: a
- Lists and tabs:
- A deeper list item ahead of its parent level uses up the parent's start:
ed36b607e8's list (lvl 0 start 3) opens with 3.1 3.2 3.3 and Word
numbers the first lvl-0 paragraph after them 4; the engine gave it 3 and
every later label ran one behind. - A numbering label tabs to the first typed stop past it inside its
hanging gutter: a custom left stop the paragraph sets, or, failing one,
the hanging indent; anumstop is Word's own, re-derived from the
level, and does not count (TabStop.numbering). f23fc5de2e's text
starts 14.2pt in as Word's does, not 35.45; 36 corpus documents, 217
paragraphs. - A right-aligned list label (
lvlJc=right, ISOend) ends on the
paragraph's first-line indent, left less hanging, running into the
margin when the label is wider, and the suffix tab takes the text on
from there. Six positions measured in Word's PDFs; the four tests that
pinned the old tuck now assert Word's. - A right stop that cannot hold the text after the last tab wraps the
paragraph: the TOC right-tab path steps aside when the suffix is wider
than the room up to the stop, and the ordinary wrapper breaks the lines
(7c02cf95f3's definitions, 55.8 →...
- A deeper list item ahead of its parent level uses up the parent's start:
jubarte v0.11.0
Document editing through edit plans (rewrite, tables, lists, run formatting, footnotes, images, page setup, content controls, watermark, redact, settings, field updates), new audit, validate --repair, scrub, append, comments, diff-render and convert --pages commands, Markdown alongside Word, the jubarte-mcp MCP server, uvx/npx runners, input admission limits with a LEGACY_DOC refusal, Litera Compare conventional marks, Windows and musl Python wheels, and another docx-to-PDF round against Word 16 probes. Breaking: WmlDocument::document_byte_array is private (use bytes()).
Summary. Edit plans become a document editor (rewrite, insert_table, list, format_run, insert_footnote, insert_image, page_setup, fill_control, watermark, redact, settings, fields update, and occurrence targeting); new jubarte audit, validate (with --repair and --original), scrub, append, comments, diff-render and convert --pages; Markdown alongside Word; a jubarte-mcp MCP server and uvx/npx runners. Inputs are admitted under limits before anything inflates, and a legacy .doc or encrypted file is refused with LEGACY_DOC. The default conventional marks follow Litera Compare. Python wheels for Windows and musl Linux. docx-to-PDF follows Word 16 in more table, text box, spacing, list, font, header, footer, note and field cases. Breaking: WmlDocument::document_byte_array is private (use bytes()), AdmissionErrorKind gains LegacyDocument, and sha1_fingerprint* are renamed fnv1a_* (deprecated aliases stay one release).
Docs. README rewritten around the task-to-command table, with per-library READMEs (README.crates.md, jubarte-python/README.md, jubarte-wasm/npm and cli READMEs) cut from fragments by scripts/library_readmes.py and locked by tests/readme_coverage.rs; the crate's rustdoc front page maps every task to its module, with no dead intra-doc links (release.sh now builds the docs with -D warnings); new docs/rust.md, docs/python.md, docs/javascript.md, docs/MARKDOWN.md and docs/adoption/ (MCP, install matrix, agent skills); docs/SELF_UPDATE.md covers signed releases; docs/WORD_DIFFERENCES.md, WORD_LAYOUT_RULES.md and WORD_COMPARE_RULES.md extended; GEMINI.md added; RESULTS.md removed in favour of the neurotic bench.
Added
-
scripts/release.shstep 13 runsscripts/check_release_facts.py: the
release is not done until jubarte-app'sdata/facts.jsonl, which
jubarte.pro and the Mac app print the engine's version, date, files and
release list from, names the new version, lists a wheel for every required
platform tag and is committed in the jubarte-app checkout. Step 12's
release_downstream.shnow commitsdata/with the app's release files
(the facts were left behind as "not a release file"). -
jubarte self-updatecan require a zipsign (ed25519) signature on the
release archive, checked against public keys built into the binary, so a
replaced archive is refused even when itsSHA256SUMS.txtwas replaced
too. Signing is staged: no key ships yet, and until the maintainers
commit one the updater checksSHA256SUMS.txtonly, as before. Once a
key and a first signed release are set, that release and every later
one must be signed; a--versiondowngrade to an earlier, unsigned
release is checked by its SHA-256 alone.release.ymlsigns each
archive before writingSHA256SUMS.txtwhen theZIPSIGN_PRIVATE_KEY
secret is set, and refuses to publish unsigned archives once a public
key is committed underkeys/. Setup and key rotation:
docs/SELF_UPDATE.md. -
The font report says whether each requested font was substituted: a
substitutedfield inconvert --font-reportand--report(true for
theword_substitution,genericandunknownsteps, and for
open_fallbackunless the bundled face is the requested family itself),
FontReportEntry::substituted()in Rust, and
FontResolution.substitutedwithRenderReport.substitutionsin
Python.jubarte convert --fail-on-substitutionlists each substitution
on stderr and exits 4 after writing every output, for CI. -
comparer::CompareMode { Word, PowerTools }and
WmlComparerSettings::new(mode), withwith_author,with_date,
with_detail_thresholdandwith_input_limitsbuilders, so callers can
name a supported configuration instead of writing a struct literal. The
fields stay public; nothing existing breaks. -
admission::InputLimitOverrides: key-by-key overrides of an
InputLimitsbudget parsed from JSON, unknown keys refused. -
Python
compare_documents(..., input_limits=...),
get_revisions_json(..., input_limits=...)and
CompareOptions(input_limits=...); WASMcompareDocuments(..., inputLimitsJson)andgetRevisions(..., inputLimitsJson). Hosts can now
lower the 512 MiB / 2 GiB compare budget, which a 32-bit WASM heap cannot
hold. -
WmlDocument::bytes(), the bytes the document was opened from. -
fuzz/: cargo-fuzz targets foradmission::admit,
strict_translation::strict_to_transitional_docx_withinand
compare_documents_with_settings, seeded from the repository's.docx
fixtures (fuzz/seed.sh), and a non-blocking CI job (fuzz-smoke) that
runs each for 60 seconds. Seefuzz/README.md. -
jubarte-mcp, an MCP server over stdio in the Python package
(pip install 'jubarte-redlines[mcp]'):docx_text,docx_inspect,
docx_edit,docx_render,docx_compare,docx_changes,docx_accept,
docx_reject,docx_capabilities, anddocx_validate,docx_comments,
docx_audit(which report a missing engine feature until it lands). Every
path must resolve under--root, outputs are files, and nothing is
replaced withoutoverwrite. Setup for Claude Code, Codex and Gemini CLI
in docs/adoption/mcp.md. -
Gemini CLI extension:
gemini-extension.jsonandGEMINI.mdat the
repository root;scripts/bump-version.mjskeeps the manifest version in
step withCargo.toml. -
Python:
Document.inspect_json()returns the engine's inspect snapshot as
JSON text. -
jubarte diff-render A B(convert::diff_render) lays out and rasterizes
both documents at one resolution (--dpi, default 100) and compares the
pages pixel for pixel: each page'schanged_ratio, the box around the
changed pixels, andonly_infor a page one side lacks.--out-dirwrites
a-page-NN.png,b-page-NN.pnganddiff-page-NN.png(b's page with the
change painted magenta and boxed;--no-overlayskips it) for the pages
that differ, anddiff.json;--jsonprints it. Exits 0 when no page
differs and 5 when one does, so a CI step can gate on it. Python:
jubarte_redlines.diff_render(a, b, dpi=)returns aRenderDiffof
PageDiffs, andpython -m jubarte_redlines diff-rendermatches the
binary. -
jubarte convert FILE --png --pages 1-3,7rasterizes only those pages
(counted from 1) after one layout pass of the whole document; the page
report still covers every page.RenderRequest.pages(zero-based) in
Rust,Document.to_png(pages=)andDocument.render(pages=)in Python. A
page past the end isConvertError::PageOutOfRange.RenderRequestis no
longerCopy. -
capabilitiesreportsoperations.diff_renderand
operations.page_ranges. -
Edit plans take
"existing_revisions": "keep": another party's tracked
changes stay tracked, byte for byte, and the plan's edits become new
revisions beside them by the plan's author and date (ids after the
highest in the package), emitted directly instead of through compare.
The clean copy has the plan's edits applied and theirs still tracked;
the report's revision counts and the patch (patch.diff, Python
EditResult.diff) cover the plan's own changes. Edits inside their
revisions, and deleting, merging or reformatting a paragraph whose mark
or properties they changed, are refused. New tables are inserted rows,
list numbering and paragraph formatting arew:pPrChange, and comment
replies, resolutions and deletions carry into the redline. New
markdown::patch_own_changes, capability
operations.edit_keeps_revisions, and Pythonexisting_revisions="keep". -
watermarkedit operation ({"kind":"watermark","text":"DRAFT"}, with
optionalcolor,diagonalandfont; PythonEditPlan.watermark)
writes Word's own VML text watermark, in itsWatermarkscontent
control, into every default header. A first section without a default
header gets a new header part with its relationship, content-type
override andw:headerReference. The comparison base carries the same
watermark, so the redline holds it untracked. One per document: a second
one, or a header that already holds one, is refused with
UNSUPPORTED_STRUCTURE. -
jubarte validate FILE(validate::validate, PythonDocument.validate,
WASMvalidateDocument): Word-validity findings beyond the schema, as
data. Each finding carries a stablecode(TEXT_INSIDE_DELETION,
MC_UNBOUND_PREFIX,DANGLING_RELATIONSHIP, ...), the part and element
path, whether Word refuses or repairs the file for it (word_fatal) and
whether--repairfixes it. The Ring-1 invariants the test suite gated
every produced package on (tests/common/validity.rs) now live in the
library, with the fivejubarte debugtriage checks
(debug::findings) behind them. Exit 0 clean, 2 findings, 1 unreadable;
--jsonprints one object per finding. -
jubarte validate FILE --repair OUT.docx(validate::repair,
Document.repair,repairDocument) fixes the findings with a
deterministic fix (unboundmc:Choice Requiresprefixes,w:tinside a
deletion,w:delTextoutside one or under a move source, bookmarks in
single-value content controls, dangling relationship attributes,
duplicate drawing and revision ids, paragraph ids outside Word's range,
table cells without a last paragraph, orphan comment anchors) and lists
what r...
jubarte v0.10.1
Per-change accept/reject, Word-parity Reject All, and a large docx-to-PDF fidelity round against Word 16 probes: keep-with-next by compatibility mode, table row splits and cantSplit rows, floating tables, header/footer inheritance, borders, shading and pictures, comment balloons. New jubarte debug --check render, debug diff, debug -c text. Priority docx-to-PDF set: pages matching Word 170 -> 182 of 199.
The Windows x86-64 binary is not attached: the Windows runner could not check out this tag (test fixture paths exceed its path limit). Use the PyPI package, npm package or
cargo install jubarte-redlineson Windows until it follows.
Summary. Tracked changes can be accepted or rejected one at a time, as Word's Accept / Reject This Change, from the CLI, edit plans (resolve_revisions), Python and WASM; Reject All matches Word on text and mark state; docx-to-PDF follows Word far more closely on page breaks, keep-with-next, table rows, floating tables, headers, footers and comment balloons; jubarte debug gains --check render, diff A B [C …] and -c text.
Docs. New docs/WORD_COMMENT_BALLOONS.md (when Word draws no comment balloon) and docs/api/ snapshots of the Rust and WASM API surface for release drift review; docs/WORD_DIFFERENCES.md extended; VERSIONING.md documents the step-5 API-docs review; the jubarte-documents skill and the npm README cover per-change accept/reject.
Changed
- The tests no longer need neurotic_docx_bench (or the local
_to_improve_accepted_changesfolder) checked out beside this repository.
The 537 documents they read are copied intotests/corpus/under the
relative paths the tests name. Their provenance, including the
docx-corpus ODC-By attribution, is in
LICENSES/LicenseRef-Bench-Fixtures.txt. About 180 fixtures had
moved in the bench, so the tests naming them skipped without a word;
they run again, andtests/bench_fixture_paths_resolve.rsfails on any
named copy that is missing.
Added
-
jubarte debug FILE --check renderlists what each story part should put
on the page, for diagnosing a PDF that scores low against Word's. It reads
every part: body, headers, footers, footnotes, endnotes and comments, with
their text boxes. For each part it lists:- the sections;
- tables, with their size, style, float position, direct and cell shading,
the table style's shading, and the first row's text; - frames and anchored drawings;
- fields such as PAGE;
- paragraph styles and shading;
- text colour, highlight, run shading and hidden text, direct or from a
style; - requested fonts;
- whether a replaced run's deletion comes before its insertion or after
it.
A
(layout)entry adds jubarte's page count and the face each requested
font resolved to (installed, embedded, altName or a substitute), plus the
docDefaults, theme andfontTable.xmlfonts. With two files it prints
only the lines that differ. -
jubarte debug diff A B [C …]compares two or more packages element by
element instead of line by line: styles pair by type and name, paragraphs
by their text (a rewritten one meets the one it replaced), tables by
their first text, cells by position, notes and comments by text, headers
and footers by the section role that shows them. Each hunk names the
element's path (word/styles.xml › style paragraph "Body Text" › rPr)
and prints the property items or run segments not every file holds; with
three or more files each line names the files holding it. rsids,
paragraph ids, revision ids/authors/dates, relationship ids (a link or
image shows what it points to instead), docProps save stamps and counts,
attribute order, on/off values and empty property blocks are dropped
unless--raw; a part that is not well-formed XML is reported, not
compared.--style,--para-textand-pnarrow the report,--full
prints the shown elements' common lines too without counting them. The
same in the library (jubarte::debug::diff::diff). -
Tracked changes one at a time, as Word's Accept / Reject This Change:
jubarte changes FILElists every change with an id (body:rev:12,
header1:rev:3), its kind, target, author and text (--jsonfor JSON
lines);jubarte accept/rejecttake--id,--authorand--kind
(repeatable; a change must match every flag given) and keep every other
change tracked. Ids stay valid while changes remain, both sides of a move
resolve together, and a change'sinsidenames the change whose
resolution takes it along. The same in the library
(jubarte::changes::{list_changes, accept_changes, reject_changes}),
Python (Document.changes(),Document.accept(ids=, authors=, kinds=))
and WASM (listChanges,acceptChanges,rejectChanges). -
Edit plans take
resolve_revisions: {"accept": FILTER, "reject": FILTER}
to resolve a selection of the tracked changes before editing; what it
leaves followsexisting_revisions. A change selected by both sides is
refused withREVISION_CONFLICT, an id the document lacks with
UNKNOWN_CHANGE; the report lists the resolved ids. -
jubarte debug FILE -c textprints each story part's paragraphs with
{+inserted+}/[-deleted-]runs, the paragraph mark's revision state
and table rows;-c xmlprints part XML one element per line without
namespace declarations, rsids or paragraph ids. With two files both print
only the lines that differ, part by part.-c runsistextwith each
paragraph's direct properties and each run's direct formatting, so two
builds compare formatting without XML noise.
Fixed
- Consecutive paragraphs in a text box are spaced by the larger of one's
space after and the next one's space before, as in a table cell. The
first paragraph's auto space before and the last one's auto space after
are dropped. Before, the two were added together. - A tab whose next default stop lies past the right edge, with no custom
stop left on the line, moves to the next line and is measured from that
line's start, as Word does in compatibility modes 14 and 15. - A table row whose cells are centred or bottom-aligned breaks at the page
end like any other row. Before, it stayed whole and moved to the next
page. The docxide suite's education_consultant_posting now starts its
Background row on page 1, as Word does. - A Korean font the machine lacks (charset 81, or Hangul in its name) is
painted in Batang when the font table calls it roman, and in Malgun
Gothic otherwise, as Word 16 paints it. Before, it fell back to Microsoft
YaHei, which has no Hangul, so the text was lost. - Hangul in a run whose fonts are named only for Latin text is painted in
the run's East Asian face, as ideographs and kana already were. With
an empty themea:eaand a ko-KR East Asian language, that face is
the theme's Hang script font. Before, the Latin face drew nothing for
the syllables: the docxide suite's Korean conference form lost "발표"
from its title. - Trailing tabs count toward a centred or right-aligned line; only
trailing spaces hang past it. Before, the tabs were measured as glyphs
and taken off, so cb4f8b4a43's centred "Date:" and its 14 tabs sat
61pt in from the margin, not 3.6pt. - A table row moves to the next page whole when one of its cells can put
nothing on the page, as Word 16 does whatever the row's vertical
alignment. Before, the other cells' first lines stayed behind. - A justified line holding only tabs keeps their stops and dot leaders.
Before, the justified painter drew the tabs as text, so the docxide
suite's air_pollution_permit_form lost two of its four dotted lines. - A missing script-family font (a
w:family="script"font-table entry
such as Vivaldi) paints in Calibri, as Word 16 does. Before, it fell to
Cambria, whose wider 100pt bold "Messiah" wrapped and gave
handels_messiah a ninth page. w:contextualSpacingdrops the space between same-style paragraphs
inside a table cell, as it already did in the body. Before, cb4f8b4a43's
Title lines stepped 28pt apart, not Word's 20pt.- Below compatibility mode 15, a table style's font size overrides the
Normal style's in unstyled cells only when Normal is 11 or 12pt, as in
Word 16. Before, it overrode any size, so the cf02 redline's 10.5pt
Normal cells painted at 11pt and ran to 12 pages, not Word's 11. - A table row splitting at the page end measures a cell's first cut
against the room its margins leave. Before, the empty head was sized as
a default 11pt paragraph, so 3138fff3a6's two-line 8pt titles placed no
line and their rows moved to the next page whole, where Word splits
them. - From compatibility mode 15, a cell paragraph cut by a table row split
keeps widow/orphan control: two lines on each side, or it moves to the
next page whole, as Word 16 does. Below mode 15, Word still cuts it
anywhere. Before, compat-15 two-line cells split one line each side:
30f195a272's first table row started on page 1, where Word's starts on
page 2. - A table too wide to sit beside a square-wrapped picture starts below
it, as in Word 16; a narrower table still sits beside it. Before, the
table ran over the picture: 109f20a2b3's continued table painted
across its header logo, 74pt higher than Word's. - A "keep with next" paragraph that ends on a page's last line, its
successor pushed over, keeps its tail with that successor as Word does.
From compatibility mode 15 it splits and carries its last two lines
over (one with widow control off). Older modes move it whole, together
with the keep-with-next paragraphs chained before it. A chain of short
keep-with-next paragraphs now stays with the body below it, and its
spacing no longer counts the space between two paragraphs twice. A
chain of unsplittable ones taller than a page moves to a fresh page
once and then fills pages. A keepLines paragraph comes whole with the
heading above it.
P...
jubarte v0.10.0
0.10.0 makes documents editable by agents (inspect, JSON edit plans, tracked redlines, headers/footers/notes) across the CLI, Python and WASM, adds --mode word|powertools, convert --png and self-update, and fixes Word-mode text boxes, section headers and footers, and comment anchors.
Summary. Documents are now editable by agents: inspect, JSON edit plans that write a clean copy plus a tracked redline, and headers, footers and notes as editable stories, from the CLI, Python and WASM alike; Word-mode redlines keep changed text boxes, per-section headers and footers, and comment anchors where Word does.
Docs. README links the page-by-page engine comparison site (top banner, Benchmarks, Find us) and documents self-update and --mode word|powertools; new docs/WORD_DIFFERENCES.md, docs/SELF_UPDATE.md, the jubarte-documents agent skill and the Acme letter example; the jubarte-wasm npm README covers the agent API; VERSIONING.md and bump-version.mjs make release.sh the source of truth.
Added
jubarte A B --mode word|powertoolsnames the compare presets:word
(the default, Word Compare's layout) orpowertools(the classic
PowerTools fallback, still also spelled--powertools-faithful).
docs/WORD_DIFFERENCES.mdlists where jubarte's redline still differs
from Word's and which mode gives which.jubarte-wasmgains the agent surface the CLI and Python already have:
inspectDocument,documentMarkdown,sourceSha256,applyEditPlan
andpreviewEditPlan(refusals returned as data with their code and
per-operation outcomes),editReportJsonlandcapabilities, in the
full and slim builds.- Input admission for the agent surfaces:
inspect,text,editand
the Python/WASM facades over them refuse a package before parsing it when
it breaks a resource budget (64 MiB file, 10,000 entries, 64 MiB per
inflated part, 256 MiB inflated in total, XML nesting 256), repeats a
part name, uses an unsafe entry path, encryption or compression other
than deflate, or is not a WordprocessingML package. Refusals carry the
codesINPUT_LIMIT,DUPLICATE_PART,UNSUPPORTED_PACKAGE,
INVALID_PACKAGEandINVALID_XML;capabilitiesreports the budgets.
Compare keeps its historical tolerance. jubarte inspectreads a document as numbered paragraphs (JSON with a
source hash, or Markdown) with styles, numbering, formatting spans and
the structures an edit cannot address (fields, hyperlinks, content
controls, revisions);jubarte capabilitiesreports what the build can
do.jubarte editapplies a JSON plan of uniquely anchored operations
(replace,insert,delete,comment,insert_paragraph,
delete_paragraph,format_paragraph,merge_paragraphs)
to a copy, previews it, and writes the clean copy, the tracked redline
and a per-operation report. A plan that carries the source's SHA-256
(source_sha256) is refused for any other source; without it the plan
runs unguarded and the report flags that. An
anchor that is not unique, or text inside a field (simple or complex,
even one with an empty result), hyperlink, content control or revision,
is refused. Comment text must be nonempty without control characters,
and severalinsert_paragraphoperations after one anchor keep plan
order.jubarte convert --pngrenders pages to PNG (1–1200 dpi, at most 2^28
pixels a page) and checks every output path before the first write.- Python:
jubarte_redlines.read()returns aDocumentwith inspect, edit,
convert, compare and accept/reject methods, andpython -m jubarte_redlinesexposes the same commands (compare is the explicit
comparesubcommand there);EditPlanError.message
carries the engine's detail. - Edit plans restyle and join paragraphs.
format_paragraphsets a
paragraph style (by id or name; an unknown one is refused with
UNKNOWN_STYLEand the defined ids), alignment, line spacing as a
multiple and space before/after in points, as one tracked property change
that reject undoes.merge_paragraphsjoins the next paragraph onto one,
with an optional separator; the joined paragraph keeps the second one's
properties, as Word's accept of a deleted mark does, and bookmarks and
comment ranges between the two move to the join.replaceandinsert
take aformat(bold, italic, underline, a Word highlight colour) for the
new text only. The PythonEditPlangains the matching builders. replacetakes"whole": true(Pythonwhole=True): the redline shows
all offinddeleted, then all of the replacement inserted, as typing
over a selection with Track Changes on does, instead of Word Compare's
word-level diff that keeps shared words. Deletions the comparer placed
just outside the change are gathered in, formatting stays on each side,
and a comment on the change stays on the inserted text. When the diff
cannot be regrouped the operation keeps the word-level redline and its
report line carries amessagesaying why.- Headers, footers, footnotes and endnotes are editable stories.
inspect
lists them understoriesandjubarte textprints them after the body,
each paragraph under an id such asheader1:p:0orfootnotes:p:2. An
edit plan addresses them by that id or by adding"story": "footer1"to
anindex,starts_withorcontainsselector (those search the body
otherwise). The change is tracked in the story's own part,"whole": true
included, and the report counts header and footer revisions. Comments
stay body-only, since Word cannot anchor one in a header or footer. The
PythonSnapshotgainsstories. - Adoption guides, workflow examples and a document-operations agent skill.
The Acme letter example now shipsmake_letter.py, which writes its
source letter byte for byte, and its plan runs all twelve edits. jubarte self-update [--check] [--yes] [--version X]installs a GitHub
release after checking its SHA-256 against the release's
SHA256SUMS.txt. It contacts GitHub only when run; nothing checks for
updates in the background. Build with--no-default-features --features clifor a binary without it (docs/SELF_UPDATE.md).
Changed
- quick-xml 0.41 → 0.42 for input admission and XML checks. Admission now
transcodes UTF-16 parts (SharePointcustomXmlitems, which Word opens)
before scanning them, so the new release's UTF-8 validation refuses none
of the 500 fixture documents; bytes that are not UTF-8 still decode
lossily, as before. - Dependencies: clap 4.6.7, flate2 1.1.10 and serde_json 1.0.151 across the
engine, Python, WASM, in-process and app workspaces; quick-xml 0.42 in
the app and in the WASM build's patchedrdocx-opc; the ooxmlsdk test
oracle 0.12. scripts/release.shrequires a sixth note,
--how-readme-and-other-docs-were-updated, written under the changelog
summary and into the release commit, next to a list of the docs changed
since the previous tag.scripts/bump-version.mjswarns that
release.shis the source of truth for releases.
Fixed
-
jubarte-wasm: the patchedrdocx-opcdecodes escaped attribute values
when it reads relationships and content types, so a hyperlink target
holding&is written back once instead of as&amp;. -
A text box whose text changed no longer disappears from a Word-mode
redline. Three late passes that rebuild a revised paragraph from the text
of its insertion and deletion also read the text inside the box, so the
drawing and its VML fallback were dropped and both copies of the box's
text landed in the anchoring paragraph ("…of the postOverall…"); accepting
the redline did not give the revised document. They now leave any
paragraph alone whose runs hold anything besides text. Present since at
least 0.7.1 (fixtures_500 00b81efae883). -
Word mode marks a text box's changed words inside the one box, as Word
does, in the DrawingML shape and its VML fallback alike, instead of
deleting the old box and inserting the new one. A shape wrapped in
mc:AlternateContentis now a word of its own in the Word-mode diff, as
a bare drawing already was, so a changed box no longer takes the
unchanged shape or text beside it into its replacement (a deleted and
reinserted VML group repeated its shape id; fixtures_500 003329b501a7).
Changing one word in every text box of the fixture documents now keeps
all their boxes (22 of 22 documents). -
A document whose sections have their own headers or footers no longer
redlines them against the wrong section's: they were paired by kind and
type alone, so every section's default footer met the last section's,
and an unchanged "Page 1 of 4" footer came out deleted and reinserted as
"Page 4 of 4". Parts pair by section now, and an unchanged part is left
alone. -
Word mode no longer invents paragraph property changes. The last
paragraph of a story with a few words revised kept its spacing live only
when the paragraph was replaced whole; otherwise its space before moved
into aw:pPrChange, and an unchanged justified paragraph got an empty
one. Word records neither (Word's own redlines of a body, a header and a
text box), and the parity ladder's heading-4 pair drops the two
w:pPrChanges Word does not write. -
Changes in a header or footer that carries a relationship (a logo, a
hyperlink) are now in the redline. The comparer skipped every such part
and kept the original's, so the redline silently lost the change and
accepting it did not give the revised document. It now diffs the part
whenever each relationship the revised part uses means the same thing in
the original's (same type and target, same bytes for an image), which is
the case for an edit plan and for most comparisons of two versions. -
A paragraph selector given as a bare id string (
"paragraph": "body:p:88",
as the agent skill's own example writes it) failed to parse with "da...
jubarte v0.9.3
Redlines Word refused to open now open. A field whose code changed is replaced whole, and a same-code field stays whole, as in Word's redline. Table, list and section markup follow Word, and the same input writes the same bytes.
Summary. Redlines are now scored against Word's own redline of each pair, rendered by Word, and this release fixes what that exposed: redlines Word refused to open, blank field codes, missing fonts, and misplaced equations.
Fixed
-
A field whose code stayed while its result changed now stays one field, as in Word's redline. Only the result's words are marked. Before, the field's begin, separate and end were glued to the result's first and last words, so "Contaminated Sites Act 2003" against "Firearms Act 1973" (a STYLEREF title) matched only " Act ". That left a deleted and an inserted field whose ends crossed, and Word refused the redline (db433183 × 9377099d).
-
A field whose code changed is now replaced whole, nested fields included, as Word's redline does. Every part of a field carries the codes of the fields around it, so an
IFthat lost\*MERGEFORMATno longer keeps its begin and inner DOCPROPERTY fields while the rest is half deleted (98bf5f3d × a3701d36). -
A changed table of contents no longer opens inside its replacement. Word mode's head junction folds a short deleted title into the first inserted paragraph when both share a word. When that title was a TOC's first entry, the fold carried the deleted TOC's begin above the inserted TOC's end, and the two fields crossed (98bf5f3d × a3701d36, "Part 1—Preliminary" against "Part 1—Introduction"). A fold that would carry half a field across other fields no longer happens.
-
The same input writes the same bytes. Another dependency turns on the
zipcrate'stimefeature, and with it every package entry carried the time of the write, so two runs a second apart gave different files. Entries are now dated 1980-01-01, as Office dates its own. -
Two point comments inside one run now keep their places. An anchor that ended one text piece of a multi-piece run went after the whole run, so "Note" landed after "東京" and behind "Second note". The run now splits between the pieces.
-
Rejecting every change in center_alignment × center_aligned_bold now restores the original. A pass forced the inserted "This" and "text" into kept text while the next paragraph still deleted them, so the rejected document read "This text This document …". The cross-paragraph stream already keeps those words where Word does, so the pass is gone. The revised tail now fuses into the sole deleted paragraph and keeps that paragraph's deleted mark, so accepting no longer leaves an empty last paragraph.
-
Replaced regions now follow Word's replace-gap grammar, which Docxodus 12 decodes.
- Empty paragraphs. An empty paragraph found in both documents no longer anchors two unrelated regions. It pairs only as part of Word's pilcrow chain, or as the two stories' final marks. The chain breaks when an original paragraph with words faces an empty one.
- Interior replaces. Inside the body, a replaced region keeps every old and new paragraph whole: new paragraphs first, each under an inserted mark, then the old ones under deleted marks. Before, "TWO" fused into "e" and "A" into "a" (list_with_table_break × broken_complex_list, docxide 13.2 vs Docxodus 93.3).
- The story's tail. When the revised document's last paragraph holds text, that text fuses into the first deleted paragraph of the tail, even across a deleted table. That paragraph keeps its own properties and deleted mark, so accepting every change no longer leaves a stray empty paragraph (support_tickets_table × support_tickets_summary, diff_doc2 × numwords). The same holds when the original runs on past it to its own end (bullet_list × calibri_bold_italic: "Calibri bold italic …" opens "Apples", then "Bananas" to "Grapes" deleted). A new title facing an unrelated old one is inserted whole before the old one's deletion (pirates × table_left_indent, as Word does).
- Final empty paragraphs. When both documents end on an empty paragraph, those two final marks stay paired behind a trailing deletion.
- Result. Paragraph-structure agreement with Word's own redlines over 747 pool pairs rose from 0.9276 to 0.9331: 50 pairs improved and 9 dropped, none by more than 0.08.
-
Redlines keep the bookmarks of both documents, as Word's Compare does. The WmlComparer port dropped every bookmark, so each updated TOC line,
PAGEREFandREFprinted "Error! Bookmark not defined." (file_21 × file_22 lost all 582). A bookmark in both documents appears once, at its place in the revised text. A bookmark only in the original stays beside its deleted text. Ids never collide with revision ids, and Word's hidden_GoBackis dropped, as Word does. Pool pairs with more broken references than Word's own redline went from 29 to 0. Comment anchors gain two fixes from the same pass: moved text now counts once on each side, and a second anchor inside a run that holds several text pieces no longer reorders that text. -
The redline's Normal style now follows Word's rules for merging the two
documents' defaults. Word writes B's docDefaults indents, justification,
line-unit spacing and borders into Normal (and neutralizes the ones only A
sets), writes B's run defaults whenever either Normal stores paragraph or
run properties, writes only the language attributes that change, writes the
implicit 10pt complex-script size, reads a document with no docDefaults at
Word's factory values (after 160, line 278, kern 2), and finds a
LibreOffice Normal (style0) by its name. Across 738 corpus pairs, Normal
run properties that differ from Word's drop from 79 to 38 and paragraph
properties from 54 to 46. -
Point comments (a comment reference with no range markers) survive the
redline. The carryover only mapped ranges, so a point comment was dropped,
and with it the whole comments part when it was the only one. It is now
written as an empty range right after the text it follows, the form
Word's own redline uses (comments.docx comment 2, in comments ×
complex_style_attr and clear_formatting × comments). An empty range is
written as one group: a lone start used to land in the next paragraph. -
A list copied from the revised document keeps its picture bullets: the
bullet definitions and their images now travel with the list. The list's
levels had named a picture bullet the merged numbering never defined, and
Word refused to open the redline (italic_rstyle_combos ×
paragraph_indent_normal_styles: harness 0 → 53.2; Docxodus 0). When both
documents define the same bullet id, the revised bullet takes a fresh id
and keeps its own image. A part's first new relationship is nowrId1, as
Word numbers them, notrId0. -
The final paragraph marks are also paired when the replaced original has a
single paragraph: its text is deleted into the revised empty final
paragraph. The deleted text had joined the revised document's last content
paragraph and the revised final paragraph was dropped, so accepting the
redline lost that paragraph (fields_attrs1 × cli_legacy sample: harness
23.4, Docxodus 73.1). -
A paragraph property the revised document adds to a kept paragraph is
recorded inw:pPrChangewhatever it is, as Word records it; only added
alignment or spacing were recorded, so rejecting the redline kept, for
example, an added outline level. -
When an unrelated document replaces the original and ends on an empty
paragraph, the two final paragraph marks are paired as Word pairs them:
the original's last paragraph is deleted into the revised final paragraph,
which keeps the revised properties. The redline had given that paragraph
the original's bare properties, and Word's taller final line, which moves
the redline onto a second page, was missing (diff_after8 ×
doc_with_spacing: harness 2.8, Docxodus 93.9). The revised document's last
content paragraph keeps its own inserted mark instead of folding into the
first deleted paragraph; a head junction on a shared word still folds. -
A run of changed body paragraphs is compared as one stream of words and
paragraph marks, as Word does (port of Docxodus's DocxDiff in-gap pairing
and cross-paragraph segmenter,comparer::cross_para): a kept word may now
sit across a paragraph mark, and rejecting the redline restores the
original instead of repeating a re-kept phrase. Word-faithful gates keep a
gap unpaired where Word does: a region with no paragraph pair streams only
when the revised side has no more paragraphs and its first kept word opens
a paragraph on both sides; a lopsided same-slot pair needs three shared
content words; a window of function words alone carries nothing across a
mark; a table between two changed runs keeps the LCS pairing; and the next
paragraph takes a same-slot pair when it shares at least twice the content
words in order (bold_rstyle × bold_vals). -
An inserted tail ahead of a deleted tail pairs the two final paragraph
marks, as Word does: the last inserted paragraph joins the first deleted
one with a deleted mark, so accepting the redline no longer leaves an
empty paragraph the revised document never had (bullet_list_bold ×
bullet_list). -
Word-mode table margins follow Word:
tblInd/tblCellMarof 10 twips is
stamped on a bordered table only when the document the table comes from
(the original for a wholly deleted table) has no default table style. 94
bordered tables fromTableNormaldocuments stay bare in Word's redlines;
stamping them shifted every row below (file_46 × file_47). -
Synthesized numbering for a dangling
numIdcopies Word's level geometry:
anumtab at the text indent and a full 720-twip hanging indent, so level
0 puts its number at the margin. -
Redlines no longer cross or pack complex fields, which crashed Word
("Con...
jubarte v0.9.2
jubarte 0.9.2: Word-fidelity pass on DOCX to PDF (Word-probed layout rules, painted revision marks, comment balloons, East Asian layout, smaller PDFs with --compress). The redline engine is unchanged. See CHANGELOG.md.
Summary. A Word-fidelity pass on jubarte convert: layout rules reconstructed from live-Word probes, painted revision marks (--revisions conventional|word|custom), Word's comment balloons and change bars, East Asian layout, and smaller PDFs (--compress averages 0.70x Word's size on 2,102 documents). The redline engine is unchanged from 0.9.0.
A Word-fidelity pass on jubarte convert, driven by a 500 + 500-file corpus
of Word-exported PDFs (HF superdoc-dev/docx-corpus, plus 451 Word redlines
of them). Every rule was reconstructed from a live-Word probe — synthetic
document, Word export, measured numbers — and is written down with its probe
and implementing commit in docs/WORD_LAYOUT_RULES.md.
The per-document bake-off on the 500-file set moved from 129/500 to 284/500
wins between milestone merges (mean Jaccard 0.134 → 0.360 at the first).
The redline engine is unchanged: compare_documents emits the same bytes as
0.9.1. All work is in src/convert, the CLI, and the OPC package layer.
Performance
- Smaller PDFs, same pages. Subset faces now also drop glyph names
(postformat 3), unused metrics and everycmap/namerecord the PDF
does not read; a collection (.ttc) face subsets like a lone font instead
of embedding the whole collection. A CID font's/Wlists only the glyph
ids the pages use (a CJK face's widths were 212 KB of a 319 KB PDF). A
picture repeated on many pages is embedded once. Plain glyph runs in the
same font, size, colour and tracking share one text object and move by
relativeTd, so a page is no longer oneBT … ETper glyph. Word-device
glyphs (11.04/16.08pt body sizes) keep their ownq … cm … Q: MuPDF hints
them differently when the 0.24 scale sits in the text matrix. - With
--compressthe average PDF is smaller than Word's on every corpus:
145 KB against Word's 208 KB (0.70×) over the 2,102 clean documents and
377 KB against 449 KB (0.84×) over the 1,416 redlines. Glyph positions are unchanged and the docxide-metrics
scores with them: mean Jaccard 0.6469 clean / 0.4654 redlines against
0.6468 / 0.4656 without--compress.
Added
- Painted revision marks.
jubarte convert --revisions conventional|word|custom
with--revision-paletteforcustom(library:PdfOptions::revisions,
RevisionStyle/RevisionPalette; Python:
docx_to_pdf(…, revisions=…, revision_palette=…); wasm/npm:
docxToPdf(bytes, compress?, revisions?, revisionPalette?)).
conventional(the default) is the legal-redline convention — deletions
red struck through, insertions blue double-underlined, moves green;
wordreproduces Microsoft Word's own markup, including the per-author
colour palette by first appearance, and is what the fidelity gates
measure. - Word's redline chrome. A document with comments — or an inserted or
deleted table cell — gets Word's balloon pane, scaled by the right margin;
change bars stand where Word draws them. - Fonts install beside the binary.
scripts/install.sh(Windows:
scripts\install.ps1) copies the bundled open faces — Roboto Condensed
(the exact files Word's cloud cache draws) and Selawik, Microsoft's
metric-compatible stand-in for Segoe UI — into$JUBARTE_FONT_DIRor the
per-user font directory instead of embedding them. Word's own cloud-font
cache is read too, including localized family names and folders with
numeric file names; macOS fonts are found by family name and Mac Roman
names, not only file names. - East Asian layout. Word's CJK fallback faces and East Asian families
named outsidefontTable.xml; verticalw:textDirection="tbRl"sections;
ideographic line breaking with kinsoku; closing CJK punctuation hangs past
the right edge; Latin inside East Asian text takes theasciiface and a
quarter-em gap; Chinese and Taiwanese counting list formats; East Asian
faces take Word's 1.3× line. - Right-to-left.
w:bidiparagraphs mirror their alignment and indents;
w:bidiVisualtables run from the right margin; RTL lines paint in visual
order; RTL runs take theirw:szCssize;minorBidi/majorBiditheme
slots take the complex-script face; Thaana fallback; a missing Arabic or
Hebrew charset face is Arial. w:altChunk. HTML and MHT alternate content renders.- Frames.
w:framePr— including a style'sframePr, and in headers
and footers — floats its paragraph; page-anchored body frames float as
boxes; auto-height frames grow to their paragraphs; a frame narrows every
paragraph beside it and needs more than an inch of column to do so; with
room on both sides its label sits on the left. - Text boxes and shapes. Header/footer text boxes; shapes and text boxes
anchored in table cells and inside content controls; list paragraphs in
boxes keep their marker and indent;sizeRelpercentages take their own
frame andspAutoFitboxes fit their text; VML groups place children in
group coordinates and honourgrpFill; VML lines stroke where Word draws
them;w10:wrap; VML pictures honour theimagedatacrop; drawing groups
and canvases; custom geometry paths in EMU with bound-arc tessellation;
a:prstClr/a:sysClrcolours; block arrows point their own way. - Effects.
a:softEdgefades,a:duotonerecolouring, Word's "Washout"
picture watermark, ellipse picture crops, text outlines with no fill, and
w:whorizontal text scale measured as well as painted. - Metafiles and rasters. EMF paths, Béziers and window/viewport mapping;
WMFMETA_POLYPOLYGONwith 0-based object handles; BMP and GIF pictures;
CMYK photos embed as RGB. - Fields and forms.
FORMCHECKBOXlegacy form fields;PAGEcounts the
page, in header/footer text boxes too;w:fldDatastays binary. - Page furniture.
tblCellSpacinggaps; continuous sections that switch
columns and side margins mid-page; page borders, including the medium-gap
inward band and page-relative placement;vAlign=centercentres the
layout box;titlePgwithout a first-page part leaves page one bare; the
page background lays out a header where the section has none; each
SmartArt diagram paints its own drawing; a chart withoutc:titlepaints
no title. - Word's layout rules (probed, then implemented):
docGridcharSpace in
4096ths of a point, halved for half-width; compat mode 15; widow/orphan
control on by default as in Word;keepNext/keepLinesholding table
rows together;pageBreakBeforenever skipping a page;contextualSpacing
per paragraph; baselines on the 0.24pt device grid and the left margin on
the 1/300in grid; justified Word 2013 lines squeezing their spaces to keep
a word. /ToUnicodeon every Identity-H font, so text copies and searches out
of the exported PDF; a glyph shaped from several characters maps to all of
them.
Fixed
- List markers with an
hAnsi-only face. A numbering level whose
w:rFontsnames onlyhAnsi(e.g. Arial Unicode MS) painted its ASCII
number in that face and took its taller line;hAnsicovers non-ASCII
characters only, so1.now stays in the text's ASCII face and line
pitch, as in Word. - Floating objects and wrapping. Square/tight/through wraps carve the
wrap polygon — a blocked line steps past the polygon, not the extent;
topAndBottomfloats push later lines; tight page banners move the
paragraph above their anchor; behind-text boxes paint under the body;
distT/distBstay off the body for header floats; front floats stack by
relativeHeightacross pictures and boxes;wp:alignaligns within its
relativeFromframe; a float anchored after a page-spanning paragraph
lands on its last page; floating tables keep the lines above their offset,
break across pages when taller than the page, and lift to keep a page of
rows. - Tables. Autofit measures a word by the pieces it may break into and
widens to the longest word — a too-narrow column takes the saved grid, a
spanning cell widens its columns, vertical text keeps its width;pctvs
dxawidths undernoWrap; a zerodxawidth is auto;gridBefore/
gridAfterleave their grid columns empty;tblPrEx; cell margins and
tcBordersedges apply per Word's order; vertically merged cells grow the
last row they span; nested tables add no tail to their cell;w:hideMark
rows and cells; PHPWord tables withouttblIndstay at the margin; a word
wider than its cell breaks at the edge. - Headers and footers. Floats, text boxes and pictures — wrapping into
rows, beside text, behind text, framed — sit where Word puts them; tabs
past the margin wrap and wrapped lines drop the logo height; lines take
the paragraph's style,jc, line rule and spacing; bottom borders take
room; tracked changes and ptabs paint. - Lines, fonts and measurement. A line is as tall as its tallest painted
face, marker included;hhea— not OS/2 typo — metrics for the single
line box, with typo-metrics faces putting the gap above the text; GDI
external leading above the first baseline; glyphs laid out at the authored
size and painted at the device size; hidden-mark paragraphs, cells and
rows take no line; letter spacing in headers/footers and character spacing
in the wrap measure; a missing font paints in its installedaltName. - Tabs, lists and indents. A hanging indent is an implicit tab stop; a
list marker lifts only its own line; numbering-levelpPr/jcand
lvlOverrideapply; the label's tab lands on the hanging indent, not a
later right stop; a typed lone symbol does not hang like a bullet;w:cr
ends the line and a line feed insidew:tis a space. - **Spacing...