Commit Graph
583 Commits
Author SHA1 Message Date
Dylan M. Taylor 3b985e3813 Use chrono instead of hand-rolled UTC conversion (#1479)
* Use chrono for UTC/calendar conversion instead of hand-rolled copies

Replaces the hand-rolled civil / civil_from_days / days_from_civil copies (the
146_097 / 719_468 magic numbers) across the logging timestamp, text-variable
dates, EPUB dcterms:modified, PDF metadata dates and PPTX core.xml dates with
chrono, added as a workspace dependency (default-features = false, std only, so
wasm is unaffected).

- apps/designcraft logging `timestamp` (private civil_from_days deleted).
- doc text variables: `civil` and `days_from_civil` use chrono::NaiveDate;
  `format_date` and `parse_iso_date` keep their own format/parse logic and
  still call these helpers.
- epub `iso8601`, pdf `civil`, pptx `civil`: UTC datetime via
  chrono::DateTime::from_timestamp.

Output is byte-identical and pinned by the existing date, ISO-date, EPUB,
image, EPS and logging tests. The OOXML/EPUB dcterms timestamps keep their
exact strings. Each converted helper keeps a proleptic-Gregorian fallback for
dates outside chrono's NaiveDate range (and for the serial/overflow dates
parse_iso_date can hand days_from_civil), so no input changes or panics.
AGENTS.md now says time goes through chrono.

Follow-up to storytold/cadcraft#1785

* Never panic on a clock outside chrono's range; keep 5-digit years unprefixed

logging.rs timestamp(): chrono's From<SystemTime> unwraps, so a clock past
year 262143 panicked inside the logger. Use the fallible DateTime::from_timestamp
and read such a clock, or a pre-epoch clock, as the epoch like the old saturating
code did. Years past 9999 are written with {:04} like before, because %Y adds a
'+'. Tests cover both cases.

epub iso8601(): write the year with {:04} instead of %Y, matching the old
{year:04} output, so years past 9999 are not prefixed with '+'. Test added.

AGENTS.md: the "Time goes through chrono" rule now notes the narrow fallback
for dates outside chrono::NaiveDate's range, which crates/doc/src/vars.rs
retains (Howard Hinnant's algorithm).

* Don't build an unrepresentable SystemTime in the far-future test

UNIX_EPOCH + Duration::from_secs(u64::MAX / 2) overflows Windows'
SystemTime and panicked before timestamp() ran, failing the Windows
test job. Build it with checked_add and only assert where it exists.

* Route out-of-range dates through chrono's 400-year cycle, not hand-rolled copies

doc/vars.rs kept hand-rolled Hinnant `civil_fallback`/`days_from_civil_fallback`
copies for dates outside chrono's ±262143-year range, and AGENTS.md carved a
"narrow fallback" exception for them, which left calendar arithmetic outside
chrono after all.

Replace both copies with the Gregorian 400-year cycle (146_097 days): chrono
converts the day/year within one cycle and whole cycles are added back via i128
(so i64::MIN/MAX can't overflow). `civil` is reached by the system clock through
`format_date(now(), ..)`, so it must stay exact; `days_from_civil` only ever
sees a 4-digit parsed year via `parse_iso_date`, but it uses the same cycle form
for consistency. Both reproduce the pre-chrono proleptic Gregorian result, so
outputs are unchanged (verified against the old formula over wide ranges and at
the i64 extremes).

New test pins dates past chrono's range on both sides of the epoch, through the
public `format_date` and round-tripping `days_from_civil`/`civil`, with values
from the old formula. Removed the AGENTS.md "narrow fallback" exception.

epub/pdf/pptx `civil`/`iso8601` are left as-is: the PR already dropped their
hand-rolled copies for pure chrono, and their remaining epoch (`unwrap_or_default`)
fallback only triggers for a metadata/clock timestamp past year 262143 -- the
live-clock/file-mtime case FredApps accepted, with outputs that clamp the year
anyway (pptx 0..9999, PDF DateTime). No hand-rolled copy remains there to replace.

Tested: cargo test -p designcraft-doc; fmt --check; clippy --all-targets -D warnings.

* Carry an overflowing day into the next month again in parse_iso_date

parse_iso_date accepts any day 1..=31, and the old days_from_civil
carried a day past the end of its month into the next one (2019-02-30 is
2019-03-02). from_ymd_opt rejects such a day, and unwrap_or(0) turned it
into 1970-01-01. chrono now places the 1st of the month and the day is
added on, which restores the old normalization. Tested through days_from_civil
and parse_iso_date.

* time: preserve old out-of-range date conversion (no epoch fallback)

`DateTime::from_timestamp(...).unwrap_or_default()` silently reset any
instant past chrono's year range to 1970-01-01, changing output the old
hand-rolled Howard-Hinnant code converted exactly (e.g. 10_000_000_000_000 s
was 318857-05-20 17:46:40, not the epoch). Restore the old conversion domain
with a cycle-based helper that routes the calendar through chrono (the
calendar repeats every 400 years = 146_097 days, so month/day/time come from
chrono and only the year is carried, keeping every i64 exact), then apply each
format's existing year clamp afterwards.

- apps/designcraft/src/logging.rs: `timestamp` via `utc_parts`; keep the
  pre-1970 epoch clamp; u64 secs past i64::MAX saturate, not epoch.
- crates/epub/src/lib.rs: `iso8601` via `utc_parts`.
- crates/pdf/src/export.rs: `civil` cycle-based; `date_time` keeps its
  0..=9999 year clamp.
- crates/pptx/src/package.rs: `civil` cycle-based, keeps its 0..=9999 year
  clamp on the year only (month/day/time keep the real instant).

Pinned old outputs in tests (through the public helpers):
  logging u64::MAX/2 s -> 292277026596-12-04T15:30:07.000Z (was epoch)
  epub 10_000_000_000_000 -> 318857-05-20T17:46:40Z
       -10_000_000_000_000 -> -314918-08-13T06:13:20Z
       i64::MAX -> 292277026596-12-04T15:30:07Z
       i64::MIN -> -292277022657-01-27T08:29:52Z
  pdf  civil(10_000_000_000_000) -> (318857,5,20,17,46,40)
  pptx civil(10_000_000_000_000) -> (9999,5,20,17,46,40) (year clamped)

* Don't build a year-318857 SystemTime unchecked in the logging test

Windows' SystemTime only reaches about year 30828, so
UNIX_EPOCH + Duration::from_secs(10_000_000_000_000) panicked before
timestamp() ran and failed the Windows test job. Build it with
checked_add and assert where the platform can hold it. The year-10000
case fits everywhere and stays unconditional.

* Name the 400-year range extension as the one exception to the time rule

The rule said never to hand-roll calendar math, but the range fix
(shifting by whole 400-year Gregorian cycles so dates past chrono's year
range keep their old output) is cycle arithmetic, so the rule and the code
disagreed. The rule now names that exception: the library still computes
month, day and time, and only whole cycles are carried in the year.

* Say the library computes month and day; time-of-day is plain arithmetic

Narrows the range-extension wording: the date library computes the
calendar fields, and splitting seconds into hours, minutes and seconds
is ordinary arithmetic, which is what the code does.
2026-10-21 19:36:27 -04:00
paaspaas00 687b96a16f Merge remote-tracking branch 'origin/main' into codex/preserve-cjk-auto-overrides 2026-10-21 14:13:51 +02:00
paaspaas00andClaude Opus 5.5 85932f18ab Paragraph rules: Type, Gap Color and Tint, Keep In Frame
A paragraph rule has InDesign's Type: solid, the thick/thin stripes, dashed, dotted, straight hash,
wavy, or a stroke style of the document. Composition draws it as that shape in the rule's box
(dashes and dots as boxes and circles, stripes, waves and hashes as their outlines), with Gap
Color and Gap Tint underneath; a pattern that can't be laid out draws solid. Keep In Frame keeps a
rule above the first line of a frame inside the frame.

The Paragraph Rules page (paragraph style options and the Paragraph Rules dialog) is laid out as
InDesign's: Weight and Type, Color and Tint, Gap Color and Gap Tint, Width and Offset, Left and
Right Indent, Keep In Frame. IDML reads and writes RuleAboveType/RuleBelowType, the gap colour and
tint, and KeepRuleAboveInFrame. A rule's new settings at their defaults stay out of the JSON.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 11:29:19 +02:00
moppeeerandClaude Opus 5.5 75d72c69b5 Effects: blend modes for Drop Shadow, Inner Shadow and Outer Glow
As InDesign's Effects dialog › Blending › Mode: the shadows start Multiply
and the outer glow Screen in white; the renderer blends each with what's
behind it, the commands take mode, IDML reads and writes the drop shadow's
BlendMode. Effects saved before read Normal, as they were drawn.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 08:28:10 +02:00
moppeeerandClaude Opus 5.5 54c369418a PR-1257: document TOC bookmark ownership across IDML and the createBookmarks default
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 18:16:30 +02:00
aiongg 9c2aa1333a Merge branch 'print-color' into print-color-separations 2026-10-20 13:53:02 +00:00
aiongg 1284bf400d Merge print-color-preview into print-color-separations
Export PDF, File > Print and Separations Preview share one print
pipeline: print_pdf builds the print PDF of a set of pages (flattener,
rasterized layers and effects, PDF boxes, the writer, the blend space
outside print color), pdf_export adds Export PDF's view options for
file.exportPdf and file.print, and the preview separates print_pdf's
page per spread. Gradient stops on spot swatches resolve through the
same InkSet as fills (inks::spot_output). The hayro patches move to
craft-libs 8fbfea8 with craft-pdf, craft-prepress and krilla.
2026-10-20 13:50:13 +00:00
aiongg 99cb64a779 Print color: US spelling in the new code and docs 2026-10-20 13:36:14 +00:00
aiongg 6c9709e11f Print color: gradient stops keep the swatch they were given
A stop can name a colour swatch (spot, process, tint, [Paper]) next to its
display colour. swatch.create {gradient}, object.gradient, the Gradient panel
and IDML StopColor keep the reference; renames follow it, swatch edits refresh
the stop's colour and a deleted swatch leaves a plain colour. The field is
optional and omitted for plain stops, so saved documents load and save as
before.
2026-10-20 13:12:29 +00:00
aiongg c6d980b25f Print color: inks on craft-prepress's InkSet
The Ink Manager settings stay saved by swatch name; PDF export and the
ink commands build a craft_prepress::InkSet from the document's spot
swatches and those settings, and ink.options refuses alias loops. The
hayro crates come from craft-libs at the craft-pdf revision, which
craft-prepress's plate mode needs.
2026-10-20 12:53:54 +00:00
con3codeandClaude Opus 5.5 5248ad683c Mojikumi: a middle dot beside a bracket keeps only its quarter em (JLREQ 3.1.4)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013phQ3ekq6uGBEWD6FjFYG2
2026-10-20 19:33:13 +09:00
Claude 987c8bbeed Merge remote-tracking branch 'upstream/main' into xref-edited-text
# Conflicts:
#	ROADMAP.md
#	crates/idml/src/tests.rs
2026-10-19 20:21:38 +00:00
moppeeer b23422b34d PR-1255: refresh against main and resolve review findings 2026-10-19 14:44:46 +02:00
Brandon Thomas f78bf721e8 Merge storytold/main into codex/delete-parent-pages 2026-10-19 02:52:27 -04:00
Brandon Thomas fcb83fd12d Merge storytold/main into codex/delete-parent-pages 2026-10-19 02:51:25 -04:00
Brandon Thomas 5e94f78c2e Merge storytold/main into screen-mode-bleed-slug 2026-10-19 02:51:20 -04:00
Brandon Thomas 992a171b78 Merge pull request #1117 from Silvertree2010/fix/h1992-table-moverow-table-dropcells-ca
Refuse table row moves across the header/footer boundary
2026-10-19 02:50:34 -04:00
Hamza b8ae284a4a View: Bleed and Slug screen modes show their area; draw the slug guide
Bleed and Slug were selectable from the toolbar, the View menu and
`view.screenMode`, and were stored, but the canvas only ever asked whether the
mode was Preview or Presentation. Both therefore drew exactly what Normal drew:
guides, frame edges, the other non-printing marks, and content reaching across
the pasteboard.

They are preview modes now, and what each spread shows reaches out to its box:
the bleed for Bleed, the bleed and the slug for Slug (Slug shows every printing
element, and a slug narrower than the bleed does not pull the edge back in).
Preview and Presentation still stop at the trim.

The slug guide was never drawn either, so the Slug colour in Preferences ›
Guides & Pasteboard was stored, exported and round-tripped while nothing read
it. It is drawn beside the bleed guide, in that colour.

`Spread::bleed_rect`'s body becomes `outset_rect`, which measures either offset
from the page edges, with `bleed_rect` and `slug_rect` as its named callers.
2026-10-18 20:47:01 +05:00
moppeeer 81913c2f48 TOC-1: localize separator picker and address review cleanup 2026-10-18 16:07:02 +02:00
moppeeer e8ec23a88d TOC-1: preserve right-indent separators and expose their picker 2026-10-18 15:47:55 +02:00
moppeeer 924d9c1b73 TOC-2: generate and refresh nested PDF bookmarks 2026-10-18 13:24:56 +02:00
harrisonoconnorhover 9434e96dbf fix: integrate parent deletion with current main (#956) 2026-10-17 17:52:04 -04:00
Claude 4e263ad821 Cross-references keep edited text and break at spaces
InDesign keeps text typed over a cross-reference's generated text (until the
cross-reference is updated), with its page number still live. The IDML
importer dropped that text and generated it again from the format, so a
credits list whose format reads "Last, First. ... YYYY. Volume X, issue Y."
showed those placeholders instead of the names and dates typed in.

- CrossRef gains `text` (edited text, `<pageNum />` for the live page) and
  `page` (the page last shown). Import keeps saved text that no longer
  reads as its format produces it; export writes it back with InDesign's
  XRefPageNumber variable. Setting a format drops the edits, as InDesign does.
- Edited text whose destination is gone shows its saved page, not "??".
- Spaces in a cross-reference's text are word spaces the line may break at
  (it was set as one unbreakable piece, so long ones broke mid-word).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CgGsJkVe5GYG9qK6VFdECa
2026-10-17 20:00:06 +00:00
Aykut FiratandClaude Opus 5.5 08ce444054 Tables: cell First Baseline offset (Cell Options › Text › First Baseline)
Cells and cell styles had no first-baseline setting, so every cell set its first line by the
Ascent offset (the font's typographic ascender). IDML documents that set FirstBaselineOffset on a
cell style (CapHeight is common, often through a table style's body region) composed every row
taller than the document asked for. With a font whose typographic ascender is a full em (Avenir),
each row grew about 2.6 pt and long tables overset, losing their last rows.

- doc: Cell.first_baseline / first_baseline_min; CellStyle.first_baseline / first_baseline_min,
  applied by CellStyle::apply_to and carried through resolve_cell_style's BasedOn chain.
- idml: FirstBaselineOffset and MinimumFirstBaselineOffset read and written on cells and cell styles.
- compose: a cell's text frame uses the cell's first baseline offset and minimum.
- engine: table.setCell, style.cell.create/edit take firstBaseline / firstBaselineMin; table.get
  reports them; style.cell.create fromSelection captures them.
- tests: import through BasedOn and a table style's body region, a cell's own override, IDML round
  trip; composer cap height vs ascent and a fixed minimum; command parameters and validation.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 15:07:46 -04:00
con3codeandClaude Opus 5.5 46596a9d19 Mojikumi: close up consecutive punctuation in the Japanese sets only
The Chinese sets keep the larger blank between two marks, as before:
JLREQ's rule is Japanese, and Chinese styles (Taiwan's centred, full-width
marks among them) set consecutive punctuation their own way.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013phQ3ekq6uGBEWD6FjFYG2
2026-10-17 23:42:21 +09:00
con3codeandClaude Opus 5.5 e06f4134d4 Mojikumi: move the consecutive punctuation test clear of #1127's
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013phQ3ekq6uGBEWD6FjFYG2
2026-10-17 23:08:18 +09:00
con3codeandClaude Opus 5.5 aa55877b7a Mojikumi: consecutive punctuation closes up
Opening brackets together, and closing brackets, commas and full stops
together, now touch (JLREQ 3.1.4): 」。 and 「『 no longer keep half an
em between them. A closing bracket and an opening one stay half an em
apart, and other neighbours keep the larger blank, as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013phQ3ekq6uGBEWD6FjFYG2
2026-10-17 22:51:34 +09:00
moppeeerandGPT-6 Astra a7e1c0e974 Format.NullableOverrides: preserve automatic annotation and list-tab values
Use the existing explicit-null deserializer for Ruby and Kenten sizes, tints and stroke weights, plus the automatic list tab. Extend native ZIP and legacy JSON inheritance regressions for local and named-style clears, absent overrides and explicit values. Found when a no-edit course-book roundtrip discarded eight CJK defaults.

Co-Authored-By: GPT-6 Astra <noreply@openai.com>
2026-10-16 17:52:53 +02:00
harrisonoconnorhover 91a32730ef fix: allow deleting parent pages (#956) 2026-10-16 08:02:23 -04:00
NetterSilverandClaude Haiku 5.5 230b29be60 Refuse table row moves across the header/footer boundary
Found while testing crates/doc/src/table.rs:882 (Table::move_row, called from crates/engine/src/cmd/table.rs:1437 and :1369)

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 05:54:41 +00:00
aiongg 21a6e6d90f Merge branch 'batch-tB' into next-integ
# Conflicts:
#	docs/gaps.md
2026-10-16 04:48:56 +00:00
aiongg 833db788af Merge branch 'batch-tA' into next-integ 2026-10-16 04:48:48 +00:00
aiongg 3021a8f893 Merge PR #1075: Thread the loaded text cursor into a text frame inside a group
crates/tools/src/tests.rs: the branch's tests, plus the PR's new loaded_text_cursor_threads_into_a_frame_inside_a_group.
2026-10-16 04:26:31 +00:00
aiongg d04d4de8fe Integration: coordinates must be finite (#1050)
file.place, place.drop, placed media and Fixed Width text frame columns refuse a rect that overflows to infinity or NaN. Saving refuses a document with non-finite item geometry before writing anything, so a file that won't load is never written.
2026-10-16 04:18:29 +00:00
aiongg 52cf755179 Merge PR #1070: Table of Contents: page number and separator character styles, Sort Entries in Alphabetical Order
crates/engine/src/tests_idml.rs: keep both new tests.
crates/ui-egui/src/i18n.rs: keep both sides' rows.
2026-10-16 04:10:56 +00:00
aiongg 42eaf774e9 Merge PR #1068: Table › Convert Rows › To Header, To Body and To Footer
crates/engine/src/cmd/table.rs: add the PR's three commands before the branch's select commands, which keep their target descriptions (#803).
docs/gaps.md: keep the branch's text; Convert Rows leaves the list of missing menu items.
2026-10-16 04:08:22 +00:00
aiongg ae544f8c05 Integration: cargo fmt 2026-10-16 04:08:00 +00:00
aiongg 6e4f122b8c Merge PR #1100: Placed greyscale and bitmap images take the swatch applied to them (IDML, screen, PDF)
Graphic literals (cli perf, doc assets/item, engine datamerge/export/file/links/mathexpr/media/table/sample, epub, format, pdf, pptx, render, tools, ui-egui properties, and tests): name the branch's clip and wrap (#561, #381) and the PR's fill.
crates/doc/src/item.rs: Graphic keeps wrap and gains fill.
crates/idml/src/import.rs: read the image swatch, then the branch's styled FrameFittingOption lookup; the graphic keeps its wrap.
crates/idml/src/tests/assets.rs: keep both new tests.
crates/render/src/lib.rs: keep #362's crop-box sizing of placed PDFs and colour the resulting pixmap.
crates/render/tests/colorize.rs, crates/epub/tests/grouped_reading_order.rs, crates/compose/src/tests.rs: literals name the branch's Asset and Graphic fields.
crates/pdf/src/export.rs: the soft mask interpolates by the standard's raster mode (the branch has no per-exporter interpolate switch).
docs/gaps.md, docs/target-app-parity.md: status lines list both sides' changes; gap text keeps the clipping-path items and adds the image swatch.
2026-10-16 04:07:57 +00:00
aiongg ca3929d0df Merge PR #1040: Reject threading that would create linked-story ancestor cycles 2026-10-16 03:59:56 +00:00
aiongg 57d083216a Merge PR #1081: Fill and stroke with nothing selected set the colours of new shapes, as InDesign's document defaults
Conflicts:
- crates/doc/src/lib.rs: DocSettings keeps the ruler origin and zero point fields and adds new_object; both defaults kept.
- crates/engine/src/cmd/style.rs: object.color keeps the batch's colour-model description, adds the nothing-selected sentence and the has_doc enable rule.
- crates/ui-egui/src/toolbar.rs: kept both tests (stroke gradient undo, new-shape colours from the proxy).
- ROADMAP.md: progress-log line moved under "Newest first." in the log's format.
2026-10-16 03:58:51 +00:00
aiongg 6f38f5a7b0 Merge PR #1061: Styles: captured text follows later style edits 2026-10-16 03:57:03 +00:00
aiongg ddac0751ff Integration: #1058 delete_pages keeps its doc comment, page_has_items has its own 2026-10-16 03:56:59 +00:00
aiongg f1406df005 Merge PR #1058: Pages: deleting pages that contain objects asks first, with Don't show again
Conflicts:
- crates/ui-egui/src/dialogs.rs: kept the branch's dialog code (wider New Document, Crop Amount, Cell Options) and added #1058's deletePages title, width, body, confirm arm and delete_pages() at the branch's indentation.
- crates/ui-egui/src/i18n.rs, i18n/{ar,de,it,ja,ka,pt_br,uk}.rs: kept both sides.
2026-10-16 03:56:53 +00:00
aiongg ec3593e4f9 Integration: #1034 the canvas draws the bleed from Spread::bleed_rect, which uses the outer pages; ROADMAP line matches the diff 2026-10-16 03:56:20 +00:00
aiongg 44e529ad6f Merge PR #1034: Objects snap to the bleed guide 2026-10-16 03:55:05 +00:00
aiongg 894bbe17e4 Integration: #1099 keep the text-threads status line, progress-log line under Newest first, A0 preset test 2026-10-16 03:54:26 +00:00
aiongg bb6b16fedc Merge PR #1099: Add A0, A1 and A2 poster page presets 2026-10-16 03:53:35 +00:00
aiongg 1061c44763 Integration: #325 New Document: mobile presets don't turn on the primary text frame
Primary Text Frame autoflow is still an open gap, and file.new {preset} goes through the same
builder. The TABS doc comment goes back onto TABS; the de and ja rows of the removed
"No saved presets yet" text go.
2026-10-16 03:48:21 +00:00
aiongg a59edb861a Merge PR #325 (new head c96d3e1fa): New Document window
Next keeps its tree (the PR's earlier commits, with #683's Preview on top) plus the PR's two new commits, 3d91e6460 (a tab selects its first preset; Saved starts with [Default]) and c96d3e1fa (the eight-unit Units list; mobile presets turn on the primary text frame).
crates/engine/src/cmd/file.rs, crates/ui-egui/src/dialogs.rs, crates/ui-egui/src/lib.rs: keep next's side (Preview, new_document::create).
crates/ui-egui/src/new_document.rs: next's side plus the two new commits.
crates/ui-egui/src/i18n.rs, i18n/it.rs: next's rows minus the removed "No saved presets yet" text.
docs/target-app-parity.md: keep next's New Document line (with Preview).
ROADMAP.md: the New Document line was already there; keep one.
2026-10-16 03:48:01 +00:00
aiongg b1421d53e4 Integration: slices give every endnote and editorial marker a payload
next's extract fills missing payloads with empty notes so the slice is a valid
story; main's test expected the slice to hold only the payloads the source had.
The pasted result is the same either way.
2026-10-16 03:28:14 +00:00