Parser tests
What counts as prose in each language, and the one placement rule they share. The rules are writing.
import { describe, expect, it } from "vite-plus/test";
import { firstParagraph, parseFile } from "../src/parser.js";
const bodies = (source: string, ext: string) => parseFile(source, ext).map((c) => c.body);
describe("parseFile: JS/TS/CSS (/** @prose */)", () => {
it("reads each marked comment in source order, with its span", () => {
const source = `/** @prose\n * File summary.\n */\nimport x from "y";\n\n/** @prose\n * A later one.\n */\nconst a = 1;\n`;
const comments = parseFile(source, "js");
expect(comments.map((c) => c.body)).toEqual(["File summary.", "A later one."]);
expect(source.slice(comments[1]!.start, comments[1]!.end)).toBe(
"/** @prose\n * A later one.\n */",
);
});
it("reads `*\\/` in a comment as `*/`, which would have ended the comment", () => {
expect(bodies("/** @prose\n * Ends with *\\/ in text.\n */\nconst a = 1;\n", "ts")).toEqual([
"Ends with */ in text.",
]);
});
it("ignores unmarked comments, and @note, which is no longer read", () => {
const source = `// just a code comment\n/** normal jsdoc, no @prose */\n/** @note A question. */\nconst a = 1;\n`;
expect(parseFile(source, "js")).toEqual([]);
});
it("strips the leading ` * ` gutter from continuation lines", () => {
expect(bodies(`/** @prose\n * Line one.\n * Line two.\n */\n`, "js")).toEqual([
"Line one.\nLine two.",
]);
});
it("eats a body line's own leading * when that line skips the gutter (documented landmine)", () => {
// Every continuation line is expected to carry the ` * ` gutter. A line that skips it and
// starts with a literal `*` of its own, like a Markdown bullet, loses that `*`.
expect(bodies(`/** @prose\n * Intro.\n* a bullet\n */\n`, "js")).toEqual(["Intro.\na bullet"]);
});
it("isn't fooled by regex literals or division", () => {
for (const line of ["const RE = /`/g;", "const ratio = a / b;", "const RE = /[a/b]/g;"]) {
expect(bodies(`${line}\n/** @prose After. */\nconst c = 1;`, "js")).toEqual(["After."]);
}
});
it("skips a `/*` inside a CSS string", () => {
const css = `a::before { content: "/** @prose not one */"; }\n/** @prose Real. */\n`;
expect(bodies(css, "css")).toEqual(["Real."]);
});
});
describe("parseFile: HTML (<!-- @prose -->)", () => {
it("takes body lines as-is, with no gutter stripping", () => {
const source = `<!-- @prose\nLine one.\n * looks like a bullet but is not a gutter\n-->\n<h1>hi</h1>\n`;
expect(bodies(source, "html")).toEqual([
"Line one.\n * looks like a bullet but is not a gutter",
]);
});
});
describe("parseFile: YAML, TOML and .gitignore (# @prose)", () => {
it("reads a run of # lines from the marker, gutter stripped", () => {
for (const ext of ["yaml", "toml", "gitignore"]) {
expect(bodies("# @prose\n# # Run\n#\n# Starts it.\n\nname = 1\n", ext)).toEqual([
"# Run\n\nStarts it.",
]);
}
});
it("ends a comment at the next marker line, so two can sit back to back", () => {
const source = "# @prose\n# File.\n# @prose\n# Next.\nport = 8080\n";
const comments = parseFile(source, "toml");
expect(comments.map((c) => c.body)).toEqual(["File.", "Next."]);
expect(source.slice(comments[1]!.start, comments[1]!.end)).toBe("# @prose\n# Next.");
});
it("ignores unmarked # comments", () => {
expect(parseFile("# just a comment\nname: demo\n", "yaml")).toEqual([]);
});
});
describe("parseFile: .svelte (script/style as TS/CSS, markup as HTML)", () => {
it("merges comments from all three parts in source order, at their place in the file", () => {
const source = [
'<script lang="ts">',
"/** @prose\n * # Title\n * From the script.\n */",
'import x from "y";',
"</script>",
"",
"<!-- @prose From the markup. -->",
"<h1>hi</h1>",
"",
"<style>",
"/** @prose From the style. */",
"h1 { color: red; }",
"</style>",
"",
].join("\n");
const comments = parseFile(source, "svelte");
expect(comments.map((c) => c.body)).toEqual([
"# Title\nFrom the script.",
"From the markup.",
"From the style.",
]);
expect(source.slice(comments[2]!.start, comments[2]!.end)).toBe(
"/** @prose From the style. */",
);
});
});
describe("parseFile: .html reads its script and style parts too", () => {
it("finds prose in a template's script and in a style", () => {
const source = [
"<!-- @prose Element. -->",
"<template>",
"<script>",
"/** @prose Why the helper. */",
"const x = 1;",
"</script>",
"</template>",
"<style>",
"/** @prose Why the selector. */",
"h1 { color: red; }",
"</style>",
"",
].join("\n");
expect(parseFile(source, "html").map((c) => c.body)).toEqual([
"Element.",
"Why the helper.",
"Why the selector.",
]);
});
it("reads a <script> mentioned in a comment as the comment's text", () => {
const source = [
"<!-- @prose",
"A slot can hold `<script>` tags.",
"-->",
"<p>x</p>",
"<script>",
"/** @prose Why the script. */",
"go();",
"</script>",
"",
].join("\n");
expect(parseFile(source, "html").map((c) => c.body)).toEqual([
"A slot can hold `<script>` tags.",
"Why the script.",
]);
});
});
describe("a comment counts when it starts its own line, at any depth", () => {
it("reads a class method by method", () => {
const ts = [
"/** @prose File. */",
"export class Parser {",
"\t/** @prose",
"\t * Runs it.",
"\t */",
"\trun(): void {}",
"",
"\tfield = {",
"\t\t/** @prose Deep. */",
"\t\tb: 1,",
"\t};",
"}",
].join("\n");
expect(bodies(ts, "ts")).toEqual(["File.", "Runs it.", "Deep."]);
});
it("leaves one that shares its line with code in the code", () => {
expect(bodies("/** @prose File. */\ncall(/** @prose Inline. */ 1);\n", "ts")).toEqual([
"File.",
]);
expect(
bodies("/** @prose File. */\n.a { /** @prose Inline. */ color: red; }\n", "css"),
).toEqual(["File."]);
expect(bodies("<!-- @prose File. -->\n<p>Hi <!-- @prose Inline. --></p>\n", "html")).toEqual([
"File.",
]);
expect(bodies("# @prose\n# File.\nname: demo # @prose Inline.\n", "yaml")).toEqual(["File."]);
});
it("reads an indented one inside a CSS rule or a YAML mapping", () => {
expect(bodies(".a {\n /** @prose Inside. */\n color: red;\n}\n", "css")).toEqual(["Inside."]);
const yaml = "a:\n # @prose\n # Inside.\n c: 2\n";
const comments = parseFile(yaml, "yaml");
expect(comments.map((c) => c.body)).toEqual(["Inside."]);
expect(yaml.slice(comments[0]!.start, comments[0]!.end)).toBe("# @prose\n # Inside.");
});
});
describe("firstParagraph", () => {
it("skips a leading heading and returns the first paragraph", () => {
expect(firstParagraph("# Title\n\nFirst paragraph.\n\nSecond paragraph.")).toBe(
"First paragraph.",
);
});
it("returns the whole text when there is no heading or second paragraph", () => {
expect(firstParagraph("Just one paragraph.")).toBe("Just one paragraph.");
});
});