The paragraph box, and the added-text box, each keep one Undo history under
one egui id, shared by every tab. A box cleared it when it opened on new
text, but a box in another tab on the same text (two copies of a document)
kept it: typing taken back with Undo in one tab came back with Redo in the
other, from the key or from the menu. The document that last used the
history is now kept beside it, and the history is cleared whenever another
document's box takes it over.
Tests: the paragraph and added-text cases, each with two copies of a
document open; the paragraph case tries the menu's Redo too.
The paragraph box, and the added-text box, each keep one Undo history under
one egui id, shared by every tab. A box cleared it when it opened on new
text, but a box in another tab on the same text (two copies of a document)
kept it: typing taken back with Undo in one tab came back with Redo in the
other. The document that last used the history is now kept beside it, and
the history is cleared whenever another document's box takes it over.
Tests: the paragraph and added-text cases, each with two copies of a
document open.
`/Annots` may hold an annotation dictionary inline instead of a reference
(the standard asks for references, but files do it and PdfCraft accepts
them). Deleting a comment or clearing redaction marks only matched
referenced entries, so an inline reply (`/IRT`) or pop-up (`/Parent`) of
what went stayed behind, pointing at nothing. The pop-up an inline reply
or an inline comment names (`/Popup`) stayed too. This predates the shared
walk.
`goes_with` matches an entry against what is going, inline entries
included; `delete_annotation` and `clear_marks` filter with it. The walk
follows the pop-up an inline reply names, and deleting an inline comment
takes the pop-up it names.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An incremental save wrote every object added during the session, whether
anything still referred to it or not. A redaction mark added and then
cleared (or a comment added and deleted) was therefore written into the
file as an unreferenced object, with its QuadPoints, overlay text and
author.
`SaveOptions::drop_unlinked` writes such additions as free entries instead:
objects whose numbers the file never used and that nothing the update
writes refers to, walked from the trailer and the file's changed objects
through additions only. `/Size` and the numbering stay as before, so undo
past the save can bring an object back under its number. The option is off
by default because the standalone document timestamp is found by scanning,
not by reference; `Session::save_bytes` (Save, and the automation save)
turns it on. Full saves already left these objects out.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Clear all wrote `/Annots []` on a page that had no annotations and replaced
an indirect `/Annots` array with an inline copy. It also left replies to a
mark in place, so their `/IRT` kept the cleared mark in the document (and in
the comment list, as a reply to nothing).
`clear_marks` now writes the list through the annot crate's `set_annots`
(made public), as deleting a comment does, and removes each mark's replies
and pop-ups with it. The "what goes with a deleted comment" walk moves from
`delete_annotation` into `with_replies_and_popups`, which both use; it visits
each annotation once instead of sweeping the list until nothing changes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- A row aligned on its own lost its alignment when the paragraph was then
given a colour, size or font: the rewrite left the rows' alignments out
once they had been applied. They now go with every rewrite.
- egui drops a box's selection while the box is unfocused, so after one
alignment or format a second choice went to the whole paragraph or box.
The selection is given back when formatting returns the focus.
- Redo from the menu or toolbar applied the box's typing first, a new edit
that dropped the document's Redo. It now redoes in the box, as the key does.
alignment_frame found the column by the left edge most of the page's other
rows share. On a page of two columns, a title over the column with fewer rows
took the other column's edge, failed to fit in it, and was aligned in its own
box. Only rows reaching across the title now count.
Tests: the two-column case; a two-colour title (the styled-region path) is
framed too; a justified paragraph sent with its rows split by line breaks, as
the editor sends it, still fills every row but the last.
Finishing a box trimmed its text before comparing it with the item, so
an unchanged box whose text was saved with trailing spaces finished as
an edit. Before an Undo or Redo step that edit was applied, clearing
the document's Redo. A box matching its item is now no edit.
The editor of a box narrower than its 60-point minimum was anchored by
the box's own alignment. A box whose lines are all right-aligned (or
centred) on their own while the box stays Left wasn't anchored, so its
text was placed against the wider editor's edge, past the box. The
lines are now compared with each other and the editor keeps the edge
they share.
Without new text, text_edit rewrote a paragraph from its text joined
with spaces, so its rows were rewrapped into one and only the first
entry of `aligns`, which counts rows, had any effect. With `aligns`
given, the paragraph's own rows (its editor text) are kept, as in the
app.
The editor is at least 60 points wide on screen, so a short box can be
clicked into. With the box's alignment applied, right-aligned or centred
text in a box narrower than that was placed against the wider editor's
edge, past the box's right edge. The editor now reaches past a narrow
box away from the edge its text keeps, so the text stays where the page
draws it.
With typing undone in a paragraph or added text box, its text matches
the page again, so Undo and Redo went to the document. Redo then closed
the box and redid a document change (or nothing), instead of putting the
typing back as the box's own Redo did before. And Shift+Cmd+Z is also
Cmd+Z to the Undo shortcut, so even a Redo the box kept was taken as a
document Undo.
Redo stays with the box while its own history has typing to put back
(the test egui makes when it handles the key), and Shift+Cmd+Z is never
a document Undo.
Centring one highlighted line of a paragraph moves it out of the
paragraph's rows, so the next alignment of it saw a one-row paragraph
and aligned it in its own box: Left left it where it was, and since a
single row grows to the page's margin, each Centre moved it further
right. An alignment for a one-row paragraph is now laid out across the
column of text it sits in (the left edge the page's other text uses, to
the furthest right those rows reach), or its own box when no column
holds it.
Format text's alignment always re-aligned the whole paragraph or added
text box, and clicking it closed an added text box, losing the
selection, so one line couldn't be aligned. With text selected,
alignment now applies to the lines (text between line breaks) the
selection touches; with only the caret, to the whole box as before.
- Added text stores each line's alignment in its /PCAdded parameters
(/Aligns, null where a line follows /Align). A justified line's last
row is not stretched.
- An edited paragraph sends its rows' alignments with each rewrite
while it is open (BlockStyle::aligns), in both the single-style and
the styled-region rewrite.
- Both editors draw each line by its own alignment, and the alignment
buttons show the selected line's. An added text box stays open while
Format text is used, its style applied at once; clicks are judged by
where they were pressed, so a touch or pen tap on the panel doesn't
close either editor.
- Automation: `aligns` on page_add_text, content_update and text_edit.
Ctrl+Z is left to a focused text field, so in the paragraph editor it
went to the box's own Undo history. That history still held the
paragraph's text from before the last formatting change, so Ctrl+Z put
it back into a box laid out for the page after the change, a layout
the page never had, and the document kept the change. The box shared
its id across paragraphs too, so it could put back another paragraph's
text.
The history of the paragraph editor and the added-text box now starts
at what the page has. Undo and Redo keep the keys only while the box
holds typing the page doesn't have; then they reach the document. An
open box is applied (as clicking away applies it) and closed before an
Undo or Redo step, so it never shows a state the document has left;
text it can't apply keeps it open and stops the step.
Opening a centred or right-aligned Add text box drew its text at the left
edge, a layout the box never had, and Ctrl+Z inside the box (which goes to
the text, not the document) left it showing that way. The editor now lays
the text out with the box's alignment. Justified text is still shown
left-aligned while editing (TextEdit can't justify).
CodeRabbit: a recognition run reads a snapshot of the document and its result
names a page by index. If the document changed while it ran (a page deleted or
moved), Session::apply_ocr would put the words on whatever page now has that
index. An Edit-started run now remembers the page and the document's edit
generation (AutoOcr); when its result arrives after a change it is dropped and
the page's "tried" mark is cleared so the page in view is judged again. A run
the user asked for is unchanged. The unit test uses a made-up result (no OCR
models) and fails without the check.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Rebasing onto main met its annotation listing fixes (c82dc91a, dfde9078):
inspect now lists each annotation once, at the first page and position
that lists it, told apart by the object its reference chain ends at,
and reads a page-shared /Annots array under its first page only. The
rebase collects reply slots in that same pass and matches /IRT by the
object it resolves to, so a reply finds its comment wherever the
listing keeps it.
The test fails with the slots keyed by the raw /Annots reference (as
before the rebase) and with /IRT matched by its raw reference.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`link_replies` makes an annotation whose /IRT leads nowhere a comment of
its own. For a status or checkmark that meant a card in the Comments
panel (and an invisible hit area on the page, and a root in
comment_list) that the header's count left out; before threading by
reference, most of these were hidden. The realistic case is a comment
deleted from another page than its status replies.
`Annotation::is_comment` (not a reply, not a status or checkmark) is now
the one test for "listed as a comment": the panel's cards, page badges,
filter menus and count, the page's hit-testing, comment_list's roots and
the summary all use it. A Replace Text caret still shows in its
strikeout's card: it holds the replacement text, and the count already
treats the pair as one comment.
From CodeRabbit review on #4061.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
From review:
- `link_replies` only finds parents among the annotations both readers
list, and neither lists links, form widgets, pop-ups or other
non-markup annotations (`is_comment_subtype`), so an /IRT to one of
those leaves its annotation a comment of its own. Its doc comment now
says so; the UI edge-case test adds an /IRT to a link and to a form
field, and the engine test an /IRT to a link (after an edit too).
Both fail if a reader lists links as comments.
- The Comments panel's grouping comment now says that a status or
checkmark set on a reply, at any depth, is that reply's: the card's
badge and checkmark leave it out, and comment_list reports it on the
reply.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The previous commit filed a status or checkmark under the annotation it
was set on, but comment_list only reported states for comments, so one
set on a reply dropped out of the listing. Each reply in `replies` now
carries its own `status` and `marked`, read the same way as a
comment's, and the tool description says so.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A status or checkmark is a state reply whose /IRT names the annotation
it applies to (ISO 32000-2 §12.5.6.2). comment_list gathered state
replies at any depth of a thread (since #839 listed nested replies), so
a checkmark or status set on a reply was reported as the comment's own,
while the Comments panel, comment properties and the page's context
menu showed the comment unmarked. comment_list now files them under the
annotation they were set on, as the panel does.
A UI regression pins the card and the page's menu (it also fails if the
menu counts checkmarks at any depth), and an automation one pins
comment_list.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The comment tools resolved `id` (the comment's /NM, as comment_list
reports it) to the first annotation with that name. /NM need not be
unique: in a PyMuPDF file every page's first comment is `fitz-A0`, so
comment_reply, comment_set_status and the rest acted on page 1's
comment whatever page the caller meant. A shared id is now refused
with a message pointing at `page` + `index`, which always name one
comment. A new end-to-end test also checks that comment_list threads
such a file by reference.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Follow-up to threading by /IRT reference, from review:
- An /IRT that reaches no listed comment (the annotation itself, a
loop, a pop-up, an object no page lists) left the annotation a reply
shown nowhere, though still counted. `link_replies` now settles the
links in both readers (inspect on open, the engine's list after an
edit): a reply keeps its parent only when the chain reaches a comment
within 64 steps, and anything else stays a comment of its own.
- A reply to a reply was never shown in the Comments panel (only
comment_list followed the chain, #806), but the new header line
counted it. The panel now files every text reply under the comment
its thread starts from (`thread_tops`); a status or checkmark stays
with the annotation it was set on.
- A Replace Text caret (/RT /Group) is grouped with its strikeout, not
a reply to it, so the header no longer counts it (new `grouped`
field on both readers).
- The engine summary and comment_list gather replies once instead of
rescanning every annotation per reply (quadratic on large reviews).
- Inspect takes the first listing in page order instead of a min over
an unordered map.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The header keeps Acrobat's count of comments and adds the replies in
small text below it ("16 replies"), left out when there are none and
counting the whole document rather than the current search. Status
changes and checkmarks are not replies. The plural label is in all 33
catalogues with each language's plural forms; ATTRIBUTION.toml records
the new hashes and that the label is an AI-assisted translation not yet
reviewed by native speakers.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KvugvPrjZtPVudhh7cMa54
Replies were matched to their parent by /NM across the whole document.
/NM need not be unique: PyMuPDF names the first comment on every page
`fitz-A0`, so every reply in such a file was filed under page 1's
comment, and the other pages showed none. Both comment readers (render
inspect on open, annot summaries after an edit) now resolve /IRT to the
parent's page and /Annots position by object reference, and the
Comments panel, comment properties, the canvas thread, the engine's
comment summary and the comment_list tool thread by that.
The Comments header now counts comments without their replies, as
Acrobat does (28 rather than 44 for the reported file), and a reply's
thread line runs from the avatar above it (the comment's or the
previous reply's) to its own instead of a 12 px stub.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KvugvPrjZtPVudhh7cMa54
Local CodeRabbit review follow-ups for the expanded All tools view:
- The search field promises tools and commands, but an entry with a menu
(Watermark: Add, Update, Remove) matched only by its own name, so
"remove watermark" found nothing. Entries now also match by their menu
commands' names, in English or the UI language.
- With the command palette open over the view, Escape closed the view as
well as the palette. The palette now owns Escape, as dialogs do.
Each new test was seen to fail with its fix reverted.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A content-only edit re-reads Document::added() and marks() only when the
edit itself is one that changes them. Edit::Batch has Scope::Content when
all its parts do, but content_refresh_lists never looked inside it, so a
batch of AddText/DeleteContent or AddWatermark edits left both lists stale
until the next full refresh or undo. It now answers for its parts.
Co-Authored-By: Opus 5.5 medium ultracode <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017gEgu9NcTcPqKovsLyfPxK
* G14: Romanian: translate the 65 catalog rows still in English
The Romanian catalog landed in main through the integration branch
(bf3a199f, PR #2505); the keys main gained since were filled with English.
This translates them: calculation order, the zoom box, rich-text formatting,
print options, Pages thumbnail size, Reshape comment, the CMS signed-attribute
and short-RSA-key warnings, and the page-numbering Start and page Subset
labels. Romanian translations main already has (the text field table) are
kept as they are. ATTRIBUTION.toml carries the new SHA-256.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JadgGo4G2GHaAMNb3qyygk
* G14: Romanian: quote „data” in the CMS warning; Subset reads Selecție
Review of #4016: the content type "data" is quoted so it isn't read as "the
date", and the page Subset label and the booklet subset label both read
"Selecție" instead of "Filtru" / the anglicism "Subset broșură".
ATTRIBUTION.toml carries the new SHA-256.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JadgGo4G2GHaAMNb3qyygk
---------
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* M9: visible signatures read upright on rotated pages
A visible digital signature placed on a page with /Rotate was drawn in
user space, so its graphic and details turned with the page: on scans
stored landscape with /Rotate 270 the signature came out sideways.
Stamps (#1398) and Fill & Sign marks (#2392) were already corrected;
signatures were not.
A new signature widget on a turned page gets /MK /R equal to the page
rotation (inherited rotation included, via pdfcraft_model::pages), and
its appearance is laid out in the box as displayed (sides swapped for a
quarter turn) with a /Matrix from pdfcraft_model::view_matrix_for that
maps it exactly onto /Rect. Signing an existing field follows that
field's own /MK /R, so a field without one is drawn as before. Pages
that are not turned produce the same bytes as before.
Tests: sign crate (layout equals the upright box's, /MK /R, /Matrix,
inherited /Rotate, existing fields with and without /MK /R, malformed
/MK /R) and an engine render test on 0/90/180/270 pages with the
quadrant image. Both fail with the fix reverted.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Keep one evidence/notes pair in the sign.digital parity entry
---------
Co-authored-by: vjeko2404 <vjekoslav@outlook.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* Save As and exports open in the document's own folder
Save As, saving a document that needs a location, the Office and
comment/form data exports, Optimize and Sign now start the file dialog in
the folder of the open PDF instead of the system's default. A new
document and an email attachment's temporary copy keep the old behaviour.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Match the full Outlook temporary-copy layout before skipping the document folder
Anchor is_mail_temp_copy to the cache layout (one random folder, then the file) and
to the Packages\Microsoft.OutlookForWindows_<id> folder, so ordinary paths that merely
contain those names keep the document's own folder. Regression cases added.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* M2: text extraction finishes a column before the next one across a paragraph gap (#3658)
The block order took the top-most unread block next, so after the first
paragraph of a left column it jumped to the top of the right column and
read the rest of the left column last. Now the nearest block below the
one just read, overlapping it horizontally and at most 2.5 heights of
its last line below it, comes next, unless an unread block sits above it
in its own span (a column above a full-width heading or footnote),
beside it on its left, or shares its first line (the first lines overlap
by at least 30% of the taller one: a table or form row, which still
reads across, also with labels and values in different sizes or on
shifted baselines; a line that only touches it or a small footnote
marker is not a row). Footers, footnotes and captions further away keep
their old place. Extraction, Find and copy share this order.
Known limit: when a paragraph in the next column starts on the same line
as the continuation (text on a baseline grid), it counts as a row and
the top-most rule applies, so the order of #3658 comes back there.
Telling prose from a row in that case needs more evidence.
Fixes#3658
* M14: update ctutils to 0.4.5 for RUSTSEC-2026-0385
The dependency-advisory gate rejects ctutils 0.4.2 (non-constant-time
mask generation and u128 comparisons, fixed in 0.4.4). It comes in
through crypto-bigint for the signing crates. Only its version and
checksum change in Cargo.lock.
(cherry picked from commit 1f8811761a)
* M4: the Bookmarks panel shows each bookmark's colour and style (#3193)
ISO 32000-2 §12.3.3: an outline item's /C is "An array of three numbers
in the range 0.0 to 1.0, representing the components in the DeviceRGB
colour space of the colour that shall be used for the outline entry's
text" (Table 151), and its /F flags say italic (bit 1) and bold (bit 2)
(Table 152). Both were ignored: every title was drawn in the theme's
text colour and face.
The inspector (OutlineItem) and the bookmark list (organize::Bookmark,
used once a document is edited) now read them: each component is kept
within 0-1, and black (the default) or a /C that isn't three numbers
reads as no colour. The panel draws bold titles in the semibold face,
italic ones slanted, and the colour as given, lightened towards the text
colour only as far as it needs to read on the panel (3:1), so dark blue
stays visible in the dark theme. bookmark_list reports color, bold and
italic where they differ from the defaults.
Mutants, each failing a test: italic or bold bit ignored (both readers,
the panel), black kept as a colour, no clamping, a four-number /C
accepted, colour ignored or truncated in the panel, the contrast floor
removed, always jumping to the text colour. Checks for non-finite
numbers were equivalent (clamping already covers any size), so they
are gone.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* M4: set a bookmark's style and colour from its menu or bookmark_set_style (#3193)
#3193 asks for a way to mark bookmarks by colour, for example chapters
that are rarely used. ISO 32000-2 §12.3.3 gives every outline item a
title colour (/C, Table 151, DeviceRGB, default black) and style flags
(/F, Table 152: bit 1 italic, bit 2 bold, default 0); the previous
commit shows them, and this one lets the user change them.
- organize: set_bookmark_color writes /C, removing it for black (the
default) and refusing a component outside 0-1; set_bookmark_style
sets or clears the bold and italic bits, keeps any bit the table
doesn't define, and removes /F when no bit is left.
- engine: SetBookmarkColor and SetBookmarkStyle edits, undoable, under
the same assemble permission as the other bookmark edits. Their undo
labels are the menus' own names, already translated in every
catalogue.
- ui-egui: the bookmark menu gains Style (Bold and Italic check boxes)
and Colour (the comment swatch grid, with Other colour...). Only
existing UI strings are used.
- automation: bookmark_set_style takes any of color ([r, g, b], 0-1),
bold and italic, and changes only what it is given.
Mutants, each failing a test: no range check (library and tool), black
kept as /C, a bit not set, a bit not cleared, an untouched bit cleared,
/F 0 written, the bits swapped (library and menu), the tool accepting
nothing to change or ignoring bold/italic, the menu's colour ignored.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* M4: a colour and a style set in one bookmark_set_style call are one undo step (#3193)
Review on #4171 (CodeRabbit). Given both, the tool applied two edits,
so undo took back only the style and left the colour, and a failing
second edit left the first applied. They now go in one Edit::Batch
named "Style", all or nothing. "Colour" and "Style" join EDIT_LABELS,
and "Style" BATCH_LABELS, so the history can translate them.
The test sets colour and style together and undoes once; with the two
edits applied separately it fails.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* M12: set a table's Summary and a header cell's Scope (#4103, #4104)
accessibility_set_tag_property takes `summary` (Table tags) and `scope`
(TH tags: Row, Column or Both). Both are Table attributes, written into
the tag's attribute object with /O /Table in /A: an existing one is
edited in place, keeping other attribute objects and their revision
numbers; one reached by reference is copied into the tag's own /A first,
so tags sharing it keep their value; a new one is added when there is
none. Clearing drops the key, an attribute object left with only /O,
and an empty /A. The tag type is checked after the role map.
accessibility_tags lists both. Setting a summary clears the
table-summary finding.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* M12: edit the Table attribute object that holds the value (#4103, #4104)
With more than one /O /Table attribute object in a tag's /A, setting a
summary or scope edited the first one even when a later one held the
key, and clearing left that later value in place (both still read back
as the old value). Setting now edits the object holding the key (else
the first Table object, else a new one), and clearing removes the key
from every Table object holding it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* M6: calculations read their notation only from a comment, and a regex after a condition as a regex
Two findings from CodeRabbit's review of #3550, both reproduced first:
- parse_calculate and calculation_sites found BVCALC and EVCALC anywhere
in the script, so `var s = "BVCALC a EVCALC";` read as the notation `a`:
renaming `a` rewrote the string, and the form calculated `a` instead of
running the script. The lexer now keeps the text of each comment, and
the notation is read from the first comment that holds BVCALC and the
EVCALC after it (notation_range), by both the Calculate tab and a
rename. A line comment counts, and a comment that never ends runs to
the end of the script, as before.
- A `/` after any `)` divided, so after the condition of `if`, `while`,
`for` or `with` a regular expression was read as code: renaming `a`
rewrote `if (flag) /getField("a")/.test(x)`, and in
`if (flag) /"/.test(x)` the quote started a string that hid the
script's real getField calls. The lexer now notes which `(` opened
such a condition (not a method of that name, `o.if(x)`), and a `/`
after its `)` starts a regular expression.
af::tests::notation_is_read_only_from_a_comment,
af::tests::a_regex_after_a_condition_is_not_read_as_code and
tests::a_script_that_spells_the_markers_in_a_string_runs_as_a_script
fail without the fix. Parity, ROADMAP.md and docs/roadmap.md updated.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BfMGHQNgdq5yf5tKZHGR3z
* M6: form.props-calculate notes that a regex right after a brace is still read as code
From CodeRabbit's review of this PR. The lexer still divides after `}`,
which ends a block as often as an object literal, so in
`if (x) { } /"/.test(s); event.value = getField("a").value;` the quote
starts a string and the getField is hidden from a rename and from the
calculation order. Checked with a scratch test before writing it down.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BfMGHQNgdq5yf5tKZHGR3z
* M6: a line comment ends at any line terminator, so code after `\r` is still read
From CodeRabbit's review of this PR. A `//` comment ran to the next `\n`,
so with `\r` line endings it hid the rest of the script: a rename missed
the getField calls after it (since #3550), and a string after it that
spells BVCALC ... EVCALC read as the notation. A `/` taken for the start
of a regular expression likewise looked past `\r` for its end, and could
swallow the next line's code.
Both now stop at any of JavaScript's line terminators (`\n`, `\r`,
U+2028, U+2029; line_ends_at), and U+2028 and U+2029 between tokens are
skipped rather than read as the start of a word.
af::tests::a_line_comment_ends_at_any_line_terminator fails without each
of the three changes: with `\r`, the string read as notation `a`; with
the comment fixed but not the regex scan, `i++ / 2;\r` swallowed the
getField after it; without skipping U+2028, the getField after it was
not a call.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BfMGHQNgdq5yf5tKZHGR3z
* M6: a word, number or regex's flags end at U+2028 and U+2029
From CodeRabbit's review of this PR. The scans for an identifier, a
number and a regular expression's flags took every byte past ASCII as
part of a word, U+2028 and U+2029 included, so in `x\u{2028}getField("a")`
the getField was part of one word and not a call: a rename missed it, and
the calculation order didn't read it.
`word` now takes a position and is false where line_ends_at is true, so
the three scans share one rule. The arm that skips U+2028 and U+2029
between tokens stays: without it, the punctuation arm would step one byte
into the character.
af::tests::a_token_ends_at_a_line_separator fails without the fix, for a
word, a number and a regex's flags alike; it and
a_line_comment_ends_at_any_line_terminator fail without the skipping arm.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BfMGHQNgdq5yf5tKZHGR3z
* M6: the `while` that ends a `do` loop counts as a condition, said and tested
A `/` after the `)` of a do-while starts the next statement, so it starts a
regular expression there too. The comment on BEFORE_CONDITION says so, and
a_regex_after_a_condition_is_not_read_as_code covers it (from review).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BfMGHQNgdq5yf5tKZHGR3z
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* fix(ui): keep colour popups open when "+" opens the custom colour picker
The colour popups used egui's default menu close behaviour (CloseOnClick),
which closes on any click, including the "+" swatch inside the popup. The
picker's open state was remembered, so it appeared only on the next open.
Close these popups only on clicks outside them: the quick bar's colour and
fill popups, the comment context menu's Colour submenu and Format text's
colour popup. Picking a swatch or pressing Apply still closes them.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* contributors: credit agent-reed by GitHub profile name
* test(ui): pin the fill popup and context-menu Colour submenu keeping their picker open
The quick bar's Fill colour popup and the comment context menu's Colour
submenu each get a kittest test: "+" shows the picker without closing the
popup, and choosing a colour still closes it. Both fail with the fix reverted.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* M3: a Launch link that names a PDF opens it in PdfCraft (#4168)
Documents that cross-reference each other often use Launch actions to
open another PDF next to them (#4168: 268 links such as
..\se\se_ST.pdf). PdfCraft follows no Launch action, so clicking them
did nothing, while the same link written as a GoToR opens.
ISO 32000-2 §12.6.4.6: a Launch action "launches an application or
opens or prints a document". Starting programs is not something a
document should be able to do, but opening a PDF is what a remote
Go-To already does. So a Launch action whose /F names a .pdf file, and
that has no /Win launch parameters (application, operation,
arguments), is read as the remote Go-To to that file's first page
(LinkTarget::RemoteGoTo with launch: true). It then goes through the
same path as one: PDF only, --root confinement, confirmation for
absolute paths, opened as a document and never handed to the OS.
Windows writers separate such paths with \, where §7.11.2.1 uses /, so
a path without any / has its \ read as /. doc_info's link target says
"action": "Launch" or "GoToR".
Tests: launch_actions_that_name_a_pdf_open_it_like_a_remote_link
(relative with \, a file specification, an executable, /Win, a bare
.pdf, a mixed path, a URL file system) and
a_launch_link_to_a_pdf_opens_it_like_a_remote_link (link_open opens
the PDF in the root; a program link opens nothing). Mutants, each
failing: the /Win check dropped, \ always or never read as /, any
extension, a bare ".pdf", case-sensitive ".pdf", Launch not read.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* M3: a Launch link with a stray /D still opens its PDF (#4168)
Launch (ISO 32000-2 Table 207) has no /D, but launch_pdf went through
remote_go_to, which rejects the action when /D isn't a valid GoToR
destination. A Launch naming c.pdf with /D 0 stayed Other(Launch).
The /F parsing is now shared (action_file) and Launch ignores /D.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0175FWj4xFhMhSH3UpMU8DSC
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
A note's /Name picks its icon, and ISO 32000-2 §12.5.6.4, Table 175 asks
every viewer to provide icons for seven standard names: Comment, Key,
Note, Help, NewParagraph, Paragraph, Insert ("Additional names may be
supported as well"). PdfCraft drew two shapes: Comment, and a page for
everything else. So choosing Key, Help, Insert, NewParagraph or
Paragraph in Properties changed nothing visible, and notes in received
files that use these names, or further ones such as Check or Star, all
looked like the same page. Recolouring such a note redrew it as a page.
note_icon now draws a shape of its own for each standard name and for
seven further names in common use (Check, Circle, Cross, CrossHairs,
Star, RightArrow, UpArrow). They are simple geometric drawings written
for PdfCraft, filled in the note's colour with a dark outline like the
existing two. NoteIcon gains the further names (NoteIcon::ALL); the
Properties chooser and comment_add offer all fourteen. Another name
still draws as Note, the table's default.
Tests: every_note_icon_draws_its_own_shape (all fourteen differ; an
unknown name and a missing one draw Note) and
a_star_note_keeps_its_star_when_recoloured. Mutants, each failing them:
from_name limited to the standard seven, Star or Key drawn as Note, an
unknown name drawn as Comment. One survivor reshaped Circle while
keeping it distinct, which the distinctness test rightly allows.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>