Commit Graph
1660 Commits
Author SHA1 Message Date
VoiceLessQ 89ccfbb18c Autoflow: count each forced move once, and only visible breaks (#1209)
fit() counted every break character in the overset text and every
Start Paragraph option. A break followed by a paragraph whose option it
already meets moved the text once but asked for two frames, and breaks
that tracked changes delete or a hidden condition hides asked for
frames that composing never uses. Autoflow added those pages and
removed them again: 200 chapters that end in page breaks and start on
the next page took 61095 ids and 430 ms, now 798 ids and 16 ms.

Each paragraph composing places now counts once, when a visible break
ends the paragraph before it or its start option sends it on; after a
break it is at the top of a frame, where the option only fixes odd or
even page parity. A visible break inside a paragraph counts once more.
previous_visible_break and the new visible_break in designcraft-compose
are public, so the estimate and the composer use one rule.
2026-10-28 23:08:41 -02:00
VoiceLessQ ca85be89d2 Autoflow: skip deleted paragraphs and ask for every forced frame at once (#1209)
The start option lookup now reads the next paragraph composing places,
past tracked-deleted ones. Frames that start options and breaks force
are counted and asked for in one round, so a story with a paragraph per
page no longer gets one frame per round.
2026-10-28 22:32:07 -02:00
VoiceLessQ e248b63165 Autoflow: a frame a Start Paragraph option ends early sets no fill rate (fixes #1209)
fit() counted the frame before a paragraph that starts on the next
column, frame, page or odd/even page as a full frame. With a short
title before such a paragraph, autoflow estimated a page per few bytes
left and added up to 2000 pages before removing them again: placing
2000 words took 35.6 s in the app, now 0.36 s, with the same pages.

The option is resolved through the paragraph style, so an imported
Word style counts the same as a local override.
2026-10-28 21:46:56 -02:00
VoiceLessQ ba1ee2329b Give anchored objects fresh ids when copying text frames (fixes #1522)
Copying a text frame cloned its story but left the objects anchored in
it with the original's item ids. Save does not validate, load runs
Document::check and rejects duplicate ids, so a duplicated or pasted
frame with an inline graphic produced a file that failed to open with
"not a DesignCraft document: duplicate item id".

renumber (Duplicate, Alt-drag copy, Paste, Paste into text, parent item
overrides) and the data merge copy now give every anchored item in the
copied story, its table cells, footnotes and endnotes a fresh id.
Anchored text frames from IDML get their own story copy. Assets are not
adopted again: the story copy already remapped them.

The IDML export tests that relied on duplication to create shared ids
now set the shared id explicitly, so the export guard stays covered.
Files already saved with duplicate ids still do not open.
2026-10-28 21:10:50 -02:00
aiongg 855d606325 Show file and message dialogs without blocking the frame (fixes macOS crash) (#1961)
* Show file and message dialogs without blocking the frame

A blocking rfd dialog inside the egui frame runs AppKit's modal loop; a
file dragged from Finder and held over the window then re-enters winit's
handler and aborts the app. Dialogs now go through craft-dialogs: a
command without a path asks with a continuation and returns, the host
shows the dialog at the end of the frame, and the continuation runs with
the answer on a later frame, in the document it was asked for. Commands
given a path stay synchronous. The replace-file question is a message
box from the same queue. Quit and Close wait for a Save As dialog's
answer. clippy.toml disallows rfd::FileDialog and rfd::MessageDialog.

* Raise the open file panel when File > Open is chosen again

* Repin craft-dialogs to the craft-libs PR head

* Queue dialogs asked by different code or documents; hold answers behind newer dialogs

Two asks for the same file dialog were treated as one, so Place for one
document and then Choose Image, or Place again for another document, ran
the first ask's action. An ask is now skipped (and the open panel
raised) only when the same code asked for the same dialog for the same
document. An answer that would replace an egui dialog opened since the
asking (an unsaved-changes question, Preferences) waits until it closes;
Quit counts it as pending.

* Repin craft-dialogs to the craft-libs PR head

* Allow the file dialogs' existence checks in the file guard

* Repin craft-dialogs to storytold/craft-libs main
2026-10-28 17:13:45 -04:00
Claude 11f4364705 Merge storytold/main (v0.14.0) into xref-edited-text
The IDML import and export of edited cross-reference text move to import/story.rs and
export/story.rs with upstream's split.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CgGsJkVe5GYG9qK6VFdECa
2026-10-28 18:09:43 +00:00
Claude 94e0e75249 Merge storytold/main (v0.14.0) into running-header-order
The running-header tests move to cmd/variables/tests.rs with upstream's split.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CgGsJkVe5GYG9qK6VFdECa
2026-10-28 18:00:57 +00:00
NetterSilverandClaude Haiku 5.5 79b871b94f Strip story marker characters from placed plain text files
Found while testing crates/textimport/src/lib.rs:140 (plain) and crates/engine/src/cmd/place_text.rs:372 (place_text)

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-28 15:18:35 +00:00
Hamza 2f403698e0 An export says what text its pages had no room for (#1465)
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.
2026-10-28 18:53:45 +05:00
Hamza 352609117f A table size Create Table cannot mean is refused
A size that was given but is not one a table can have was corrected in
silence: {"rows": 0} made a table with one row, -1 and 1.5 made the default
four, and 10000 was capped at 500 -- so a script asking for a shape the
command cannot honour got a different table and no indication of it.

A size that is absent still takes the default; a size that is present and
outside the range is refused, naming the key and the range.

Rebased onto the post-split main: `table.insert` moved to
table/structure.rs and the tests to table/sizing.rs. The helper is named
`given` rather than `count`, which that module already imports from
table/target.rs for the repeat count of a row or column insert.
2026-10-28 18:20:13 +05:00
Dylan M. Taylor 5b21472180 Refactor engine for strict Clippy checks 2026-10-28 11:39:26 +00:00
Hamza ff35b3561f Data merge: an extended-length drive path is a local file, not a refusal
Review caught a wrong assertion in my own test. `\\?\C:\x` is the
extended-length spelling of `C:\x`: craft-docpath parses it as
Root::VerbatimDrive, is_absolute, and its own suite classifies it
PathKind::Local. The test asserted it was refused, so it would have
failed on Windows -- the one platform it runs on.

A data file can already name C:\secret\x.png, so refusing the long
spelling would have shut out long paths without shutting anything out.
It is now asserted the other way, and the refusal list keeps a device
path in its place.

Each shape was checked against the pinned craft-docpath through
resolve(), which recognises Windows forms on every platform, so the
verdicts are first-hand and not read off the source: Network for the
extended UNC prefix, Device for \\.\pipe, GLOBALROOT, CON and NUL.png,
and Ok for \\?\C:\secret\x.png.
2026-10-28 15:33:12 +05:00
Hamza a30744fed1 Drop the unused ImageCache inspection methods
`len` and `is_empty` were added for a test that ended up counting reads
through `io::touched` instead, so they were dead code -- and the gate runs
`cargo clippy --workspace --all-targets -- -D warnings`, which turns
`dead_code` into a failure. Hosted CI was red on this PR.

My local check had been `cargo clippy -p designcraft-engine --all-targets`
without `-D warnings`, which is why I missed it; re-ran it the way the gate
does.

Found by the review bot, confirmed against the CI log.
2026-10-28 11:53:56 +05:00
Hamza 7c25cd97c4 Data merge: an image named by many records is read once (#1595)
`place_image` stat'd and read its file for every record, so a sheet naming one
image in many rows read it once per row -- up to 256 MiB each time. The audit's
case is a 100,000-row CSV naming one large file: about 25 TB of I/O for one
merge.

A merge now carries an `ImageCache`: the bytes by resolved path, read once and
shared. The asset takes the cached `Arc` rather than a copy, so the records
share one buffer as well. The cache stops growing past 256 MB of distinct
files, so a merge over many large images is slow rather than out of memory.

Measured by counting what the engine hands the file system (`io::touched`):

    without the cache   50 reads for 25 records   (a stat and a read each)
    with it             2

A note on the test, because the first one I wrote was useless: asserting on the
bytes the document holds passes either way, since `add_asset` already dedupes
identical content. Memory never showed this; the I/O is the bug, so the test
counts reads.

Audit DC-14. Its second half -- capping the pages a merge produces -- is not
here: how many pages a merge may make is a product decision, and the records
are the user's own data rather than something arriving from outside.
2026-10-27 15:25:27 +05:00
Hamza 93ab34f515 Data merge: the UNC refusal is tested on Windows too (#1601)
`image_path_tests` was `#[cfg(all(test, unix))]`, so the assertion that matters
most ran only where it matters least: a merge image field naming another
computer must resolve to nothing, because on Windows an outbound SMB connection
hands over NTLM credentials. That refusal was unverified on Windows.

The refusals now run everywhere -- nothing in them is unix-specific, since
`image_candidates` delegates to `craft-docpath` -- and the Windows shapes that
file refuses are asserted where they have meaning: `\\?\UNC\...`,
`\\.\pipe\...`, `\\?\C:\...` and the legacy device names.

Resolution beside the data and the document stays unix-only: it asserts what an
absolute path means, which cannot be checked on Windows from here.

No production code changes. On macOS both cross-platform tests pass; Windows CI
is the first real run of the Windows ones, which is the point.

Audit DC-20 (its testable part; the privacy note about absolute local paths
matches InDesign and is a product decision, not a bug).
2026-10-27 14:16:27 +05:00
NetterSilverandClaude Haiku 5.5 7fcebedf98 Drop XML-forbidden control characters in file.exportXml
Found while testing crates/engine/src/cmd/xml.rs:40 (esc, used by para_xml at :68 and the href at :104)

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-27 09:07:12 +00:00
aiongg 42993ae680 test(engine): read only test cfgs and any pub(…) mod as test modules in the file guard
Split files share test helpers through pub(super)/pub(crate) test modules. The guard read only `mod` and `pub mod` as a test module, so it scanned their fixtures as production code. `attr.contains("test")` also matched `#[cfg(not(test))]`, so such a module was skipped; it is scanned now.
2026-10-26 23:06:07 +00:00
aiongg 08f633ae48 refactor(fonts): split fontdb.rs into fontdb/ 2026-10-26 23:05:57 +00:00
NetterSilverandClaude Haiku 5.5 4f03e10594 Reject invalid XML tag names in xml.mapStyle
Found while testing crates/engine/src/cmd/xml.rs:~290 (the xml.mapStyle command; the export is in export_xml)

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-26 21:37:07 +00:00
aiongg 8f601148b6 refactor(engine): split cmd/layout.rs into cmd/layout/ 2026-10-26 21:00:23 +00:00
aiongg a1ec465207 refactor(engine): split cmd/style.rs into cmd/style/ 2026-10-26 21:00:23 +00:00
aiongg 7afc1af676 refactor(engine): split cmd/text.rs into cmd/text/ 2026-10-26 21:00:23 +00:00
aiongg d93acf7120 refactor(engine): split cmd/export.rs into cmd/export/ 2026-10-26 21:00:23 +00:00
aiongg b2f1ad1867 refactor(engine): split cmd/file.rs into cmd/file/ 2026-10-26 21:00:23 +00:00
aiongg 18e03a3035 refactor(engine): split cmd/table.rs into cmd/table/ 2026-10-26 21:00:23 +00:00
aiongg 5f53d41387 docs(engine): name the file guard test's new path 2026-10-26 19:02:05 +00:00
aiongg b0dc1578b5 refactor(engine): move the loose tests_*.rs files to tests/ 2026-10-26 18:18:14 +00:00
aiongg 2e52c9fcd0 refactor(engine): move the tests module of cmd/place_text.rs to cmd/place_text/tests.rs 2026-10-26 18:18:14 +00:00
aiongg d4bd716ebf refactor(engine): move the text_clipboard_tests module of cmd/edit.rs to cmd/edit/text_clipboard_tests.rs 2026-10-26 18:18:14 +00:00
aiongg 8d36acd1c0 refactor(engine): move the tests module of cmd/load_styles.rs to cmd/load_styles/tests.rs 2026-10-26 18:18:14 +00:00
aiongg 8ba7fd46b9 refactor(engine): move the tests module of cmd/variables.rs to cmd/variables/tests.rs 2026-10-26 18:18:14 +00:00
aiongg 3b0e299180 refactor(engine): move the tests module of cmd/links.rs to cmd/links/tests.rs 2026-10-26 18:18:14 +00:00
aiongg d9fb8cfbfa refactor(engine): move the text_tests module of cmd/export.rs to cmd/export/text_tests.rs 2026-10-26 18:18:14 +00:00
aiongg f07d0900b3 refactor(engine): move the tests module of cmd/package.rs to cmd/package/tests.rs 2026-10-26 18:18:14 +00:00
aiongg 4e40f85f9b refactor(engine): move the tests module of math.rs to math/tests.rs 2026-10-26 18:18:14 +00:00
aiongg 8e0b6d5c9a refactor(engine): move the color_tests module of cmd/style.rs to cmd/style/color_tests.rs 2026-10-26 18:18:13 +00:00
aiongg 9ca1c5c83a refactor(engine): move the tests module of cmd/pdflayers.rs to cmd/pdflayers/tests.rs 2026-10-26 18:18:13 +00:00
aiongg eb65f9c93a refactor(engine): move the tests module of cmd/find.rs to cmd/find/tests.rs 2026-10-26 18:18:13 +00:00
aiongg 37ada05f77 refactor(engine): move the tests module of recovery.rs to recovery/tests.rs 2026-10-26 18:18:13 +00:00
aiongg 7445b8eee7 refactor(engine): move cmd/datamerge/mod.rs to cmd/datamerge.rs 2026-10-26 18:15:31 +00:00
aiongg 8b56a75f19 refactor(engine): move cmd/mod.rs to cmd.rs 2026-10-26 18:15:31 +00:00
aiongg ae732c3a90 Pin the export time in the page-list export test (#1850) 2026-10-25 23:20:52 -04:00
aiongg 2e6df922c8 Merge remote-tracking branch 'upstream/main' into print-color-separations
# Conflicts:
#	Cargo.lock
#	crates/engine/src/cmd/export.rs
2026-10-26 02:05:49 +00:00
aiongg 7cc4f7111c Merge pull request #1384 from aiongg/print-color-x1a
Print color: PDF/X-1a export through craft-prepress's flattener
2026-10-25 21:59:30 -04:00
aiongg 3c19a24e68 Merge pull request #1374 from aiongg/print-color
Print color: overprint, knockout, CMYK blending and spot gradients through craft-pdf
2026-10-25 21:58:20 -04:00
Dylan M. Taylor 42463ce99d Reduce cognitive complexity in variables, PDF graphics, compose frames and menu shortcuts (#1847)
* refactor(engine): split convert_all's per-story plan into convert_plan

Resolves SonarCloud rust:S3776 on convert_all (cmd/variables.rs).

* refactor(pdf): split graphic_unclipped into per-kind helpers

Resolves SonarCloud rust:S3776 on graphic_unclipped (pdf/src/export.rs).

* refactor(compose): extract wrap exclusions and frame text area from frame_specs_placed

Resolves SonarCloud rust:S3776 on frame_specs_placed (compose/src/lib.rs).

* refactor(ui): extract command shortcut lookup from shortcut_key

Resolves SonarCloud rust:S3776 on shortcut_key (ui-egui/src/menus.rs).
2026-10-25 21:25:52 -04:00
Dylan M. Taylor e413cdf59b Reduce cognitive complexity in find, lists, PDF font/layer scans; fix wildcard import (#1846)
* test(pdf): replace wildcard super import in registration_tests

Resolves SonarCloud rust:S2208 (new code).

* test(engine): extract child-scope rejection assertions in find tests

Resolves SonarCloud rust:S3776 on find_scope_children_error_atomically_and_parent_requests_stay_top_level.

* refactor(engine): extract per-paragraph list update from apply_list

Resolves SonarCloud rust:S3776 on apply_list.

* refactor(pdf): extract form scanning from FontScan::content

Resolves SonarCloud rust:S3776 on FontScan::content (pdfx/fonts.rs).

* refactor(pdf): extract layer helpers from optional_content::prepare

Resolves SonarCloud rust:S3776 on prepare (optional_content.rs).
2026-10-25 21:25:48 -04:00
aiongg d90881a8a9 Merge branch 'print-color' into print-color-separations
# Conflicts:
#	Cargo.toml
2026-10-26 01:11:35 +00:00
aiongg c856db68f1 Merge branch 'print-color' into print-color-x1a
# Conflicts:
#	Cargo.toml
2026-10-26 01:11:11 +00:00
aiongg 240dbcf731 Merge remote-tracking branch 'upstream/main' into print-color
# Conflicts:
#	Cargo.toml
#	crates/idml/src/export.rs
2026-10-26 01:10:39 +00:00