Skip to content

Commit 0ac7d66

Browse files
authored
Merge pull request #705 from w3c/export-minutes-tool-multiline-code
Update minute export tool to support rendered code
2 parents 5d1ab85 + c5edc76 commit 0ac7d66

File tree

1 file changed

+102
-9
lines changed

1 file changed

+102
-9
lines changed

_minutes/export-minutes.html

+102-9
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@
2222
}
2323
#extraInfoOutput {
2424
white-space: pre-wrap;
25-
height: 7em;
25+
height: 8em;
26+
overflow-y: auto;
2627
}
2728
#input, #output {
2829
flex: 1;
@@ -75,6 +76,10 @@
7576
- Issues: ${serializeIssues(issues)}
7677
- PRs: ${serializeIssues(prs)}
7778
- Mentioned issues without link to issue: ${serializeIssues(mentionedWithoutLink)}`;
79+
if (markdownText.includes("```")) {
80+
extraInfoOutput.textContent += `
81+
WARNING: ${markdownText.match(/```/g).length / 2} code blocks (\`\`\`) found. You should verify the rendered output!`;
82+
}
7883
};
7984

8085
/**
@@ -86,7 +91,7 @@
8691
- Replace boldfaced with **xx**
8792
- Replace italic with _xx_
8893
- Replace links with [text](anchor)
89-
- Replace h1, h2, h3 with #, ## and ###
94+
- Replace h1, h2, h3, h4 with #, ##, ### and ####
9095
- Format h1 header for consistency.
9196
- Replace ol,ul and li with correctly indented list items.
9297
- Fixup whitespace.
@@ -95,12 +100,9 @@
95100
let root = elemRootInput.cloneNode(true);
96101

97102
// Apply code formatting first, before escaping characters.
98-
for (let c of root.querySelectorAll(`span[style*="font-family:'Courier New'"]`)) {
99-
c.prepend("`");
100-
c.append("`");
101-
// replaceAllInTextNodes skips ` only if they are in the same text node.
102-
c.normalize();
103-
}
103+
// To avoid interference by transformations below, the code is replaced
104+
// with placeholders, which we should restore in the end.
105+
const { finalRestoreCodeBlocks } = replaceAllCodeBlocks(root);
104106

105107
// Escape < to avoid rendering as HTML.
106108
replaceAllInTextNodes(root, "<", "&lt;");
@@ -148,6 +150,9 @@
148150
for (let h of root.querySelectorAll("h3")) {
149151
h.prepend(`\n### `);
150152
}
153+
for (let h of root.querySelectorAll("h4")) {
154+
h.prepend(`\n#### `);
155+
}
151156

152157
for (let li of root.querySelectorAll("li")) {
153158
let level = 0;
@@ -190,7 +195,7 @@
190195
elem.after("\n");
191196
}
192197
// Blank line after every header.
193-
for (let elem of root.querySelectorAll("h1,h2,h3")) {
198+
for (let elem of root.querySelectorAll("h1,h2,h3,h4")) {
194199
elem.after("\n\n");
195200
}
196201

@@ -218,6 +223,8 @@
218223
// Trim leading whitespace.
219224
textContent = textContent.trim();
220225

226+
textContent = finalRestoreCodeBlocks(textContent);
227+
221228
return textContent;
222229
}
223230

@@ -248,6 +255,92 @@
248255
node.parentNode.replaceChild(document.createTextNode(proposed), node);
249256
}
250257
}
258+
259+
// Replaces code elements in |root| with.
260+
function replaceAllCodeBlocks(root, getPlaceholder) {
261+
// To prevent code blocks from being affected by text-based transformations
262+
// in the end, replace the text with placeholders.
263+
const codeTexts = new Map();
264+
let nextCodeId = 1000;
265+
function getPlaceholder(txt) {
266+
// Assuming that minutes will never contain MINUTE_PLACEHOLDER_.
267+
let placeholder = `^^^MINUTE_PLACEHOLDER_${nextCodeId++}===`;
268+
codeTexts.set(placeholder, txt);
269+
return placeholder;
270+
}
271+
function restorePlaceholders(txt) {
272+
return txt.replace(
273+
/\^\^\^MINUTE_PLACEHOLDER_\d+===/g,
274+
placeholder => codeTexts.get(placeholder)
275+
);
276+
}
277+
278+
// First pass: Detect code lines (possibly multiline code) and inline code.
279+
for (let c of root.querySelectorAll(`span[style*="font-family"][style*="monospace"]`)) {
280+
if (c.style.fontFamily.includes("monospace")) {
281+
if (c.closest("[this_is_really_a_code_block]")) {
282+
// Already processed (determined that parent is code block).
283+
continue;
284+
}
285+
if (
286+
c.parentNode.tagName === "P" &&
287+
!c.parentNode.querySelector(`span[style*="font-family"]:not([style*="monospace"])`)
288+
) {
289+
// Part of code block.
290+
c.parentNode.setAttribute("this_is_really_a_code_block", "");
291+
} else {
292+
// Has siblings that is not code.
293+
c.setAttribute("this_is_really_a_code_block", "");
294+
}
295+
}
296+
}
297+
// Second pass: Collapse multiline code with ```, use ` otherwise.
298+
for (let c of root.querySelectorAll("[this_is_really_a_code_block]")) {
299+
if (!root.contains(c)) {
300+
// Already processed and remove()d below.
301+
continue;
302+
}
303+
let codeNodes = [];
304+
for (
305+
let nod = c;
306+
nod?.matches?.("[this_is_really_a_code_block],br");
307+
nod = nod.nextSibling
308+
) {
309+
codeNodes.push(nod);
310+
}
311+
let codeText = "";
312+
for (let nod of codeNodes) {
313+
// br can be top-level, sole child of p, or wrapped in span.
314+
for (let br of nod.querySelectorAll("br")) {
315+
br.replaceWith("\n");
316+
}
317+
codeText += nod.textContent;
318+
if (nod.tagName === "P" || nod.tagName === "BR") {
319+
codeText += "\n";
320+
}
321+
}
322+
codeText = codeText.replace(/\n+$/, "");
323+
324+
// Replace actual content with placeholder to prevent other logic such as
325+
// the link wrapping / text replacement logic from mangling the code block.
326+
c.textContent = getPlaceholder(codeText);
327+
328+
if (codeText.trim().includes("\n")) {
329+
c.textContent = "```\n" + codeText + "\n```";
330+
} else {
331+
c.textContent = "`" + codeText + "`";
332+
}
333+
// codeNodes[0] === c; remove all except c.
334+
codeNodes.slice(1).forEach(nod => nod.remove());
335+
}
336+
337+
function finalRestoreCodeBlocks(textContent) {
338+
textContent = restorePlaceholders(textContent);
339+
return textContent;
340+
}
341+
342+
return { finalRestoreCodeBlocks };
343+
}
251344
</script>
252345
</body>
253346
</html>

0 commit comments

Comments
 (0)