forked from github/docs
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcode-header.ts
More file actions
135 lines (121 loc) · 4.46 KB
/
Copy pathcode-header.ts
File metadata and controls
135 lines (121 loc) · 4.46 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
130
131
132
133
134
135
/**
* 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 } from 'hast'
interface LanguageConfig {
name: string
// Using any for language properties that can vary (aliases, extensions, etc.)
[key: string]: any
}
type Languages = Record<string, LanguageConfig>
const languages = yaml.load(fs.readFileSync('./data/code-languages.yml', 'utf8')) as Languages
// Using any due to conflicting unist/hast type definitions between dependencies
const matcher = (node: any): boolean =>
node.type === 'element' &&
node.tagName === 'pre' &&
// For now, limit to ones with the copy or prompt meta,
// but we may enable for all examples later.
(getPreMeta(node).copy || getPreMeta(node).prompt) &&
// Don't add this header for annotated examples.
!getPreMeta(node).annotate
export default function codeHeader() {
// Using any due to conflicting unist/hast type definitions between dependencies
return (tree: any) => {
// Using any due to conflicting unist/hast type definitions between dependencies
visit(tree, matcher, (node: any, index: number | undefined, parent: any) => {
if (index !== undefined && parent) {
parent.children[index] = wrapCodeExample(node, tree)
}
})
}
}
// Using any due to conflicting unist/hast type definitions between dependencies
function wrapCodeExample(node: any, tree: any): Element {
const lang: string = node.children[0].properties.className?.[0].replace('language-', '')
const code: string = node.children[0].children[0].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 })
// Using any because fromParse5 expects VFile but we only have a string
// This is safe because parse5 only needs the string content
const btnIconElement = fromParse5(btnIconAst, { file: btnIconHtml as any })
return btnIconElement as Element
}
// Using any due to conflicting unist/hast type definitions between dependencies
// node can be various mdast/hast node types, return value contains meta properties from code blocks
export function getPreMeta(node: any): Record<string, any> {
// Here's why this monstrosity works:
// https://github.com/syntax-tree/mdast-util-to-hast/blob/c87cd606731c88a27dbce4bfeaab913a9589bf83/lib/handlers/code.js#L40-L42
return node.children[0]?.data?.meta || {}
}