Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support custom code fences (#89) #90

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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
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