feat(career): theme polish, rich-text bullets, entry ordering, outline API
Phase 4.5 backend enablers. - Themes (priority 3): AtsFriendly flag on single-column themes (surfaced in GET /api/cv/themes), print-quality page-break rules (entries never split across a page; headings stay with content; widow/orphan control), darkened the creative sidebar for AA contrast. - Rich text (priority 1): bullets/summary support **bold**, *italic*, __underline__, [text](url) via a safe inline pass — everything is HTML-escaped first, so no user tag can survive; only the whitelist emits markup. - Entry ordering (priority 1): CvSectionSetting.ItemOrder reorders entries within a section by ItemKey, never touching the master profile. - Outline API: GET /api/cv/outline returns the master profile as sections+entries with ItemKeys, so the Content tab can render editable per-item rows. - 3 new tests (22 total in the builder suite). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -124,6 +124,46 @@ public sealed class CvBuilderTests
|
||||
Assert.DoesNotContain("class=\"sidebar\"", minimal.Html);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Bullets_support_safe_inline_markdown_and_escape_everything_else()
|
||||
{
|
||||
var renderer = new ThemedCvRenderer();
|
||||
var profile = new StructuredCvProfile { Summary = { "**bold** and *italic* and __under__ and [site](https://x.io) <script>alert(1)</script>" } };
|
||||
var model = CvVariantResolver.Build(profile, new CvVariantSettings(), "F", null);
|
||||
var html = renderer.Render(model, CvThemeCatalog.Resolve("modern"), new CvVariantSettings { ThemeId = "modern" }).Html;
|
||||
Assert.Contains("<strong>bold</strong>", html);
|
||||
Assert.Contains("<em>italic</em>", html);
|
||||
Assert.Contains("<u>under</u>", html);
|
||||
Assert.Contains("<a href=\"https://x.io\">site</a>", html);
|
||||
Assert.Contains("<script>", html); // the real tag is escaped, inert
|
||||
Assert.DoesNotContain("<script>", html);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Item_order_reorders_entries_without_touching_the_master()
|
||||
{
|
||||
var profile = Rich(); // jobs: job1 (Senior Eng), job2 (Eng)
|
||||
var settings = new CvVariantSettings
|
||||
{
|
||||
Sections = { new CvSectionSetting { Key = "experience", ItemOrder = new() { "job2", "job1" } } },
|
||||
};
|
||||
var model = CvVariantResolver.Build(profile, settings, "F", null);
|
||||
var exp = model.Sections.First(s => s.Key == "experience");
|
||||
Assert.Equal(new[] { "Eng", "Senior Eng" }, exp.Entries.Select(e => e.Title));
|
||||
Assert.Equal(new[] { "Senior Eng", "Eng" }, profile.Jobs.Select(j => j.Title)); // master untouched
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Rendered_output_has_print_break_rules_and_themes_expose_ats_flag()
|
||||
{
|
||||
var renderer = new ThemedCvRenderer();
|
||||
var model = CvVariantResolver.Build(Rich(), new CvVariantSettings(), "F", null);
|
||||
var html = renderer.Render(model, CvThemeCatalog.Resolve("ats-classic"), new CvVariantSettings { ThemeId = "ats-classic" }).Html;
|
||||
Assert.Contains("break-inside:avoid", html);
|
||||
Assert.True(CvThemeCatalog.Resolve("ats-classic").AtsFriendly);
|
||||
Assert.False(CvThemeCatalog.Resolve("technical").AtsFriendly); // sidebar = not ATS-safe
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Accent_override_reaches_the_css()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user