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/docs/modules/Configuration.ts.md b/docs/modules/Configuration.ts.md index 38835fb..c1773c1 100644 --- a/docs/modules/Configuration.ts.md +++ b/docs/modules/Configuration.ts.md @@ -37,38 +37,21 @@ Added in v1.0.0 ```ts export declare const ConfigurationSchema: Schema.Struct<{ - $schema: Schema.PropertySignature<"?:", string | undefined, never, "?:", string | undefined, never> - projectHomepage: Schema.PropertySignature<"?:", string | undefined, never, "?:", string | undefined, never> - srcDir: Schema.PropertySignature<"?:", string | undefined, never, "?:", string | undefined, never> - outDir: Schema.PropertySignature<"?:", string | undefined, never, "?:", string | undefined, never> - theme: Schema.PropertySignature<"?:", string | undefined, never, "?:", string | undefined, never> - enableSearch: Schema.PropertySignature<"?:", boolean | undefined, never, "?:", boolean | undefined, never> - enforceDescriptions: Schema.PropertySignature<"?:", boolean | undefined, never, "?:", boolean | undefined, never> - enforceExamples: Schema.PropertySignature<"?:", boolean | undefined, never, "?:", boolean | undefined, never> - enforceVersion: Schema.PropertySignature<"?:", boolean | undefined, never, "?:", boolean | undefined, never> - exclude: Schema.PropertySignature< - "?:", - readonly string[] | undefined, - never, - "?:", - readonly string[] | undefined, - never + $schema: Schema.optional + projectHomepage: Schema.optional + srcDir: Schema.optional + outDir: Schema.optional + theme: Schema.optional + enableSearch: Schema.optional + enforceDescriptions: Schema.optional + enforceExamples: Schema.optional + enforceVersion: Schema.optional + exclude: Schema.optional> + parseCompilerOptions: Schema.optional< + Schema.Union<[typeof Schema.String, Schema.Record$]> > - parseCompilerOptions: Schema.PropertySignature< - "?:", - string | { readonly [x: string]: unknown } | undefined, - never, - "?:", - string | { readonly [x: string]: unknown } | undefined, - never - > - examplesCompilerOptions: Schema.PropertySignature< - "?:", - string | { readonly [x: string]: unknown } | undefined, - never, - "?:", - string | { readonly [x: string]: unknown } | undefined, - never + examplesCompilerOptions: Schema.optional< + Schema.Union<[typeof Schema.String, Schema.Record$]> > }> ``` diff --git a/docs/modules/Domain.ts.md b/docs/modules/Domain.ts.md index bcaf7a1..3545ecf 100644 --- a/docs/modules/Domain.ts.md +++ b/docs/modules/Domain.ts.md @@ -252,7 +252,13 @@ Added in v1.0.0 **Signature** ```ts -export type Example = string +export type Example = { + body: string + fences?: { + start: string + end: string + } +} ``` Added in v1.0.0 diff --git a/src/Core.ts b/src/Core.ts index 839da59..aae7d90 100644 --- a/src/Core.ts +++ b/src/Core.ts @@ -150,7 +150,7 @@ const getExampleFiles = (modules: ReadonlyArray) => "examples", `${prefix}-${exampleId}-${doc.name}-${i}.ts` ), - `${content}\n`, + `${content.body}\n`, true // make the file overwritable ) ) 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\"" }