#282's list test checks left-aligned paragraphs; with #284 a justified list item's first line also ends at the column edge and its text starts at the left indent. The test runs both alignments.
Conflict: crates/compose/src/lib.rs: next's drop cap aware tab closure with #282's left-indent tab stop argument
Conflict: crates/compose/src/tests.rs: kept both
In a hanging indent (a negative first-line indent), the tab after a
bullet or number went to the next default tab stop, past the left
indent: the first line's text started late and pushed the line past the
column edge.
The left indent is now a left tab stop of its own. A tab before it
reaches it unless an explicit stop comes first, so the first line's
text lines up with the lines below.
Conflict: crates/compose/src/lib.rs: start options, the break after the previous paragraph and a break inside a paragraph go through a local `go` that sends NextColumn to next's span-aware next_column (#276) and the rest to #285's Cursor::start; the in-paragraph break takes #283's `brk` (the line's last glyph); #283's ends_para uses is_break_char; Cursor keeps next's resume() and adds #285's start/next_page (#285's old next_baseline header dropped, next's is kept); the table branch is #285's
Conflict: crates/idml/src/tests.rs: tests only; kept both
Conflict: crates/textimport/src/docx.rs: tests only; kept both
A break character is stored as the last character of the paragraph it
ends, followed by the usual '\n' separator. The story model, paragraph
ranges, styles, editing and every exporter keep splitting paragraphs on
'\n' only, so the change stays in the places that create, lay out or
write break characters:
- IDML import writes the break and ends the paragraph, so the range that
follows keeps its own paragraph style. NextOddPage and NextEvenPage map
to new odd and even page break characters; IDML export writes a
paragraph that ends in a break as that one Br with its
ParagraphBreakType, so export and re-import give the same story.
- Word and RTF import, Type > Insert Break Character (with odd and even
page breaks added) and typing end the paragraph after a break. Typing
between a break and its return goes to the next paragraph; deleting
either deletes both.
- Layout: a break that ends a paragraph leaves no empty line; the next
paragraph starts in the next column, frame or page, as a paragraph start
option would. Page breaks and Next Page start options move to the next
frame on a later page; odd and even ones to the next page with that
page number parity (no such frame: overset). A break inside a paragraph
from an older document still moves the rest of that paragraph on.
Storing the break as a paragraph separator of its own would have changed
every place that counts or splits paragraphs on '\n'.
#284 justifies only the text after a line's last tab; next's mojikumi justification (from the merged CJK PRs) distributed the line's extra space over the whole line, moving the text before the tab. It now works on the same segment.
Conflict: crates/compose/src/lib.rs: tab loop keeps #282's tab_advance and records #284's last tab (no stop = default left tab); squeeze_last keeps next's mojikumi condition with #284's seg_justifies; single-word gaps keep next's moji filter after #284's skip(seg)
Conflict: crates/compose/src/tests.rs: kept both
#282 measures tabs in the line breaker from where layout starts the line; next's drop caps (#83) start lines beside the drop cap after it (on the right in RTL). The breaker's tab closure uses the same indent as next's width closure and line placement; measured from the frame edge, a tab looked wider and the line broke a word early.
Conflict: crates/compose/src/breaker.rs: next's greedy (end_elastic, kinsoku priority 2, unconditional shrink) with #282's tab advance in its three uses of the glyph advance
A line ended by a column, frame or page break takes the paragraph's
last-line alignment, so a Left Justify paragraph sets it flush left.
A line ended by a forced line break stays justified.
The break that moves the following text to the next column or frame
is the line's last glyph, so a space after the break no longer keeps
the text in the same column.
Line breaking gave tabs no width: a tab only reached its stop when the
line was laid out, so a line holding a tab ran past the measure by the
tab's advance. A footnote whose separator is a tab overflowed its first
line by the gap between the number and the tab stop, and body lines
with tabs did the same.
The greedy breaker now asks for each tab's advance at its position on
the line, computed by the same tab_advance helper line layout uses.
#274 draws column rules from the frame's columns and breaks them where a
paragraph's lines cross the gutter. The span-columns fix puts text after a
spanning paragraph below it (a band of columns under the heading). The test
sets a heading mid-frame in three columns and checks that each gutter's rule
stops above the heading and resumes at its bottom, with text after the
heading flowing below it.
Conflict: crates/compose/src/lib.rs: next computes spacing_for earlier; kept the fix's spanning/span_paras/next_column lines and dropped its duplicate spacing line
Conflict: crates/compose/src/tests.rs: tests only; kept both
Conflict: crates/idml/src/import.rs: next's text_frame_options(&[&El]) object-style chain with #274's column rule fields; #274's frame_options takes the prefs list and looks up ColumnRuleStrokeColor through Prefs; the frame site resolves the rule colour's swatch after building options from the chain (the chain borrows the importer)
Conflict: crates/idml/src/tests.rs: tests only; kept both
Conflict: crates/compose/src/tests.rs: tests only; kept both
Conflict: crates/pdf/src/tests.rs: tests only; kept both
A spanning paragraph closes the band of columns above it. Those columns
are balanced first and the paragraph sits below the deepest of them; the
columns after it start below the paragraph, so text that moves to the
next column no longer restarts at the frame top over the heading. A
spanning paragraph that doesn't fit moves to the next frame. Span counts
below the column count set the paragraph across the first columns only.
Text Frame Options gains Tint %, Top Inset, Bottom Inset and Horizontal
Offset for the rule, and object.textFrameOptions and document.inspect
take and report them. IDML reads the ColumnRule attributes of
TextFramePreference on frames and object styles and writes them when a
rule is set.
A frame with Column Rule on gets a bar centred in each gutter, inside the
insets, broken around paragraphs that span columns; right-to-left and
vertical frames included. object.textFrameOptions takes columnRuleWeight
and columnRuleColor (unknown swatches are an error) and an inset array
whose null sides stay; document.inspect reports the rule.
Conflict: ROADMAP.md: kept next's IDML1 line and #263's Paragraph Rules line
Conflict: crates/compose/src/lib.rs: Rule Above uses #263's first line and Column/Text width with #83's rule_color() for colour and tint
Conflict: crates/engine/src/cmd/text.rs: type.para params doc lists next's drop cap fields and #263's ruleAbove/ruleBelow; format_paras is #263's
Conflict: crates/ui-egui/src/dialogs.rs: kept both confirm arms (characterStyleOptions, paragraphRules) and both sets of tests
The #126 merge resolution moved #126's composite-font note label shaping into #81's
piece pass and passed #81's StyleRuns; the resulting call is longer than one line.
#129's first commit (4f8d230) holds #124 and #126, which are already merged. Each conflicted file is a three-way merge of #129's own commits (4f8d230..pr/129) onto this branch, which takes #129's side against #126 and keeps the chain's (#81/#84) code.
Conflict: ROADMAP.md: kept both entries
Conflict: crates/compose/src/shape.rs: #129's Mojikumi metrics added; #81's pieces, the #126 composite-note piece and the #124 inline-object metrics unchanged
Conflict: crates/compose/src/tests.rs: tests only; #129's tests added, no duplicates of #124/#126's
Conflict: crates/engine/src/cmd/fonts.rs: #129's pub(super) for_each_story; #126's tests kept once
Conflict: crates/engine/src/cmd/preflight.rs: #129's nested-story paragraph rules; the used_fonts loop kept once
Conflict: crates/engine/src/tests.rs: tests only; #129's test added
Conflict: crates/fonts/src/fontdb.rs: #126 content (same in #129); no change
Conflict: crates/idml/src/import.rs: #129's Mojikumi import added
Conflict: crates/idml/src/tests.rs: tests only; #129's tests added
Conflict: crates/ui-egui/src/dialogs.rs: #126 content (same in #129); no change
Conflict: crates/ui-egui/src/lib.rs: #126 content (same in #129); no change
Conflict: crates/compose/src/shape.rs: #81 collects paint-only pieces and shapes them later, #126 shapes composite-font note labels inline. A footnote reference in a composite font becomes a Piece with `composite_note`, shaped alone in the piece pass with #126's per-entry label code (from the run's env, as in #126); other references stay ordinary pieces. #126's label fallback in shape_segment passes #81's StyleRuns.
Conflict: crates/fonts/src/fontdb.rs: #126's canonical_family lookup sits at the top of exact_style_traits, so #81's styles/traits_of/linked_style and #247's format-suffix fallback all use it; face() resolves canonical, then canonical without the format suffix, then #126's substitute_family, through #247's find_or_load; has_family is canonical_family as named or without the suffix
Conflict: crates/ui-egui/src/lib.rs: kept #84's system-clipboard copy match and added #126's Find Font on open and _fonts reset after it
Conflict: crates/ui-egui/src/dialogs.rs: OK label match has #146's Save and #126's Replace All; #80's alert-only OK kept, #126's Done label for findFont; find_font keeps #143's font_combo with #126's FALLBACK_FAMILY default
Conflict: crates/compose/src/tests.rs, crates/idml/src/tests.rs: tests only; kept both
Conflict: ROADMAP.md: kept both entries
Conflict: crates/compose/src/shape.rs: inline object on the baseline: #124's rule (its box alone sets the extents, not max with the font's) applied to every metric #83 tracks (ascent, typo_ascent, cap, xh); the above-line arm keeps #83's four fields
Conflict: crates/compose/src/tests.rs: tests only; kept both
Conflict: crates/engine/src/tests.rs: tests only; kept both
Conflict: crates/idml/src/tests.rs: tests only; kept both
Conflict: crates/idml/src/import.rs: kept #230's Underline/StrikeThru colour line (Text Color resets) and #83's tint() helper
Conflict: ROADMAP.md: kept the chain's Text engine line and added #230's FID2 line
Restore the previous preset and character-class coverage. Remove regional punctuation and Tsume policy changes while retaining cluster, tracking, and spacing-allocation repairs. Preserve finite unsupported rows for diagnostics instead of rejecting whole documents.
Resolve imported composite fonts by resource identity and preserve literal
display names. Export AppliedFont as an object reference instead of registering
a composite definition as an ordinary font family.
Share composite lookup and typed font usage between composition, font inventory,
and preflight. Include nested cells and notes, and report only component fonts
selected by the document's characters.
Add synthetic coverage for reference round trips, footnote character mapping
and metrics, nested font inventory, component replacement, and undo.
Resolve forward story references before importing embedded frames and groups.
Compose embedded stories, retain anonymous-story objects for screen and PDF
rendering, and invalidate layout caches when embedded frame geometry changes.
Measure inline objects by geometry, fill open contours, reject cyclic frame
references, and distinguish unplaced stories from located overset text.
Add synthetic round-trip, composition, preflight, pixel-rendering, and PDF
regressions. Update the roadmap with the supported import and rendering paths.
An empty paragraph's mark format now also supplies #83's first-baseline
heights through line_metrics.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#83's Keep Lines Together tests change expectation: with the last baseline
allowed on the bottom inset, the first frame holds one more line, so the
10-line paragraph splits 7/3 instead of 6/4 (the keep rule is unchanged).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Vertical Type is now the story's direction: the full-descent check reads
FrameSpec::vertical, and the vertical-extent test sets the story vertical.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>