Skip to content

Commit ec7a87b

Browse files
authored
feat(cv-v2): markdown cleanup, widget extraction, drop preset style wrapper (#60)
* Route preset rich-text through MarkdownInline / RichParagraphRenderer instead of touching the engine MarkDownParser directly. Presets that used .rich(...) now share one adapter. * Extract shared body renderers (EntryCompactRenderer, ProjectRenderer, LabelValueRenderer, SkillLineRenderer, SkillTableRenderer) and shared widgets (FlowSectionHeader, Masthead, ProfileBand, SectionModule, CardWidget) so v2 presets stay thin orchestrators. * Drop the duplicated 'private static DocumentTextStyle style(...)' wrapper from BlueBanner, ClassicSerif, CompactMono, EditorialBlue, NordicClean. All call sites now use CvTextStyles.of(...) directly and the unused FontName imports are removed. * Inline single-use isEducation / isProjects helpers in EditorialBlue via SectionLookup, and replace inline DocumentTextStyle.builder() for the BlueBanner banner-title with CvTextStyles.of for consistency. All cv/v2 pixel baselines unchanged: CvV2VisualParityTest passes 9/9 without -Dgraphcompose.visual.approve, smoke tests 24/24, full suite 932/932.
1 parent 4eb6f7c commit ec7a87b

30 files changed

Lines changed: 1891 additions & 1122 deletions

docs/templates/v2-layered/authoring-presets.md

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,9 +60,11 @@ visual decision you can read like a recipe.
6060
<a id="the-widget-catalog"></a>
6161
## The widget catalog
6262

63-
Today, four widget classes live in
63+
The CV widget classes live in
6464
`com.demcha.compose.document.templates.cv.v2.widgets`. Each has a
65-
small set of named variants.
65+
small set of named variants. Generic widgets that can be reused by
66+
CVs, proposals, invoices, and cover letters live one package higher
67+
in `com.demcha.compose.document.templates.widgets`.
6668

6769
### `Headline` — top-of-document name
6870

@@ -102,6 +104,24 @@ small set of named variants.
102104
| `SectionHeader.flat(host, title, color, theme)` | Large bold title in a given colour, no panel |
103105
| `SectionHeader.flatSpacedCaps(host, title, color, theme, titleStyle)` | Small left spaced-caps title in a soft colour, no panel |
104106
| `SectionHeader.tickLabel(host, title, theme, color, tickWidth[, titleStyle])` | Short accent tick above compact uppercase label |
107+
| `SectionHeader.upperRule(host, title, theme, titleStyle, ruleColor, ruleWidth)` | Uppercase label with short rule below |
108+
| `SectionHeader.spacedCapsRule(host, title, theme, titleStyle, ruleColor, ruleWidth, ruleThickness, ruleMargin)` | Spaced-caps label with short rule below |
109+
110+
### Higher-order CV widgets
111+
112+
| Widget | Visual |
113+
|---|---|
114+
| `Masthead.centered(host, identity, theme, style)` | Centred editorial identity block: name, optional title, metadata, link row |
115+
| `FlowSectionHeader.banner(...)` / `FlowSectionHeader.label(...)` | Page-flow-level headers where the surrounding rules are outside the body section |
116+
| `ProfileBand.render(...)` | Tinted/ruled summary block with markdown-aware body text |
117+
| `SectionModule.tick(...)` / `SectionModule.upperRule(...)` | Named rail/card module that combines a section-header variant with caller-supplied body content |
118+
119+
### Shared document widgets
120+
121+
| Widget | Visual |
122+
|---|---|
123+
| `TableWidget.fixed(...)` / `TableWidget.grid(...)` | Configurable tables/grids with borders, fills, zebra rows, padding, typography, and column count |
124+
| `CardWidget.render(...)` | Reusable card/container shell with spacing, padding, fill, stroke, and corner radius |
105125

106126
The separator glyph used by `ContactLine`, the bullet glyph used by
107127
`RowRenderer`, and other character-level choices come from

src/main/java/com/demcha/compose/document/templates/cv/v2/AUTHORS.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -363,6 +363,14 @@ change ` | ` to ` · ` or anything else.
363363
| `SectionHeader.flat(host, title, color, theme)` | large bold title in a given colour, no panel | ModernProfessional |
364364
| `SectionHeader.flatSpacedCaps(host, title, color, theme, titleStyle)` | small spaced-caps title in a soft colour, no panel | CenteredHeadline, ClassicSerif |
365365
| `SectionHeader.tickLabel(host, title, theme, color, tickWidth[, titleStyle])` | short accent tick above compact uppercase label | CompactMono |
366+
| `SectionHeader.upperRule(host, title, theme, titleStyle, ruleColor, ruleWidth)` | uppercase label with short rule below | NordicClean |
367+
| `SectionHeader.spacedCapsRule(host, title, theme, titleStyle, ruleColor, ruleWidth, ruleThickness, ruleMargin)` | spaced-caps label with short rule below | ClassicSerif |
368+
369+
Use `FlowSectionHeader` when the rule/title treatment belongs to the
370+
page flow rather than inside an existing body section. `BlueBanner`
371+
uses its filled-banner variant; `EditorialBlue` uses its ruled-label
372+
variant. Use `SectionModule` when a rail/card module is simply
373+
`SectionHeader` plus caller-supplied content.
366374

367375
Note that `flat` and `flatSpacedCaps` take a `DocumentColor`
368376
argument — the section title colour is the preset's signature
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package com.demcha.compose.document.templates.cv.v2.components;
2+
3+
import com.demcha.compose.document.style.DocumentColor;
4+
import com.demcha.compose.document.style.DocumentTextDecoration;
5+
import com.demcha.compose.document.style.DocumentTextStyle;
6+
import com.demcha.compose.font.FontName;
7+
8+
/**
9+
* Small factory for preset-local text styles.
10+
*/
11+
public final class CvTextStyles {
12+
private CvTextStyles() {
13+
}
14+
15+
public static DocumentTextStyle of(FontName font,
16+
double size,
17+
DocumentTextDecoration decoration,
18+
DocumentColor color) {
19+
return DocumentTextStyle.builder()
20+
.fontName(font)
21+
.size(size)
22+
.decoration(decoration)
23+
.color(color)
24+
.build();
25+
}
26+
}
Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
package com.demcha.compose.document.templates.cv.v2.components;
2+
3+
import com.demcha.compose.document.dsl.SectionBuilder;
4+
import com.demcha.compose.document.node.TextAlign;
5+
import com.demcha.compose.document.style.DocumentInsets;
6+
import com.demcha.compose.document.style.DocumentTextStyle;
7+
import com.demcha.compose.document.templates.cv.v2.data.CvEntry;
8+
9+
import java.util.Locale;
10+
11+
/**
12+
* Compact entry renderer for editorial/card/rail presets where title,
13+
* subtitle, and date are packed tighter than the canonical
14+
* two-column {@link EntryRenderer}.
15+
*/
16+
public final class EntryCompactRenderer {
17+
private EntryCompactRenderer() {
18+
}
19+
20+
public static void twoColumnTitleDateBody(SectionBuilder host,
21+
CvEntry entry,
22+
String rowName,
23+
DocumentTextStyle titleStyle,
24+
DocumentTextStyle dateStyle,
25+
DocumentTextStyle subtitleStyle,
26+
DocumentTextStyle bodyStyle,
27+
double rowSpacing,
28+
double titleWeight,
29+
double dateWeight,
30+
DocumentInsets subtitleMargin,
31+
DocumentInsets bodyMargin,
32+
double bodyLineSpacing,
33+
boolean uppercaseTitle) {
34+
host.addRow(rowName, row -> row
35+
.spacing(rowSpacing)
36+
.weights(titleWeight, dateWeight)
37+
.addSection("Title", titleColumn -> titleColumn
38+
.padding(DocumentInsets.zero())
39+
.addParagraph(paragraph -> paragraph
40+
.text(formattedTitle(entry.title(),
41+
uppercaseTitle))
42+
.textStyle(titleStyle)
43+
.align(TextAlign.LEFT)
44+
.margin(DocumentInsets.zero())))
45+
.addSection("Date", dateColumn -> dateColumn
46+
.padding(DocumentInsets.zero())
47+
.addParagraph(paragraph -> paragraph
48+
.text(MarkdownInline.plainText(entry.date()))
49+
.textStyle(dateStyle)
50+
.align(TextAlign.RIGHT)
51+
.margin(DocumentInsets.zero()))));
52+
53+
if (!entry.subtitle().isBlank()) {
54+
host.addParagraph(paragraph -> paragraph
55+
.text(MarkdownInline.plainText(entry.subtitle()))
56+
.textStyle(subtitleStyle)
57+
.align(TextAlign.LEFT)
58+
.margin(subtitleMargin));
59+
}
60+
RichParagraphRenderer.render(host, entry.body(), bodyStyle,
61+
bodyLineSpacing, bodyMargin);
62+
}
63+
64+
public static void slashMeta(SectionBuilder host,
65+
CvEntry entry,
66+
DocumentTextStyle titleStyle,
67+
DocumentTextStyle metaStyle,
68+
double lineSpacing,
69+
DocumentInsets margin) {
70+
host.addParagraph(paragraph -> paragraph
71+
.textStyle(titleStyle)
72+
.lineSpacing(lineSpacing)
73+
.align(TextAlign.LEFT)
74+
.margin(margin)
75+
.rich(rich -> {
76+
rich.style(MarkdownInline.plainText(entry.title()),
77+
titleStyle);
78+
MarkdownInline.appendPlainIfPresent(rich, " / ",
79+
entry.subtitle(), metaStyle);
80+
MarkdownInline.appendPlainIfPresent(rich, " / ",
81+
entry.date(), metaStyle);
82+
}));
83+
}
84+
85+
public static void slashSubtitleDate(SectionBuilder host,
86+
CvEntry entry,
87+
DocumentTextStyle titleStyle,
88+
DocumentTextStyle subtitleStyle,
89+
DocumentTextStyle dateStyle,
90+
double lineSpacing,
91+
DocumentInsets margin) {
92+
host.addParagraph(paragraph -> paragraph
93+
.textStyle(titleStyle)
94+
.lineSpacing(lineSpacing)
95+
.align(TextAlign.LEFT)
96+
.margin(margin)
97+
.rich(rich -> {
98+
rich.style(MarkdownInline.plainText(entry.title()),
99+
titleStyle);
100+
MarkdownInline.appendPlainIfPresent(rich, " / ",
101+
entry.subtitle(), subtitleStyle);
102+
MarkdownInline.appendPlainIfPresent(rich, " / ",
103+
entry.date(), dateStyle);
104+
}));
105+
}
106+
107+
public static void titleDateBody(SectionBuilder host,
108+
CvEntry entry,
109+
DocumentTextStyle titleStyle,
110+
DocumentTextStyle dateStyle,
111+
DocumentTextStyle subtitleStyle,
112+
DocumentTextStyle bodyStyle,
113+
String datePrefix,
114+
double headerLineSpacing,
115+
DocumentInsets headerMargin,
116+
DocumentInsets subtitleMargin,
117+
DocumentInsets bodyMargin,
118+
double bodyLineSpacing,
119+
boolean uppercaseTitle) {
120+
host.addParagraph(paragraph -> paragraph
121+
.textStyle(titleStyle)
122+
.lineSpacing(headerLineSpacing)
123+
.align(TextAlign.LEFT)
124+
.margin(headerMargin)
125+
.rich(rich -> {
126+
rich.style(formattedTitle(entry.title(), uppercaseTitle),
127+
titleStyle);
128+
if (!entry.date().isBlank()) {
129+
rich.style(datePrefix, titleStyle);
130+
rich.style(MarkdownInline.plainText(entry.date()),
131+
dateStyle);
132+
}
133+
}));
134+
if (!entry.subtitle().isBlank()) {
135+
host.addParagraph(paragraph -> paragraph
136+
.text(MarkdownInline.plainText(entry.subtitle()))
137+
.textStyle(subtitleStyle)
138+
.align(TextAlign.LEFT)
139+
.margin(subtitleMargin));
140+
}
141+
RichParagraphRenderer.render(host, entry.body(), bodyStyle,
142+
bodyLineSpacing, bodyMargin);
143+
}
144+
145+
public static void titleSubtitleDateBody(SectionBuilder host,
146+
CvEntry entry,
147+
DocumentTextStyle titleStyle,
148+
DocumentTextStyle subtitleStyle,
149+
DocumentTextStyle dateStyle,
150+
DocumentTextStyle bodyStyle,
151+
String subtitlePrefix,
152+
String datePrefix,
153+
double headerLineSpacing,
154+
DocumentInsets headerMargin,
155+
DocumentInsets bodyMargin,
156+
double bodyLineSpacing) {
157+
host.addParagraph(paragraph -> paragraph
158+
.textStyle(titleStyle)
159+
.lineSpacing(headerLineSpacing)
160+
.align(TextAlign.LEFT)
161+
.margin(headerMargin)
162+
.rich(rich -> {
163+
rich.style(MarkdownInline.plainText(entry.title()),
164+
titleStyle);
165+
MarkdownInline.appendPlainIfPresent(rich, subtitlePrefix,
166+
entry.subtitle(), subtitleStyle);
167+
MarkdownInline.appendPlainIfPresent(rich, datePrefix,
168+
entry.date(), dateStyle);
169+
}));
170+
RichParagraphRenderer.render(host, entry.body(), bodyStyle,
171+
bodyLineSpacing, bodyMargin);
172+
}
173+
174+
private static String formattedTitle(String title, boolean uppercase) {
175+
String clean = MarkdownInline.plainText(title);
176+
return uppercase ? clean.toUpperCase(Locale.ROOT) : clean;
177+
}
178+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package com.demcha.compose.document.templates.cv.v2.components;
2+
3+
import com.demcha.compose.document.dsl.SectionBuilder;
4+
import com.demcha.compose.document.node.TextAlign;
5+
import com.demcha.compose.document.style.DocumentInsets;
6+
import com.demcha.compose.document.style.DocumentTextStyle;
7+
8+
/**
9+
* Renders compact "Label: rich markdown value" rows used by CV detail sections.
10+
*/
11+
public final class LabelValueRenderer {
12+
private LabelValueRenderer() {
13+
}
14+
15+
public static void render(SectionBuilder host,
16+
String label,
17+
String value,
18+
DocumentTextStyle labelStyle,
19+
DocumentTextStyle valueStyle,
20+
double lineSpacing,
21+
DocumentInsets margin) {
22+
host.addParagraph(paragraph -> paragraph
23+
.textStyle(valueStyle)
24+
.lineSpacing(lineSpacing)
25+
.align(TextAlign.LEFT)
26+
.margin(margin)
27+
.rich(rich -> {
28+
rich.style(normalizedLabel(label) + ":", labelStyle);
29+
if (value != null && !value.isBlank()) {
30+
rich.style(" ", valueStyle);
31+
MarkdownInline.append(rich, value, valueStyle);
32+
}
33+
}));
34+
}
35+
36+
static String normalizedLabel(String label) {
37+
String value = MarkdownInline.plainText(label).trim();
38+
while (value.endsWith(":")) {
39+
value = value.substring(0, value.length() - 1).trim();
40+
}
41+
return value;
42+
}
43+
}

src/main/java/com/demcha/compose/document/templates/cv/v2/components/MarkdownInline.java

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,4 +42,30 @@ public static void append(RichText rich, String text,
4242
rich.style(textRun.text(), runStyle);
4343
}
4444
}
45+
46+
public static void appendTrimmed(RichText rich, String text,
47+
DocumentTextStyle baseStyle) {
48+
append(rich, text == null ? "" : text.trim(), baseStyle);
49+
}
50+
51+
public static void appendPlainIfPresent(RichText rich, String prefix,
52+
String value,
53+
DocumentTextStyle style) {
54+
String clean = plainText(value);
55+
if (!clean.isBlank()) {
56+
rich.style(prefix + clean, style);
57+
}
58+
}
59+
60+
public static String plainText(String value) {
61+
if (value == null) {
62+
return "";
63+
}
64+
return value
65+
.replace("**", "")
66+
.replace("__", "")
67+
.replace("`", "")
68+
.replace("*", "")
69+
.replace("_", "");
70+
}
4571
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package com.demcha.compose.document.templates.cv.v2.components;
2+
3+
/**
4+
* Splits legacy project labels like "GraphCompose (Java, PDFBox)" into display title and stack.
5+
*/
6+
public record ProjectLabel(String title, String stack) {
7+
public ProjectLabel {
8+
title = title == null ? "" : title;
9+
stack = stack == null ? "" : stack;
10+
}
11+
12+
public static ProjectLabel parse(String value) {
13+
String clean = MarkdownInline.plainText(value).trim();
14+
int stackOpen = clean.lastIndexOf('(');
15+
if (stackOpen > 0 && clean.endsWith(")")) {
16+
return new ProjectLabel(
17+
clean.substring(0, stackOpen).trim(),
18+
clean.substring(stackOpen + 1, clean.length() - 1).trim()
19+
);
20+
}
21+
return new ProjectLabel(clean, "");
22+
}
23+
}

0 commit comments

Comments
 (0)