Skip to content

Commit 964ef74

Browse files
authored
Merge pull request #150 from eecs485staff/multi-line-codeblocks
[Code Blocks] Preserve HTML tags/attributes across lines
2 parents 4f47135 + 34dd1c3 commit 964ef74

File tree

3 files changed

+77
-17
lines changed

3 files changed

+77
-17
lines changed

VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
1.6.3
1+
1.6.4.d

src_js/components/main_content/__tests__/useEnhancedCodeBlocks.test.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,9 @@ jest.mock('../../../Config', () => ({
88

99
const CONSOLE_BLOCK = `<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span>python3 <span class="nt">--version</span> <span class="c"># NOTE: Your Python version may be different.</span>
1010
<span class="go">Python 3.7.4
11+
Copyright (c) 2001-2019 Python Software Foundation.
1112
</span></code></pre></div></div>`;
12-
const CONSOLE_BLOCK_NUM_LINES = 2;
13+
const CONSOLE_BLOCK_NUM_LINES = 3;
1314

1415
const PLAINTEXT_BLOCK = `<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ pwd
1516
/users/seshrs
@@ -52,7 +53,7 @@ describe('useEnhancedCodeBlocks', () => {
5253

5354
const tbody = codeblocks[0].childNodes[0] as HTMLElement;
5455
expect(tbody.childElementCount).toBe(CONSOLE_BLOCK_NUM_LINES);
55-
expect(CONSOLE_BLOCK_NUM_LINES).toBe(2);
56+
expect(CONSOLE_BLOCK_NUM_LINES).toBe(3);
5657

5758
let row, lineNum;
5859

@@ -72,6 +73,16 @@ describe('useEnhancedCodeBlocks', () => {
7273
expect((row.childNodes[1] as HTMLElement).innerHTML).toBe(
7374
'<span class="go">Python 3.7.4</span>',
7475
);
76+
77+
// Row 3
78+
row = tbody.childNodes[2] as HTMLElement;
79+
expect(row.tagName).toBe('TR');
80+
expect(row.childElementCount).toBe(2);
81+
lineNum = row.childNodes[0] as HTMLElement;
82+
expect(lineNum.getAttribute('data-line-number')).toBe('3');
83+
expect((row.childNodes[1] as HTMLElement).innerHTML).toBe(
84+
'<span class="go">Copyright (c) 2001-2019 Python Software Foundation.</span>',
85+
);
7586
});
7687

7788
describe('code selection by clicking line numbers', () => {

src_js/components/main_content/useEnhancedCodeBlocks.tsx

Lines changed: 63 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -41,22 +41,22 @@ export default function useEnhancedCodeBlocks(
4141
// First enhance codeblocks formatted by Jekyll + Rouge
4242
const numCodeBlocks = enhanceBlocks(
4343
mainElRef.current.querySelectorAll('div.highlighter-rouge'),
44-
getRawContentsFromJekyllRougeCodeblock,
44+
getCodeElFromJekyllRougeCodeblock,
4545
0,
4646
);
4747
// Then attempt to enhance ordinary <pre> blocks.
4848
enhanceBlocks(
4949
mainElRef.current.querySelectorAll('pre'),
50-
getRawContentsFromPreCodeblock,
50+
getCodeElFromPreCodeblock,
5151
numCodeBlocks,
5252
);
5353

5454
return () => {};
5555
}
5656

57-
function getRawContentsFromJekyllRougeCodeblock(
57+
function getCodeElFromJekyllRougeCodeblock(
5858
codeblock: HTMLElement,
59-
): string | null {
59+
): HTMLElement | null {
6060
// The original structure of a codeblock:
6161
// <div
6262
// class="highlighter-rouge language-[lang]"
@@ -87,13 +87,10 @@ function getRawContentsFromJekyllRougeCodeblock(
8787
return null;
8888
}
8989

90-
return (codeEl as HTMLElement).innerHTML;
90+
return codeEl as HTMLElement;
9191
}
9292

93-
function getRawContentsFromPreCodeblock(
94-
codeblock_: HTMLElement,
95-
): string | null {
96-
let codeblock = codeblock_;
93+
function getCodeElFromPreCodeblock(codeblock: HTMLElement): HTMLElement | null {
9794
// The structure of a <pre> codeblock:
9895
// <pre>
9996
// <code> <!-- OPTIONAL -->
@@ -104,9 +101,9 @@ function getRawContentsFromPreCodeblock(
104101
codeblock.childNodes.length === 1 &&
105102
codeblock.firstElementChild?.tagName === 'CODE'
106103
) {
107-
codeblock = codeblock.firstElementChild as HTMLElement;
104+
return codeblock.firstElementChild as HTMLElement;
108105
}
109-
return codeblock.innerHTML.trim();
106+
return codeblock;
110107
}
111108

112109
/**
@@ -116,7 +113,7 @@ function getRawContentsFromPreCodeblock(
116113
*/
117114
function enhanceBlocks(
118115
codeblocks: NodeListOf<HTMLElement>,
119-
getContents: (node: HTMLElement) => string | null,
116+
getCodeEl: (node: HTMLElement) => HTMLElement | null,
120117
startId = 0,
121118
): number {
122119
let nextCodeBlockId = startId;
@@ -141,10 +138,11 @@ function enhanceBlocks(
141138
return;
142139
}
143140

144-
const codeblockContents = getContents(codeblock);
145-
if (codeblockContents == null) {
141+
const codeblockContentsEl = getCodeEl(codeblock);
142+
if (codeblockContentsEl == null) {
146143
return;
147144
}
145+
const codeblockContents = getCodeblockContents(codeblockContentsEl);
148146

149147
const title = codeblock.dataset['title'] || null;
150148
const anchorId = title
@@ -529,3 +527,54 @@ function createCodeBlockAnchorId(
529527
): string {
530528
return `${slugify(title)}-${codeblockNumericId}`;
531529
}
530+
531+
/**
532+
* Given a codeblock / pre element, return a string reprensenting the HTML of
533+
* the codeblock.
534+
*
535+
* One edge case that this method handles: Lines split within a single span.
536+
* Consider the following codeblock:
537+
* ```html
538+
* <code><span class="c">Line 1</span>
539+
* <span class="c">Line 2</span>
540+
* <span class="c">Line 3
541+
* Line 4</span></code>
542+
* ```
543+
* Since the rest of the code assumes that "\n" characters separate lines, we
544+
* need to ensure that each line starts with its own span if necessary. The
545+
* output of this method should be:
546+
* ```html
547+
* <code><span class="c">Line 1</span>
548+
* <span class="c">Line 2</span>
549+
* <span class="c">Line 3</span>
550+
* <span class="c">Line 4</span></code>
551+
* ```
552+
*/
553+
function getCodeblockContents(codeEl: HTMLElement): string {
554+
const resultNode = codeEl.cloneNode() as HTMLElement;
555+
codeEl.childNodes.forEach((childNode) => {
556+
if (childNode.nodeType === Node.ELEMENT_NODE) {
557+
if (
558+
(childNode as HTMLElement).tagName === 'SPAN' &&
559+
childNode.textContent != null
560+
) {
561+
const lines = childNode.textContent.split('\n');
562+
lines.forEach((line, i) => {
563+
// Ignore empty lines within a span, but still insert the \n.
564+
if (line) {
565+
const lineEl = childNode.cloneNode() as HTMLElement;
566+
lineEl.textContent = line;
567+
resultNode.appendChild(lineEl);
568+
}
569+
// Append a new line except after the last line in this span
570+
if (i < lines.length - 1) {
571+
resultNode.appendChild(document.createTextNode('\n'));
572+
}
573+
});
574+
}
575+
} else {
576+
resultNode.appendChild(childNode.cloneNode(true));
577+
}
578+
});
579+
return resultNode.innerHTML.trim();
580+
}

0 commit comments

Comments
 (0)