-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcontent.ts
More file actions
117 lines (102 loc) · 4.53 KB
/
Copy pathcontent.ts
File metadata and controls
117 lines (102 loc) · 4.53 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
import { parseFrontmatter } from './frontmatter';
import type {
ContentDoc,
ContentSource,
RelatedAdvisor,
SectionId,
} from '../config/contentSchema';
import type { DimensionId } from '../types';
// Pure content index for the "Learn" layer. Articles are Markdown files under /content, imported at
// BUILD time via import.meta.glob (statically analyzable, no runtime fetch — offline-capable and
// SEO-friendly if we ever prerender). Frontmatter is validated strictly by the build guard
// (scripts/check-content.mjs); here we read already-valid files. Unit-tested in content.test.ts.
const asStr = (v: unknown, fallback = ''): string => (typeof v === 'string' ? v : fallback);
const asArr = (v: unknown): string[] => (Array.isArray(v) ? (v as unknown[]).map((x) => String(x)) : []);
function toSources(v: unknown): ContentSource[] {
if (!Array.isArray(v)) return [];
return (v as Record<string, unknown>[]).map((s) => ({
label: asStr(s.label),
venue: asStr(s.venue),
year: Number(s.year) || 0,
url: asStr(s.url),
}));
}
function toRelated(v: unknown): RelatedAdvisor {
const o = (v ?? {}) as Record<string, unknown>;
return {
dimensions: asArr(o.dimensions) as DimensionId[],
options: asArr(o.options),
};
}
// Bilingual bodies live in ONE file: the English body, an optional `<!-- lang:id -->` line, then the
// Indonesian body. Articles not yet translated (no delimiter) fall back to English for both langs.
const BODY_LANG_DELIM = /^<!--\s*lang:id\s*-->\s*$/m;
function splitBody(body: string): { body_en: string; body_id: string } {
const m = BODY_LANG_DELIM.exec(body);
if (!m) return { body_en: body, body_id: body };
const en = body.slice(0, m.index).trimEnd();
const id = body.slice(m.index + m[0].length).replace(/^\n+/, '');
return { body_en: en, body_id: id || en };
}
function toDoc(raw: string): ContentDoc {
const { data, body } = parseFrontmatter(raw);
const { body_en, body_id } = splitBody(body);
return {
title_id: asStr(data.title_id),
title_en: asStr(data.title_en),
slug: asStr(data.slug),
section: asStr(data.section) as SectionId,
audience: asArr(data.audience) as ContentDoc['audience'],
summary_tldr_id: asStr(data.summary_tldr_id),
summary_tldr_en: asStr(data.summary_tldr_en),
evidence_strength: asStr(data.evidence_strength) as ContentDoc['evidence_strength'],
last_reviewed: asStr(data.last_reviewed),
review_due: asStr(data.review_due),
translation_status: asStr(data.translation_status) as ContentDoc['translation_status'],
related_advisor: toRelated(data.related_advisor),
sources: toSources(data.sources),
status: (asStr(data.status) as ContentDoc['status']) || 'draft',
author: asStr(data.author),
body_en,
body_id,
};
}
// Eager glob: every article is bundled with the content chunk (the Learn view is lazy-loaded).
const MODULES = import.meta.glob('/content/**/*.md', {
query: '?raw',
import: 'default',
eager: true,
}) as Record<string, string>;
const ALL: ContentDoc[] = Object.values(MODULES)
.map(toDoc)
.filter((d) => d.status === 'published')
.sort((a, b) => a.slug.localeCompare(b.slug));
/** All published articles. */
export function allContent(): ContentDoc[] {
return ALL;
}
/** Published articles in a section. */
export function contentBySection(section: SectionId): ContentDoc[] {
return ALL.filter((d) => d.section === section);
}
/** A single article by slug (undefined if not found / not published). */
export function contentBySlug(slug: string): ContentDoc | undefined {
return ALL.find((d) => d.slug === slug);
}
/** Title in the active language (falls back to the other language if one is empty). */
export function docTitle(doc: ContentDoc, lang: 'en' | 'id'): string {
return lang === 'id' ? doc.title_id || doc.title_en : doc.title_en || doc.title_id;
}
/** TL;DR in the active language. */
export function docTldr(doc: ContentDoc, lang: 'en' | 'id'): string {
return lang === 'id' ? doc.summary_tldr_id || doc.summary_tldr_en : doc.summary_tldr_en || doc.summary_tldr_id;
}
/** Markdown body in the active language (falls back to the other when one is empty). */
export function docBody(doc: ContentDoc, lang: 'en' | 'id'): string {
return lang === 'id' ? doc.body_id || doc.body_en : doc.body_en || doc.body_id;
}
/** True when the article's 12-month review window has lapsed (for the "needs review" badge). */
export function isReviewDue(doc: ContentDoc, now = new Date()): boolean {
const due = new Date(doc.review_due);
return !Number.isNaN(due.getTime()) && due.getTime() < now.getTime();
}