feat(career): theme polish, rich-text bullets, entry ordering, outline API
CI and Deploy / test (push) Failing after 1m55s
CI and Deploy / deploy (push) Has been skipped

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:
cesnimda
2026-07-18 14:38:35 +02:00
parent 585047da9e
commit e3b255f226
7 changed files with 121 additions and 7 deletions
+40
View File
@@ -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("&lt;script&gt;", 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()
{