- Align Left, Center, Right, Justify Left, Justify All Lines and
type.align leave the document and the undo list alone when every
target paragraph already has that alignment as its own value.
- On the Mac, Em Space, En Space and Current Page Number from the menu
bar also do nothing while Quick Apply or a dialog is open, an input
method is composing or a canvas context menu is open, the cases that
stop the window's own shortcuts.
- Tests for both, and for Justify All Lines across threaded frames,
over a paragraph style, through IDML export and import, and with
parameters it does not take.
object.textFrameOptions refuses a gutter outside 0 to 15552 pt, a column
count outside 1 to 40 (it was clamped) and an on/off option that is not
true or false (it was skipped). The Control bar's Columns field stops at
40, as the other column fields do. The params text gives the ranges and
lists flexibleWidth.
type.para and paragraph style create and edit refuse left and right
indents outside 0 to 15552 pt and first and last line indents outside
-15552 to 15552 pt. A style made from the selection is checked with the
values it takes from the selection.
style.paragraph.apply and style.paragraph.redefine check that Minimum,
Desired and Maximum stay in order in each justification row they change,
over the overrides paragraphs keep, and refuse the change otherwise.
type.para, style.paragraph.create and style.paragraph.edit accepted any
number for paragraph spacing and justification. They now refuse Space
Before, Space After and same-style spacing outside 0 to 15552 pt, word
spacing outside 0% to 1000%, letter spacing outside -100% to 500%, glyph
scaling outside 50% to 200%, Auto Leading outside 0% to 500%, and a
Single Word Justification other than left, center, right or
fullyJustified. Each justification row needs Minimum <= Desired <=
Maximum over the values the paragraph or style resolves to after the
change, including the styles based on an edited style and the
paragraphs, table cells and notes that use them.
object.textFrameOptions refuses an inset outside 0 to 15552 pt or a list
that is not four sides, and a verticalJustification, firstBaseline,
autoSize or columnsKind that is not one of its names. These were applied
or skipped without an error before. IDML import reads these values as
before.
type.align with no `align` key applied Align Left, so a call that
misspelled the key, or passed a bare string, changed the paragraphs to
left alignment instead of failing. The command now returns an error,
leaves the text unchanged and records no undo step. A named value still
applies, and `align: null` still removes the override.
* Text: adjust hyperlink and placeholder source ranges on story edits (#1219)
Text-anchored hyperlinks and data-merge placeholders store byte ranges
(`start..end`) pointing into story text. When text earlier in the story is
inserted or deleted, these offsets remained unchanged, causing hyperlink
targets and data-merge placeholders to drift across unrelated text or
point to invalid UTF-8 boundaries.
This change:
- Implements `Document::adjust_text_ranges` in `designcraft-doc` to shift
or resize text hyperlinks and placeholders when story text is edited.
Pure insertions at or before the link shift it forward; insertions
inside expand the link's span; deletions before shift it back; deletions
consuming or overlapping the link clamp to surviving text or prune it if
entirely consumed.
- Cleans up obsolete hyperlinks and placeholders in `set_story_text`.
- Wires range adjustment into engine text commands: typing, deleting,
selection replacement, autocorrect, `story.replaceRange`, find/change,
and spelling change.
- Adds comprehensive unit tests in `designcraft-doc` and 7 integration
regression tests in `crates/engine/tests/hyperlink_offsets.rs`.
* fix(engine): adjust hyperlink and placeholder ranges for physically removed text under track changes
* fix(engine): adjust text ranges for formatted paste, case change, and drag move
* fix(engine): record change case adjustments per character to preserve enclosed hyperlinks
* fix(engine): adjust text ranges for track changes, inline markers, and preserve zero-length ranges
- Route Track Changes accept/reject deletions through adjust_text_ranges, and transfer replaced text hyperlinks to inserted text so links survive accept.
- Hook inline marker operations to adjust_text_ranges (notes, endnotes, xref, anchored objects, index markers, tables, editorial notes, unvar, and place text).
- Replace datamerge::shift_after with adjust_text_ranges.
- Only prune ranges that the edit emptied (s < e && s_new >= e_new), preserving zero-length ranges.
- Drop inverted ranges first and use saturating arithmetic in remap_range.
- Preserve doc.hyperlinks and placeholders ordering in move_text_ranges by mutating in place.
- Remove dead wrapper mark_deleted and call mark_deleted_reporting.
- Fix changes.track invocation parameter to 'on'.
- Add comprehensive regression tests in doc and engine covering all review items.
* fix(text): guard cell find/change, pre-record tracked replaces, shift para xref anchors
* fix(text): record stable IDs for tracked replacement redirects
* fix(text): reconcile range adjustments for review feedback
* fix(text): adjust ranges on delete surrounding and clean up dead auto edit
* Prune text ranges on TOC index and live caption regeneration
* test(engine): format regenerating_index_prunes_story_hyperlinks line wrap
The warning from #2190 looked only at the attributes passed. A composite
font made by style.compositeFont.set isn't an installed family, so
applying one warned "Missing font: Mixed" even when its entries were
installed. And a fontStyle-only call, or a family change that kept a
style the new family lacks, said nothing.
After a call that sets or clears fontFamily or fontStyle, type.char now
tallies the fonts the formatted text resolves to, with the code
font.list uses (composite entries per character), and warns about the
missing ones and substitute styles. At a caret it checks the font the
next typing gets. tally_story becomes tally_range over a byte range,
and describe_fonts is the part of used_fonts that type.char reuses.
A fontFamily the app doesn't have was applied with a null result, so a
script that asked for Courier Prime exported the PDF in the substitute
and exited 0. The font is still applied, since documents can carry
missing fonts and Find Font and preflight list them, but type.char now
returns {"warnings": ["Missing font: Courier Prime (using Source Sans 3)"]},
or "Missing font style: ..." for a style the family doesn't have, in
preflight's words. Other calls still return null.
- type.justifyAll ("Justify all lines", ⇧⌘F) next to Justify Left ⇧⌘J.
- Insert White Space › Em Space and En Space become app.insertEmSpace /
app.insertEnSpace, so the menu (and the Mac menu bar) shows ⇧⌘M / ⇧⌘N;
they are enabled at an insertion point only.
- Paragraph Rules… gets ⌥⌘J, shown in the Paragraph panel menu.
- On the Mac a menu-bar insert (Em/En Space, Current Page Number) does
nothing while a text field has focus, instead of typing at the canvas
caret behind it.
The warnings composed every story once with no page. For a frame on a parent
page that is wrong twice over, and I measured both:
exportPdf {"pages": [2]}
["the story in frame 9: 76 character(s) do not fit and are not in the
export"]
Page 2 used no parent at all, so nothing of that frame was in its export to be
missing -- and the warning named no page, because a parent frame has no
document page of its own. It now resolves the pages that draw the parent page
and reports only those:
{"pages": [1]} ["page 1: 76 character(s) ... frame 9 ..."]
{"pages": [2]} []
The second half is page-dependent text. A parent frame 112 pt wide holding
"Continued on page <marker>" fits while the number is one digit and does not
once it is two, so composing it once could not tell:
{"pages": [1]} []
{"pages": [9]} []
{"pages": [10]} ["page 10: 1 character(s) ..."]
{} ["pages 10, 11, 12: 1 character(s) ..."]
Each page that shows the parent is composed as that page, and the pages are
grouped by how much they lose, so a story still gets one line.
Also from review: `story.get` now says `overset` is a UTF-8 byte offset into
`text`, not a character index. A test measures the difference on French text,
where the offset runs ahead of the character count.
A page-based export leaves out the text its frames cannot fit, and said
nothing about it: an imported 58-page form lost whole sentences -- a
data-consent clause, a statutory rule, the last row of two tables -- with
`warnings` empty while preflight reported 19 overset stories.
The PDF and deck exports now report it, in the words preflight uses and with
the same count, naming the page and the frame. A reflowable export carries the
whole story, so it has nothing to report.
Carries the review fix: a spread export puts the whole spread on the sheet, so
choosing page 2 of a facing-pages document exports page 3 as well and page 3's
overset is as absent from the file as page 2's would be. The selection is
widened to the spreads it touches first. A page on another spread is still not
reported, so the filter keeps doing what it is for.
Rebased onto the post-split main: the PDF command is in export/pdf.rs, the
deck one in export/formats.rs, and preflight.rs is unchanged by the split. The
helper sits before export_pdf rather than between first_page_ref's doc comment
and its function, which is where the pre-split patch had put it.
Pin the final egui, winit and shared UI revisions and reconcile current main.
Preserve document-epoch input and screenshot boundaries, host action ordering,
and Windows path regression coverage. Add click/End batch characterization.
Validation: all nine Linux CI stages pass (3230 tests, zero failures); Windows
CI and bounded native editing workflows pass on the same production source.
* Type: story Optical Margin Alignment with a size, and Ignore Optical Margin (engine, IDML)
A story's Optical Margin Alignment applies to every paragraph except those
with Ignore Optical Margin; hang amounts scale with the story's size.
story.options sets it, type.para and paragraph styles take
ignoreOpticalMargin. IDML imports and exports StoryPreference
OpticalMarginAlignment/OpticalMarginSize and IgnoreEdgeAlignment. The
per-paragraph opticalMargin switch keeps hanging at the text's own size.
Part of #947.
* Type: the Story panel, and Ignore Optical Margin in the Paragraph panel menu and Paragraph Style Options
Window > Type & Tables > Story sets Optical Margin Alignment and its size
for the stories of the text selection or the selected text frames through
story.options, and is disabled without either. type.selectionAttrs
reports the stories' values as storyOptions, null where they differ.
The Paragraph panel menu toggles ignoreOpticalMargin, and Paragraph Style
Options > Indents and Spacing has an Ignore Optical Margin checkbox.
Part of #947.
* ROADMAP: the story Optical Margin Alignment entry names its default and scope
* chore: resolve SonarCloud maintainability issues
- Replace lambda closures with method references (S1612)
- Replace wildcard imports with explicit imports (S2208)
- Refactor complex functions with extracted helpers (S3776)
- Replace OR pattern with range pattern in math parser (S9047)
* style: apply rustfmt to warichu helper
* fix(compose): revert slice conversion in tests and remove unused var in warichu
* Group warichu row layout scalars to satisfy S107 parameter limit
* style: satisfy rustfmt in warichu.rs
---------
Co-authored-by: Dylan Taylor <dylan@craftmaker.dev>
Nothing checked type size. `type.char {"size": 0}` set type with no size at
all, a negative size went in the same way, and 1e9 pt asked the composer for a
line a million times the height of the page — all of it silent, and all of it
saved to the document.
The range is the one this engine already applies to a ruby annotation's size,
0.1 to 1296 pt, which is InDesign's: "size 0: 0.1 to 1296 pt".
It sits in `validate_char_fill`'s place in the chain, so every command that
takes character attributes gets it: `type.char`, `find.change`, the four
`style.character` / `style.paragraph` create and edit commands, and the two
table paths that already validated a fill.
The test covers five refused sizes, the three in range including both ends, and
two of the style commands. Without the check, `{"size": 0}` applies again.
Not from a ticket: found probing `type.char`.
New Character Style with text selected starts from what the text has of
its own over its paragraph style (its character style's attributes and
its overrides), as InDesign's "[None] + Bold + color", and Apply Style to
Selection (on) gives the text the new style with no override left. A new
style's dialog is titled New Paragraph Style / New Character Style.
type.selectionAttrs reports the text's own character attributes as
ownChars. Found in the Learn Skills Daily course, chapter 22.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Conflicts:
- crates/engine/src/cmd/text.rs: took #1000's unit_at split of select_at; its two revision bumps use the batch's bump_revision().
- crates/tools/src/text.rs: kept the batch's preedit and table-border fields, BorderDrag and the press-time border check; #1000's `unit` replaces `unit_selected`, and the drag branch checks the border first, then #1000's unit drag.
Conflicts:
- crates/engine/src/cmd/text.rs: kept #531's paraMixed report and used #1060's override counts (paraOverrides, charOverrides, characterStyleOverrides) in it.
- crates/ui-egui/src/panels/styles.rs: kept both row-menu items and both sets of tests; #1064's test passes #1060's new `overridden` argument.
- docs/ui-parity.md: kept both paragraphs and both change notes; Redefine Style is no longer listed as a gap (#1064).
Conflicts:
- crates/engine/src/cmd/text.rs: kept the kern_pair_tests module and added pick_up_format_tests after it.
- crates/tools/src/xform.rs: the one hit test keeps its spread (`hit` is the whole `cx.hit` result) and feeds both color.sample's matchFrom and type.pickUpFormat.
- ROADMAP.md: progress-log line moved under "Newest first."
Double-clicking selected a word, but holding the button and moving did
nothing. `text.extendTo` now takes `unit`: after a `text.selectAt`, the
selection grows by whole units from the unit first selected, in either
direction, as InDesign does. The Type tool sends the unit its clicks
chose with every drag.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011K1peyrh5MdZ6ttHkVq8Gh
#84 landed on main squashed (0f4f382b0), so the merge was resolved against #84's
tip as the base: main's version of #84 stands and only this branch's IME changes
are applied on top.
Conflicts and how each was resolved:
- crates/engine/src/cmd/text.rs: removed main's text.compose command, compose_text
and its compose_tests; text.insert keeps main's reset of the typing format after a
successful edit, skipped while an IME preview is open; reselection bumps the
revision.
- crates/engine/src/lib.rs: removed Session.composition and the hook that ended it
in execute_unguarded; set_active/add_document end the composition and reset the
Find scope; both test modules kept.
- crates/engine/src/tooling.rs: removed main's composition() and its
end_composition (the opposite semantics); begin_interaction no longer ends a
composition.
- crates/engine/src/cmd/edit.rs, file.rs, guard.rs, recovery.rs: main's code with
bump_revision in place of revision arithmetic; Revert takes the fresh state's
revision.
- crates/ui-egui/src/canvas.rs: removed main's IME block, composition highlight and
remembered caret rect; this branch's ime_event/ime_output stay. Main's dead-key
test is ported to these events (the accent stays as typed when the caret moves).
- crates/ui-egui/src/lib.rs, menus.rs: both sides' fields and checks.
- apps/designcraft/src/main.rs, apps/designcraft-web/src/web.rs: both sides.
- docs/control-protocol.md: ui.ime row plus main's ui.click/ui.drag params.
Also: the remaining `revision += 1` sites on main use bump_revision, and two tests
compare revisions with `>`; the IME candidate window follows the caret on type on a
path; IME status lines in ROADMAP.md, gaps, ui-, localization- and target-app-parity
docs and roadmap.md.
The Typography panel's Other Features section lists the otf::TOGGLES tags no other section shows. Choosing a figure style or width alone also sets the other axis (the one all of the selection has, else Proportional or Lining), so IDML export writes a figure style instead of Default; turning one off clears both axes. Standard Ligatures reads and sets the ligatures attribute through type.char, and type.openTypeFeatures reports liga from it.