Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion site/data.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@
}
},
"$defs": {
"stringList": { "type": "array", "items": { "type": "string" } },
"stringList": { "type": "array", "items": { "type": "string", "minLength": 1 } },
"entryBase": {
"type": "object",
"required": [
Expand Down
22 changes: 16 additions & 6 deletions site/lib/frontmatter.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { renderInline } from "./markdown.mjs";
import { unquoteScalar } from "./shared.mjs";

/** Splits the inside of a flow list on commas that are not inside quotes. */
function splitFlowList(inner) {
function splitFlowList(inner, onEmptyItem) {
const parts = [];
let cur = "";
let quote = null;
Expand All @@ -33,7 +33,17 @@ function splitFlowList(inner) {
} else cur += ch;
}
parts.push(cur);
return parts.map((p) => unquoteScalar(p.trim()));

// YAML permits one comma after the final item. Other empty items are invalid.
if (parts.length && parts[parts.length - 1].trim() === "") parts.pop();
return parts.flatMap((part) => {
const value = unquoteScalar(part.trim());
if (value === "") {
onEmptyItem?.();
return [];
}
return [value];
});
}

/**
Expand Down Expand Up @@ -77,7 +87,7 @@ export function unclosedQuote(raw) {
return q === '"' ? "double" : "single";
}

export function parseScalar(raw) {
export function parseScalar(raw, onEmptyListItem) {
const v = stripComment(raw).trim();
if (v === "true") return true;
if (v === "false") return false;
Expand All @@ -87,7 +97,7 @@ export function parseScalar(raw) {
}
if (v.startsWith("[") && v.endsWith("]")) {
const inner = v.slice(1, -1).trim();
return inner ? splitFlowList(inner) : [];
return inner ? splitFlowList(inner, onEmptyListItem) : [];
}
return v;
}
Expand Down Expand Up @@ -116,7 +126,7 @@ export function parseFrontmatter(text) {
const value = parseScalar(item[1]);
const open = unclosedQuote(item[1]);
if (open) errors.push(`line ${lineNo}: list item opens a ${open} quote that is never closed`);
else if (value === null) errors.push(`line ${lineNo}: list item is empty (a value starting with # is a comment; quote it)`);
else if (value === null || value === "") errors.push(`line ${lineNo}: list item is empty (a value starting with # is a comment; quote it)`);
else if (cutAtHash(item[1], value)) errors.push(`line ${lineNo}: list item is cut at " #" (read as a comment); quote the value if the # is part of it`);
else meta[listKey].push(value);
}
Expand All @@ -142,7 +152,7 @@ export function parseFrontmatter(text) {
errors.push(`line ${lineNo}: "${line.trim()}" needs a space after the colon ("${key}: value")`);
return;
}
const value = parseScalar(rawValue);
const value = parseScalar(rawValue, () => errors.push(`line ${lineNo}: "${key}" has an empty item in its flow list`));
const open = unclosedQuote(rawValue);
if (open) errors.push(`line ${lineNo}: "${key}" opens a ${open} quote that is never closed`);
if (cutAtHash(rawValue, value)) {
Expand Down
8 changes: 8 additions & 0 deletions site/lib/lib.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -640,6 +640,14 @@ const f1 = fm([
]);
eq("frontmatter undoes quote escapes", f1.meta.attribution_string, 'Music "{title}" by {artist}');
eq("frontmatter keeps a quoted comma inside one list item", JSON.stringify(f1.meta.tags), JSON.stringify(["sci-fi, space", "ui", "it's"]));
const trailingFlow = fm(["tags: [a, b,]"]);
eq("frontmatter accepts a trailing flow-list comma", JSON.stringify(trailingFlow.meta.tags), JSON.stringify(["a", "b"]));
eq("frontmatter trailing flow-list comma has no errors", trailingFlow.errors.length, 0);
const emptyFlow = fm(["tags: [x, , y]"]);
has("frontmatter rejects an empty flow-list item", emptyFlow.errors.join(), 'line 2: "tags" has an empty item in its flow list');
eq("frontmatter omits an invalid empty flow-list item", JSON.stringify(emptyFlow.meta.tags), JSON.stringify(["x", "y"]));
const emptyBlock = fm(["tags:", ' - ""']);
has("frontmatter rejects an empty quoted block-list item", emptyBlock.errors.join(), "line 3: list item is empty");
eq("frontmatter reads a block list", JSON.stringify(f1.meta.formats), JSON.stringify(["PNG", "SVG"]));
eq("frontmatter reads booleans", f1.meta.commercial, true);
eq("frontmatter has no errors on good input", f1.errors.length, 0);
Expand Down