From dec3a4303fd35fc5659ed868296b28107a61adf9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20Ve=C4=8Derek?= Date: Wed, 6 Nov 2024 08:21:05 +0100 Subject: [PATCH] Support custom code fences (#89) --- .changeset/bright-dragons-enjoy.md | 5 ++ src/Domain.ts | 8 +- src/Markdown.ts | 16 ++-- src/Parser.ts | 23 ++++-- test/Markdown.test.ts | 4 +- test/Parser.test.ts | 122 +++++++++++++++++++++++++---- 6 files changed, 146 insertions(+), 32 deletions(-) create mode 100644 .changeset/bright-dragons-enjoy.md diff --git a/.changeset/bright-dragons-enjoy.md b/.changeset/bright-dragons-enjoy.md new file mode 100644 index 0000000..6fd610b --- /dev/null +++ b/.changeset/bright-dragons-enjoy.md @@ -0,0 +1,5 @@ +--- +"@effect/docgen": minor +--- + +Support custom code fences when rendering examples diff --git a/src/Domain.ts b/src/Domain.ts index 52e81eb..9c93e93 100644 --- a/src/Domain.ts +++ b/src/Domain.ts @@ -25,7 +25,13 @@ export interface Module extends NamedDoc { * @category model * @since 1.0.0 */ -export type Example = string +export type Example = { + body: string + fences?: { + start: string + end: string + } +} /** * @category model diff --git a/src/Markdown.ts b/src/Markdown.ts index 2d30848..7e22038 100644 --- a/src/Markdown.ts +++ b/src/Markdown.ts @@ -25,8 +25,8 @@ const createHeaderPrinter = (level: number) => (content: string): string => const MarkdownPrinter = { bold: (s: string) => `**${s}**`, - fence: (language: string, content: string) => - "```" + language + "\n" + content + "\n" + "```\n\n", + fence: (start: string, content: string, end: string) => + start + "\n" + content + "\n" + end + "\n\n", paragraph: (...content: ReadonlyArray) => "\n" + content.join("") + "\n\n", strikethrough: (content: string) => `~~${content}~~`, h1: createHeaderPrinter(1), @@ -56,17 +56,19 @@ const printDescription = (d: Option.Option): string => const printSignature = (s: string): string => MarkdownPrinter.paragraph(MarkdownPrinter.bold("Signature")) + - MarkdownPrinter.paragraph(MarkdownPrinter.fence("ts", s)) + MarkdownPrinter.paragraph(MarkdownPrinter.fence("```ts", s, "```")) const printSignatures = (ss: ReadonlyArray): string => MarkdownPrinter.paragraph(MarkdownPrinter.bold("Signature")) + - MarkdownPrinter.paragraph(MarkdownPrinter.fence("ts", ss.join("\n"))) + MarkdownPrinter.paragraph(MarkdownPrinter.fence("```ts", ss.join("\n"), "```")) -const printExamples = (es: ReadonlyArray): string => +const printExamples = (es: ReadonlyArray): string => es - .map((code) => + .map(({ body, fences }) => MarkdownPrinter.paragraph(MarkdownPrinter.bold("Example")) + - MarkdownPrinter.paragraph(MarkdownPrinter.fence("ts", code)) + MarkdownPrinter.paragraph( + MarkdownPrinter.fence(fences?.start ?? "```ts", body, fences?.end ?? "```") + ) ) .join("\n\n") diff --git a/src/Parser.ts b/src/Parser.ts index 5b0f67f..671389c 100644 --- a/src/Parser.ts +++ b/src/Parser.ts @@ -158,17 +158,30 @@ const getDescription = (name: string, comment: Comment) => return comment.description }) -const mdCodeBlockStart = /^(```|~~~)[^\n]*\n/ -const mdCodeBlockEnd = /\n(```|~~~)$/ -const stripCodeBlocksFromExample = (example: string) => - example.replace(mdCodeBlockStart, "").replace(mdCodeBlockEnd, "") +const fencedExampleRegex = + /^(?(```|~~~)[^\n]*)\n(?[\S\s]*)(?\n(```|~~~))$/ +const parseExample = (body: string) => { + const example = fencedExampleRegex.exec(body) + + if (example === null) { + return { body } + } + + return { + body: example?.groups?.body ?? "", + fences: { + start: example?.groups?.fenceStart?.trim() ?? "```ts", + end: example?.groups?.fenceEnd?.trim() ?? "```" + } + } +} const getExamplesTag = (name: string, comment: Comment, isModule: boolean) => Effect.gen(function*(_) { const config = yield* _(Configuration.Configuration) const source = yield* _(Source) const examples = Record.get(comment.tags, "example").pipe( - Option.map(flow(Array.getSomes, Array.map(stripCodeBlocksFromExample))), + Option.map(flow(Array.getSomes, Array.map(parseExample))), Option.getOrElse(() => []) ) if (Array.isEmptyArray(examples) && config.enforceExamples && !isModule) { diff --git a/test/Markdown.test.ts b/test/Markdown.test.ts index 426fb64..f0eb427 100644 --- a/test/Markdown.test.ts +++ b/test/Markdown.test.ts @@ -12,7 +12,7 @@ const testCases = { Option.some("a class"), Option.some("1.0.0"), false, - ["example 1"], + [{ body: "example 1", fences: { start: "```ts", end: "```" } }], Option.some("category") ), "declare class A { constructor() }", @@ -84,7 +84,7 @@ const testCases = { Option.some("a function"), Option.some("1.0.0"), true, - ["example 1"], + [{ body: "example 1", fences: { start: "```ts", end: "```" } }], Option.none() ), ["declare const func: (test: string) => string"] diff --git a/test/Parser.test.ts b/test/Parser.test.ts index d298c5f..5d0fa6a 100644 --- a/test/Parser.test.ts +++ b/test/Parser.test.ts @@ -521,8 +521,8 @@ describe("Parser", () => { ], since: Option.some("1.0.0"), examples: [ - "assert.deepStrictEqual(f(1, 2), { a: 1, b: 2 })", - "assert.deepStrictEqual(f(3, 4), { a: 3, b: 4 })" + { body: "assert.deepStrictEqual(f(1, 2), { a: 1, b: 2 })" }, + { body: "assert.deepStrictEqual(f(3, 4), { a: 3, b: 4 })" } ], category: Option.none() } @@ -556,8 +556,11 @@ describe("Parser", () => { ], since: Option.some("1.0.0"), examples: [ - "assert.deepStrictEqual(f(1, 2), { a: 1, b: 2 })", - "assert.deepStrictEqual(f(3, 4), { a: 3, b: 4 })" + { + body: "assert.deepStrictEqual(f(1, 2), { a: 1, b: 2 })", + fences: { start: "```ts", end: "```" } + }, + { body: "assert.deepStrictEqual(f(3, 4), { a: 3, b: 4 })" } ], category: Option.none() } @@ -565,7 +568,7 @@ describe("Parser", () => { ) }) - it("should parse multiline examples even when enclosed in code blocks using backticks", () => { + it("should parse multiline examples even when enclosed in code blocks using backticks", () => { expectSuccess( `/** * a description... @@ -591,11 +594,14 @@ describe("Parser", () => { ], since: Option.some("1.0.0"), examples: [ - String.stripMargin( - `|assert.deepStrictEqual(f(1, 2), { a: 1, b: 2 }) + { + body: String.stripMargin( + `|assert.deepStrictEqual(f(1, 2), { a: 1, b: 2 }) - |assert.deepStrictEqual(f(3, 4), { a: 3, b: 4 })` - ) + |assert.deepStrictEqual(f(3, 4), { a: 3, b: 4 })` + ), + fences: { start: "```ts", end: "```" } + } ], category: Option.none() } @@ -629,8 +635,11 @@ describe("Parser", () => { ], since: Option.some("1.0.0"), examples: [ - "assert.deepStrictEqual(f(1, 2), { a: 1, b: 2 })", - "assert.deepStrictEqual(f(3, 4), { a: 3, b: 4 })" + { + body: "assert.deepStrictEqual(f(1, 2), { a: 1, b: 2 })", + fences: { start: "~~~ts", end: "~~~" } + }, + { body: "assert.deepStrictEqual(f(3, 4), { a: 3, b: 4 })" } ], category: Option.none() } @@ -638,7 +647,7 @@ describe("Parser", () => { ) }) - it("should parse multiline examples even when enclosed in code blocks using backticks", () => { + it("should parse multiline examples even when enclosed in code blocks using backticks", () => { expectSuccess( `/** * a description... @@ -664,11 +673,90 @@ describe("Parser", () => { ], since: Option.some("1.0.0"), examples: [ - String.stripMargin( - `|assert.deepStrictEqual(f(1, 2), { a: 1, b: 2 }) + { + body: String.stripMargin( + `|assert.deepStrictEqual(f(1, 2), { a: 1, b: 2 }) + + |assert.deepStrictEqual(f(3, 4), { a: 3, b: 4 })` + ), + fences: { start: "~~~ts", end: "~~~" } + } + ], + category: Option.none() + } + ] + ) + }) + + it("should parse twoslash examples using backtick fences", () => { + expectSuccess( + `/** + * a description... + * @since 1.0.0 + * @example + * \`\`\`ts twoslash + * assert.deepStrictEqual(f(1, 2), { a: 1, b: 2 }) + * \`\`\` + * @example + * assert.deepStrictEqual(f(3, 4), { a: 3, b: 4 }) + * @deprecated + */ + export const f = (a: number, b: number): { [key: string]: number } => ({ a, b })`, + Parser.parseFunctions, + [ + { + _tag: "Function", + deprecated: true, + description: Option.some("a description..."), + name: "f", + signatures: [ + "export declare const f: (a: number, b: number) => { [key: string]: number; }" + ], + since: Option.some("1.0.0"), + examples: [ + { + body: "assert.deepStrictEqual(f(1, 2), { a: 1, b: 2 })", + fences: { start: "```ts twoslash", end: "```" } + }, + { body: "assert.deepStrictEqual(f(3, 4), { a: 3, b: 4 })" } + ], + category: Option.none() + } + ] + ) + }) - |assert.deepStrictEqual(f(3, 4), { a: 3, b: 4 })` - ) + it("should parse twoslash examples using tilde fences", () => { + expectSuccess( + `/** + * a description... + * @since 1.0.0 + * @example + * ~~~ts twoslash + * assert.deepStrictEqual(f(1, 2), { a: 1, b: 2 }) + * ~~~ + * @example + * assert.deepStrictEqual(f(3, 4), { a: 3, b: 4 }) + * @deprecated + */ + export const f = (a: number, b: number): { [key: string]: number } => ({ a, b })`, + Parser.parseFunctions, + [ + { + _tag: "Function", + deprecated: true, + description: Option.some("a description..."), + name: "f", + signatures: [ + "export declare const f: (a: number, b: number) => { [key: string]: number; }" + ], + since: Option.some("1.0.0"), + examples: [ + { + body: "assert.deepStrictEqual(f(1, 2), { a: 1, b: 2 })", + fences: { start: "~~~ts twoslash", end: "~~~" } + }, + { body: "assert.deepStrictEqual(f(3, 4), { a: 3, b: 4 })" } ], category: Option.none() } @@ -1532,7 +1620,7 @@ export const foo = 'foo'`, description: Option.some("This is the foo export."), since: Option.some("1.0.0"), deprecated: false, - examples: [`import { foo } from 'test'\n\nconsole.log(foo)`], + examples: [{ body: `import { foo } from 'test'\n\nconsole.log(foo)` }], category: Option.some("foo"), signature: "export declare const foo: \"foo\"" }