-
Notifications
You must be signed in to change notification settings - Fork 66.3k
Expand file tree
/
Copy pathcode-header.ts
More file actions
129 lines (116 loc) · 3.99 KB
/
code-header.ts
File metadata and controls
129 lines (116 loc) · 3.99 KB
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
/**
* Adds a bar above code blocks that shows the language and a copy button.
* Optionally, adds a prompt button to Copilot Chat blocks.
*/
import yaml from 'js-yaml'
import fs from 'fs'
import { visit } from 'unist-util-visit'
import { h } from 'hastscript'
import octicons from '@primer/octicons'
import { parse } from 'parse5'
import { fromParse5 } from 'hast-util-from-parse5'
import murmur from 'imurmurhash'
import { getPrompt } from './copilot-prompt'
import { generatePromptId } from '../lib/prompt-id'
import type { Element, Root } from 'hast'
interface LanguageConfig {
name: string
[key: string]: string | string[] | boolean | undefined
}
type Languages = Record<string, LanguageConfig>
const languages = yaml.load(fs.readFileSync('./data/code-languages.yml', 'utf8')) as Languages
export default function codeHeader() {
return (tree: Root) => {
visit(tree, 'element', (node, index, parent) => {
const el = node as Element
if (
el.tagName !== 'pre' ||
!(getPreMeta(el).copy || getPreMeta(el).prompt) ||
getPreMeta(el).annotate
)
return
if (index !== undefined && parent && 'children' in parent) {
;(parent as Element).children[index] = wrapCodeExample(el, tree)
}
})
}
}
function wrapCodeExample(node: Element, tree: Root): Element {
const codeChild = node.children[0] as Element
const classNames = codeChild.properties.className as string[] | undefined
const lang: string = classNames?.[0]?.replace('language-', '') ?? ''
const textNode = codeChild.children[0] as { value: string }
const code: string = textNode.value
const subnav = null // getSubnav() lives in annotate.ts, not needed for normal code blocks
const hasPrompt: boolean = Boolean(getPreMeta(node).prompt)
const promptResult = hasPrompt ? getPrompt(node, tree, code) : null
const hasCopy: boolean = Boolean(getPreMeta(node).copy) // defaults to true
const headerHast = header(
lang,
code,
subnav,
promptResult?.element ?? null,
hasCopy,
promptResult?.promptContent,
)
return h('div', { className: 'code-example' }, [headerHast, node])
}
export function header(
lang: string,
code: string,
subnav: Element | null = null,
prompt: Element | null = null,
hasCopy: boolean = true,
promptContent?: string,
): Element {
const codeId: string = murmur('js-btn-copy').hash(code).result().toString()
return h(
'header',
{
class: [
'd-flex',
'flex-items-center',
'flex-justify-between',
'p-2',
'text-small',
'rounded-top-1',
'border-top',
'border-left',
'border-right',
],
},
[
h('span', { className: 'flex-1' }, languages[lang]?.name),
subnav,
prompt,
hasCopy
? h(
'button',
{
class: ['js-btn-copy', 'btn', 'btn-sm', 'tooltipped', 'tooltipped-nw'],
'aria-label': `Copy ${languages[lang]?.name} code to clipboard`,
'data-clipboard': codeId,
},
btnIcon(),
)
: null,
h('pre', { hidden: true, 'data-clipboard': codeId }, code),
promptContent
? h('pre', { hidden: true, id: generatePromptId(promptContent) }, promptContent)
: null,
],
)
}
function btnIcon(): Element {
const btnIconHtml: string = octicons.copy.toSVG()
const btnIconAst = parse(String(btnIconHtml), { sourceCodeLocationInfo: true })
const btnIconElement = fromParse5(btnIconAst)
return btnIconElement as Element
}
// node can be various hast element types, return value contains meta properties from code blocks
export function getPreMeta(node: Element): Record<string, unknown> {
// Here's why this monstrosity works:
// https://github.com/syntax-tree/mdast-util-to-hast/blob/c87cd606731c88a27dbce4bfeaab913a9589bf83/lib/handlers/code.js#L40-L42
const firstChild = node.children[0] as Element | undefined
return (firstChild?.data as Record<string, Record<string, unknown>> | undefined)?.meta || {}
}