1/**
2 * @since 4.0.0
3 */
4
5import { readFile } from "node:fs/promises"
6
7/**
8 * Represents an executable TypeScript code snippet.
9 *
10 * @category models
11 * @since 4.0.0
12 */
13export interface Snippet {
14 readonly source: string
15 readonly line: number
16 readonly name: string | undefined
17}
18
19/**
20 * Identifies how documentation is embedded in the source text.
21 *
22 * @category models
23 * @since 4.0.0
24 */
25export type SourceFormat = "jsdoc" | "markdown"
26
27/**
28 * Returns whether a file uses Markdown or MDX documentation syntax.
29 *
30 * @category guards
31 * @since 4.0.0
32 */
33export const isMarkdown = (file: string): boolean => file.endsWith(".md") || file.endsWith(".mdx")
34
35const jsdocPattern = /\/\*\*[\s\S]*?\*\//g
36const fencePattern = /(?:```|~~~)(.*?)\n([\s\S]*?)(?:(?:```|~~~)|$)/g
37const runnableMarker = "import.meta.vitest"
38const namePattern = /(?:^|\s)name=(?:"([^"]+)"|'([^']+)'|([^\s]+))/
39
40const lineNumberAt = (source: string): (offset: number) => number => {
41 const starts = [0]
42 for (let index = 0; index < source.length; index++) {
43 if (source.charCodeAt(index) === 10) starts.push(index + 1)
44 }
45
46 return (offset) => {
47 let low = 0
48 let high = starts.length
49 while (low < high) {
50 const middle = (low + high) >>> 1
51 if (starts[middle] <= offset) low = middle + 1
52 else high = middle
53 }
54 return low
55 }
56}
57
58const snippets = (
59 text: string,
60 offset: number,
61 lineAt: (offset: number) => number,
62 jsdoc = false
63): ReadonlyArray<Snippet> => {
64 return Array.from(text.matchAll(fencePattern)).flatMap((match) => {
65 const metadata = match[1].toLowerCase()
66 if ((!metadata.startsWith("ts") && !metadata.startsWith("typescript")) || !metadata.includes(runnableMarker)) {
67 return []
68 }
69
70 const name = namePattern.exec(match[1])
71 const source = jsdoc ? match[2].replace(/^[ \t]*\* ?/gm, "").trim() : match[2].trim()
72
73 return [{
74 source,
75 line: lineAt(offset + (match.index ?? 0)),
76 name: name?.[1] ?? name?.[2] ?? name?.[3]
77 }]
78 })
79}
80
81/**
82 * Extracts marked TypeScript code snippets from documentation text.
83 *
84 * **Details**
85 *
86 * Extraction uses an ordered text scan and does not parse the containing source file.
87 *
88 * @category extraction
89 * @since 4.0.0
90 */
91export const extract = (source: string, format: SourceFormat = "jsdoc"): ReadonlyArray<Snippet> => {
92 if (!source.includes(runnableMarker)) {
93 return []
94 }
95
96 const lineAt = lineNumberAt(source)
97 return format === "markdown"
98 ? snippets(source, 0, lineAt)
99 : Array.from(source.matchAll(jsdocPattern)).flatMap((match) => snippets(match[0], match.index ?? 0, lineAt, true))
100}
101
102/**
103 * Reads a file and extracts its marked code snippets.
104 *
105 * @category extraction
106 * @since 4.0.0
107 */
108export const extractFile = (file: string): Promise<ReadonlyArray<Snippet>> =>
109 readFile(file, "utf8").then((source) => extract(source, isMarkdown(file) ? "markdown" : "jsdoc"))