-
Notifications
You must be signed in to change notification settings - Fork 87
/
createGuide.js
113 lines (96 loc) · 3.49 KB
/
createGuide.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
import fs from 'fs';
import path from 'path';
import marked from 'marked';
import hljs from 'highlight.js';
// Register dummy language for plain text code blocks.
hljs.registerLanguage('text', () => ({}));
export default function createGuide() {
const languages = Object.create(null);
for (const lang of fs.readdirSync('guide')) {
if (fs.statSync(`guide/${lang}`).isDirectory()) {
languages[lang] = JSON.stringify(create_guide(lang));
}
}
return languages;
}
function create_guide(lang) {
const files = fs.readdirSync(`guide/${lang}`).filter(file => {
return file[0] !== '.' && path.extname(file) === '.md';
});
return files.map(file => {
const markdown = fs.readFileSync(`guide/${lang}/${file}`, 'utf-8');
let match = /---\n([\s\S]+?)\n---/.exec(markdown);
const front_matter = match[1];
let content = markdown.slice(match[0].length);
const metadata = {};
front_matter.split('\n').forEach(pair => {
const colon_index = pair.indexOf(':');
metadata[pair.slice(0, colon_index).trim()] = pair.slice(colon_index + 1).trim();
});
// syntax highlighting
let uid = 0;
const highlighted = {};
content = content.replace(/```([\w-]+)?\n([\s\S]+?)```/g, (match, lang, code) => {
// TODO add bash everywhere and change default to text
const { value } = hljs.highlight(code, { language: lang || 'bash' });
highlighted[++uid] = value;
return `@@${uid}`;
});
const renderer = new marked.Renderer();
const tocItems = [];
renderer.heading = (text, level, raw, slugger) => {
const id = slugger.slug(raw);
if (level === 3) {
tocItems.push({ id, text, subSubSections: [] });
} else if (level === 4 && tocItems.length > 0) {
const previousTocItem = tocItems[tocItems.length - 1];
previousTocItem.subSubSections.push({ id, text });
}
return `<h${level} id="${id}"><a class="anchor" href="guide/${lang}/#${id}"><img src="/images/anchor.svg" alt=""></a>${text}</h${level}>`;
};
const html = marked(content, { renderer })
.replace(/<p>(<a class='open-in-repl'[\s\S]+?)<\/p>/g, '$1')
.replace(/<p>@@(\d+)<\/p>/g, (match, id) => {
return `<pre><code>${highlighted[id]}</code></pre>`;
})
.replace(/^\t+/gm, match => match.split('\t').join(' '))
.replace(/<!-- html:([\w.-]+) -->/g, (_, fileName) =>
fs.readFileSync(`guide/en/${fileName}`, 'utf8')
)
.replace(/<!-- mermaid:([\w.-]+)\.mmd -->/g, (_, fileName) => getSvgGraph(fileName));
const subsections = [];
const pattern = /<h3 id="(.+?)">(.+?)<\/h3>/g;
while ((match = pattern.exec(html))) {
const slug = match[1];
const title = match[2]
// Remove <code> tags
.replace(/<\/?code>/g, '')
// Remove first HTML tag
.replace(/<(\w+).*>.*<\/\1>/, '')
// Unescape quotes
.replace(/"/g, '"')
.replace(/'/g, "'")
// Remove parts in parentheses: foo (bar) -> foo
.replace(/\((\w+).*\)/, '');
subsections.push({ slug, title });
}
return {
html,
metadata,
subsections,
tocItems,
slug: file.replace(/^\d+-/, '').replace(/\.md$/, '')
};
});
}
// This will avoid a layout shift and wrong anchor positions by making sure the
// element has the correct size before the file is loaded
function getSvgGraph(fileName) {
const fileContent = fs.readFileSync(`static/graphs/${fileName}.svg`, 'utf8');
const [, width, height] = fileContent.match(/viewBox="0 0 ([\d.]+) ([\d.]+)"/);
return `
<object
data="graphs/${fileName}.svg"
type="image/svg+xml"
style="max-width:80vw; height:min(${height}px,calc(80vw * ${height / width}))"></object>`;
}