Skip to content

Commit 71cff05

Browse files
committed
refactor(articles): simplify frontmatter parsing
1 parent 00c22d8 commit 71cff05

7 files changed

Lines changed: 216 additions & 1009 deletions

lib/articles/frontmatter-parser.ts

Lines changed: 98 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,8 @@
11
import matter from "gray-matter"
22

3-
// ─── New API ────────────────────────────────────────────────────────────────
4-
53
export interface SourceFrontMatter {
64
slug: string
75
title: string
8-
"chapter-title"?: string
9-
"intro-title"?: string
106
description?: string
117
index: number
128
"is-advanced"?: boolean
@@ -17,70 +13,96 @@ export interface TranslationFrontMatter {
1713
translates: string
1814
"translated-from-revision": string
1915
title?: string
20-
"chapter-title"?: string
21-
"intro-title"?: string
2216
description?: string
2317
banner?: { src: string; alt?: string }
2418
}
2519

26-
interface ParseSourceFrontMatterOptions {
27-
allowTitlelessFolder?: boolean
20+
export interface SourceReadmeFrontMatter {
21+
slug: string
22+
"chapter-title": string
23+
"intro-title"?: string
24+
index: number
2825
}
2926

30-
// ─── Allowed / legacy key sets ─────────────────────────────────────────────
27+
export interface TranslationReadmeFrontMatter {
28+
translates: string
29+
"translated-from-revision": string
30+
"chapter-title": string
31+
"intro-title"?: string
32+
}
33+
34+
type BannerFrontMatter = { src: string; alt?: string }
3135

3236
const SOURCE_ALLOWED_KEYS = new Set([
3337
"slug",
3438
"title",
35-
"chapter-title",
36-
"intro-title",
3739
"description",
3840
"index",
3941
"is-advanced",
4042
"banner",
41-
"author",
4243
])
4344

4445
const TRANSLATION_ALLOWED_KEYS = new Set([
4546
"translates",
4647
"translated-from-revision",
4748
"title",
48-
"chapter-title",
49-
"intro-title",
5049
"description",
5150
"banner",
5251
])
5352

54-
const LEGACY_KEYS = new Set([
55-
"title-en",
56-
"chapter-title-en",
57-
"intro-title-en",
58-
"date",
59-
"lastmod",
60-
"co-authors",
53+
const SOURCE_README_ALLOWED_KEYS = new Set([
54+
"slug",
55+
"chapter-title",
56+
"intro-title",
57+
"index",
6158
])
6259

63-
// ─── Helpers ────────────────────────────────────────────────────────────────
60+
const TRANSLATION_README_ALLOWED_KEYS = new Set([
61+
"translates",
62+
"translated-from-revision",
63+
"chapter-title",
64+
"intro-title",
65+
])
6466

65-
function checkLegacyKeys(data: Record<string, unknown>): void {
66-
for (const key of Object.keys(data)) {
67-
if (LEGACY_KEYS.has(key)) {
68-
throw new Error(`legacy key '${key}' not allowed`)
69-
}
70-
}
71-
}
67+
// ─── Helpers ────────────────────────────────────────────────────────────────
7268

7369
function checkAdditionalProperties(
7470
data: Record<string, unknown>,
7571
allowedKeys: Set<string>
7672
): void {
7773
for (const key of Object.keys(data)) {
78-
if (!allowedKeys.has(key) && !LEGACY_KEYS.has(key)) {
74+
if (!allowedKeys.has(key)) {
7975
throw new Error(`unknown key '${key}' not allowed`)
8076
}
8177
}
8278
}
8379

80+
function parseRequiredString(
81+
data: Record<string, unknown>,
82+
key: string
83+
): string {
84+
const value = data[key]
85+
if (typeof value !== "string") {
86+
throw new Error(`missing required key '${key}'`)
87+
}
88+
return value
89+
}
90+
91+
function parseRequiredNonEmptyString(
92+
data: Record<string, unknown>,
93+
key: string
94+
): string {
95+
const value = parseRequiredString(data, key)
96+
if (value === "") {
97+
throw new Error(`missing required key '${key}'`)
98+
}
99+
return value
100+
}
101+
102+
function parseOptionalString(value: unknown): string | undefined {
103+
return typeof value === "string" ? value : undefined
104+
}
105+
84106
function parseIndex(value: unknown): number {
85107
if (typeof value === "number" && Number.isInteger(value)) {
86108
return value
@@ -96,7 +118,7 @@ function parseIndex(value: unknown): number {
96118

97119
function parseBanner(
98120
value: unknown
99-
): { src: string; alt?: string } | undefined {
121+
): BannerFrontMatter | undefined {
100122
if (typeof value !== "object" || value === null) return undefined
101123
const obj = value as Record<string, unknown>
102124
if (typeof obj.src !== "string") return undefined
@@ -108,36 +130,19 @@ function parseBanner(
108130

109131
// ─── Parsers ────────────────────────────────────────────────────────────────
110132

111-
export function parseSourceFrontMatter(
112-
content: string,
113-
options: ParseSourceFrontMatterOptions = {}
114-
): SourceFrontMatter {
133+
function parseFrontMatterData(content: string): Record<string, unknown> {
115134
const { data } = matter(content)
116-
const raw = data as Record<string, unknown>
135+
return data as Record<string, unknown>
136+
}
117137

118-
checkLegacyKeys(raw)
138+
export function parseSourceFrontMatter(content: string): SourceFrontMatter {
139+
const raw = parseFrontMatterData(content)
119140
checkAdditionalProperties(raw, SOURCE_ALLOWED_KEYS)
120141

121-
if (typeof raw.slug !== "string" || raw.slug === "") {
122-
throw new Error("missing required key 'slug'")
123-
}
124-
const title = typeof raw.title === "string" ? raw.title : ""
125-
126-
if (!options.allowTitlelessFolder && title === "") {
127-
throw new Error("missing required key 'title'")
128-
}
129-
130142
return {
131-
slug: raw.slug,
132-
title,
133-
"chapter-title":
134-
typeof raw["chapter-title"] === "string"
135-
? raw["chapter-title"]
136-
: undefined,
137-
"intro-title":
138-
typeof raw["intro-title"] === "string" ? raw["intro-title"] : undefined,
139-
description:
140-
typeof raw.description === "string" ? raw.description : undefined,
143+
slug: parseRequiredNonEmptyString(raw, "slug"),
144+
title: parseRequiredNonEmptyString(raw, "title"),
145+
description: parseOptionalString(raw.description),
141146
index: parseIndex(raw.index),
142147
"is-advanced": raw["is-advanced"] === true ? true : undefined,
143148
banner: parseBanner(raw.banner),
@@ -147,34 +152,48 @@ export function parseSourceFrontMatter(
147152
export function parseTranslationFrontMatter(
148153
content: string
149154
): TranslationFrontMatter {
150-
const { data } = matter(content)
151-
const raw = data as Record<string, unknown>
152-
153-
checkLegacyKeys(raw)
155+
const raw = parseFrontMatterData(content)
154156
checkAdditionalProperties(raw, TRANSLATION_ALLOWED_KEYS)
155157

156-
if (typeof raw.translates !== "string" || raw.translates === "") {
157-
throw new Error("missing required key 'translates'")
158+
return {
159+
translates: parseRequiredNonEmptyString(raw, "translates"),
160+
"translated-from-revision": parseRequiredNonEmptyString(
161+
raw,
162+
"translated-from-revision"
163+
),
164+
title: parseOptionalString(raw.title),
165+
description: parseOptionalString(raw.description),
166+
banner: parseBanner(raw.banner),
158167
}
159-
if (
160-
typeof raw["translated-from-revision"] !== "string" ||
161-
raw["translated-from-revision"] === ""
162-
) {
163-
throw new Error("missing required key 'translated-from-revision'")
168+
}
169+
170+
export function parseSourceReadmeFrontMatter(
171+
content: string
172+
): SourceReadmeFrontMatter {
173+
const raw = parseFrontMatterData(content)
174+
checkAdditionalProperties(raw, SOURCE_README_ALLOWED_KEYS)
175+
176+
return {
177+
slug: parseRequiredString(raw, "slug"),
178+
"chapter-title": parseRequiredNonEmptyString(raw, "chapter-title"),
179+
"intro-title": parseOptionalString(raw["intro-title"]),
180+
index: parseIndex(raw.index),
164181
}
182+
}
183+
184+
export function parseTranslationReadmeFrontMatter(
185+
content: string
186+
): TranslationReadmeFrontMatter {
187+
const raw = parseFrontMatterData(content)
188+
checkAdditionalProperties(raw, TRANSLATION_README_ALLOWED_KEYS)
165189

166190
return {
167-
translates: raw.translates,
168-
"translated-from-revision": raw["translated-from-revision"],
169-
title: typeof raw.title === "string" ? raw.title : undefined,
170-
"chapter-title":
171-
typeof raw["chapter-title"] === "string"
172-
? raw["chapter-title"]
173-
: undefined,
174-
"intro-title":
175-
typeof raw["intro-title"] === "string" ? raw["intro-title"] : undefined,
176-
description:
177-
typeof raw.description === "string" ? raw.description : undefined,
178-
banner: parseBanner(raw.banner),
191+
translates: parseRequiredNonEmptyString(raw, "translates"),
192+
"translated-from-revision": parseRequiredNonEmptyString(
193+
raw,
194+
"translated-from-revision"
195+
),
196+
"chapter-title": parseRequiredNonEmptyString(raw, "chapter-title"),
197+
"intro-title": parseOptionalString(raw["intro-title"]),
179198
}
180199
}

package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,6 @@
110110
"@types/papaparse": "^5.5.2",
111111
"@types/react": "^19.2.17",
112112
"@types/three": "^0.185.0",
113-
"ajv": "^8.20.0",
114113
"gitnexus": "^1.6.9",
115114
"husky": "^9.1.7",
116115
"lint-staged": "^17.0.8",

pnpm-lock.yaml

Lines changed: 0 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

scripts/article-frontmatter.schema.json

Lines changed: 0 additions & 112 deletions
This file was deleted.

0 commit comments

Comments
 (0)