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:
@@ -35,6 +35,7 @@ public sealed class CvRenderSection
|
||||
|
||||
public sealed class CvRenderEntry
|
||||
{
|
||||
public string? Key { get; set; } // master ItemKey, for override + reorder targeting
|
||||
public string? Title { get; set; }
|
||||
public string? Subtitle { get; set; }
|
||||
public string? Meta { get; set; }
|
||||
@@ -106,6 +107,10 @@ public static class CvVariantResolver
|
||||
{
|
||||
if (cfg.Hidden) continue;
|
||||
if (!string.IsNullOrWhiteSpace(cfg.Title)) section.Title = cfg.Title!.Trim();
|
||||
if (cfg.ItemOrder is { Count: > 0 } && section.Entries.Count > 1)
|
||||
{
|
||||
section.Entries = ReorderByKey(section.Entries, cfg.ItemOrder);
|
||||
}
|
||||
}
|
||||
if (!section.IsEmpty) model.Sections.Add(section);
|
||||
}
|
||||
@@ -137,6 +142,7 @@ public static class CvVariantResolver
|
||||
if (ov?.Hidden == true) continue;
|
||||
section.Entries.Add(new CvRenderEntry
|
||||
{
|
||||
Key = job.Id,
|
||||
Title = Trim(ov?.Title) ?? Trim(job.Title),
|
||||
Subtitle = Trim(ov?.Subtitle) ?? JoinDot(job.Company, job.Location),
|
||||
Meta = DateRange(job.Start, job.End, job.IsCurrent),
|
||||
@@ -157,6 +163,7 @@ public static class CvVariantResolver
|
||||
var title = string.IsNullOrWhiteSpace(ed.QualificationLevel) ? Trim(ed.Qualification) : $"{ed.Qualification} ({ed.QualificationLevel})";
|
||||
section.Entries.Add(new CvRenderEntry
|
||||
{
|
||||
Key = ed.Id,
|
||||
Title = Trim(ov?.Title) ?? title,
|
||||
Subtitle = Trim(ov?.Subtitle) ?? JoinDot(ed.Institution, ed.Location),
|
||||
Meta = DateRange(ed.Start, ed.End, false),
|
||||
@@ -175,6 +182,7 @@ public static class CvVariantResolver
|
||||
if (ov?.Hidden == true) continue;
|
||||
section.Entries.Add(new CvRenderEntry
|
||||
{
|
||||
Key = pr.Id,
|
||||
Title = Trim(ov?.Title) ?? Trim(pr.Name),
|
||||
Subtitle = Trim(ov?.Subtitle) ?? JoinDot(pr.Role, pr.Location),
|
||||
Meta = DateRange(pr.Start, pr.End, false),
|
||||
@@ -194,6 +202,7 @@ public static class CvVariantResolver
|
||||
if (ov?.Hidden == true) continue;
|
||||
section.Entries.Add(new CvRenderEntry
|
||||
{
|
||||
Key = c.Id,
|
||||
Title = Trim(ov?.Title) ?? Trim(c.Name),
|
||||
Subtitle = Trim(ov?.Subtitle) ?? JoinDot(c.Issuer, c.Location),
|
||||
Meta = Trim(c.Date),
|
||||
@@ -216,6 +225,18 @@ public static class CvVariantResolver
|
||||
return section;
|
||||
}
|
||||
|
||||
// Sort entries by the variant's explicit ItemOrder; entries whose key isn't listed (or is null)
|
||||
// keep their master order and come after the ordered ones.
|
||||
private static List<CvRenderEntry> ReorderByKey(List<CvRenderEntry> entries, List<string> order)
|
||||
{
|
||||
var rank = new Dictionary<string, int>(StringComparer.Ordinal);
|
||||
for (var i = 0; i < order.Count; i++) if (!string.IsNullOrWhiteSpace(order[i])) rank[order[i]] = i;
|
||||
return entries
|
||||
.Select((e, i) => (e, primary: e.Key != null && rank.TryGetValue(e.Key, out var r) ? r : int.MaxValue, i))
|
||||
.OrderBy(x => x.primary).ThenBy(x => x.i)
|
||||
.Select(x => x.e).ToList();
|
||||
}
|
||||
|
||||
private static CvItemOverride? OverrideFor(CvVariantSettings settings, string? itemKey)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(itemKey)) return null;
|
||||
|
||||
Reference in New Issue
Block a user