Each implementation captures the user's data without rendering
- * decisions; the surrounding {@code Module} composer translates a
- * {@code Block} into one or more {@code DocumentNode} instances using
- * the active typography, palette, and spacing tokens.
- *
- *
The sealed permit list is intentionally exhaustive: every body
- * shape that a CV / cover-letter / invoice / proposal preset can
- * declare today is one of the eight concrete records. To add a new
- * body shape, extend the {@code permits} list and update the Module
- * composer to handle the new variant.
- *
- *
Block records are immutable and safe to reuse across documents.
- */
-public sealed interface Block
- permits ParagraphBlock,
- BulletListBlock,
- NumberedListBlock,
- IndentedBlock,
- KeyValueBlock,
- MultiParagraphBlock,
- WorkHistoryBlock,
- EducationBlock {
-}
diff --git a/src/main/java/com/demcha/compose/document/templates/blocks/BulletListBlock.java b/src/main/java/com/demcha/compose/document/templates/blocks/BulletListBlock.java
deleted file mode 100644
index 0de8aefb5..000000000
--- a/src/main/java/com/demcha/compose/document/templates/blocks/BulletListBlock.java
+++ /dev/null
@@ -1,27 +0,0 @@
-package com.demcha.compose.document.templates.blocks;
-
-import java.util.List;
-import java.util.Objects;
-
-/**
- * A {@link Block} that renders as a bullet-pointed list of items.
- *
- *
Use this for skill lists, project bullet points, or any flat
- * enumeration where each item is one line of text.
- *
- * @param items list items in source order (must not be null; may be
- * empty; individual items must not be null)
- */
-public record BulletListBlock(List items) implements Block {
-
- /**
- * Compact constructor that defensively copies the supplied list and
- * validates that no item is null.
- *
- * @throws NullPointerException if {@code items} or any item is null
- */
- public BulletListBlock {
- Objects.requireNonNull(items, "items");
- items = List.copyOf(items);
- }
-}
diff --git a/src/main/java/com/demcha/compose/document/templates/blocks/EducationBlock.java b/src/main/java/com/demcha/compose/document/templates/blocks/EducationBlock.java
deleted file mode 100644
index 83c42b366..000000000
--- a/src/main/java/com/demcha/compose/document/templates/blocks/EducationBlock.java
+++ /dev/null
@@ -1,84 +0,0 @@
-package com.demcha.compose.document.templates.blocks;
-
-import java.util.List;
-import java.util.Objects;
-
-/**
- * A {@link Block} that captures a stack of education / certification
- * entries with each field (degree, institution, year, details)
- * supplied separately so presets can place them precisely without
- * re-parsing a concatenated source string.
- *
- *
This is the preferred shape for "Education",
- * "Education & Certifications", "Qualifications" or any module
- * whose body is a list of degree / course entries. The
- * {@code BoxedSections} preset renders each {@link Item} with the
- * same structured layout as {@code WorkHistoryBlock}: degree bold on
- * the left, year right-aligned on the same row, institution italic
- * on the next line under the degree, and details as a full-width
- * paragraph beneath. Other presets fall back to a single inline
- * paragraph per item.
- *
- *
Legacy alternative. Authors may still pass
- * education as a {@link MultiParagraphBlock} of pipe-separated
- * strings — e.g.
- * {@code "**Degree** - Institution | Year. Details..."} — and the
- * legacy parser tries to interpret them. Prefer
- * {@code EducationBlock} in new code: the structured fields are
- * explicit, do not depend on the parser's separator and date
- * heuristics (which over-trigger on prose containing stray hyphens
- * like "First-class"), and survive copy-paste from spreadsheets
- * without quoting concerns.
- *
- * @param items education entries in source order, most-recent-first
- * by convention (must not be null; may be empty;
- * individual items must not be null)
- */
-public record EducationBlock(List items) implements Block {
-
- /**
- * Compact constructor that defensively copies the supplied list and
- * validates that no item reference is null.
- *
- * @throws NullPointerException if {@code items} or any element is
- * null
- */
- public EducationBlock {
- Objects.requireNonNull(items, "items");
- items = List.copyOf(items);
- }
-
- /**
- * One row in an education stack. All four fields are required
- * non-null strings but may be blank — a blank {@code institution}
- * collapses the subtitle line, a blank {@code details} collapses
- * the body paragraph, and a blank {@code year} renders the degree
- * row without a right-aligned year column.
- *
- * @param degree degree / qualification name, e.g.
- * {@code "MSc Computer Science"} or
- * {@code "Oracle Java Certification"}
- * @param institution awarding institution, e.g.
- * {@code "University of Manchester"} or
- * {@code "Professional track"}
- * @param year year or year range, e.g. {@code "2021"},
- * {@code "2018-2021"}
- * @param details additional details (honours, thesis,
- * specialisation, course content); free prose,
- * may contain inline markdown
- * ({@code **bold**}, {@code *italic*})
- */
- public record Item(String degree, String institution, String year, String details) {
-
- /**
- * Compact constructor: rejects null fields. Use empty strings
- * for absent values rather than null.
- */
- public Item {
- Objects.requireNonNull(degree, "degree");
- Objects.requireNonNull(institution, "institution");
- Objects.requireNonNull(year, "year");
- Objects.requireNonNull(details, "details");
- }
- }
-}
diff --git a/src/main/java/com/demcha/compose/document/templates/blocks/IndentedBlock.java b/src/main/java/com/demcha/compose/document/templates/blocks/IndentedBlock.java
deleted file mode 100644
index 6a9626c46..000000000
--- a/src/main/java/com/demcha/compose/document/templates/blocks/IndentedBlock.java
+++ /dev/null
@@ -1,54 +0,0 @@
-package com.demcha.compose.document.templates.blocks;
-
-import java.util.List;
-import java.util.Objects;
-
-/**
- * A {@link Block} that renders as a list of indented title-plus-body
- * entries.
- *
- *
Use this for Education ({@code "MSc Computer Science"} title +
- * {@code "University of Manchester | 2021"} body) or Projects
- * ({@code "TaskFlow Studio"} title + project description body) modules.
- * The title sits flush left in a bold weight; the body is indented and
- * follows in the regular body style.
- *
- * @param items entries in source order (must not be null; may be empty;
- * individual entries must not be null)
- */
-public record IndentedBlock(List items) implements Block {
-
- /**
- * Compact constructor that defensively copies the supplied list and
- * validates that no item reference is null.
- *
- * @throws NullPointerException if {@code items} or any element is null
- */
- public IndentedBlock {
- Objects.requireNonNull(items, "items");
- items = List.copyOf(items);
- }
-
- /**
- * One title-plus-body entry inside an {@link IndentedBlock}.
- *
- * @param title bold leading line such as a degree, project name, or
- * role (must not be null; may be empty)
- * @param body indented body text such as institution, dates, or
- * project description (must not be null; may be empty)
- */
- public record Item(String title, String body) {
-
- /**
- * Compact constructor that rejects null references for either
- * field.
- *
- * @throws NullPointerException if {@code title} or {@code body}
- * is null
- */
- public Item {
- Objects.requireNonNull(title, "title");
- Objects.requireNonNull(body, "body");
- }
- }
-}
diff --git a/src/main/java/com/demcha/compose/document/templates/blocks/KeyValueBlock.java b/src/main/java/com/demcha/compose/document/templates/blocks/KeyValueBlock.java
deleted file mode 100644
index e2178f2cf..000000000
--- a/src/main/java/com/demcha/compose/document/templates/blocks/KeyValueBlock.java
+++ /dev/null
@@ -1,74 +0,0 @@
-package com.demcha.compose.document.templates.blocks;
-
-import java.util.List;
-import java.util.Map;
-import java.util.Objects;
-import java.util.stream.Collectors;
-
-/**
- * A {@link Block} that renders as a list of bold-key followed-by-value
- * pairs on the same baseline.
- *
- *
Typical use is the Additional Information module of a CV
- * ({@code "Languages: English (Fluent), German (Intermediate)"},
- * {@code "Work Eligibility: Eligible to work in the UK"}). The key is
- * rendered in bold and a colon separator is appended automatically by
- * the Module composer.
- *
- * @param entries key-value entries in source order; insertion order is
- * preserved (must not be null; may be empty; individual
- * entries must not be null)
- */
-public record KeyValueBlock(List entries) implements Block {
-
- /**
- * Compact constructor that defensively copies the supplied list and
- * validates that no entry reference is null.
- *
- * @throws NullPointerException if {@code entries} or any element is null
- */
- public KeyValueBlock {
- Objects.requireNonNull(entries, "entries");
- entries = List.copyOf(entries);
- }
-
- /**
- * Convenience factory that converts a {@link Map} into the ordered
- * list form. Useful when callers already have a {@code LinkedHashMap}
- * or {@code Map.of(...)} of key-value pairs.
- *
- * @param map source map; insertion order is preserved
- * @return new {@code KeyValueBlock} with one entry per map pair
- * @throws NullPointerException if {@code map} or any key/value is null
- */
- public static KeyValueBlock fromMap(Map map) {
- Objects.requireNonNull(map, "map");
- List entries = map.entrySet().stream()
- .map(e -> new Entry(e.getKey(), e.getValue()))
- .collect(Collectors.toList());
- return new KeyValueBlock(entries);
- }
-
- /**
- * One bold-key plus value pair inside a {@link KeyValueBlock}.
- *
- * @param key bold leading label such as {@code "Languages"} (must
- * not be null; may be empty)
- * @param value value text following the colon separator (must not be
- * null; may be empty)
- */
- public record Entry(String key, String value) {
-
- /**
- * Compact constructor that rejects null references for either
- * field.
- *
- * @throws NullPointerException if {@code key} or {@code value}
- * is null
- */
- public Entry {
- Objects.requireNonNull(key, "key");
- Objects.requireNonNull(value, "value");
- }
- }
-}
diff --git a/src/main/java/com/demcha/compose/document/templates/blocks/MultiParagraphBlock.java b/src/main/java/com/demcha/compose/document/templates/blocks/MultiParagraphBlock.java
deleted file mode 100644
index b6ed34e73..000000000
--- a/src/main/java/com/demcha/compose/document/templates/blocks/MultiParagraphBlock.java
+++ /dev/null
@@ -1,34 +0,0 @@
-package com.demcha.compose.document.templates.blocks;
-
-import java.util.List;
-import java.util.Objects;
-
-/**
- * A {@link Block} that renders as several paragraphs separated by
- * paragraph spacing.
- *
- *
Use this for cover-letter bodies, multi-paragraph experience
- * descriptions, or any module whose body is a sequence of discrete
- * paragraphs rather than a list of enumerated items. The surrounding
- * Module composer applies the active {@code Spacing.paragraphSpacing}
- * between adjacent paragraphs.
- *
- * @param paragraphs paragraph texts in source order (must not be null;
- * may be empty; individual paragraphs must not be null;
- * blank paragraphs are kept as-is to preserve the
- * user's intended whitespace)
- */
-public record MultiParagraphBlock(List paragraphs) implements Block {
-
- /**
- * Compact constructor that defensively copies the supplied list and
- * validates that no paragraph reference is null.
- *
- * @throws NullPointerException if {@code paragraphs} or any element
- * is null
- */
- public MultiParagraphBlock {
- Objects.requireNonNull(paragraphs, "paragraphs");
- paragraphs = List.copyOf(paragraphs);
- }
-}
diff --git a/src/main/java/com/demcha/compose/document/templates/blocks/NumberedListBlock.java b/src/main/java/com/demcha/compose/document/templates/blocks/NumberedListBlock.java
deleted file mode 100644
index 8966ff07a..000000000
--- a/src/main/java/com/demcha/compose/document/templates/blocks/NumberedListBlock.java
+++ /dev/null
@@ -1,29 +0,0 @@
-package com.demcha.compose.document.templates.blocks;
-
-import java.util.List;
-import java.util.Objects;
-
-/**
- * A {@link Block} that renders as a numbered list of items.
- *
- *
Use this for ordered procedures, ranked items, or step-by-step
- * descriptions where the position carries meaning. The Module composer
- * uses an arabic-numeral marker by default; presets can override the
- * marker style during composition.
- *
- * @param items list items in source order (must not be null; may be
- * empty; individual items must not be null)
- */
-public record NumberedListBlock(List items) implements Block {
-
- /**
- * Compact constructor that defensively copies the supplied list and
- * validates that no item is null.
- *
- * @throws NullPointerException if {@code items} or any item is null
- */
- public NumberedListBlock {
- Objects.requireNonNull(items, "items");
- items = List.copyOf(items);
- }
-}
diff --git a/src/main/java/com/demcha/compose/document/templates/blocks/ParagraphBlock.java b/src/main/java/com/demcha/compose/document/templates/blocks/ParagraphBlock.java
deleted file mode 100644
index 1bfee463b..000000000
--- a/src/main/java/com/demcha/compose/document/templates/blocks/ParagraphBlock.java
+++ /dev/null
@@ -1,39 +0,0 @@
-package com.demcha.compose.document.templates.blocks;
-
-import com.demcha.compose.document.node.TextAlign;
-
-import java.util.Objects;
-
-/**
- * A {@link Block} that renders as a single paragraph of body text.
- *
- *
Use this for short prose modules such as Professional Summary, or
- * for any module whose body is one continuous block of text rather than
- * an enumerated list.
- *
- * @param text paragraph text content (must not be null; may be empty)
- * @param align horizontal alignment for the paragraph (defaults to
- * {@link TextAlign#LEFT} when null)
- */
-public record ParagraphBlock(String text, TextAlign align) implements Block {
-
- /**
- * Compact constructor that normalises the alignment default and
- * rejects a null text reference.
- *
- * @throws NullPointerException if {@code text} is null
- */
- public ParagraphBlock {
- Objects.requireNonNull(text, "text");
- align = align == null ? TextAlign.LEFT : align;
- }
-
- /**
- * Convenience constructor that defaults alignment to left.
- *
- * @param text paragraph text content
- */
- public ParagraphBlock(String text) {
- this(text, TextAlign.LEFT);
- }
-}
diff --git a/src/main/java/com/demcha/compose/document/templates/blocks/WorkHistoryBlock.java b/src/main/java/com/demcha/compose/document/templates/blocks/WorkHistoryBlock.java
deleted file mode 100644
index 434c65256..000000000
--- a/src/main/java/com/demcha/compose/document/templates/blocks/WorkHistoryBlock.java
+++ /dev/null
@@ -1,73 +0,0 @@
-package com.demcha.compose.document.templates.blocks;
-
-import java.util.List;
-import java.util.Objects;
-
-/**
- * A {@link Block} that captures a stack of work-history entries with
- * each field (title, organisation, date, description) supplied
- * separately so presets can place them precisely without re-parsing a
- * concatenated source string.
- *
- *
This is the preferred shape for "Professional
- * Experience", "Work History", or any module whose body is a list of
- * job entries. The {@code BoxedSections} preset renders each
- * {@link Item} as a row with the title bold on the left and the date
- * right-aligned, the organisation italic on the next line under the
- * title, and the description as a full-width paragraph beneath. Other
- * presets fall back to a single inline paragraph per item.
- *
- *
Legacy alternative. Authors may still pass work
- * history as a {@link MultiParagraphBlock} of pipe-separated strings —
- * e.g. {@code "**Title**, Organisation | *Date* — Description"} — and
- * {@code BoxedSections} parses that shape for backward compatibility.
- * Prefer {@code WorkHistoryBlock} in new code: the structured fields
- * are explicit, do not depend on the parser's separator heuristics,
- * and survive copy-paste from spreadsheets without quoting concerns.
- *
- * @param items work-history entries in source order, oldest-last by
- * convention (must not be null; may be empty;
- * individual items must not be null)
- */
-public record WorkHistoryBlock(List items) implements Block {
-
- /**
- * Compact constructor that defensively copies the supplied list and
- * validates that no item reference is null.
- *
- * @throws NullPointerException if {@code items} or any element is
- * null
- */
- public WorkHistoryBlock {
- Objects.requireNonNull(items, "items");
- items = List.copyOf(items);
- }
-
- /**
- * One row in a work-history stack. All four fields are required
- * non-null strings but may be blank — a blank {@code organisation}
- * collapses the subtitle line, a blank {@code description}
- * collapses the body paragraph, and a blank {@code date} renders
- * the title row without a right-aligned date column.
- *
- * @param title role / position, e.g. {@code "Senior Platform Engineer"}
- * @param organisation employer or organisation, e.g. {@code "Northwind Systems"}
- * @param date date range or label, e.g. {@code "2024-Present"}, {@code "Jan 2023 – Mar 2024"}
- * @param description what the role delivered, free prose; may
- * contain inline markdown ({@code **bold**},
- * {@code *italic*})
- */
- public record Item(String title, String organisation, String date, String description) {
-
- /**
- * Compact constructor: rejects null fields. Use empty strings
- * for absent values rather than null.
- */
- public Item {
- Objects.requireNonNull(title, "title");
- Objects.requireNonNull(organisation, "organisation");
- Objects.requireNonNull(date, "date");
- Objects.requireNonNull(description, "description");
- }
- }
-}
diff --git a/src/main/java/com/demcha/compose/document/templates/blocks/package-info.java b/src/main/java/com/demcha/compose/document/templates/blocks/package-info.java
deleted file mode 100644
index 7f3f6c4af..000000000
--- a/src/main/java/com/demcha/compose/document/templates/blocks/package-info.java
+++ /dev/null
@@ -1,27 +0,0 @@
-/**
- * Templates v2 body block kinds — paragraph, list, key-value, table, custom.
- *
- *
Block kinds describe what content appears inside a module body. A
- * preset declares a block kind once (e.g. {@code Module.bulletList(items)})
- * and the renderer expands it according to the active theme and spacing
- * tokens.
- *
- *
This package will be populated during Phase B of the Templates v2
- * migration with at least:
- *
- *
- *
{@code ParagraphBlock} — single paragraph of body text.
- *
{@code BulletListBlock} — bullet-pointed items.
- *
{@code NumberedListBlock} — numbered items.
- *
{@code IndentedBlock} — items with indented continuation
- * (typical for education / projects entries with a bold key
- * and indented body).
- *
{@code KeyValueBlock} — bold key + value on the same line
- * (typical for "Languages: ..." rows).
- *
{@code MultiParagraphBlock} — several paragraphs with
- * paragraph-spacing between.
Header is a stateless composer: configure it once with theme and
- * spacing, then call {@link #compose(Input)} for each spec. The
- * structure is fixed (name → contact → links), but the visual
- * placement is selected up-front via the style factory:
- *
- *
- *
{@link #rightAligned(BusinessTheme, Spacing)} — content
- * right-justified at the top right of the page (Modern
- * Professional pattern).
- *
- *
- *
Additional placement variants (centered, left-banner, monogram)
- * will be added when their preset migrations begin in Phase E. The
- * overall design (a short list of named factories) keeps the API
- * predictable: a preset writer scans the factory list, picks the one
- * matching their layout, and is done.
- */
-public final class Header {
-
- private final TextAlign alignment;
- private final BusinessTheme theme;
- private final Spacing spacing;
- private final String namePrefix;
- private final DocumentTextStyle nameStyleOverride;
- private final DocumentTextStyle contactStyleOverride;
- private final DocumentTextStyle linkStyleOverride;
-
- private Header(TextAlign alignment,
- BusinessTheme theme,
- Spacing spacing,
- DocumentTextStyle nameStyleOverride,
- DocumentTextStyle contactStyleOverride,
- DocumentTextStyle linkStyleOverride) {
- this.alignment = alignment;
- this.theme = theme;
- this.spacing = spacing;
- this.namePrefix = "Header";
- this.nameStyleOverride = nameStyleOverride;
- this.contactStyleOverride = contactStyleOverride;
- this.linkStyleOverride = linkStyleOverride;
- }
-
- /**
- * Returns a header configured for right-aligned placement.
- *
- *
The name appears in the {@code h1} style of the active theme
- * with the contact line and links line stacked below it, all
- * flush with the right edge of the content area.
- *
- * @param theme active business theme
- * @param spacing active spacing tokens
- * @return new right-aligned header composer
- * @throws NullPointerException if either argument is null
- */
- public static Header rightAligned(BusinessTheme theme, Spacing spacing) {
- Objects.requireNonNull(theme, "theme");
- Objects.requireNonNull(spacing, "spacing");
- return new Header(TextAlign.RIGHT, theme, spacing, null, null, null);
- }
-
- /**
- * Returns a copy of this header that uses {@code style} for the
- * subject's name, overriding the theme's {@code h1} default.
- *
- * @param style replacement name text style; pass {@code null} to
- * clear an existing override
- * @return updated header
- */
- public Header withNameStyle(DocumentTextStyle style) {
- return new Header(alignment, theme, spacing, style, contactStyleOverride, linkStyleOverride);
- }
-
- /**
- * Returns a copy of this header that uses {@code style} for the
- * contact line (address / phone), overriding the theme's
- * {@code caption} default.
- *
- * @param style replacement contact text style; pass {@code null} to
- * clear an existing override
- * @return updated header
- */
- public Header withContactStyle(DocumentTextStyle style) {
- return new Header(alignment, theme, spacing, nameStyleOverride, style, linkStyleOverride);
- }
-
- /**
- * Returns a copy of this header that uses {@code style} for each
- * link run (email / LinkedIn / GitHub / etc.), overriding the
- * paragraph-level fallback. Typical usage is an underlined accent
- * style so the link labels render visibly clickable.
- *
- * @param style replacement link text style; pass {@code null} to
- * clear an existing override
- * @return updated header
- */
- public Header withLinkStyle(DocumentTextStyle style) {
- return new Header(alignment, theme, spacing, nameStyleOverride, contactStyleOverride, style);
- }
-
- /**
- * Composes the header into a single {@link DocumentNode}.
- *
- * @param input header input data
- * @return container node holding the rendered name, contact, and
- * links rows
- * @throws NullPointerException if {@code input} is null
- */
- public DocumentNode compose(Input input) {
- Objects.requireNonNull(input, "input");
-
- List rows = new ArrayList<>(3);
- rows.add(nameRow(input.name()));
-
- String contact = joinPipe(input.contactItems());
- if (!contact.isEmpty()) {
- rows.add(contactRow(contact));
- }
-
- if (!input.links().isEmpty()) {
- rows.add(linksRow(input.links()));
- }
-
- return new ContainerNode(
- namePrefix,
- rows,
- /* spacing */ 0.0,
- DocumentInsets.zero(),
- DocumentInsets.zero(),
- /* fillColor */ null,
- /* stroke */ null,
- /* cornerRadius */ null,
- /* borders */ null);
- }
-
- private ParagraphNode nameRow(String name) {
- DocumentTextStyle style = nameStyleOverride != null
- ? nameStyleOverride
- : theme.text().h1();
- return new ParagraphNode(
- namePrefix + ".name",
- name,
- null,
- style,
- alignment,
- /* lineSpacing */ 0.0,
- /* bulletOffset */ "",
- /* indentStrategy */ null,
- /* link */ null,
- /* bookmark */ null,
- /* padding */ DocumentInsets.zero(),
- /* margin */ new DocumentInsets(
- 0.0, 0.0, spacing.headerLineSpacing(), 0.0),
- /* autoSize */ null);
- }
-
- private ParagraphNode contactRow(String contact) {
- DocumentTextStyle style = contactStyleOverride != null
- ? contactStyleOverride
- : theme.text().caption();
- return new ParagraphNode(
- namePrefix + ".contact",
- contact,
- null,
- style,
- alignment,
- 0.0,
- "",
- null,
- null,
- null,
- DocumentInsets.zero(),
- new DocumentInsets(0.0, 0.0, spacing.headerLineSpacing(), 0.0),
- null);
- }
-
- private ParagraphNode linksRow(List links) {
- // Build inline runs: for each non-blank link, emit one InlineTextRun
- // for the label (carrying its own DocumentLinkOptions when the URL is
- // non-blank, making the run a clickable hyperlink in PDF backends
- // that honour link metadata) and one plain run for the " | "
- // separator between links. When a link-style override is set, each
- // link label run receives that explicit style so the link reads
- // visibly clickable (typical preset choice: accent colour with
- // underline decoration); separators stay on the paragraph fallback.
- List runs = new ArrayList<>(links.size() * 2);
- boolean separatorPending = false;
- for (Link link : links) {
- if (link == null || link.label() == null || link.label().isBlank()) {
- continue;
- }
- if (separatorPending) {
- runs.add(new InlineTextRun(" | "));
- }
- DocumentLinkOptions linkOptions = link.url() == null || link.url().isBlank()
- ? null
- : new DocumentLinkOptions(link.url().trim());
- runs.add(new InlineTextRun(link.label().trim(), linkStyleOverride, linkOptions));
- separatorPending = true;
- }
- DocumentTextStyle paragraphStyle = contactStyleOverride != null
- ? contactStyleOverride
- : theme.text().caption();
- return new ParagraphNode(
- namePrefix + ".links",
- /* text */ "",
- /* inlineRuns */ runs,
- paragraphStyle,
- alignment,
- 0.0,
- "",
- null,
- null,
- null,
- DocumentInsets.zero(),
- DocumentInsets.zero(),
- null);
- }
-
- private static String joinPipe(List parts) {
- if (parts == null || parts.isEmpty()) {
- return "";
- }
- StringBuilder sb = new StringBuilder();
- for (String part : parts) {
- if (part == null || part.isBlank()) {
- continue;
- }
- if (sb.length() > 0) {
- sb.append(" | ");
- }
- sb.append(part.trim());
- }
- return sb.toString();
- }
-
- /**
- * One labelled link in the header links row. Empty {@code url}
- * means the label is rendered as plain text rather than a
- * clickable hyperlink — useful for visible-but-non-clickable
- * email addresses or social-handle labels.
- *
- * @param label visible label text (e.g. "alex@example.dev",
- * "LinkedIn", "GitHub"); must not be null or blank
- * @param url target URI with scheme (e.g. {@code https://...},
- * {@code mailto:...}); empty / blank means no link
- */
- public record Link(String label, String url) {
-
- /**
- * Compact constructor that rejects null or blank labels and
- * normalises {@code null} url to empty.
- *
- * @throws NullPointerException if {@code label} is null
- * @throws IllegalArgumentException if {@code label} is blank
- */
- public Link {
- Objects.requireNonNull(label, "label");
- if (label.isBlank()) {
- throw new IllegalArgumentException("label must not be blank");
- }
- url = url == null ? "" : url;
- }
-
- /**
- * Convenience factory for an inactive (non-clickable) link.
- *
- * @param label visible label text
- * @return link with empty URL
- */
- public static Link plain(String label) {
- return new Link(label, "");
- }
-
- /**
- * Convenience factory for an active hyperlink.
- *
- * @param label visible label text
- * @param url target URI with scheme
- * @return active link
- */
- public static Link active(String label, String url) {
- return new Link(label, url);
- }
- }
-
- /**
- * Header input data — the user-facing identity block carried by a
- * {@code CvSpec} or similar parent spec.
- *
- * @param name document subject's name (required, non-blank)
- * @param contactItems contact-row items joined with " | " separators
- * (typically address, phone); null or blank
- * items are skipped
- * @param links link-row entries joined with " | " separators
- * (typically email, LinkedIn, GitHub); each
- * {@link Link} carries its label plus an
- * optional URL that becomes a clickable
- * hyperlink in PDF backends honouring link
- * metadata
- */
- public record Input(String name, List contactItems, List links) {
-
- /**
- * Compact constructor that defensively copies the supplied
- * lists and rejects a null name.
- *
- * @throws NullPointerException if {@code name} is null
- */
- public Input {
- Objects.requireNonNull(name, "name");
- contactItems = contactItems == null ? List.of() : List.copyOf(contactItems);
- links = links == null ? List.of() : List.copyOf(links);
- }
- }
-}
diff --git a/src/main/java/com/demcha/compose/document/templates/components/Module.java b/src/main/java/com/demcha/compose/document/templates/components/Module.java
deleted file mode 100644
index 29d94c091..000000000
--- a/src/main/java/com/demcha/compose/document/templates/components/Module.java
+++ /dev/null
@@ -1,562 +0,0 @@
-package com.demcha.compose.document.templates.components;
-
-import com.demcha.compose.document.templates.core.text.MarkdownText;
-
-import com.demcha.compose.document.node.*;
-import com.demcha.compose.document.style.DocumentInsets;
-import com.demcha.compose.document.style.DocumentTextStyle;
-import com.demcha.compose.document.templates.blocks.*;
-import com.demcha.compose.document.templates.themes.Spacing;
-import com.demcha.compose.document.theme.BusinessTheme;
-
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Objects;
-
-/**
- * Templates v2 module — a section heading paired with a body
- * {@link Block}.
- *
- *
Module is the smallest unit a preset places into a layout slot.
- * It owns the rendering decision for translating a {@link Block} into
- * the engine's {@link DocumentNode} primitives, using the active
- * {@link BusinessTheme} and {@link Spacing} tokens.
- *
- *
For now Module supports a single {@link Style} variant —
- * {@link #headingFlat(BusinessTheme)} — which produces a flat coloured
- * heading flush with the left edge above the body. Additional style
- * variants ({@code headingBoxed}, {@code headingUnderline}) will be
- * added in subsequent preset migrations as concrete needs emerge.
- */
-public final class Module {
-
- private final String name;
- private final String title;
- private final Block body;
- private final Style style;
-
- private Module(String name, String title, Block body, Style style) {
- this.name = Objects.requireNonNull(name, "name");
- this.title = Objects.requireNonNull(title, "title");
- this.body = Objects.requireNonNull(body, "body");
- this.style = Objects.requireNonNull(style, "style");
- }
-
- /**
- * Creates a module with the given identifier, heading title, body
- * block, and rendering style.
- *
- * @param name semantic name of the module — used for snapshot
- * stability and layout graph paths (must not be null)
- * @param title heading text rendered above the body (must not be
- * null; may be empty to suppress the heading)
- * @param body body block (must not be null)
- * @param style heading-rendering style (use {@link #headingFlat})
- * @return new module
- * @throws NullPointerException if any argument is null
- */
- public static Module of(String name, String title, Block body, Style style) {
- return new Module(name, title, body, style);
- }
-
- /**
- * Returns the module's semantic name.
- *
- * @return module name
- */
- public String name() {
- return name;
- }
-
- /**
- * Returns the module's heading text.
- *
- * @return module title
- */
- public String title() {
- return title;
- }
-
- /**
- * Returns the module's body block.
- *
- * @return body block
- */
- public Block body() {
- return body;
- }
-
- /**
- * Composes the module into a single {@link DocumentNode} suitable
- * for adding to a layout slot.
- *
- * @param theme active business theme — provides typography and
- * palette tokens
- * @param spacing active spacing tokens
- * @return container node holding the heading and body
- * @throws NullPointerException if either argument is null
- */
- public DocumentNode compose(BusinessTheme theme, Spacing spacing) {
- Objects.requireNonNull(theme, "theme");
- Objects.requireNonNull(spacing, "spacing");
-
- List children = new ArrayList<>(2);
-
- if (!title.isEmpty()) {
- children.add(headingParagraph(theme, spacing));
- }
- children.add(renderBody(theme, spacing));
-
- return new ContainerNode(
- name,
- children,
- /* spacing */ 0.0,
- /* padding */ DocumentInsets.zero(),
- /* margin */ DocumentInsets.zero(),
- /* fillColor */ null,
- /* stroke */ null,
- /* cornerRadius */ null,
- /* borders */ null);
- }
-
- private ParagraphNode headingParagraph(BusinessTheme theme, Spacing spacing) {
- DocumentTextStyle headingStyle = style.headingStyle != null
- ? style.headingStyle
- : theme.text().h2();
- return new ParagraphNode(
- name + ".heading",
- title,
- /* inlineRuns */ null,
- headingStyle,
- TextAlign.LEFT,
- /* lineSpacing */ 0.0,
- /* bulletOffset */ "",
- /* indentStrategy */ null,
- /* link */ null,
- /* bookmark */ null,
- /* padding */ DocumentInsets.zero(),
- /* margin */ new DocumentInsets(
- style.marginAbove,
- 0.0,
- style.marginBelow,
- 0.0),
- /* autoSize */ null);
- }
-
- private DocumentTextStyle bodyStyle(BusinessTheme theme) {
- return style.bodyStyle != null ? style.bodyStyle : theme.text().body();
- }
-
- private DocumentNode renderBody(BusinessTheme theme, Spacing spacing) {
- if (body instanceof ParagraphBlock p) {
- return renderParagraph(p, theme, spacing);
- }
- if (body instanceof BulletListBlock b) {
- return renderList(b.items(), ListMarker.bullet(), "bullet", theme, spacing);
- }
- if (body instanceof NumberedListBlock n) {
- return renderList(n.items(), ListMarker.custom("1."), "numbered", theme, spacing);
- }
- if (body instanceof MultiParagraphBlock m) {
- return renderMultiParagraph(m, theme, spacing);
- }
- if (body instanceof IndentedBlock i) {
- return renderIndented(i, theme, spacing);
- }
- if (body instanceof KeyValueBlock k) {
- return renderKeyValue(k, theme, spacing);
- }
- if (body instanceof WorkHistoryBlock w) {
- return renderWorkHistory(w, theme, spacing);
- }
- if (body instanceof EducationBlock e) {
- return renderEducation(e, theme, spacing);
- }
- throw new IllegalStateException("Unsupported module body: " + body);
- }
-
- /**
- * Default rendering for {@link WorkHistoryBlock} when the active
- * preset has not overridden the dispatch (e.g. {@code BoxedSections}
- * renders this block as a structured title/date row + company
- * subtitle + description paragraph). Generic presets degrade to a
- * single concatenated paragraph per item, mirroring the legacy
- * {@code MultiParagraphBlock} format
- * ({@code "**Title**, Organisation | *Date* — Description"}) so
- * the data still renders sensibly without requiring every preset
- * to special-case the new block type.
- */
- private DocumentNode renderWorkHistory(WorkHistoryBlock block, BusinessTheme theme, Spacing spacing) {
- List sources = new ArrayList<>(block.items().size());
- for (WorkHistoryBlock.Item item : block.items()) {
- sources.add(concatStructuredFields(
- item.title(), item.organisation(), item.date(), item.description(),
- ", ", " | ", " — ",
- /* italicizeDate */ true));
- }
- return renderConcatenatedParagraphs(sources, "workEntry", theme, spacing);
- }
-
- /**
- * Default rendering for {@link EducationBlock} when the active
- * preset has not overridden the dispatch (e.g. {@code BoxedSections}
- * renders this block with the same structured layout as a work
- * entry — degree bold left, year right, institution italic below,
- * details paragraph beneath). Generic presets degrade to a single
- * concatenated paragraph per item so the data still renders
- * sensibly without requiring every preset to special-case the new
- * block type.
- */
- private DocumentNode renderEducation(EducationBlock block, BusinessTheme theme, Spacing spacing) {
- List sources = new ArrayList<>(block.items().size());
- for (EducationBlock.Item item : block.items()) {
- sources.add(concatStructuredFields(
- item.degree(), item.institution(), item.year(), item.details(),
- " - ", " | ", ". ",
- /* italicizeDate */ false));
- }
- return renderConcatenatedParagraphs(sources, "educationEntry", theme, spacing);
- }
-
- /**
- * Concatenates the four fields of a structured entry (work history
- * or education) into a single markdown source line, using the
- * supplied separators. The first field is rendered bold, the third
- * is optionally italicized, and the others stay in regular weight.
- * Empty fields are skipped along with their preceding separator so
- * a missing organisation / date / description does not leave a
- * dangling {@code " | "} or {@code " — "} on screen.
- *
- *
Extracted from {@link #renderWorkHistory} and
- * {@link #renderEducation} so adding a future 4-field structured
- * block (publications, projects, etc.) does not require another
- * copy of the same string-building loop.
- */
- private static String concatStructuredFields(
- String main, String secondary, String date, String detail,
- String secondarySep, String dateSep, String detailSep,
- boolean italicizeDate) {
- StringBuilder source = new StringBuilder();
- if (main != null && !main.isBlank()) {
- source.append("**").append(main).append("**");
- }
- if (secondary != null && !secondary.isBlank()) {
- if (source.length() > 0) {
- source.append(secondarySep);
- }
- source.append(secondary);
- }
- if (date != null && !date.isBlank()) {
- if (source.length() > 0) {
- source.append(dateSep);
- }
- if (italicizeDate) {
- source.append('*').append(date).append('*');
- } else {
- source.append(date);
- }
- }
- if (detail != null && !detail.isBlank()) {
- if (source.length() > 0) {
- source.append(detailSep);
- }
- source.append(detail);
- }
- return source.toString();
- }
-
- /**
- * Wraps a list of pre-built markdown source strings into a
- * {@link ContainerNode} of {@link ParagraphNode}s using the active
- * body style and paragraph spacing. Extracted from
- * {@link #renderWorkHistory} and {@link #renderEducation} so the
- * boilerplate around constructing ParagraphNode + ContainerNode
- * lives in one place — any future 4-field structured block can
- * call this directly after building its own per-item source via
- * {@link #concatStructuredFields}.
- *
- * @param sources per-item markdown source lines
- * @param entryNameSuffix name suffix appended to {@code name}
- * for each paragraph (e.g.
- * {@code "workEntry"} → {@code ".workEntry[0]"})
- */
- private DocumentNode renderConcatenatedParagraphs(List sources, String entryNameSuffix,
- BusinessTheme theme, Spacing spacing) {
- DocumentTextStyle bodyStyle = bodyStyle(theme);
- List paragraphs = new ArrayList<>(sources.size());
- for (int i = 0; i < sources.size(); i++) {
- paragraphs.add(new ParagraphNode(
- name + "." + entryNameSuffix + "[" + i + "]",
- "",
- MarkdownText.parse(sources.get(i), bodyStyle),
- bodyStyle,
- TextAlign.LEFT,
- spacing.lineSpacing(),
- "",
- null,
- null,
- null,
- DocumentInsets.zero(),
- DocumentInsets.zero(),
- null));
- }
- return new ContainerNode(
- name + ".body",
- paragraphs,
- /* spacing */ spacing.paragraphSpacing(),
- /* padding */ leftIndent(spacing),
- DocumentInsets.zero(),
- null,
- null,
- null,
- null);
- }
-
- private ParagraphNode renderParagraph(ParagraphBlock block, BusinessTheme theme, Spacing spacing) {
- DocumentTextStyle bodyStyle = bodyStyle(theme);
- List runs = MarkdownText.parse(block.text(), bodyStyle);
- return new ParagraphNode(
- name + ".body",
- /* text */ "",
- /* inlineRuns */ runs,
- bodyStyle,
- block.align(),
- spacing.lineSpacing(),
- "",
- null,
- null,
- null,
- /* padding */ leftIndent(spacing),
- DocumentInsets.zero(),
- null);
- }
-
- private ListNode renderList(List items, ListMarker marker, String suffix,
- BusinessTheme theme, Spacing spacing) {
- return new ListNode(
- name + "." + suffix,
- items,
- marker,
- bodyStyle(theme),
- TextAlign.LEFT,
- spacing.lineSpacing(),
- spacing.listItemSpacing(),
- /* continuationIndent */ "",
- /* normalizeMarkers */ true,
- /* padding */ leftIndent(spacing),
- DocumentInsets.zero());
- }
-
- private DocumentNode renderMultiParagraph(MultiParagraphBlock block, BusinessTheme theme, Spacing spacing) {
- DocumentTextStyle bodyStyle = bodyStyle(theme);
- List paragraphs = new ArrayList<>(block.paragraphs().size());
- for (int i = 0; i < block.paragraphs().size(); i++) {
- String source = block.paragraphs().get(i);
- paragraphs.add(new ParagraphNode(
- name + ".paragraph[" + i + "]",
- "",
- MarkdownText.parse(source, bodyStyle),
- bodyStyle,
- TextAlign.LEFT,
- spacing.lineSpacing(),
- "",
- null,
- null,
- null,
- DocumentInsets.zero(),
- DocumentInsets.zero(),
- null));
- }
- return new ContainerNode(
- name + ".body",
- paragraphs,
- /* spacing */ spacing.paragraphSpacing(),
- /* padding */ leftIndent(spacing),
- DocumentInsets.zero(),
- null,
- null,
- null,
- null);
- }
-
- private DocumentNode renderIndented(IndentedBlock block, BusinessTheme theme, Spacing spacing) {
- DocumentTextStyle bodyStyle = bodyStyle(theme);
- // Title is the same family as body but rendered bold so it
- // visually separates from the indented body underneath.
- DocumentTextStyle titleStyle = DocumentTextStyle.builder()
- .fontName(bodyStyle.fontName())
- .size(bodyStyle.size())
- .decoration(com.demcha.compose.document.style.DocumentTextDecoration.BOLD)
- .color(bodyStyle.color())
- .build();
- List entries = new ArrayList<>(block.items().size() * 2);
- for (int i = 0; i < block.items().size(); i++) {
- IndentedBlock.Item item = block.items().get(i);
- // Title — bold weight via h3 style; markdown parsed for inline emphasis.
- entries.add(new ParagraphNode(
- name + ".item[" + i + "].title",
- "",
- MarkdownText.parse(item.title(), titleStyle),
- titleStyle,
- TextAlign.LEFT,
- spacing.lineSpacing(),
- "",
- null,
- null,
- null,
- DocumentInsets.zero(),
- DocumentInsets.zero(),
- null));
- // Body — regular paragraph nested one half-step beyond the
- // module body indent so the title visually sits one level above.
- entries.add(new ParagraphNode(
- name + ".item[" + i + "].body",
- "",
- MarkdownText.parse(item.body(), bodyStyle),
- bodyStyle,
- TextAlign.LEFT,
- spacing.lineSpacing(),
- "",
- null,
- null,
- null,
- /* padding */ new DocumentInsets(0.0, 0.0, 0.0, spacing.bodyIndent()),
- DocumentInsets.zero(),
- null));
- }
- return new ContainerNode(
- name + ".body",
- entries,
- spacing.listItemSpacing(),
- /* padding */ leftIndent(spacing),
- DocumentInsets.zero(),
- null,
- null,
- null,
- null);
- }
-
- private DocumentNode renderKeyValue(KeyValueBlock block, BusinessTheme theme, Spacing spacing) {
- DocumentTextStyle bodyStyle = bodyStyle(theme);
- // Key uses the bold variant of the body font so a "Key: value"
- // line shows the key emphasised on the same baseline as the
- // value, without depending on the legacy markdown pipeline.
- List rows = new ArrayList<>(block.entries().size());
- for (int i = 0; i < block.entries().size(); i++) {
- KeyValueBlock.Entry entry = block.entries().get(i);
- // Synthesize the key as a markdown bold prefix so all the
- // emphasis logic flows through the shared MarkdownText
- // parser; this also means an entry whose key already
- // contains markdown is rendered correctly.
- String composed = "**" + entry.key() + ":** " + entry.value();
- rows.add(new ParagraphNode(
- name + ".entry[" + i + "]",
- "",
- MarkdownText.parse(composed, bodyStyle),
- bodyStyle,
- TextAlign.LEFT,
- spacing.lineSpacing(),
- "",
- null,
- null,
- null,
- DocumentInsets.zero(),
- DocumentInsets.zero(),
- null));
- }
- return new ContainerNode(
- name + ".body",
- rows,
- spacing.lineSpacing(),
- /* padding */ leftIndent(spacing),
- DocumentInsets.zero(),
- null,
- null,
- null,
- null);
- }
-
- private static DocumentInsets leftIndent(Spacing spacing) {
- return new DocumentInsets(0.0, 0.0, 0.0, spacing.bodyIndent());
- }
-
- /**
- * Returns the canonical "flat heading" rendering style — heading
- * text in the active {@code h2} style, body text in the active
- * {@code body} style, flush with the left edge, with default
- * vertical margins driven by the active spacing tokens.
- *
- *
Callers can adjust the margins or override either style after
- * the fact:
- *
- * @param theme active business theme
- * @return new style instance
- * @throws NullPointerException if {@code theme} is null
- */
- public static Style headingFlat(BusinessTheme theme) {
- Objects.requireNonNull(theme, "theme");
- return new Style(
- theme.text().h2(),
- theme.text().body(),
- /* above */ 8.0,
- /* below */ 4.0);
- }
-
- /**
- * Module heading-rendering style.
- *
- * @param headingStyle text style applied to the heading line; if
- * null the active theme's {@code h2} is used
- * @param bodyStyle text style applied to the body content
- * (paragraph, list, indented body, key-value);
- * if null the active theme's {@code body} is
- * used
- * @param marginAbove vertical margin above the heading
- * @param marginBelow vertical margin below the heading (separates
- * it from the body)
- */
- public record Style(
- DocumentTextStyle headingStyle,
- DocumentTextStyle bodyStyle,
- double marginAbove,
- double marginBelow) {
-
- /**
- * Returns a copy with the given margin above the heading.
- *
- * @param value margin above in points; non-negative finite
- * @return new style instance
- */
- public Style marginAbove(double value) {
- return new Style(headingStyle, bodyStyle, value, marginBelow);
- }
-
- /**
- * Returns a copy with the given margin below the heading.
- *
- * @param value margin below in points; non-negative finite
- * @return new style instance
- */
- public Style marginBelow(double value) {
- return new Style(headingStyle, bodyStyle, marginAbove, value);
- }
-
- /**
- * Returns a copy with the given body style override.
- *
- * @param value text style for body content; null falls back to
- * the active theme's {@code body} style
- * @return new style instance
- */
- public Style bodyStyle(DocumentTextStyle value) {
- return new Style(headingStyle, value, marginAbove, marginBelow);
- }
- }
-}
diff --git a/src/main/java/com/demcha/compose/document/templates/components/package-info.java b/src/main/java/com/demcha/compose/document/templates/components/package-info.java
deleted file mode 100644
index 8fa791efa..000000000
--- a/src/main/java/com/demcha/compose/document/templates/components/package-info.java
+++ /dev/null
@@ -1,28 +0,0 @@
-/**
- * Templates v2 reusable composition components — header, module, section, sidebar.
- *
- *
Components are the small reusable building blocks that Templates v2
- * presets stitch together to render a document. Each component has a small
- * set of style variants (factory methods like {@code Header.rightAligned()},
- * {@code Header.centered()}) plus configuration setters for typography,
- * palette, and spacing tokens.
- *
- *
This package will be populated during Phase B of the Templates v2
- * migration with at least:
Pipeline role: stores a destination plus display text that shared
- * template composers translate into visible contact links.
- *
- *
Mutability: mutable Lombok-backed bean. Thread-safety:
- * not thread-safe.
- */
-@Data
-public class LinkYml {
- private LinkUrl linkUrl;
- private String displayText;
-
- /**
- * Creates an empty mutable link payload.
- */
- public LinkYml() {
- }
-
- /**
- * Starts a fluent link builder.
- *
- * @return link builder
- */
- public static Builder builder() {
- return new Builder();
- }
-
- /**
- * Creates a link payload from URL and display text.
- *
- * @param url target URL
- * @param displayText visible link text
- * @return link payload
- */
- public static LinkYml of(String url, String displayText) {
- return builder()
- .url(url)
- .displayText(displayText)
- .build();
- }
-
- /**
- * Fluent builder for template hyperlink metadata.
- */
- public static final class Builder {
- private LinkUrl linkUrl;
- private String displayText;
-
- private Builder() {
- }
-
- /**
- * Sets the typed link URL.
- *
- * @param linkUrl link URL
- * @return this builder
- */
- public Builder linkUrl(LinkUrl linkUrl) {
- this.linkUrl = linkUrl;
- return this;
- }
-
- /**
- * Sets the link URL from text.
- *
- * @param url target URL
- * @return this builder
- */
- public Builder url(String url) {
- this.linkUrl = url == null || url.isBlank() ? null : new LinkUrl(url);
- return this;
- }
-
- /**
- * Sets the visible link text.
- *
- * @param displayText visible link text
- * @return this builder
- */
- public Builder displayText(String displayText) {
- this.displayText = displayText;
- return this;
- }
-
- /**
- * Builds the mutable link payload.
- *
- * @return link payload
- */
- public LinkYml build() {
- LinkYml link = new LinkYml();
- link.setLinkUrl(linkUrl);
- link.setDisplayText(displayText);
- return link;
- }
- }
-}
diff --git a/src/main/java/com/demcha/compose/document/templates/data/common/package-info.java b/src/main/java/com/demcha/compose/document/templates/data/common/package-info.java
deleted file mode 100644
index 019b6939f..000000000
--- a/src/main/java/com/demcha/compose/document/templates/data/common/package-info.java
+++ /dev/null
@@ -1,4 +0,0 @@
-/**
- * Shared contact and link data used by canonical document templates.
- */
-package com.demcha.compose.document.templates.data.common;
diff --git a/src/main/java/com/demcha/compose/document/templates/data/package-info.java b/src/main/java/com/demcha/compose/document/templates/data/package-info.java
index c04f0cc22..bc0c92ede 100644
--- a/src/main/java/com/demcha/compose/document/templates/data/package-info.java
+++ b/src/main/java/com/demcha/compose/document/templates/data/package-info.java
@@ -3,7 +3,6 @@
* {@code document.templates} layer.
*
*
Concrete public data types live in child packages such as
- * {@code data.invoice}, {@code data.proposal}, and {@code data.schedule}.
- * Shared contact/link types live in {@code data.common}.
+ * {@code data.invoice}, {@code data.proposal}, and {@code data.schedule}.
*/
package com.demcha.compose.document.templates.data;
diff --git a/src/main/java/com/demcha/compose/document/templates/package-info.java b/src/main/java/com/demcha/compose/document/templates/package-info.java
index 6d7694422..95c130f17 100644
--- a/src/main/java/com/demcha/compose/document/templates/package-info.java
+++ b/src/main/java/com/demcha/compose/document/templates/package-info.java
@@ -2,10 +2,10 @@
* Canonical reusable template layer built on top of the GraphCompose semantic
* {@code document.*} API.
*
- *
This package tree contains the public contracts, data objects, themes, and
- * built-in templates that compose through
- * {@link com.demcha.compose.document.api.DocumentSession}. The package is the
- * canonical template surface for V2 and replaces the older deprecated template
- * namespace as the documented default.
+ *
This package tree holds the public template contract
+ * ({@link com.demcha.compose.document.templates.api.DocumentTemplate}), the
+ * shared {@code templates.core} layer, and the layered per-family template
+ * stacks (cv, cover-letter, invoice, proposal) that compose through
+ * {@link com.demcha.compose.document.api.DocumentSession}.
Pipeline role: this is the seam between backend-neutral template
- * scene definitions and the concrete authoring target used during one
- * composition pass. The canonical session-backed implementation writes into
- * {@link com.demcha.compose.document.api.DocumentSession}.
- *
- *
This interface is public for advanced extensions, but it is not the
- * recommended starting point for normal template consumers.
The default implementation keeps the target surface small: module
- * composition is still lowered into existing paragraph, list, table,
- * divider, and page-break operations.
The default implementation keeps custom targets small by lowering a
- * list into paragraph blocks. The canonical session target overrides this
- * method and emits a native list node.
This class is intentionally package-private. Public authoring code should
- * never need engine value objects; templates still use some internal support
- * specs while the remaining package cleanup continues.
Pipeline role: keeps module gaps, body text spacing, list item
- * spacing, and reusable cell padding in one support-level object so built-in
- * templates do not hide layout rules in scattered local constants.
- *
- * @param rootSpacing spacing between top-level blocks in the root flow
- * @param sectionMargin margin applied to section/module headings
- * @param subsectionMargin margin applied to nested section blocks
- * @param blockMargin margin applied to regular body blocks when a template
- * needs an explicit top gap
- * @param bodyLineSpacing extra spacing between wrapped body text lines
- * @param bodyItemSpacing extra spacing between list items
- * @param tableLineSpacing extra spacing between multiple text lines inside
- * table cells
- * @param markerlessContinuationIndent prefix used for wrapped continuation
- * lines in markerless rows
- * @param bodyPadding default body block padding
- * @param compactCellPadding reusable padding for compact table cells
- * @param contentCellPadding reusable padding for content-heavy table cells
- * @author Artem Demchyshyn
- */
-public record TemplateLayoutPolicy(
- double rootSpacing,
- Margin sectionMargin,
- Margin subsectionMargin,
- Margin blockMargin,
- double bodyLineSpacing,
- double bodyItemSpacing,
- double tableLineSpacing,
- String markerlessContinuationIndent,
- Padding bodyPadding,
- Padding compactCellPadding,
- Padding contentCellPadding
-) {
- /**
- * Creates a normalized layout policy.
- */
- public TemplateLayoutPolicy {
- validateSpacing(rootSpacing, "rootSpacing");
- validateSpacing(bodyLineSpacing, "bodyLineSpacing");
- validateSpacing(bodyItemSpacing, "bodyItemSpacing");
- validateSpacing(tableLineSpacing, "tableLineSpacing");
- sectionMargin = sectionMargin == null ? Margin.zero() : sectionMargin;
- subsectionMargin = subsectionMargin == null ? Margin.zero() : subsectionMargin;
- blockMargin = blockMargin == null ? Margin.zero() : blockMargin;
- markerlessContinuationIndent = markerlessContinuationIndent == null ? "" : markerlessContinuationIndent;
- bodyPadding = bodyPadding == null ? Padding.zero() : bodyPadding;
- compactCellPadding = compactCellPadding == null ? Padding.zero() : compactCellPadding;
- contentCellPadding = contentCellPadding == null ? Padding.zero() : contentCellPadding;
- }
-
- /**
- * Returns the compact business rhythm used by the executive CV template.
- *
- * @return layout policy for executive CV documents
- */
- public static TemplateLayoutPolicy executiveCv() {
- return new TemplateLayoutPolicy(
- 0.0,
- Margin.top(7),
- Margin.top(2),
- Margin.top(2),
- 1.8,
- 1.4,
- 0.0,
- " ",
- new Padding(0, 0, 1, 9),
- new Padding(2, 0, 2, 0),
- new Padding(7, 8, 7, 8));
- }
-
- /**
- * Returns the default rhythm shared by built-in business documents.
- *
- * @return layout policy for invoice/proposal style templates
- */
- public static TemplateLayoutPolicy businessDocument() {
- return new TemplateLayoutPolicy(
- 10.0,
- Margin.top(6),
- Margin.top(4),
- Margin.top(3),
- 2.0,
- 2.0,
- 1.2,
- " ",
- Padding.zero(),
- new Padding(2, 0, 2, 0),
- new Padding(7, 8, 7, 8));
- }
-
- /**
- * Creates a top-only margin after validating the spacing value.
- *
- * @param value top margin value
- * @return top-only margin
- */
- public Margin top(double value) {
- validateSpacing(value, "value");
- return Margin.top(value);
- }
-
- /**
- * Creates a full margin after validating each spacing value.
- *
- * @param top top margin
- * @param right right margin
- * @param bottom bottom margin
- * @param left left margin
- * @return validated margin
- */
- public Margin margin(double top, double right, double bottom, double left) {
- validateSpacing(top, "top");
- validateSpacing(right, "right");
- validateSpacing(bottom, "bottom");
- validateSpacing(left, "left");
- return new Margin(top, right, bottom, left);
- }
-
- /**
- * Applies the root rhythm before a nested module body block.
- *
- * @param margin block-specific margin to preserve
- * @return margin whose top value includes the root spacing token
- */
- public Margin withRootSpacingTop(Margin margin) {
- Margin safeMargin = margin == null ? Margin.zero() : margin;
- return margin(
- rootSpacing + safeMargin.top(),
- safeMargin.right(),
- safeMargin.bottom(),
- safeMargin.left());
- }
-
- private static void validateSpacing(double value, String label) {
- if (value < 0 || Double.isNaN(value) || Double.isInfinite(value)) {
- throw new IllegalArgumentException(label + " must be finite and non-negative: " + value);
- }
- }
-}
diff --git a/src/main/java/com/demcha/compose/document/templates/support/common/TemplateLifecycleLog.java b/src/main/java/com/demcha/compose/document/templates/support/common/TemplateLifecycleLog.java
deleted file mode 100644
index 08d838012..000000000
--- a/src/main/java/com/demcha/compose/document/templates/support/common/TemplateLifecycleLog.java
+++ /dev/null
@@ -1,125 +0,0 @@
-package com.demcha.compose.document.templates.support.common;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.util.concurrent.TimeUnit;
-
-/**
- * Shared lifecycle logger for built-in canonical templates.
- *
- *
The logger intentionally records only template identifiers, spec types,
- * and timings. It must not log document text, contact values, addresses, or
- * other user-provided content.
- */
-public final class TemplateLifecycleLog {
- private static final Logger LOG = LoggerFactory.getLogger("com.demcha.compose.templates.lifecycle");
-
- private TemplateLifecycleLog() {
- }
-
- /**
- * Records the start of one template composition pass.
- *
- * @param templateId stable template identifier
- * @param spec template specification object
- * @return monotonic start time used by {@link #success(String, Object, long)}
- */
- public static long start(String templateId, Object spec) {
- long startNanos = System.nanoTime();
- LOG.debug("template.compose.start templateId={} specType={}", templateId, specType(spec));
- return startNanos;
- }
-
- /**
- * Records successful completion of one template composition pass.
- *
- * @param templateId stable template identifier
- * @param spec template specification object
- * @param startNanos monotonic start time returned by {@link #start(String, Object)}
- */
- public static void success(String templateId, Object spec, long startNanos) {
- LOG.debug(
- "template.compose.end templateId={} specType={} durationMs={}",
- templateId,
- specType(spec),
- TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNanos));
- }
-
- /**
- * Records a failed template composition pass without logging user content.
- *
- * @param templateId stable template identifier
- * @param spec template specification object
- * @param startNanos monotonic start time returned by {@link #start(String, Object)}
- * @param failure failure raised during template composition
- */
- public static void failure(String templateId, Object spec, long startNanos, Throwable failure) {
- LOG.debug(
- "template.compose.failed templateId={} specType={} durationMs={} errorType={}",
- templateId,
- specType(spec),
- TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNanos),
- failure.getClass().getSimpleName());
- }
-
- /**
- * Records the start of one template module composition pass.
- *
- * @param module module specification
- * @return monotonic start time used by {@link #moduleSuccess(TemplateModuleSpec, long)}
- */
- public static long moduleStart(TemplateModuleSpec module) {
- long startNanos = System.nanoTime();
- LOG.debug(
- "template.module.compose.start moduleName={} blockCount={}",
- moduleName(module),
- blockCount(module));
- return startNanos;
- }
-
- /**
- * Records successful completion of one template module composition pass.
- *
- * @param module module specification
- * @param startNanos monotonic start time returned by {@link #moduleStart(TemplateModuleSpec)}
- */
- public static void moduleSuccess(TemplateModuleSpec module, long startNanos) {
- LOG.debug(
- "template.module.compose.end moduleName={} blockCount={} durationMs={}",
- moduleName(module),
- blockCount(module),
- TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNanos));
- }
-
- /**
- * Records a failed template module composition pass without logging user content.
- *
- * @param module module specification
- * @param startNanos monotonic start time returned by {@link #moduleStart(TemplateModuleSpec)}
- * @param failure failure raised during module composition
- */
- public static void moduleFailure(TemplateModuleSpec module, long startNanos, Throwable failure) {
- LOG.debug(
- "template.module.compose.failed moduleName={} blockCount={} durationMs={} errorType={}",
- moduleName(module),
- blockCount(module),
- TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNanos),
- failure.getClass().getSimpleName());
- }
-
- private static String specType(Object spec) {
- return spec == null ? "null" : spec.getClass().getSimpleName();
- }
-
- private static String moduleName(TemplateModuleSpec module) {
- if (module == null || module.name() == null || module.name().isBlank()) {
- return "unnamed";
- }
- return module.name();
- }
-
- private static int blockCount(TemplateModuleSpec module) {
- return module == null ? 0 : module.blocks().size();
- }
-}
diff --git a/src/main/java/com/demcha/compose/document/templates/support/common/TemplateListSpec.java b/src/main/java/com/demcha/compose/document/templates/support/common/TemplateListSpec.java
deleted file mode 100644
index 21d245799..000000000
--- a/src/main/java/com/demcha/compose/document/templates/support/common/TemplateListSpec.java
+++ /dev/null
@@ -1,71 +0,0 @@
-package com.demcha.compose.document.templates.support.common;
-
-import com.demcha.compose.document.node.ListMarker;
-import com.demcha.compose.document.node.TextAlign;
-import com.demcha.compose.engine.components.content.text.TextStyle;
-import com.demcha.compose.engine.components.style.Margin;
-import com.demcha.compose.engine.components.style.Padding;
-
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * Immutable list instruction used by shared template scene composers.
- *
- * @param name semantic list name used in snapshots and layout graph paths
- * @param items list item texts in source order
- * @param marker marker rendered before each item
- * @param style shared item text style
- * @param align horizontal item alignment
- * @param lineSpacing extra spacing between wrapped lines in one item
- * @param itemSpacing extra spacing between list items
- * @param continuationIndent prefix used only for wrapped continuation lines when marker is hidden
- * @param normalizeMarkers whether input items may include pre-existing markers
- * @param padding list padding
- * @param margin list margin
- * @author Artem Demchyshyn
- */
-public record TemplateListSpec(
- String name,
- List items,
- ListMarker marker,
- TextStyle style,
- TextAlign align,
- double lineSpacing,
- double itemSpacing,
- String continuationIndent,
- boolean normalizeMarkers,
- Padding padding,
- Margin margin
-) {
- /**
- * Creates a normalized template list instruction.
- */
- public TemplateListSpec {
- name = name == null ? "" : name;
- items = normalizeItems(items);
- marker = marker == null ? ListMarker.bullet() : marker;
- style = style == null ? TextStyle.DEFAULT_STYLE : style;
- align = align == null ? TextAlign.LEFT : align;
- continuationIndent = continuationIndent == null ? "" : continuationIndent;
- padding = padding == null ? Padding.zero() : padding;
- margin = margin == null ? Margin.zero() : margin;
- if (lineSpacing < 0 || Double.isNaN(lineSpacing) || Double.isInfinite(lineSpacing)) {
- throw new IllegalArgumentException("lineSpacing must be finite and non-negative: " + lineSpacing);
- }
- if (itemSpacing < 0 || Double.isNaN(itemSpacing) || Double.isInfinite(itemSpacing)) {
- throw new IllegalArgumentException("itemSpacing must be finite and non-negative: " + itemSpacing);
- }
- }
-
- private static List normalizeItems(List items) {
- if (items == null || items.isEmpty()) {
- return List.of();
- }
- List normalized = new ArrayList<>(items.size());
- for (String item : items) {
- normalized.add(item == null ? "" : item);
- }
- return List.copyOf(normalized);
- }
-}
diff --git a/src/main/java/com/demcha/compose/document/templates/support/common/TemplateModuleBlock.java b/src/main/java/com/demcha/compose/document/templates/support/common/TemplateModuleBlock.java
deleted file mode 100644
index 3c7091ad1..000000000
--- a/src/main/java/com/demcha/compose/document/templates/support/common/TemplateModuleBlock.java
+++ /dev/null
@@ -1,233 +0,0 @@
-package com.demcha.compose.document.templates.support.common;
-
-import java.util.Objects;
-import java.util.function.Consumer;
-
-/**
- * One renderable body block inside a reusable template module.
- *
- *
Pipeline role: keeps higher-level template modules semantic while
- * still rendering through the existing paragraph, list, table, divider, and
- * page-break target methods.
Pipeline role: groups one optional heading paragraph plus ordered
- * body blocks so compose-first templates can express section-level structure
- * without introducing a new layout primitive or changing pagination
- * semantics.
- *
- * @param name semantic module name used in diagnostics and graph-oriented
- * tooling
- * @param title optional module title paragraph, or {@code null} when omitted
- * @param blocks ordered body blocks rendered after the title
- * @author Artem Demchyshyn
- */
-public record TemplateModuleSpec(
- String name,
- TemplateParagraphSpec title,
- List blocks
-) {
- /**
- * Creates a normalized module instruction.
- */
- public TemplateModuleSpec {
- name = name == null ? "" : name;
- blocks = blocks == null ? List.of() : List.copyOf(blocks);
- }
-}
diff --git a/src/main/java/com/demcha/compose/document/templates/support/common/TemplateParagraphSpec.java b/src/main/java/com/demcha/compose/document/templates/support/common/TemplateParagraphSpec.java
deleted file mode 100644
index 843187b89..000000000
--- a/src/main/java/com/demcha/compose/document/templates/support/common/TemplateParagraphSpec.java
+++ /dev/null
@@ -1,83 +0,0 @@
-package com.demcha.compose.document.templates.support.common;
-
-import com.demcha.compose.document.node.DocumentLinkOptions;
-import com.demcha.compose.document.node.InlineTextRun;
-import com.demcha.compose.document.node.TextAlign;
-import com.demcha.compose.engine.components.content.text.TextIndentStrategy;
-import com.demcha.compose.engine.components.content.text.TextStyle;
-import com.demcha.compose.engine.components.style.Margin;
-import com.demcha.compose.engine.components.style.Padding;
-
-import java.util.List;
-
-/**
- * Immutable paragraph instruction used by shared template scene composers.
- *
- * @param name semantic paragraph name used in snapshots
- * @param text paragraph text when inline runs are not supplied
- * @param inlineTextRuns optional styled inline runs in source order
- * @param style paragraph text style
- * @param align horizontal text alignment
- * @param lineSpacing extra space between wrapped lines
- * @param bulletOffset first-line prefix for list-like paragraph paths
- * @param indentStrategy hanging/first-line indent strategy
- * @param linkOptions optional paragraph link metadata
- * @param padding inner padding
- * @param margin outer margin
- * @author Artem Demchyshyn
- */
-public record TemplateParagraphSpec(
- String name,
- String text,
- List inlineTextRuns,
- TextStyle style,
- TextAlign align,
- double lineSpacing,
- String bulletOffset,
- TextIndentStrategy indentStrategy,
- DocumentLinkOptions linkOptions,
- Padding padding,
- Margin margin
-) {
- /**
- * Normalizes paragraph defaults for shared template composition.
- */
- public TemplateParagraphSpec {
- name = name == null ? "" : name;
- inlineTextRuns = inlineTextRuns == null ? List.of() : List.copyOf(inlineTextRuns);
- text = text == null ? "" : text;
- style = style == null ? TextStyle.DEFAULT_STYLE : style;
- align = align == null ? TextAlign.LEFT : align;
- bulletOffset = bulletOffset == null ? "" : bulletOffset;
- indentStrategy = indentStrategy == null ? TextIndentStrategy.NONE : indentStrategy;
- padding = padding == null ? Padding.zero() : padding;
- margin = margin == null ? Margin.zero() : margin;
- }
-
- /**
- * Creates a paragraph instruction from plain text.
- *
- * @param name semantic paragraph name
- * @param text paragraph text
- * @param style paragraph text style
- * @param align horizontal text alignment
- * @param lineSpacing extra line spacing
- * @param bulletOffset first-line marker or prefix
- * @param indentStrategy text indent strategy
- * @param linkOptions optional link metadata
- * @param padding inner padding
- * @param margin outer margin
- */
- public TemplateParagraphSpec(String name,
- String text,
- TextStyle style,
- TextAlign align,
- double lineSpacing,
- String bulletOffset,
- TextIndentStrategy indentStrategy,
- DocumentLinkOptions linkOptions,
- Padding padding,
- Margin margin) {
- this(name, text, List.of(), style, align, lineSpacing, bulletOffset, indentStrategy, linkOptions, padding, margin);
- }
-}
diff --git a/src/main/java/com/demcha/compose/document/templates/support/common/TemplateRowSpec.java b/src/main/java/com/demcha/compose/document/templates/support/common/TemplateRowSpec.java
deleted file mode 100644
index 206f12437..000000000
--- a/src/main/java/com/demcha/compose/document/templates/support/common/TemplateRowSpec.java
+++ /dev/null
@@ -1,60 +0,0 @@
-package com.demcha.compose.document.templates.support.common;
-
-import com.demcha.compose.engine.components.style.Margin;
-import com.demcha.compose.engine.components.style.Padding;
-
-import java.util.List;
-
-/**
- * Horizontal row instruction for canonical template scene composers.
- *
- * @param name semantic row name
- * @param columns vertical sections rendered left-to-right
- * @param weights optional per-column width weights
- * @param gap horizontal gap between columns
- * @param padding inner row padding
- * @param margin outer row margin
- */
-public record TemplateRowSpec(
- String name,
- List columns,
- List weights,
- double gap,
- Padding padding,
- Margin margin
-) {
- /**
- * Creates a normalized row specification.
- */
- public TemplateRowSpec {
- name = name == null ? "" : name;
- columns = columns == null ? List.of() : List.copyOf(columns);
- weights = weights == null ? List.of() : List.copyOf(weights);
- if (!weights.isEmpty() && weights.size() != columns.size()) {
- throw new IllegalArgumentException("weights size must match columns size.");
- }
- for (Double weight : weights) {
- if (weight == null || weight <= 0 || Double.isNaN(weight) || Double.isInfinite(weight)) {
- throw new IllegalArgumentException("weights must be positive finite numbers: " + weights);
- }
- }
- if (gap < 0 || Double.isNaN(gap) || Double.isInfinite(gap)) {
- throw new IllegalArgumentException("gap must be finite and non-negative: " + gap);
- }
- padding = padding == null ? Padding.zero() : padding;
- margin = margin == null ? Margin.zero() : margin;
- }
-
- /**
- * Creates a weighted row with zero padding and margin.
- *
- * @param name semantic row name
- * @param columns row columns
- * @param weights per-column weights
- * @param gap horizontal gap
- * @return row specification
- */
- public static TemplateRowSpec weighted(String name, List columns, List weights, double gap) {
- return new TemplateRowSpec(name, columns, weights, gap, Padding.zero(), Margin.zero());
- }
-}
diff --git a/src/main/java/com/demcha/compose/document/templates/support/common/TemplateSceneSupport.java b/src/main/java/com/demcha/compose/document/templates/support/common/TemplateSceneSupport.java
deleted file mode 100644
index e9af0800c..000000000
--- a/src/main/java/com/demcha/compose/document/templates/support/common/TemplateSceneSupport.java
+++ /dev/null
@@ -1,333 +0,0 @@
-package com.demcha.compose.document.templates.support.common;
-
-import com.demcha.compose.document.node.DocumentLinkOptions;
-import com.demcha.compose.document.node.InlineTextRun;
-import com.demcha.compose.document.node.ListMarker;
-import com.demcha.compose.document.node.TextAlign;
-import com.demcha.compose.engine.components.content.text.TextIndentStrategy;
-import com.demcha.compose.engine.components.content.text.TextStyle;
-import com.demcha.compose.engine.components.style.Margin;
-import com.demcha.compose.engine.components.style.Padding;
-
-import java.awt.*;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Objects;
-
-/**
- * Shared helper methods used by canonical scene composers.
- *
- * @author Artem Demchyshyn
- */
-public final class TemplateSceneSupport {
- private TemplateSceneSupport() {
- }
-
- /**
- * Creates a normalized paragraph instruction.
- *
- * @param name semantic paragraph name
- * @param text paragraph text
- * @param style text style
- * @param align horizontal alignment
- * @param lineSpacing extra line spacing
- * @param padding inner padding
- * @param margin outer margin
- * @return paragraph instruction
- */
- public static TemplateParagraphSpec paragraph(String name,
- String text,
- TextStyle style,
- TextAlign align,
- double lineSpacing,
- Padding padding,
- Margin margin) {
- return paragraph(name, text, style, align, lineSpacing, null, padding, margin);
- }
-
- /**
- * Creates a normalized paragraph instruction with optional hyperlink metadata.
- *
- * @param name semantic paragraph name
- * @param text paragraph text
- * @param style text style
- * @param align horizontal alignment
- * @param lineSpacing extra line spacing
- * @param linkOptions optional link metadata
- * @param padding inner padding
- * @param margin outer margin
- * @return paragraph instruction
- */
- public static TemplateParagraphSpec paragraph(String name,
- String text,
- TextStyle style,
- TextAlign align,
- double lineSpacing,
- DocumentLinkOptions linkOptions,
- Padding padding,
- Margin margin) {
- return new TemplateParagraphSpec(name, text, List.of(), style, align, lineSpacing, "", TextIndentStrategy.NONE, linkOptions, padding, margin);
- }
-
- /**
- * Creates a normalized paragraph instruction from inline text runs.
- *
- * @param name semantic paragraph name
- * @param inlineRuns inline text runs
- * @param style fallback text style
- * @param align horizontal alignment
- * @param lineSpacing extra line spacing
- * @param padding inner padding
- * @param margin outer margin
- * @return paragraph instruction
- */
- public static TemplateParagraphSpec inlineParagraph(String name,
- List inlineRuns,
- TextStyle style,
- TextAlign align,
- double lineSpacing,
- Padding padding,
- Margin margin) {
- return new TemplateParagraphSpec(name, "", inlineRuns, style, align, lineSpacing, "", TextIndentStrategy.NONE, null, padding, margin);
- }
-
- /**
- * Creates a normalized paragraph instruction with legacy block-indent semantics.
- *
- * @param name semantic paragraph name
- * @param text paragraph text
- * @param style text style
- * @param align horizontal alignment
- * @param lineSpacing extra line spacing
- * @param bulletOffset first-line marker or prefix
- * @param indentStrategy text indent strategy
- * @param padding inner padding
- * @param margin outer margin
- * @return paragraph instruction
- */
- public static TemplateParagraphSpec blockParagraph(String name,
- String text,
- TextStyle style,
- TextAlign align,
- double lineSpacing,
- String bulletOffset,
- TextIndentStrategy indentStrategy,
- Padding padding,
- Margin margin) {
- return blockParagraph(name, text, style, align, lineSpacing, bulletOffset, indentStrategy, null, padding, margin);
- }
-
- /**
- * Creates a normalized paragraph instruction with legacy block-indent semantics
- * and optional hyperlink metadata.
- *
- * @param name semantic paragraph name
- * @param text paragraph text
- * @param style text style
- * @param align horizontal alignment
- * @param lineSpacing extra line spacing
- * @param bulletOffset first-line marker or prefix
- * @param indentStrategy text indent strategy
- * @param linkOptions optional link metadata
- * @param padding inner padding
- * @param margin outer margin
- * @return paragraph instruction
- */
- public static TemplateParagraphSpec blockParagraph(String name,
- String text,
- TextStyle style,
- TextAlign align,
- double lineSpacing,
- String bulletOffset,
- TextIndentStrategy indentStrategy,
- DocumentLinkOptions linkOptions,
- Padding padding,
- Margin margin) {
- return new TemplateParagraphSpec(name, text, List.of(), style, align, lineSpacing, bulletOffset, indentStrategy, linkOptions, padding, margin);
- }
-
- /**
- * Creates a normalized list instruction.
- *
- * @param name semantic list name
- * @param items list items
- * @param marker list marker
- * @param style item text style
- * @param align horizontal alignment
- * @param lineSpacing extra wrapped-line spacing
- * @param itemSpacing extra space between items
- * @param padding inner padding
- * @param margin outer margin
- * @return list instruction
- */
- public static TemplateListSpec list(String name,
- List items,
- ListMarker marker,
- TextStyle style,
- TextAlign align,
- double lineSpacing,
- double itemSpacing,
- Padding padding,
- Margin margin) {
- return list(name, items, marker, style, align, lineSpacing, itemSpacing, "", padding, margin);
- }
-
- /**
- * Creates a normalized list instruction with markerless continuation indentation.
- *
- * @param name semantic list name
- * @param items list items
- * @param marker list marker
- * @param style item text style
- * @param align horizontal alignment
- * @param lineSpacing extra wrapped-line spacing
- * @param itemSpacing extra space between items
- * @param continuationIndent markerless continuation indent text
- * @param padding inner padding
- * @param margin outer margin
- * @return list instruction
- */
- public static TemplateListSpec list(String name,
- List items,
- ListMarker marker,
- TextStyle style,
- TextAlign align,
- double lineSpacing,
- double itemSpacing,
- String continuationIndent,
- Padding padding,
- Margin margin) {
- return new TemplateListSpec(
- name,
- items,
- marker,
- style,
- align,
- lineSpacing,
- itemSpacing,
- continuationIndent,
- true,
- padding,
- margin);
- }
-
- /**
- * Creates a normalized divider instruction.
- *
- * @param name semantic divider name
- * @param width divider width
- * @param thickness divider thickness
- * @param color divider color
- * @param margin outer margin
- * @return divider instruction
- */
- public static TemplateDividerSpec divider(String name,
- double width,
- double thickness,
- Color color,
- Margin margin) {
- return new TemplateDividerSpec(name, width, thickness, color, margin);
- }
-
- /**
- * Joins non-blank values with the supplied delimiter.
- *
- * @param delimiter delimiter between values
- * @param values values to join
- * @return joined text
- */
- public static String joinNonBlank(String delimiter, String... values) {
- List nonBlank = new ArrayList<>();
- for (String value : values) {
- if (value != null && !value.isBlank()) {
- nonBlank.add(value.trim());
- }
- }
- return String.join(delimiter, nonBlank);
- }
-
- /**
- * Converts a list of bullet items into a multi-line text block.
- *
- * @param items bullet item text
- * @return multi-line bullet text
- */
- public static String bulletText(List items) {
- return sanitizeLines(items).stream()
- .map(item -> "• " + item)
- .reduce((left, right) -> left + "\n" + right)
- .orElse("");
- }
-
- /**
- * Removes blanks and trims each line.
- *
- * @param values raw values
- * @return sanitized non-blank values
- */
- public static List sanitizeLines(List values) {
- if (values == null || values.isEmpty()) {
- return List.of();
- }
- List sanitized = new ArrayList<>();
- for (String value : values) {
- String normalized = Objects.requireNonNullElse(value, "").trim();
- if (!normalized.isBlank()) {
- sanitized.add(normalized);
- }
- }
- return List.copyOf(sanitized);
- }
-
- /**
- * Removes the small markdown subset used across built-in template fixtures.
- *
- * @param value raw text
- * @return text without the small supported markdown subset
- */
- public static String stripBasicMarkdown(String value) {
- return Objects.requireNonNullElse(value, "")
- .replace("**", "")
- .replace("__", "")
- .replace("##", "")
- .replace("`", "")
- .replace("_", "")
- .replace("*", "");
- }
-
- /**
- * Appends a simple section header composed of a heading paragraph and a rule.
- *
- * @param target active template compose target
- * @param prefix semantic node name prefix
- * @param title section title
- * @param titleStyle title text style
- * @param ruleWidth divider width
- * @param ruleColor divider color
- * @param ruleThickness divider thickness
- * @param margin header margin
- */
- public static void addSectionHeader(TemplateComposeTarget target,
- String prefix,
- String title,
- TextStyle titleStyle,
- double ruleWidth,
- Color ruleColor,
- double ruleThickness,
- Margin margin) {
- target.addParagraph(paragraph(
- prefix + "Heading",
- Objects.requireNonNullElse(title, ""),
- titleStyle,
- TextAlign.LEFT,
- 1.0,
- Padding.zero(),
- margin));
- target.addDivider(divider(
- prefix + "Rule",
- ruleWidth,
- ruleThickness,
- ruleColor,
- Margin.zero()));
- }
-}
diff --git a/src/main/java/com/demcha/compose/document/templates/support/common/TemplateTableSpec.java b/src/main/java/com/demcha/compose/document/templates/support/common/TemplateTableSpec.java
deleted file mode 100644
index 4082c1b84..000000000
--- a/src/main/java/com/demcha/compose/document/templates/support/common/TemplateTableSpec.java
+++ /dev/null
@@ -1,49 +0,0 @@
-package com.demcha.compose.document.templates.support.common;
-
-import com.demcha.compose.engine.components.content.table.TableCellContent;
-import com.demcha.compose.engine.components.content.table.TableCellLayoutStyle;
-import com.demcha.compose.engine.components.content.table.TableColumnLayout;
-import com.demcha.compose.engine.components.style.Margin;
-import com.demcha.compose.engine.components.style.Padding;
-
-import java.util.List;
-import java.util.Map;
-
-/**
- * Immutable table instruction used by shared template scene composers.
- *
- * @param name semantic table name used in snapshots
- * @param columns negotiated table columns
- * @param rows table rows in source order
- * @param defaultCellStyle default style applied to cells without overrides
- * @param rowStyles row-specific style overrides
- * @param columnStyles column-specific style overrides
- * @param width resolved table width
- * @param padding outer table padding
- * @param margin outer table margin
- */
-public record TemplateTableSpec(
- String name,
- List columns,
- List> rows,
- TableCellLayoutStyle defaultCellStyle,
- Map rowStyles,
- Map columnStyles,
- double width,
- Padding padding,
- Margin margin
-) {
- /**
- * Normalizes table defaults and freezes table row/style collections.
- */
- public TemplateTableSpec {
- name = name == null ? "" : name;
- columns = List.copyOf(columns);
- rows = List.copyOf(rows);
- defaultCellStyle = defaultCellStyle == null ? TableCellLayoutStyle.DEFAULT : defaultCellStyle;
- rowStyles = rowStyles == null ? Map.of() : Map.copyOf(rowStyles);
- columnStyles = columnStyles == null ? Map.of() : Map.copyOf(columnStyles);
- padding = padding == null ? Padding.zero() : padding;
- margin = margin == null ? Margin.zero() : margin;
- }
-}
diff --git a/src/main/java/com/demcha/compose/document/templates/support/common/package-info.java b/src/main/java/com/demcha/compose/document/templates/support/common/package-info.java
deleted file mode 100644
index 2bfe1425c..000000000
--- a/src/main/java/com/demcha/compose/document/templates/support/common/package-info.java
+++ /dev/null
@@ -1,4 +0,0 @@
-/**
- * Shared template composition support used by canonical built-in templates.
- */
-package com.demcha.compose.document.templates.support.common;
diff --git a/src/main/java/com/demcha/compose/document/templates/support/package-info.java b/src/main/java/com/demcha/compose/document/templates/support/package-info.java
deleted file mode 100644
index 367906bcf..000000000
--- a/src/main/java/com/demcha/compose/document/templates/support/package-info.java
+++ /dev/null
@@ -1,12 +0,0 @@
-/**
- * Domain-grouped helpers, scene composers, mappers, and composition adapters
- * used by the canonical document template layer.
- *
- *
This package sits between the public template contracts in
- * {@link com.demcha.compose.document.templates.api} and the low-level semantic
- * authoring/runtime layers in {@code com.demcha.compose.document.*}. Most
- * applications should consume the public template interfaces instead of these
- * support types directly. Common composition primitives live in
- * {@code support.common}.
- */
-package com.demcha.compose.document.templates.support;
diff --git a/src/main/java/com/demcha/compose/document/templates/themes/Spacing.java b/src/main/java/com/demcha/compose/document/templates/themes/Spacing.java
deleted file mode 100644
index a2c8392da..000000000
--- a/src/main/java/com/demcha/compose/document/templates/themes/Spacing.java
+++ /dev/null
@@ -1,294 +0,0 @@
-package com.demcha.compose.document.templates.themes;
-
-import com.demcha.compose.engine.components.style.Margin;
-
-import java.util.Objects;
-
-/**
- * One source of truth for all template spacing tokens.
- *
- *
Replaces the historical per-theme {@code spacing} / {@code spacingModuleName}
- * fields plus the hard-coded {@code MINIMUM_TOP_LEVEL_MODULE_SPACING}
- * constant scattered throughout the legacy composers. Templates v2 presets
- * read every spacing decision from a {@code Spacing} instance and pass it
- * through the builder.
- *
- *
Three preset rhythms cover most cases; use {@link #builder()} for
- * fully custom spacing.
- *
- * @param moduleGap vertical gap between top-level modules (sections)
- * @param lineSpacing extra spacing between wrapped lines inside a body block
- * @param paragraphSpacing vertical gap between paragraphs in a multi-paragraph module
- * @param sectionTitleAbove margin above a section heading (separates it from previous module)
- * @param sectionTitleBelow margin below a section heading (separates it from its body)
- * @param headerLineSpacing vertical gap between contact rows in the document header
- * @param listItemSpacing vertical gap between list items (bullet, numbered, indented)
- * @param bodyIndent left padding applied to module body content so the
- * heading visually sits one level above the body
- * @param contentPadding overall padding around the document content area
- */
-public record Spacing(
- double moduleGap,
- double lineSpacing,
- double paragraphSpacing,
- double sectionTitleAbove,
- double sectionTitleBelow,
- double headerLineSpacing,
- double listItemSpacing,
- double bodyIndent,
- Margin contentPadding) {
-
- /**
- * Compact constructor that validates non-negativity and finiteness of every
- * numeric token, and rejects {@code null} {@code contentPadding}.
- *
- * @throws IllegalArgumentException if any numeric token is negative,
- * {@code NaN}, or infinite
- * @throws NullPointerException if {@code contentPadding} is {@code null}
- */
- public Spacing {
- Objects.requireNonNull(contentPadding, "contentPadding");
- validate(moduleGap, "moduleGap");
- validate(lineSpacing, "lineSpacing");
- validate(paragraphSpacing, "paragraphSpacing");
- validate(sectionTitleAbove, "sectionTitleAbove");
- validate(sectionTitleBelow, "sectionTitleBelow");
- validate(headerLineSpacing, "headerLineSpacing");
- validate(listItemSpacing, "listItemSpacing");
- validate(bodyIndent, "bodyIndent");
- }
-
- /**
- * Returns a tight spacing rhythm suitable for single-page CV templates
- * where vertical real estate is at a premium.
- *
- * @return compact spacing tokens
- */
- public static Spacing compact() {
- return new Spacing(
- /* moduleGap */ 7.0,
- /* lineSpacing */ 2.0,
- /* paragraphSpacing */ 4.0,
- /* sectionTitleAbove */ 4.0,
- /* sectionTitleBelow */ 2.0,
- /* headerLineSpacing */ 3.0,
- /* listItemSpacing */ 2.0,
- /* bodyIndent */ 12.0,
- /* contentPadding */ Margin.of(28));
- }
-
- /**
- * Returns a balanced spacing rhythm — the default for most templates
- * where readability outweighs single-page density.
- *
- * @return comfortable spacing tokens
- */
- public static Spacing comfortable() {
- return new Spacing(
- /* moduleGap */ 12.0,
- /* lineSpacing */ 3.0,
- /* paragraphSpacing */ 6.0,
- /* sectionTitleAbove */ 8.0,
- /* sectionTitleBelow */ 4.0,
- /* headerLineSpacing */ 4.0,
- /* listItemSpacing */ 3.0,
- /* bodyIndent */ 16.0,
- /* contentPadding */ Margin.of(36));
- }
-
- /**
- * Returns a generous spacing rhythm suitable for proposals, reports,
- * and other documents where breathing room signals quality.
- *
- * @return airy spacing tokens
- */
- public static Spacing airy() {
- return new Spacing(
- /* moduleGap */ 18.0,
- /* lineSpacing */ 4.0,
- /* paragraphSpacing */ 10.0,
- /* sectionTitleAbove */ 12.0,
- /* sectionTitleBelow */ 6.0,
- /* headerLineSpacing */ 5.0,
- /* listItemSpacing */ 4.0,
- /* bodyIndent */ 20.0,
- /* contentPadding */ Margin.of(48));
- }
-
- /**
- * Returns a fresh builder seeded with {@link #comfortable()} defaults.
- *
- * @return new spacing builder
- */
- public static Builder builder() {
- return new Builder(comfortable());
- }
-
- /**
- * Returns a builder seeded with this instance's tokens, suitable for
- * creating slight variants of an existing rhythm.
- *
- * @return new spacing builder pre-populated from this instance
- */
- public Builder toBuilder() {
- return new Builder(this);
- }
-
- private static void validate(double value, String name) {
- if (Double.isNaN(value) || Double.isInfinite(value)) {
- throw new IllegalArgumentException(name + " must be finite: " + value);
- }
- if (value < 0) {
- throw new IllegalArgumentException(name + " must be non-negative: " + value);
- }
- }
-
- /**
- * Mutable builder for {@link Spacing}. Every setter returns {@code this}
- * for chaining; call {@link #build()} to produce an immutable record.
- */
- public static final class Builder {
- private double moduleGap;
- private double lineSpacing;
- private double paragraphSpacing;
- private double sectionTitleAbove;
- private double sectionTitleBelow;
- private double headerLineSpacing;
- private double listItemSpacing;
- private double bodyIndent;
- private Margin contentPadding;
-
- private Builder(Spacing seed) {
- this.moduleGap = seed.moduleGap;
- this.lineSpacing = seed.lineSpacing;
- this.paragraphSpacing = seed.paragraphSpacing;
- this.sectionTitleAbove = seed.sectionTitleAbove;
- this.sectionTitleBelow = seed.sectionTitleBelow;
- this.headerLineSpacing = seed.headerLineSpacing;
- this.listItemSpacing = seed.listItemSpacing;
- this.bodyIndent = seed.bodyIndent;
- this.contentPadding = seed.contentPadding;
- }
-
- /**
- * Sets the gap between top-level modules.
- *
- * @param value non-negative finite gap in points
- * @return this builder
- */
- public Builder moduleGap(double value) {
- this.moduleGap = value;
- return this;
- }
-
- /**
- * Sets the extra spacing between wrapped lines inside a body block.
- *
- * @param value non-negative finite spacing in points
- * @return this builder
- */
- public Builder lineSpacing(double value) {
- this.lineSpacing = value;
- return this;
- }
-
- /**
- * Sets the gap between paragraphs in a multi-paragraph module.
- *
- * @param value non-negative finite gap in points
- * @return this builder
- */
- public Builder paragraphSpacing(double value) {
- this.paragraphSpacing = value;
- return this;
- }
-
- /**
- * Sets the margin above a section heading.
- *
- * @param value non-negative finite margin in points
- * @return this builder
- */
- public Builder sectionTitleAbove(double value) {
- this.sectionTitleAbove = value;
- return this;
- }
-
- /**
- * Sets the margin below a section heading.
- *
- * @param value non-negative finite margin in points
- * @return this builder
- */
- public Builder sectionTitleBelow(double value) {
- this.sectionTitleBelow = value;
- return this;
- }
-
- /**
- * Sets the gap between contact rows in the document header.
- *
- * @param value non-negative finite gap in points
- * @return this builder
- */
- public Builder headerLineSpacing(double value) {
- this.headerLineSpacing = value;
- return this;
- }
-
- /**
- * Sets the gap between list items.
- *
- * @param value non-negative finite gap in points
- * @return this builder
- */
- public Builder listItemSpacing(double value) {
- this.listItemSpacing = value;
- return this;
- }
-
- /**
- * Sets the left padding applied to module body content so the
- * heading visually sits one level above the body.
- *
- * @param value non-negative finite indent in points
- * @return this builder
- */
- public Builder bodyIndent(double value) {
- this.bodyIndent = value;
- return this;
- }
-
- /**
- * Sets the overall content padding.
- *
- * @param value non-null margin
- * @return this builder
- */
- public Builder contentPadding(Margin value) {
- this.contentPadding = Objects.requireNonNull(value, "contentPadding");
- return this;
- }
-
- /**
- * Builds an immutable {@link Spacing} record. All numeric tokens
- * are validated for non-negativity and finiteness here.
- *
- * @return new spacing record
- * @throws IllegalArgumentException if any numeric token is invalid
- * @throws NullPointerException if content padding is {@code null}
- */
- public Spacing build() {
- return new Spacing(
- moduleGap,
- lineSpacing,
- paragraphSpacing,
- sectionTitleAbove,
- sectionTitleBelow,
- headerLineSpacing,
- listItemSpacing,
- bodyIndent,
- contentPadding);
- }
- }
-}
diff --git a/src/main/java/com/demcha/compose/document/templates/themes/Typography.java b/src/main/java/com/demcha/compose/document/templates/themes/Typography.java
deleted file mode 100644
index c94eb2fd2..000000000
--- a/src/main/java/com/demcha/compose/document/templates/themes/Typography.java
+++ /dev/null
@@ -1,321 +0,0 @@
-package com.demcha.compose.document.templates.themes;
-
-import com.demcha.compose.font.FontName;
-
-import java.util.Objects;
-
-/**
- * Typography token set used by Templates v2 presets.
- *
- *
Pairs a header font and a body font with five logical type sizes
- * (display name, primary heading, secondary heading, body, small body,
- * caption). Presets read these tokens directly rather than hard-coding
- * font names or sizes, so a preset can swap typography by swapping a
- * {@code Typography} value without touching layout or styling code.
- *
- *
Five built-in presets cover the common sans-serif / serif / monospace
- * / display combinations; use {@link #builder()} for fully custom typography.
- *
- * @param headerFont font name applied to the document name and section headings
- * @param bodyFont font name applied to body text and list items
- * @param nameSize type size for the top-of-document name (largest)
- * @param headingSize type size for primary section headings
- * @param subHeadingSize type size for secondary headings (e.g. role titles)
- * @param bodySize type size for paragraph body text
- * @param smallBodySize type size for compact body text (contact lines, captions)
- * @param captionSize type size for the smallest auxiliary text
- */
-public record Typography(
- FontName headerFont,
- FontName bodyFont,
- double nameSize,
- double headingSize,
- double subHeadingSize,
- double bodySize,
- double smallBodySize,
- double captionSize) {
-
- /**
- * Compact constructor that validates non-null fonts and positive type sizes.
- *
- * @throws NullPointerException if {@code headerFont} or {@code bodyFont} is null
- * @throws IllegalArgumentException if any type size is non-positive,
- * {@code NaN}, or infinite
- */
- public Typography {
- Objects.requireNonNull(headerFont, "headerFont");
- Objects.requireNonNull(bodyFont, "bodyFont");
- validate(nameSize, "nameSize");
- validate(headingSize, "headingSize");
- validate(subHeadingSize, "subHeadingSize");
- validate(bodySize, "bodySize");
- validate(smallBodySize, "smallBodySize");
- validate(captionSize, "captionSize");
- }
-
- /**
- * Returns the default Helvetica-based typography matching the historical
- * default-theme sizing.
- *
- * @return Helvetica typography preset
- */
- public static Typography helvetica() {
- return new Typography(
- FontName.HELVETICA,
- FontName.HELVETICA,
- /* nameSize */ 28.0,
- /* headingSize */ 17.4,
- /* subHeadingSize */ 12.0,
- /* bodySize */ 10.0,
- /* smallBodySize */ 9.0,
- /* captionSize */ 8.0);
- }
-
- /**
- * Returns Times-Roman typography for templates that read formal /
- * editorial.
- *
- * @return Times-Roman typography preset
- */
- public static Typography timesRoman() {
- return new Typography(
- FontName.TIMES_ROMAN,
- FontName.TIMES_ROMAN,
- /* nameSize */ 28.0,
- /* headingSize */ 17.4,
- /* subHeadingSize */ 12.0,
- /* bodySize */ 10.0,
- /* smallBodySize */ 9.0,
- /* captionSize */ 8.0);
- }
-
- /**
- * Returns a serif-paired typography (Times-Roman header, Helvetica body)
- * for templates that want a formal heading on a clean body.
- *
- * @return mixed serif/sans typography preset
- */
- public static Typography serifMixed() {
- return new Typography(
- FontName.TIMES_ROMAN,
- FontName.HELVETICA,
- /* nameSize */ 28.0,
- /* headingSize */ 17.4,
- /* subHeadingSize */ 12.0,
- /* bodySize */ 10.0,
- /* smallBodySize */ 9.0,
- /* captionSize */ 8.0);
- }
-
- /**
- * Returns a Courier-based monospace typography for tech / engineering
- * resume templates that want a code-flavoured aesthetic.
- *
- * @return monospace typography preset
- */
- public static Typography monospace() {
- return new Typography(
- FontName.COURIER,
- FontName.COURIER,
- /* nameSize */ 26.0,
- /* headingSize */ 14.0,
- /* subHeadingSize */ 11.0,
- /* bodySize */ 9.5,
- /* smallBodySize */ 8.5,
- /* captionSize */ 8.0);
- }
-
- /**
- * Returns a display typography with larger heading sizes — suitable for
- * banner-style or hero-heading templates that want headings to dominate.
- *
- * @return display typography preset
- */
- public static Typography display() {
- return new Typography(
- FontName.HELVETICA,
- FontName.HELVETICA,
- /* nameSize */ 34.0,
- /* headingSize */ 20.0,
- /* subHeadingSize */ 13.0,
- /* bodySize */ 10.0,
- /* smallBodySize */ 9.0,
- /* captionSize */ 8.0);
- }
-
- /**
- * Returns a fresh builder seeded with {@link #helvetica()} defaults.
- *
- * @return new typography builder
- */
- public static Builder builder() {
- return new Builder(helvetica());
- }
-
- /**
- * Returns a builder seeded with this instance's tokens, suitable for
- * creating slight variants of an existing typography.
- *
- * @return new typography builder pre-populated from this instance
- */
- public Builder toBuilder() {
- return new Builder(this);
- }
-
- private static void validate(double value, String name) {
- if (Double.isNaN(value) || Double.isInfinite(value)) {
- throw new IllegalArgumentException(name + " must be finite: " + value);
- }
- if (value <= 0) {
- throw new IllegalArgumentException(name + " must be positive: " + value);
- }
- }
-
- /**
- * Mutable builder for {@link Typography}. Every setter returns {@code this}
- * for chaining; call {@link #build()} to produce an immutable record.
- */
- public static final class Builder {
- private FontName headerFont;
- private FontName bodyFont;
- private double nameSize;
- private double headingSize;
- private double subHeadingSize;
- private double bodySize;
- private double smallBodySize;
- private double captionSize;
-
- private Builder(Typography seed) {
- this.headerFont = seed.headerFont;
- this.bodyFont = seed.bodyFont;
- this.nameSize = seed.nameSize;
- this.headingSize = seed.headingSize;
- this.subHeadingSize = seed.subHeadingSize;
- this.bodySize = seed.bodySize;
- this.smallBodySize = seed.smallBodySize;
- this.captionSize = seed.captionSize;
- }
-
- /**
- * Sets the font used for the document name and section headings.
- *
- * @param font non-null font name
- * @return this builder
- */
- public Builder headerFont(FontName font) {
- this.headerFont = Objects.requireNonNull(font, "headerFont");
- return this;
- }
-
- /**
- * Sets the font used for body text and list items.
- *
- * @param font non-null font name
- * @return this builder
- */
- public Builder bodyFont(FontName font) {
- this.bodyFont = Objects.requireNonNull(font, "bodyFont");
- return this;
- }
-
- /**
- * Sets both header and body fonts to the same value, for
- * single-font typography.
- *
- * @param font non-null font name applied to both header and body
- * @return this builder
- */
- public Builder uniformFont(FontName font) {
- Objects.requireNonNull(font, "font");
- this.headerFont = font;
- this.bodyFont = font;
- return this;
- }
-
- /**
- * Sets the size for the top-of-document name.
- *
- * @param value positive finite size in points
- * @return this builder
- */
- public Builder nameSize(double value) {
- this.nameSize = value;
- return this;
- }
-
- /**
- * Sets the size for primary section headings.
- *
- * @param value positive finite size in points
- * @return this builder
- */
- public Builder headingSize(double value) {
- this.headingSize = value;
- return this;
- }
-
- /**
- * Sets the size for secondary headings.
- *
- * @param value positive finite size in points
- * @return this builder
- */
- public Builder subHeadingSize(double value) {
- this.subHeadingSize = value;
- return this;
- }
-
- /**
- * Sets the size for paragraph body text.
- *
- * @param value positive finite size in points
- * @return this builder
- */
- public Builder bodySize(double value) {
- this.bodySize = value;
- return this;
- }
-
- /**
- * Sets the size for compact body text such as contact lines.
- *
- * @param value positive finite size in points
- * @return this builder
- */
- public Builder smallBodySize(double value) {
- this.smallBodySize = value;
- return this;
- }
-
- /**
- * Sets the size for the smallest auxiliary text.
- *
- * @param value positive finite size in points
- * @return this builder
- */
- public Builder captionSize(double value) {
- this.captionSize = value;
- return this;
- }
-
- /**
- * Builds an immutable {@link Typography} record. Fonts are checked
- * for non-nullness and sizes for positivity here.
- *
- * @return new typography record
- * @throws IllegalArgumentException if any size is non-positive
- * @throws NullPointerException if a font is {@code null}
- */
- public Typography build() {
- return new Typography(
- headerFont,
- bodyFont,
- nameSize,
- headingSize,
- subHeadingSize,
- bodySize,
- smallBodySize,
- captionSize);
- }
- }
-}
diff --git a/src/main/java/com/demcha/compose/document/templates/themes/package-info.java b/src/main/java/com/demcha/compose/document/templates/themes/package-info.java
deleted file mode 100644
index 3a654598f..000000000
--- a/src/main/java/com/demcha/compose/document/templates/themes/package-info.java
+++ /dev/null
@@ -1,23 +0,0 @@
-/**
- * Templates v2 theme tokens — spacing, typography, and palette primitives.
- *
- *
This package holds the small value records that Templates v2 presets
- * use to externalise spacing, font, and colour decisions. The goal is one
- * source of truth for each token group, so that swapping a token swaps the
- * decision everywhere it is used: