Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ follow semantic versioning; release dates are ISO 8601.
the combined subtitle+date meta line of MintEditorial/SidebarPortrait experience
entries — the link's label text is shown instead. Titles without inline-Markdown
syntax render exactly as before, so the change is backward-compatible.
- **Project-card titles accept inline `[text](url)` links in the `EngineeringResume`
and `SidebarPortrait` presets.** Their hand-rolled project cards previously emitted
the title as flat styled text, so a `[label](url)` there stayed literal; it now runs
through the same link-aware path as project rows, entry titles, and body text, with
the trailing `" (stack)"` run preserved. Plain titles render exactly as before.
- The fixed-layout PPTX backend ships as `@Beta` (Experimental) in its first
release: the `document.backend.fixed.pptx` packages and the
`DocumentSession` PPTX convenience methods (`toPptxBytes`, `writePptx`,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
package com.demcha.compose.document.templates.cv.components;

import com.demcha.compose.GraphCompose;
import com.demcha.compose.document.api.DocumentPageSize;
import com.demcha.compose.document.api.DocumentSession;
import com.demcha.compose.document.node.DocumentNode;
import com.demcha.compose.document.node.ExternalLinkTarget;
import com.demcha.compose.document.node.InlineRun;
import com.demcha.compose.document.node.InlineTextRun;
import com.demcha.compose.document.node.ParagraphNode;
import com.demcha.compose.document.templates.api.DocumentTemplate;
import com.demcha.compose.document.templates.cv.data.CvDocument;
import com.demcha.compose.document.templates.cv.data.CvIdentity;
import com.demcha.compose.document.templates.cv.data.RowStyle;
import com.demcha.compose.document.templates.cv.data.RowsSection;
import com.demcha.compose.document.templates.cv.presets.EngineeringResume;
import com.demcha.compose.document.templates.cv.presets.SidebarPortrait;
import org.junit.jupiter.api.Test;

import java.util.ArrayList;
import java.util.List;

import static org.assertj.core.api.Assertions.assertThat;

/**
* End-to-end wiring for inline-link CV project-card titles: a project row whose
* label carries {@code [label](url)} markdown renders as a clickable link in the
* two presets that hand-roll their project card outside the shared
* {@code ProjectRenderer} — {@code EngineeringResume} (bordered card in the main
* column) and {@code SidebarPortrait} (stacked row in the main column). Plain
* labels stay plain and the trailing {@code " (stack)"} run is preserved, so the
* change is backward-compatible.
*
* <p>The {@link com.demcha.compose.document.templates.core.text.MarkdownInline}
* link primitive and {@link ProjectLabel} split are unit-tested separately; this
* locks the preset wiring so a refactor cannot silently revert a project title to
* a flat styled run (the state before this change), which would drop the link.</p>
*/
class ProjectTitleLinkTest {

private static CvDocument docWithProject(String label) {
return CvDocument.builder()
.identity(CvIdentity.builder()
.name("Test", "User").jobTitle("Engineer")
.contact("+1 555 0100", "user@example.com", "City").build())
.section(RowsSection.builder("Projects", RowStyle.BULLETED_STACKED)
.row(label, "Did meaningful work.")
.build())
.build();
}

/** Collects every inline text run in the composed document. */
private static List<InlineTextRun> textRuns(DocumentTemplate<CvDocument> template,
CvDocument doc) {
try (DocumentSession document = GraphCompose.document()
.pageSize(DocumentPageSize.A4).margin(28, 28, 28, 28).create()) {
template.compose(document, doc);
List<ParagraphNode> paragraphs = new ArrayList<>();
collectParagraphs(document.roots(), paragraphs);
List<InlineTextRun> runs = new ArrayList<>();
for (ParagraphNode paragraph : paragraphs) {
for (InlineRun run : paragraph.inlineRuns()) {
if (run instanceof InlineTextRun text) {
runs.add(text);
}
}
}
return runs;
} catch (Exception e) {
throw new RuntimeException(e);
}
}

private static void collectParagraphs(List<DocumentNode> nodes, List<ParagraphNode> out) {
for (DocumentNode node : nodes) {
if (node instanceof ParagraphNode paragraph) {
out.add(paragraph);
}
collectParagraphs(node.children(), out);
}
}

private static boolean isExternalLink(InlineTextRun run) {
return run.linkTarget() instanceof ExternalLinkTarget;
}

private static String uri(InlineTextRun run) {
return ((ExternalLinkTarget) run.linkTarget()).options().uri();
}

/**
* EngineeringResume's bordered project card: a link in the project title
* reaches the output, and the trailing {@code " (stack)"} run is preserved.
*/
@Test
void engineeringResumeProjectTitleBecomesLink() {
List<InlineTextRun> runs = textRuns(EngineeringResume.create(),
docWithProject("[Acme Corp](https://acme.example) (Java, PDFBox)"));
assertThat(runs).filteredOn(ProjectTitleLinkTest::isExternalLink)
.anySatisfy(run -> {
assertThat(run.text()).isEqualTo("Acme Corp");
assertThat(uri(run)).isEqualTo("https://acme.example");
});
assertThat(runs).anySatisfy(run ->
assertThat(run.text()).contains("Java, PDFBox"));
}

/**
* SidebarPortrait's stacked project row: a link in the project title reaches
* the output, and the trailing {@code " (stack)"} run is preserved.
*/
@Test
void sidebarPortraitProjectTitleBecomesLink() {
List<InlineTextRun> runs = textRuns(SidebarPortrait.create(),
docWithProject("[Acme Corp](https://acme.example) (Java, PDFBox)"));
assertThat(runs).filteredOn(ProjectTitleLinkTest::isExternalLink)
.anySatisfy(run -> {
assertThat(run.text()).isEqualTo("Acme Corp");
assertThat(uri(run)).isEqualTo("https://acme.example");
});
assertThat(runs).anySatisfy(run ->
assertThat(run.text()).contains("Java, PDFBox"));
}

/**
* Backward-compatible: a plain project title is not rendered as a link. The
* header contact block may render its own email/website links, so the control
* asserts specifically that the plain project title run is not linked.
*/
@Test
void plainProjectTitleIsNotLink() {
assertThat(projectTitleLinked(EngineeringResume.create())).isFalse();
assertThat(projectTitleLinked(SidebarPortrait.create())).isFalse();
}

private static boolean projectTitleLinked(DocumentTemplate<CvDocument> template) {
return textRuns(template, docWithProject("Acme Corp (Java, PDFBox)")).stream()
.filter(ProjectTitleLinkTest::isExternalLink)
.anyMatch(run -> run.text().equalsIgnoreCase("Acme Corp"));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -477,7 +477,8 @@ private void addProjects(SectionBuilder parent, CvSection section) {
.lineSpacing(1.06)
.margin(DocumentInsets.zero())
.rich(rich -> {
rich.style(label.title(),
MarkdownInline.append(rich,
label.title(),
projectTitleStyle());
if (!label.stack().isBlank()) {
rich.style(" (" + label.stack()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -754,7 +754,7 @@ private void addProjectsList(SectionBuilder section,
.lineSpacing(1.2)
.margin(DocumentInsets.top(topMargin))
.rich(rich -> {
rich.style(label.title(), titleStyle);
MarkdownInline.append(rich, label.title(), titleStyle);
if (!label.stack().isBlank()) {
rich.style(" (" + label.stack() + ")",
contextStyle);
Expand Down
Loading