-
Notifications
You must be signed in to change notification settings - Fork 53
/
index.js
183 lines (145 loc) · 5.81 KB
/
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
// @ts-check
// To work on this, cd to this folder:
// cd packages/twoslash-cli
// Then run:
// ./bin/twoslash.js examples/*.ts render
import { readFileSync, writeFileSync, mkdirSync, existsSync, statSync } from "fs"
import remark from "remark"
import toHAST from "mdast-util-to-hast"
import hastToHTML from "hast-util-to-html"
import { visit } from "unist-util-visit"
import { join, dirname } from "path"
import remarkShikiTwoslash from "remark-shiki-twoslash"
import { basename, extname, sep } from "path"
import { tmpdir } from "os"
export const canConvert = (path) => {
const usable = [".md", ".ts", ".js", ".tsx", ".jsx"]
if (!usable.includes(extname(path))) return false
const filename = basename(path)
if (filename.startsWith(".")) return false
const stat = statSync(path)
if (stat.isDirectory()) return false
return true
}
/** @typedef {{ from: string, to: string, splitOutCodeSamples: boolean, alsoRenderSource: boolean, lint: boolean, realFrom?: string, reactAlso?: boolean }} Args */
/** @param {Args} args */
export const runOnFile = async args => {
const { from } = args
if (!canConvert(from)) return
switch (extname(from)) {
case ".md":
return renderMarkdown(args)
default:
return renderJS(args)
}
}
/** @param {Args} args */
function renderJS(args) {
// Basically write to a tmp file as a markdown file and go through that pipeline
const { from } = args
let fileContent = readFileSync(from, "utf8")
// Support forwarding the Twoslash Config from the ts to the md
let prefix = ""
if (fileContent.startsWith("// twoslash: {")) {
const js = fileContent.split("\n")[0].replace("// twoslash: ", "")
prefix = `<!-- twoslash: ${js} -->`
fileContent = fileContent.replace(`// twoslash: ${js}\n`, "")
}
// Support forwarding codefence info for highlighting
const classes = []
if (fileContent.startsWith("// codefence: ")) {
const highlightOpts = fileContent.split("\n")[0].replace("// codefence: ", "")
classes.push(highlightOpts)
fileContent = fileContent.replace(`// codefence: ${highlightOpts}\n`, "")
}
const newFileName = tmpdir() + sep + basename(from) + ".md"
const code = toCode(prefix, extname(from).replace(".", ""), [...classes, "twoslash"], fileContent)
writeFileSync(newFileName, code)
renderMarkdown({ ...args, from: newFileName, realFrom: from, })
// Also allow for showing a before/after by supporting a flag which renders the src
if (args.alsoRenderSource) {
const newFileName = tmpdir() + sep + basename(from) + "_src.md"
const code = toCode(prefix, extname(from).replace(".", ""), classes, fileContent)
writeFileSync(newFileName, code)
renderMarkdown({ ...args, from: newFileName, realFrom: from, })
}
}
/** @param {Args} args */
async function renderMarkdown(args) {
const { from, to, splitOutCodeSamples, realFrom } = args
const fileContent = readFileSync(from, "utf8")
const settings = getSettingsFromMarkdown(fileContent, from) || {}
const markdownAST = remark().parse(fileContent)
try {
// @ts-ignore
await remarkShikiTwoslash.default(settings)(markdownAST)
} catch (error) {
console.error(`Failed to render: ${from}`)
console.error(error)
}
// Bail before writing the new versions if we're linting
if (args.lint) return
// Render directly to one file
if (!splitOutCodeSamples) {
// The dangerous bit is that we include the HTML
const hAST = toHAST(markdownAST, { allowDangerousHtml: true })
const html = hastToHTML(hAST, { allowDangerousHtml: true })
// Assume folder unless you write .html
const lastIsHTML = to.endsWith(".html")
if (!existsSync(to)) {
const hostFolder = lastIsHTML ? dirname(to) : to
mkdirSync(hostFolder, { recursive: true })
}
// Write it
const writePath = lastIsHTML ? to : join(to, basename(from).replace(".md", ".html"))
writeFileSync(writePath, html)
// Log it
console.log(` - ${realFrom || from} -> ${writePath} `)
// Also allow rendering the output into a TSX components
if (args.reactAlso) {
const twoslash = hAST.children.find(node => node.type === "raw" && node.value.includes('class="shiki'))
if (!twoslash) throw new Error(`Could not find a twoslash code sample in '${from}' for the TSX component`)
const prefix = `// Auto-generated by the twoslash-cli from ${basename(from)}`
const code = toTSX(prefix, twoslash.value)
const writePath = lastIsHTML ? to : join(to, basename(from).replace(".tsx", "").replace(".ts", "").replace(".md", ".tsx"))
writeFileSync(writePath, code)
console.log(` ${" ".repeat((realFrom || from).length)} + ${writePath} \n`)
}
} else {
if (!existsSync(to)) mkdirSync(to, { recursive: true })
if (!existsSync(join(to, "mds"))) mkdirSync(join(to, "mds"))
let index = 1
visit(markdownAST, "html", c => {
const hAST = toHAST(c, { allowDangerousHtml: true })
const html = hastToHTML(hAST, { allowDangerousHtml: true })
writeFileSync(join(to, "mds", `code-${index}.html`), html)
index++
})
console.log(` -> Wrote ${index} files to ${to}`)
}
}
function getSettingsFromMarkdown(fileContent, from) {
if (fileContent.startsWith("<!-- twoslash: {")) {
const code = fileContent.split("<!-- twoslash: ")[1].split(" -->")[0]
try {
return eval("const res = " + code + "; res")
} catch (error) {
console.error(
`Twoslash CLI: Setting custom theme settings in ${from} failed. The eval'd code is '${code}' which bailed:`
)
throw error
}
}
}
const toCode = (prefix, lang, classes, content) => `${prefix}
\`\`\`${lang} ${classes.join(" ")}
${content}
\`\`\`
`
const toTSX = (prefix, content) => `${prefix}
import React from "react"
const innerHTML = \`
${content}
\`
export const Code = () => <div dangerouslySetInnerHTML={{ __html: innerHTML }} />
`