A script whose later step failed kept the successful prefix in the document
but truncated the prefix's undo entries (and returned before the command's
aggregate undo entry was recorded), so the edits could not be undone at all.
A failed script now restores the document, selection and history from before
it started: all-or-nothing.
Regression test: frame.create followed by an unknown command is an error, the
document has no frame, nothing is left to undo, and the next edit undoes
normally. The one-undo-step success path still holds.
parse() expanded parameter entities with up to eight passes of whole-string
replacement, each pass cloning the text and re-inserting the entities'
original values. A pass count is not an output bound: a few hundred bytes of
nested entities inflate multiplicatively (a 449-byte DTD reaches ~4 MB here,
and larger chains would go to gigabytes), so a small file could exhaust
memory and CPU. Expansion now stops once it has added more than 1 MiB over
the input.
Regression test: a nested-entity DTD under 500 bytes is rejected with the
budget error, and a small inline entity still parses.
The command removed the section at a nonzero start with retain() and only then
looked it up to keep its properties, so the lookup always fell back to the
defaults: editing one field (a marker, a start number) silently reset the
style, prefix, include-prefix flag and start number. The existing section is
now read before the list is rebuilt.
Regression test: a Roman-numbered page-2 section with prefix A- and marker
First, then marker Second keeps startNumber, style, prefix and includePrefix.
Measured in Zero2Graphic's video: InDesign's placeholder paragraphs run 5,
10 (at 18 pt), 6 and 8 lines in a 5.9 cm column, about 2, 2, 3 and 3 of
DesignCraft's filler sentences. The rhythm started 3, 4, 2, 5.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The panel was a row of text buttons (Add, Subtract, …, Make Compound,
Release). InDesign's has four rows of icon buttons: Paths (Join, Open,
Close, Reverse), Pathfinder, Convert Shape and Convert Point. The icons are
new drawings in icons.rs. Two engine commands back the rows that had none:
path.join (close an open path, or join two open paths at their nearest
ends) and path.convertPoints (plain, corner, smooth, symmetrical, on the
selected points or whole paths).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`math.svg` and `math.insert` take an unbounded `latex` string, and the parser's
arg/atom/list/list_until_right recursive descent recursed once per group: 100k
nested braces, `\frac`s or `\left(`s overflowed the stack (an abort the command
guard cannot catch). Past MAX_NESTING (256) groups stop descending, so the rest
of the input is consumed iteratively.
Regression test: 100k nested braces, `\frac`s and `\left(`s typeset to an SVG;
the old code aborted with a stack overflow.
InDesign's placeholder text comes in paragraphs; Zero2Graphic's magazine
tutorial styles its second paragraph as an 18 pt lead (2:16). DesignCraft
filled the frame with one paragraph, and the last filler sentence ran into
the first ("interit.Ovid"). Paragraphs now hold 2 to 5 sentences, and an
overset fill is trimmed to the last whole sentence.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
InDesign's Pathfinder result takes the attributes (and content) of the
frontmost object, except Subtract, which keeps the backmost. DesignCraft
kept the backmost for every op but Minus Back, so a photo placed over
strips and intersected with them was deleted and the black strips stayed
(Zero2Graphic's magazine tutorial, 7:19).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Found while testing crates/doc/src/build.rs:377 (Document::insert_pages), called from crates/engine/src/cmd/layout.rs:91
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Selecting an empty paragraph (a blank line) puts a collapsed caret on its
hidden paragraph mark, the only character on that line. Previously
type.char either deferred the formatting to the next typed character (in
a non-empty story) or dropped it on the floor for composition, so there
was no way to change a blank line's height — matching InDesign's ability
to select the pilcrow and adjust leading.
- engine/cmd/text.rs: recognize an empty paragraph's own caret position
(empty_para_mark) and apply character attributes straight to that
byte's run instead of deferring to typing format.
- compose/lib.rs: an empty paragraph has no glyphs of its own, so resolve
the hidden mark's run (if overridden) for the paragraph's base leading
instead of always falling back to the paragraph's plain default.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
delete_para and delete_char previously reassigned only the story text
runs/paragraphs using the deleted style, leaving other styles' Based
On, paragraph styles' nested/nested-line/GREP style refs, the drop cap
style, and the footnote reference style pointed at a name that no
longer exists.
Extract redirect_style_refs() from rename_style() (everything it does
except updating the registry entry's own name) and reuse it from both
delete commands, so deleting a style now redirects every reference
type to the replacement style before removing the registry entry.
Also teach rename_char_refs() about drop_cap_style, which it missed
even for renames.
Add validation: name must be non-empty and refer to an existing
style, and replaceWith (if given) must refer to an existing style and
can't be the style being deleted.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Adobe's "Set up a modern magazine layout" (Brian Wood, 2:12–4:23) locks a
page frame drawn round the spread (Cmd+L), then draws the text frames on
top of it with the Type tool. In DesignCraft the press landed in the
locked frame, which the Type tool turned into a text frame, putting the
caret in it; no new frame was drawn.
- The Type tool takes a hit only on text frames (locked or not, as
InDesign lets you edit their text) and unlocked empty frames; over a
locked empty frame, the press starts a new text frame.
- text.placeCaret refuses to convert a locked empty frame (or one on a
locked layer).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Georgian is a Properties text language (IDML "Georgian" maps to ka). Georgian has no initial capitals: Mtavruli is only for all-caps text, so Change Case title and sentence case and index section heads leave Mkhedruli as it is (doc::capitalizes); upper case still gives Mtavruli.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YMTAbGNRxnMEzRvYnQGphF
Command descriptions left the new-text hyphenation default and image fitting outcomes implicit. State the default and each fitting mode's semantics, and add an MCP example that disables hyphenation. No behavior changes.
Chris Nielsen's "The Adobe InDesign Toolbox" (10:10): with the White
Arrow he clicks outside, then presses on a frame's corner and pulls it up,
turning the rectangle into a lopsided triangle. DesignCraft only looked for
anchors on objects already selected, so the press fell through to moving
the whole frame.
The Direct Selection tool now also looks at the anchors of the object
under the pointer: on one, it selects the object and drags that point, in
one undo step. Elsewhere on the object it still drags it whole; Shift-
click still toggles the selection.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Chris Nielsen's "Numbering Page Sections" (7:03): starting a section at
page 1 on a left page (the spread 8–9 selected) turns it into a lone
right-hand page 1 in InDesign, and the pages after it shuffle. DesignCraft
kept the page on the left, so odd page numbers sat on left pages.
- Document::repaginate gives each facing page the side of its number in
its section (odd: right, or left bound right to left). Document Setup's
Start Page # is one case of it.
- layout.section repaginates when a section starts, changes or is removed;
objects go with their pages, in one undo step.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Typing into a text frame never turned on the OS input method, so macOS
dead keys didn't compose: ⌥E then I typed "ei" instead of "í" (same for
⌥N, ⌥U and Japanese/Chinese input). The canvas now asks for the input
method while the Type tool has a caret and handles its events.
The text being composed shows in the story as an interaction preview
(new `text.compose` command), so it takes the format the next typed
character gets, drawn on a yellow box. On commit it's dropped and the
result typed with `text.insert` (one undo step); any other edit, tool
change or drag drops it first and tells the input method to forget it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A font whose fsType allows embedding bitmaps only (bit 9, 0x0200) was
packaged with its outlines; it is now refused like a restricted one.
char_css wrote font-weight: bold for any style containing "bold", so
Semibold showed as Bold and Light as Regular; it now writes the numeric
weight (100-900) and style from the face's style name. Same approach as
aiongg's integration branch (#259, 8f54d1cc), with his test.
coverImage was read with an uncapped std::fs::read, so a huge file or a
device like /dev/zero could exhaust memory. Read it through the data
merge's capped reader, which now also stops the read itself at the cap
(a device reports no length), as in aiongg's integration branch (#259,
6eae3620), with /dev/zero as a test.
Main moved double-click handling to the second press (click_count 2, from the mouse or the control channel); the handle double-click that fits the frame now sits there too, before main's text-frame edit.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review fix from aiongg (6db40bdb, the radial_ellipse part), ported to this
branch: a straight line's box has no ellipse, so its stroke's radial
gradient showed only the last stop once #823's gradient strokes land.
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review fix from aiongg (72d7ea2e, with the line tests of 6db40bdb), ported
to this branch: Fill::linear_axis gives screen and PDF one gradient axis,
and a box with no extent along the gradient's angle takes its longer side.
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
LYH Studio episode 6 colours the "1969" headline (8:28) and the loop's
stroke (9:15) from the Swatches panel, whose header holds InDesign's
Fill/Stroke proxy, Formatting Affects Container/Text and the Tint field.
- Header: a Fill/Stroke proxy showing the colours of what colour edits
apply to (panels::color_target; a click brings the other to the front,
as X does), the container/text buttons (app.formattingAffectsText) and
the tint of the colour in front.
- The Tint field sends only the tint, so each selected object (or run of
text) keeps its own swatch: object.fill takes a tint without a swatch,
and a stroke tint alone doesn't give a stroke-less object a stroke.
- Clicking a swatch applies it to the fill or stroke in front
(panels::apply_swatch, to the text when colours affect text); that row
is highlighted.
- Bottom row: New Color Group, New Swatch, Delete Swatch (the swatch last
clicked). Rows and the proxy are named for accessibility (and tests).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A Typography panel (Window ▸ Show Panel, and the icon column) lists the
features of the selected text's fonts, merged over those fonts, grouped as
Ligatures, Alternates, Stylistic Sets (named by the font), Figure Style,
Figure Width, Figure Position and Capitals. A section or choice appears only
when a font offers it.
Each feature is a checkbox: checked when all of the selection has it, a dash
when only some of it does, empty when none does. A feature only some of the
selection's fonts offer is italic. Mutually exclusive choices are checkboxes
too, and choosing one turns the others off.
type.openTypeFeatures reports each feature's applied and available state over
the selection. type.feature turns features on or off across the selection,
keeping each run's other features. The fonts crate reads GSUB and GPOS
feature lists and stylistic set names.
The Character panel's OpenType menu is now a button that opens or focuses the
Typography panel.
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>