Skip to content

Commit

Permalink
Support custom code fences (#89)
Browse files Browse the repository at this point in the history
  • Loading branch information
vecerek committed Nov 6, 2024
1 parent 95f136e commit dec3a43
Show file tree
Hide file tree
Showing 6 changed files with 146 additions and 32 deletions.
5 changes: 5 additions & 0 deletions .changeset/bright-dragons-enjoy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@effect/docgen": minor
---

Support custom code fences when rendering examples
8 changes: 7 additions & 1 deletion src/Domain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
16 changes: 9 additions & 7 deletions src/Markdown.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string>) => "\n" + content.join("") + "\n\n",
strikethrough: (content: string) => `~~${content}~~`,
h1: createHeaderPrinter(1),
Expand Down Expand Up @@ -56,17 +56,19 @@ const printDescription = (d: Option.Option<string>): 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>): 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>): string =>
const printExamples = (es: ReadonlyArray<Domain.Example>): 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")

Expand Down
23 changes: 18 additions & 5 deletions src/Parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
/^(?<fenceStart>(```|~~~)[^\n]*)\n(?<body>[\S\s]*)(?<fenceEnd>\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) {
Expand Down
4 changes: 2 additions & 2 deletions test/Markdown.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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() }",
Expand Down Expand Up @@ -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"]
Expand Down
122 changes: 105 additions & 17 deletions test/Parser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
Expand Down Expand Up @@ -556,16 +556,19 @@ 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()
}
]
)
})

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...
Expand All @@ -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()
}
Expand Down Expand Up @@ -629,16 +635,19 @@ 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()
}
]
)
})

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...
Expand All @@ -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()
}
Expand Down Expand Up @@ -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\""
}
Expand Down

0 comments on commit dec3a43

Please sign in to comment.