Skip to content

Commit c5edc76

Browse files
committed
Update minute export tool to support rendered code
Until now, code formatting required backticks in the source document. But Google Docs can also contain inline code (greenish text) or code blocks, via Markdown mode (Tools > Preferences > "Enable Markdown"). Previously, this was converted to plain text. With the updated tool, inline code is wrapped in single backticks, and code blocks are wrapped in triple backticks. This also adds h4 support because a recent meeting used h4 headings.
1 parent 6a533d3 commit c5edc76

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)