-
-
Notifications
You must be signed in to change notification settings - Fork 433
Expand file tree
/
Copy pathmarkdown.js
More file actions
258 lines (235 loc) · 7.63 KB
/
Copy pathmarkdown.js
File metadata and controls
258 lines (235 loc) · 7.63 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
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
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
// @ts-check
/**
* Module core/markdown
* Handles the optional markdown processing.
*
* Markdown support is optional. It is enabled by setting the `format`
* property of the configuration object to "markdown."
*
* We use marked for parsing Markdown:
* https://github.com/markedjs/marked
*
*/
import { getElementIndentation } from "./utils.js";
import { marked } from "./import-maps.js";
export const name = "core/markdown";
const gtEntity = />/gm;
const ampEntity = /&/gm;
class Renderer extends marked.Renderer {
/**
* @param {import('marked').Tokens.Code} token
* @returns {string}
*/
// @ts-expect-error - our token signature is compatible at runtime; marked's d.ts is minified
code(token) {
const { text: code, lang: infoString = "" } = token;
const { language, ...metaData } = Renderer.parseInfoString(infoString);
// regex to check whether the language is webidl
if (/(^webidl$)/i.test(language)) {
return `<pre class="idl">${code}</pre>`;
}
const html = super
.code(/** @type {any} */ ({ ...token, lang: language }))
.replace(`class="language-`, `class="`);
const { example, illegalExample } = metaData;
if (!example && !illegalExample) return html;
const title = example || illegalExample;
const className = `${language} ${example ? "example" : "illegal-example"}`;
return html.replace("<pre>", `<pre title="${title}" class="${className}">`);
}
/**
* @param {import('marked').Tokens.Image} token
*/
image(token) {
const { href, title, text } = token;
if (!title) {
return super.image(token);
}
const html = String.raw;
return html`
<figure>
<img src="${href}" alt="${text}" />
<figcaption>${title}</figcaption>
</figure>
`;
}
/**
* @param {string} infoString
*/
static parseInfoString(infoString) {
const firstSpace = infoString.search(/\s/);
if (firstSpace === -1) {
return { language: infoString };
}
const language = infoString.slice(0, firstSpace);
const metaDataStr = infoString.slice(firstSpace + 1);
let metaData;
if (metaDataStr) {
try {
metaData = JSON.parse(`{ ${metaDataStr} }`);
} catch (error) {
console.error(error);
}
}
return { language, ...metaData };
}
/**
* @param {import('marked').Tokens.Heading} token
*/
heading(token) {
const text = this.parser.parseInline(token.tokens);
const level = token.depth;
const headingWithIdRegex = /(.+)\s+{#([\w-]+)}$/;
const match = text.match(headingWithIdRegex);
if (match) {
const [, textContent, id] = match;
return `<h${level} id="${id}">${textContent}</h${level}>`;
}
return super.heading(token);
}
}
/** @type {import('marked').MarkedOptions} */
const config = {
gfm: true,
renderer: /** @type {any} */ (new Renderer()),
};
/**
* Normalize indentation by stripping the leading whitespace determined from
* the first non-empty line. This handles mixed-indentation HTML content where
* some lines (e.g., from rendered markdown inside sections) may have less
* indentation than the outer HTML structure. marked v16 is stricter: HTML
* indented 4+ spaces is treated as a code block, so we must strip outer
* indentation even when inner content has lines at column 0.
* @param {string} text
*/
function normalizeIndent(text) {
if (!text) return text;
const lines = text.trimEnd().split("\n");
const firstNonEmpty = lines.findIndex(l => l.trim());
if (firstNonEmpty === -1) return "";
const nonEmptyLines = lines.slice(firstNonEmpty);
const firstIndent = nonEmptyLines[0].search(/[^\s]/);
if (firstIndent < 1) return nonEmptyLines.join("\n");
const prefix = " ".repeat(firstIndent);
return nonEmptyLines
.map(s => (s.startsWith(prefix) ? s.slice(firstIndent) : s))
.join("\n");
}
/**
* @param {string} text
* @param {object} options
* @param {boolean} [options.inline]
* @param {boolean} [options.fromHTML]
*/
export function markdownToHtml(
text,
options = { inline: false, fromHTML: true }
) {
const normalizedLeftPad = normalizeIndent(text);
// As markdown is pulled from HTML, > and & are already escaped and
// so blockquotes aren't picked up by the parser. This fixes it.
// When text comes from a raw fetch (data-include), skip this step.
const potentialMarkdown =
options.fromHTML !== false
? normalizedLeftPad.replace(gtEntity, ">").replace(ampEntity, "&")
: normalizedLeftPad;
const result = options.inline
? marked.parseInline(potentialMarkdown, config)
: marked.parse(potentialMarkdown, config);
return result;
}
/**
* @param {string} selector
* @return {(el: Element) => Element[]}
*/
function convertElements(selector) {
return element => {
const elements = element.querySelectorAll(selector);
elements.forEach(convertElement);
return Array.from(elements);
};
}
/**
* @param {Element} element
*/
function convertElement(element) {
for (const pre of element.getElementsByTagName("pre")) {
// HTML parser implicitly removes a newline after <pre>
// which breaks reindentation algorithm
pre.prepend("\n");
}
element.innerHTML = markdownToHtml(element.innerHTML);
}
/**
* CommonMark requires additional empty newlines between markdown and HTML lines.
* This function adds them as a backward compatibility workaround.
* @param {HTMLElement} element
* @param {string} selector
*/
function workaroundBlockLevelMarkdown(element, selector) {
/** @type {NodeListOf<HTMLElement>} */
const elements = element.querySelectorAll(selector);
for (const element of elements) {
const { innerHTML } = element;
if (/^<\w/.test(innerHTML.trimStart())) {
// if the block content starts with HTML-like format
// then assume it doesn't need a workaround
continue;
}
// Double newlines are needed to be parsed as Markdown
const lines = innerHTML.split("\n");
const firstTwo = lines.slice(0, 2).join("\n");
const lastTwo = lines.slice(-2).join("\n");
if (firstTwo.trim()) {
element.prepend("\n\n");
}
if (lastTwo.trim()) {
// keep the indentation of the end tag
const indentation = getElementIndentation(element);
element.append(`\n\n${indentation}`);
}
}
}
/**
* @param {Iterable<Element>} elements
*/
function substituteWithTextNodes(elements) {
Array.from(elements).forEach(element => {
element.replaceWith(element.textContent);
});
}
const processMDSections = convertElements("[data-format='markdown']:not(body)");
const blockLevelElements =
"[data-format=markdown], section, div, address, article, aside, figure, header, main";
/**
* @param {Conf} conf
*/
export function run(conf) {
const hasMDSections = !!document.querySelector(
"[data-format=markdown]:not(body)"
);
const isMDFormat = conf.format === "markdown";
if (!isMDFormat && !hasMDSections) {
return; // Nothing to be done
}
// Only has markdown-format sections
if (!isMDFormat) {
processMDSections(document.body);
return;
}
// We transplant the UI to do the markdown processing
const rsUI = document.getElementById("respec-ui");
rsUI?.remove();
// The new body will replace the old body
const newBody = document.body.cloneNode(true);
// Marked expects markdown be flush against the left margin
// so we need to normalize the inner text of some block
// elements.
workaroundBlockLevelMarkdown(newBody, blockLevelElements);
convertElement(newBody);
// Remove links where class .nolinks
substituteWithTextNodes(newBody.querySelectorAll(".nolinks a[href]"));
// Frankenstein the whole thing back together
if (rsUI) newBody.append(rsUI);
document.body.replaceWith(newBody);
}