Skip to content

Releases: jandira-tech/jubarte-redlines

jubarte v0.12.0

Choose a tag to compare

@github-actions github-actions released this 11 Oct 17:42

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 FILE prints the agent view (read FILE; a lone name without
    an extension needs read); jubarte A B prints
    the agent view of their redline, with the read options (-o FILE writes
    it instead; compare A B still 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 new jubarte add (a paragraph,
    a comment, or a reply to c5) take their operations from flags: each -p
    starts one, so one command carries several. --author, --datetime,
    --existing-revisions (default auto: keep a file's tracked changes) and
    --editing-mode (clean copy and report only) apply to all of them;
    --out-dir defaults to <stem>.edit beside 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, WASM flagPlan/changedView.

  • Document text diff views: github (unified, text), word, normal,
    context and side-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-lines control the view. Long lines use a 70-character window
    around the first change. Text output creates no implicit Word file.

  • convert --move-comments (also on diff; 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 on diff; 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.

  • PdfOptions has 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=…) for Document.to_pdf,
    to_png and render, keyword arguments of docx_to_pdf, and
    python -m jubarte_redlines convert --move-comments / --changed-only.
    In JavaScript they are docxToPdf's two trailing arguments and
    npx jubarte-redlines convert --move-comments / --changed-only.

  • fields update writes NUMPAGES, PAGEREF and SEQ results in
    \* roman and \* alphabetic as 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." A SEQ identifier 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 public Relationships::from_xml parser. 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 B no longer writes a file unless -o is given: it prints the
    redline's agent view. jubarte compare A B is unchanged.

  • edit prints the redline's changed blocks as the agent view instead of
    the patch (patch.diff is still written); --plan and --out-dir are
    optional.

  • jubarte read (alias text) 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 as edit), tracked changes as CriticMarkup
    followed by a note with their w:id and 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 AUTHOR for one author's) / --comments none / --dates /
    --no-page-markers. edit selectors take the view's short ids (p3,
    header1.p1, t0.r1.c2). The previous [body:p:N] projection remains
    available as inspect::markdown in 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 at jubarte-app/ or named by
    JUBARTE_APP_DIR. scripts/release.sh no longer bumps or commits the
    app's files. It runs the app's scripts/release-engine.sh instead,
    with --preflight at step 0 and in full as step 13: jubarte.pro, the
    app's facts, and its version files on a release/vX.Y.Z pull request.
    Step 14 is folded into it. release_downstream.sh, check_site_live.py
    and check_release_facts.py moved 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 made jubarte validate report 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 validate no longer calls two shapes Word opens Word-fatal: a
    table cell that ends in a content control holding its last paragraph
    (its repair ...
Read more

jubarte v0.11.3

Choose a tag to compare

@github-actions github-actions released this 06 Oct 16:32

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.doc reads 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
    stay LEGACY_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 md writes <!-- 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-markers opts 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 SECONDS exits 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, and release.sh regenerates them.
  • docs/adoption/ and examples/adoption/: one page per agent skill (Anthropic
    docx, OpenAI doc, MCP) that jubarte replaces. Each row has
    side-by-side evidence with a run.sh, a test in tests/adoption.rs, a
    plan for every gap, and the measured disk each skill's container drops.
    The adoption workflow runs it all.

Fixed

  • Convert, math: OMML text is set in Cambria Math with letters from the
    mathematical italic block (m:sty p/b upright, m:nor ordinary text).
    A display equation is centred unless its m:jc says 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-substitution passed. It is now generic
    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 repaired DUPLICATE_RID. Two
    different relationships under one Id are refused. Word refuses both.
  • Command line: converting to .docx or Markdown refuses --report,
    --font-report and --fail-on-substitution instead of ignoring them.
    A page-marker warning names the step that failed, and --timeout must
    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 holding aes at 0.9.2 and
    rdocx-opc at 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_windows and str::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's nonminimal_bool flagged 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/common keeps allow(dead_code): each test binary
    uses part of it.
  • Every CLI command but convert refuses 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 --json is 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 convert is a
    subset of the CLI's (no --timeout, --fail-on-substitution or -t),
    and exit 2 also means validate findings. The npm CLI needs Node 18.3+,
    and crates.io builds need Rust 1.94+ from 0.11.3. The body: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.rs runs 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.toml and Cargo.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

Choose a tag to compare

@github-actions github-actions released this 04 Oct 00:51

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 convert paints 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:done in 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. A w:commentRangeStart at 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 and kept_span sums 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 bare w:del
      followed by w: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.
  • Package:
    • Relationships and content types written with explicit closes are read: a
      <Relationship …></Relationship> (and a Default/Override in
      [Content_Types].xml) was skipped, and with it the part it named.
      Corpus 37c6c62345 lost its footer and scored 49. PartFs::open re-reads
      every .rels and the content types with each explicit close folded in.
      1 of 2925 corpus documents; Word reads both forms alike.
  • Tables, grids and borders:
    • A row a content control wraps is painted, as Word paints it: a w:tr
      inside w: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_spans extends a lone wide tcW cell 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/gridAfter skip 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_edge sizes a double sz at
      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.
  • 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; a num stop 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, ISO end) 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 →...
Read more

jubarte v0.11.0

Choose a tag to compare

@github-actions github-actions released this 03 Oct 06:46

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.sh step 13 runs scripts/check_release_facts.py: the
    release is not done until jubarte-app's data/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.sh now commits data/ with the app's release files
    (the facts were left behind as "not a release file").

  • jubarte self-update can 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 its SHA256SUMS.txt was replaced
    too. Signing is staged: no key ships yet, and until the maintainers
    commit one the updater checks SHA256SUMS.txt only, as before. Once a
    key and a first signed release are set, that release and every later
    one must be signed; a --version downgrade to an earlier, unsigned
    release is checked by its SHA-256 alone. release.yml signs each
    archive before writing SHA256SUMS.txt when the ZIPSIGN_PRIVATE_KEY
    secret is set, and refuses to publish unsigned archives once a public
    key is committed under keys/. Setup and key rotation:
    docs/SELF_UPDATE.md.

  • The font report says whether each requested font was substituted: a
    substituted field in convert --font-report and --report (true for
    the word_substitution, generic and unknown steps, and for
    open_fallback unless the bundled face is the requested family itself),
    FontReportEntry::substituted() in Rust, and
    FontResolution.substituted with RenderReport.substitutions in
    Python. jubarte convert --fail-on-substitution lists each substitution
    on stderr and exits 4 after writing every output, for CI.

  • comparer::CompareMode { Word, PowerTools } and
    WmlComparerSettings::new(mode), with with_author, with_date,
    with_detail_threshold and with_input_limits builders, 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
    InputLimits budget parsed from JSON, unknown keys refused.

  • Python compare_documents(..., input_limits=...),
    get_revisions_json(..., input_limits=...) and
    CompareOptions(input_limits=...); WASM compareDocuments(..., inputLimitsJson) and getRevisions(..., 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 for admission::admit,
    strict_translation::strict_to_transitional_docx_within and
    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. See fuzz/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, and docx_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 without overwrite. Setup for Claude Code, Codex and Gemini CLI
    in docs/adoption/mcp.md.

  • Gemini CLI extension: gemini-extension.json and GEMINI.md at the
    repository root; scripts/bump-version.mjs keeps the manifest version in
    step with Cargo.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's changed_ratio, the box around the
    changed pixels, and only_in for a page one side lacks. --out-dir writes
    a-page-NN.png, b-page-NN.png and diff-page-NN.png (b's page with the
    change painted magenta and boxed; --no-overlay skips it) for the pages
    that differ, and diff.json; --json prints 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 a RenderDiff of
    PageDiffs, and python -m jubarte_redlines diff-render matches the
    binary.

  • jubarte convert FILE --png --pages 1-3,7 rasterizes 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=) and Document.render(pages=) in Python. A
    page past the end is ConvertError::PageOutOfRange. RenderRequest is no
    longer Copy.

  • capabilities reports operations.diff_render and
    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 are w:pPrChange, and comment
    replies, resolutions and deletions carry into the redline. New
    markdown::patch_own_changes, capability
    operations.edit_keeps_revisions, and Python existing_revisions="keep".

  • watermark edit operation ({"kind":"watermark","text":"DRAFT"}, with
    optional color, diagonal and font; Python EditPlan.watermark)
    writes Word's own VML text watermark, in its Watermarks content
    control, into every default header. A first section without a default
    header gets a new header part with its relationship, content-type
    override and w: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, Python Document.validate,
    WASM validateDocument): Word-validity findings beyond the schema, as
    data. Each finding carries a stable code (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 --repair fixes it. The Ring-1 invariants the test suite gated
    every produced package on (tests/common/validity.rs) now live in the
    library, with the five jubarte debug triage checks
    (debug::findings) behind them. Exit 0 clean, 2 findings, 1 unreadable;
    --json prints one object per finding.

  • jubarte validate FILE --repair OUT.docx (validate::repair,
    Document.repair, repairDocument) fixes the findings with a
    deterministic fix (unbound mc:Choice Requires prefixes, w:t inside a
    deletion, w:delText outside 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...

Read more

jubarte v0.10.1

Choose a tag to compare

@arthrod arthrod released this 30 Sep 23:13

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-redlines on 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_changes folder) checked out beside this repository.
    The 537 documents they read are copied into tests/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, and tests/bench_fixture_paths_resolve.rs fails on any
    named copy that is missing.

Added

  • jubarte debug FILE --check render lists 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 and fontTable.xml fonts. 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-text and -p narrow 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 FILE lists every change with an id (body:rev:12,
    header1:rev:3), its kind, target, author and text (--json for JSON
    lines); jubarte accept / reject take --id, --author and --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's inside names 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 follows existing_revisions. A change selected by both sides is
    refused with REVISION_CONFLICT, an id the document lacks with
    UNKNOWN_CHANGE; the report lists the resolved ids.

  • jubarte debug FILE -c text prints each story part's paragraphs with
    {+inserted+} / [-deleted-] runs, the paragraph mark's revision state
    and table rows; -c xml prints 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 runs is text with 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 theme a:ea and 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:contextualSpacing drops 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...
Read more

jubarte v0.10.0

Choose a tag to compare

@github-actions github-actions released this 29 Sep 04:34

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|powertools names the compare presets: word
    (the default, Word Compare's layout) or powertools (the classic
    PowerTools fallback, still also spelled --powertools-faithful).
    docs/WORD_DIFFERENCES.md lists where jubarte's redline still differs
    from Word's and which mode gives which.
  • jubarte-wasm gains the agent surface the CLI and Python already have:
    inspectDocument, documentMarkdown, sourceSha256, applyEditPlan
    and previewEditPlan (refusals returned as data with their code and
    per-operation outcomes), editReportJsonl and capabilities, in the
    full and slim builds.
  • Input admission for the agent surfaces: inspect, text, edit and
    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
    codes INPUT_LIMIT, DUPLICATE_PART, UNSUPPORTED_PACKAGE,
    INVALID_PACKAGE and INVALID_XML; capabilities reports the budgets.
    Compare keeps its historical tolerance.
  • jubarte inspect reads 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 capabilities reports what the build can
    do.
  • jubarte edit applies 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 several insert_paragraph operations after one anchor keep plan
    order.
  • jubarte convert --png renders 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 a Document with inspect, edit,
    convert, compare and accept/reject methods, and python -m jubarte_redlines exposes the same commands (compare is the explicit
    compare subcommand there); EditPlanError.message
    carries the engine's detail.
  • Edit plans restyle and join paragraphs. format_paragraph sets a
    paragraph style (by id or name; an unknown one is refused with
    UNKNOWN_STYLE and the defined ids), alignment, line spacing as a
    multiple and space before/after in points, as one tracked property change
    that reject undoes. merge_paragraphs joins 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. replace and insert
    take a format (bold, italic, underline, a Word highlight colour) for the
    new text only. The Python EditPlan gains the matching builders.
  • replace takes "whole": true (Python whole=True): the redline shows
    all of find deleted, 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 a message saying why.
  • Headers, footers, footnotes and endnotes are editable stories. inspect
    lists them under stories and jubarte text prints them after the body,
    each paragraph under an id such as header1:p:0 or footnotes:p:2. An
    edit plan addresses them by that id or by adding "story": "footer1" to
    an index, starts_with or contains selector (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
    Python Snapshot gains stories.
  • Adoption guides, workflow examples and a document-operations agent skill.
    The Acme letter example now ships make_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 cli for 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 (SharePoint customXml items, 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 patched rdocx-opc; the ooxmlsdk test
    oracle 0.12.
  • scripts/release.sh requires 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.mjs warns that
    release.sh is the source of truth for releases.

Fixed

  • jubarte-wasm: the patched rdocx-opc decodes escaped attribute values
    when it reads relationships and content types, so a hyperlink target
    holding &amp; is written back once instead of as &amp;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:AlternateContent is 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 a w: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...

Read more

jubarte v0.9.3

Choose a tag to compare

@github-actions github-actions released this 28 Sep 15:10

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 IF that lost \*MERGEFORMAT no 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 zip crate's time feature, 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, PAGEREF and REF printed "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 _GoBack is 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 now rId1, as
    Word numbers them, not rId0.

  • 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 in w:pPrChange whatever 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/tblCellMar of 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 from TableNormal documents stay bare in Word's redlines;
    stamping them shifted every row below (file_46 × file_47).

  • Synthesized numbering for a dangling numId copies Word's level geometry:
    a num tab 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...

Read more

jubarte v0.9.2

Choose a tag to compare

@github-actions github-actions released this 26 Sep 16:44

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
    (post format 3), unused metrics and every cmap/name record the PDF
    does not read; a collection (.ttc) face subsets like a lone font instead
    of embedding the whole collection. A CID font's /W lists 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
    relative Td, so a page is no longer one BT … ET per glyph. Word-device
    glyphs (11.04/16.08pt body sizes) keep their own q … cm … Q: MuPDF hints
    them differently when the 0.24 scale sits in the text matrix.
  • With --compress the 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-palette for custom (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;
    word reproduces 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_DIR or 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 outside fontTable.xml; vertical w:textDirection="tbRl" sections;
    ideographic line breaking with kinsoku; closing CJK punctuation hangs past
    the right edge; Latin inside East Asian text takes the ascii face and a
    quarter-em gap; Chinese and Taiwanese counting list formats; East Asian
    faces take Word's 1.3× line.
  • Right-to-left. w:bidi paragraphs mirror their alignment and indents;
    w:bidiVisual tables run from the right margin; RTL lines paint in visual
    order; RTL runs take their w:szCs size; minorBidi/majorBidi theme
    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's framePr, 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; sizeRel percentages take their own
    frame and spAutoFit boxes fit their text; VML groups place children in
    group coordinates and honour grpFill; VML lines stroke where Word draws
    them; w10:wrap; VML pictures honour the imagedata crop; drawing groups
    and canvases; custom geometry paths in EMU with bound-arc tessellation;
    a:prstClr/a:sysClr colours; block arrows point their own way.
  • Effects. a:softEdge fades, a:duotone recolouring, Word's "Washout"
    picture watermark, ellipse picture crops, text outlines with no fill, and
    w:w horizontal text scale measured as well as painted.
  • Metafiles and rasters. EMF paths, Béziers and window/viewport mapping;
    WMF META_POLYPOLYGON with 0-based object handles; BMP and GIF pictures;
    CMYK photos embed as RGB.
  • Fields and forms. FORMCHECKBOX legacy form fields; PAGE counts the
    page, in header/footer text boxes too; w:fldData stays binary.
  • Page furniture. tblCellSpacing gaps; continuous sections that switch
    columns and side margins mid-page; page borders, including the medium-gap
    inward band and page-relative placement; vAlign=center centres the
    layout box; titlePg without 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 without c:title paints
    no title.
  • Word's layout rules (probed, then implemented): docGrid charSpace in
    4096ths of a point, halved for half-width; compat mode 15; widow/orphan
    control on by default as in Word; keepNext/keepLines holding table
    rows together; pageBreakBefore never 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.
  • /ToUnicode on 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:rFonts names only hAnsi (e.g. Arial Unicode MS) painted its ASCII
    number in that face and took its taller line; hAnsi covers non-ASCII
    characters only, so 1. 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;
    topAndBottom floats push later lines; tight page banners move the
    paragraph above their anchor; behind-text boxes paint under the body;
    distT/distB stay off the body for header floats; front floats stack by
    relativeHeight across pictures and boxes; wp:align aligns within its
    relativeFrom frame; 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; pct vs
    dxa widths under noWrap; a zero dxa width is auto; gridBefore /
    gridAfter leave their grid columns empty; tblPrEx; cell margins and
    tcBorders edges 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 without tblInd stay 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 installed altName.
  • Tabs, lists and indents. A hanging indent is an implicit tab stop; a
    list marker lifts only its own line; numbering-level pPr/jc and
    lvlOverride apply; 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 inside w:t is a space.
  • **Spacing...
Read more