From a8e297c875270ef7792639c3acb3bdcebd93ac3c Mon Sep 17 00:00:00 2001 From: Ryo Armanda Date: Sat, 13 Feb 2021 01:17:37 +0800 Subject: [PATCH 01/19] Develop partial text highlighting --- packages/core/src/html/NodeProcessor.js | 139 ++++++++++++++++++ .../markdown-it/highlight/HighlightRule.js | 33 +++-- .../highlight/HighlightRuleComponent.js | 22 +++ packages/core/src/lib/markdown-it/index.js | 2 +- packages/core/src/utils/index.js | 14 ++ 5 files changed, 198 insertions(+), 12 deletions(-) diff --git a/packages/core/src/html/NodeProcessor.js b/packages/core/src/html/NodeProcessor.js index 18dc60117e..70397935b8 100644 --- a/packages/core/src/html/NodeProcessor.js +++ b/packages/core/src/html/NodeProcessor.js @@ -176,6 +176,142 @@ class NodeProcessor { cheerio(node).remove(); } + /* + * Code blocks + */ + + /** + * Applies pending highlighting to the code block. + * This looks into each line for highlighting data, and if found, + * traverses over the line and applies the highlight. + * @param node Root of the code block element, which is the 'pre' node + */ + static _highlightCodeBlock(node) { + const codeNode = node.children.find(c => c.name === 'code'); + if (!codeNode) { + return; + } + + codeNode.children.forEach((line) => { + if (!_.has(line.attribs, 'hl-start') || !_.has(line.attribs, 'hl-end')) { + return; + } + + const start = parseInt(line.attribs['hl-start'], 10); + const end = parseInt(line.attribs['hl-end'], 10); + + this._traverseLinePart(line, start, end); + + delete line.attribs['hl-start']; + delete line.attribs['hl-end']; + }); + } + + /** + * Traverses a line part and applies highlighting if necessary. + * @param node The node of the line part to be traversed + * @param hlStart The highlight start position, relative to the start of the line part + * @param hlEnd The highlight end position, relative to the start of the line part + * @returns {[number, boolean | [number, number]]} An array of two items. + */ + static _traverseLinePart(node, hlStart, hlEnd) { + // Return value is an array of two items: + // 1. The number of characters traversed + // 2. Highlighting data to be used by the node's parent. It can be: + // - true (ask to apply highlighting from parent) + // - false (do not process this node further) + // - array of two numbers (only for text nodes, inform parent to + // highlight the text at specified range) + + if (hlEnd <= 0) { + // Highlight end has passed, no need to traverse further + return [0, false]; + } + + if (node.type === 'text') { + // Node is a text node. It is not an inherent HTML element of its own, + // so to actually highlight this text, we have to ask to apply at its parent. + + const cleanedText = utils.unescapeHtml(node.data); + const textLength = cleanedText.length; + + if (hlStart >= textLength) { + // Highlight start is not in this text + return [textLength, false]; + } + + if (hlStart <= 0 && hlEnd >= textLength) { + // Highlight spans across the entirety of text + return [textLength, true]; + } + + // Partial text highlighting + return [textLength, [hlStart, hlEnd]]; + } + + // The remaining possibility is that node is a tag node. + // It has at least one child (to contain the text content). + // It may have more children, such as inner tag nodes. + let curr = 0; + const shouldHighlight = node.children.map((child) => { + const [traversed, data] = this._traverseLinePart( + child, hlStart - curr, hlEnd - curr, + ); + curr += traversed; + + return data; + }); + + if (shouldHighlight.every(v => v === true)) { + // Every child wants highlight to be applied at node level + // For conciseness, ask for the node's parent to highlight, if possible + return [true, curr]; + } + + // If node level highlighting is not possible, highlight the individual children as needed + // For tag nodes, it is easy, just add the highlight class + // For text nodes, it is trickier, as we have to wrap the text inside a first + // Essentially, we have to change the text node to become a tag node + + node.children.forEach((child, idx) => { + if (shouldHighlight[idx] === false) { + return; + } + + if (child.type === 'tag') { + child.attribs.class = child.attribs.class ? `${child.attribs.class} highlighted` : 'highlighted'; + return; + } + + const text = child.data; + let newElement; + + if (shouldHighlight[idx] === true) { + [newElement] = cheerio.parseHTML(`${text}`); + } else { + const [start, end] = shouldHighlight[idx]; + const cleaned = utils.unescapeHtml(text); + const split = [cleaned.substring(0, start), cleaned.substring(start, end), cleaned.substring(end)]; + const [pre, highlighted, post] = split.map(md.utils.escapeHtml); + [newElement] = cheerio.parseHTML( + `${pre}${highlighted}${post}`, + ); + } + + delete newElement.root; + node.children[idx] = newElement; + }); + + // Set the references accordingly + node.children.forEach((child, idx) => { + child.parent = node; + child.prev = idx > 0 ? node.children[idx - 1] : null; + child.next = idx < node.children.length - 1 ? node.children[idx + 1] : null; + }); + + return [curr, false]; + } + /* * Panels */ @@ -746,6 +882,9 @@ class NodeProcessor { postProcessNode(node) { try { switch (node.name) { + case 'pre': + NodeProcessor._highlightCodeBlock(node); + break; case 'panel': NodeProcessor._assignPanelId(node); break; diff --git a/packages/core/src/lib/markdown-it/highlight/HighlightRule.js b/packages/core/src/lib/markdown-it/highlight/HighlightRule.js index 2602b12353..e93c406d8f 100644 --- a/packages/core/src/lib/markdown-it/highlight/HighlightRule.js +++ b/packages/core/src/lib/markdown-it/highlight/HighlightRule.js @@ -30,21 +30,23 @@ class HighlightRule { } applyHighlight(line) { - const isLineSlice = this.ruleComponents.length === 1 && this.ruleComponents[0].isSlice; - if (this.isLineRange()) { const shouldWholeLine = this.ruleComponents.some(comp => comp.isUnboundedSlice()); return shouldWholeLine ? HighlightRule._highlightWholeLine(line) - : HighlightRule._highlightTextOnly(line); + : HighlightRule._highlightWholeText(line); } - + + const isLineSlice = this.ruleComponents.length === 1 && this.ruleComponents[0].isSlice; if (isLineSlice) { - // TODO: Implement slice-index based highlighting - return HighlightRule._highlightWholeLine(line); + const [slice] = this.ruleComponents; + return slice.isUnboundedSlice() + ? HighlightRule._highlightWholeLine(line) + : HighlightRule._highlightPartOfText(line, slice.computeLineBounds(line)); } - - return HighlightRule._highlightTextOnly(line); + + // Line number only + return HighlightRule._highlightWholeText(line); } static _highlightWholeLine(codeStr) { @@ -57,10 +59,19 @@ class HighlightRule { const content = codeStr.substr(codeStartIdx); return [indents, content]; } - - static _highlightTextOnly(codeStr) { + + static _highlightWholeText(codeStr) { const [indents, content] = HighlightRule._splitCodeAndIndentation(codeStr); - return `${indents}${content}\n` + return `${indents}${content}\n`; + } + + static _highlightPartOfText(codeStr, bounds) { + const [indents,] = HighlightRule._splitCodeAndIndentation(codeStr); + const [start, end] = bounds; + // Note: As part-of-text highlighting requires walking over the node of the generated + // html by highlight.js, highlighting will be applied in NodeProcessor instead. + // hl-start and hl-end is used to pass over the bounds. + return `${codeStr}\n`; } isLineRange() { diff --git a/packages/core/src/lib/markdown-it/highlight/HighlightRuleComponent.js b/packages/core/src/lib/markdown-it/highlight/HighlightRuleComponent.js index 63e0073028..034023c4c3 100644 --- a/packages/core/src/lib/markdown-it/highlight/HighlightRuleComponent.js +++ b/packages/core/src/lib/markdown-it/highlight/HighlightRuleComponent.js @@ -46,6 +46,28 @@ class HighlightRuleComponent { isUnboundedSlice() { return this.isSlice && this.bounds.length === 0; } + + /** + * Computes the actual bounds of the highlight rule given a line, + * comparing the rule's bounds and the line's range. + * + * If the rule does not specify a start/end bound, the computed bound will default + * to the start/end of the line. + * + * @param line The line to be checked + * @returns {[number, number]} The actual bounds computed + */ + computeLineBounds(line) { + const [lineStart, lineEnd] = [0, line.length - 1]; + if (!this.isSlice || this.isUnboundedSlice()) { + return [lineStart, lineEnd]; + } + + const [boundStart, boundEnd] = this.bounds; + const start = lineStart <= boundStart && boundStart <= lineEnd ? boundStart : lineStart; + const end = lineStart <= boundEnd && boundEnd <= lineEnd ? boundEnd : lineEnd; + return [start, end]; + } } module.exports = { diff --git a/packages/core/src/lib/markdown-it/index.js b/packages/core/src/lib/markdown-it/index.js index 335f050a9b..a9d7f952f6 100644 --- a/packages/core/src/lib/markdown-it/index.js +++ b/packages/core/src/lib/markdown-it/index.js @@ -107,7 +107,7 @@ markdownIt.renderer.rules.fence = (tokens, idx, options, env, slf) => { // wrap all lines with so we can number them str = lines.map((line, index) => { const currentLineNumber = index + 1; - const rule = highlightRules.find(rule => rule.shouldApplyHighlight(currentLineNumber)) + const rule = highlightRules.find(rule => rule.shouldApplyHighlight(currentLineNumber)); if (rule) { return rule.applyHighlight(line); } diff --git a/packages/core/src/utils/index.js b/packages/core/src/utils/index.js index 3c4036cdd6..fe8da93f61 100644 --- a/packages/core/src/utils/index.js +++ b/packages/core/src/utils/index.js @@ -9,6 +9,13 @@ const { markdownFileExts, } = require('../constants'); +const htmlUnescapedMapping = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', +}; + module.exports = { getCurrentDirectoryBase() { return path.basename(process.cwd()); @@ -105,4 +112,11 @@ module.exports = { return text.join('').trim(); }, + unescapeHtml(str) { + let unescaped = str; + Object.entries(htmlUnescapedMapping).forEach(([key, value]) => { + unescaped = unescaped.split(key).join(value); + }); + return unescaped; + }, }; From 913f05ed409026bce100bb45857d7be8e699bdb1 Mon Sep 17 00:00:00 2001 From: Ryo Armanda Date: Sat, 13 Feb 2021 01:55:55 +0800 Subject: [PATCH 02/19] Incorporate line slice syntax on line range processing --- .../markdown-it/highlight/HighlightRule.js | 26 ++++++++++++++++--- packages/core/src/lib/markdown-it/index.js | 2 +- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/packages/core/src/lib/markdown-it/highlight/HighlightRule.js b/packages/core/src/lib/markdown-it/highlight/HighlightRule.js index e93c406d8f..dbeeae37ca 100644 --- a/packages/core/src/lib/markdown-it/highlight/HighlightRule.js +++ b/packages/core/src/lib/markdown-it/highlight/HighlightRule.js @@ -29,12 +29,30 @@ class HighlightRule { return atLineNumber; } - applyHighlight(line) { + applyHighlight(line, lineNumber) { if (this.isLineRange()) { + const [startCompare, endCompare] = this.ruleComponents.map(comp => comp.compareLine(lineNumber)); + + // For cases like 2[:]-3 (or 2-3[:]), the highlight would be line highlight + // across all the ranges const shouldWholeLine = this.ruleComponents.some(comp => comp.isUnboundedSlice()); - return shouldWholeLine - ? HighlightRule._highlightWholeLine(line) - : HighlightRule._highlightWholeText(line); + if (shouldWholeLine) { + return HighlightRule._highlightWholeLine(line); + } + + if (startCompare < 0 && endCompare > 0) { + // In-between range + return HighlightRule._highlightWholeText(line); + } + + // At the range boundaries + const [start, end] = this.ruleComponents; + const appliedRule = startCompare === 0 ? start : end; + + // Instead of redefining how to highlight according to the rule (which is already laid + // out on the next few cases), we create a new HighlightRule consisting of only the applied + // rule and call apply again + return new HighlightRule([appliedRule]).applyHighlight(line, lineNumber); } const isLineSlice = this.ruleComponents.length === 1 && this.ruleComponents[0].isSlice; diff --git a/packages/core/src/lib/markdown-it/index.js b/packages/core/src/lib/markdown-it/index.js index a9d7f952f6..5addd6e5a3 100644 --- a/packages/core/src/lib/markdown-it/index.js +++ b/packages/core/src/lib/markdown-it/index.js @@ -109,7 +109,7 @@ markdownIt.renderer.rules.fence = (tokens, idx, options, env, slf) => { const currentLineNumber = index + 1; const rule = highlightRules.find(rule => rule.shouldApplyHighlight(currentLineNumber)); if (rule) { - return rule.applyHighlight(line); + return rule.applyHighlight(line, currentLineNumber); } // not highlighted From 70ff67d4257b1dbbd07f9ef659f60320265bdca4 Mon Sep 17 00:00:00 2001 From: Ryo Armanda Date: Sat, 13 Feb 2021 02:28:44 +0800 Subject: [PATCH 03/19] Update codeblock documentations --- docs/userGuide/syntax/code.mbdf | 44 +++++++++++++++++++++++++-------- 1 file changed, 34 insertions(+), 10 deletions(-) diff --git a/docs/userGuide/syntax/code.mbdf b/docs/userGuide/syntax/code.mbdf index 08e02b3358..c72b7b02f5 100644 --- a/docs/userGuide/syntax/code.mbdf +++ b/docs/userGuide/syntax/code.mbdf @@ -61,23 +61,47 @@ function add(a, b) { ##### Line highlighting -To highlight lines, add the attribute `highlight-lines` as shown below. +You can add the `highlight-lines` attribute to add highlighting to your code block. Refer to the example below for +a typical usage of the attribute. -You can specify highlighting in many different ways, depending on how you want it to be. There are two main variants: +The value of `highlight-lines` is composed of *highlight rules*, separated by commas. +These rules dictate where and how MarkBind should highlight your code block. -**Text-only highlighting** +You can specify the rules in many different ways, depending on how you want it to be. There are three main variants: +full text, partial text, or full line highlighting. -To highlight only the text portion of the line, you can just use the line numbers as is. +**Full text highlighting** -For ranges of lines, join the two line numbers with a dash sign (`-`). +To highlight the entirety of the text portion of the line, you can just use the line numbers as is, such as `3`. +Note that the numbers should correspond with the line numbers that will be displayed, so if you have changed the +numbering via the `start-from` attribute, you will have to follow that numbering as well. -**Whole-line highlighting** +For text-only range highlighting, join the two line numbers with a dash sign (`-`). -If you wish to highlight a full line (including whitespaces) or ranges of it, you can leverage MarkBind's own _line-slice_ syntax. Line-slices are in the form of `lineNumber[:]`, e.g. `2[:]`. - -This variant's format is very similar to the previous, but instead use line-slices rather than line numbers. +**Partial text highlighting** + +You can also highlight just a part of the text portion of the line. MarkBind has a _line-slice_ syntax to help you +with that. + +The line-slice syntax is of the form `lineNumber[start:end]`, where `lineNumber` is the line number (subject to the +numbering conditions as explained above), `start` and `end` denote the range of character positions +that will be highlighted. + +Character positions start from `0`, which denotes the first non-whitespace character in the line, upwards. +Note that the highlight includes character at `start` but does not include character at `end`. + +You can omit either `start` or `end` to tell MarkBind either to highlight from the start or until the end, respectively. + +Also, you can start or end a text-only range highlight with a line-slice syntax to indicate partial text +highlight at the beginning or end of the range, respectively. + +**Full line highlighting** + +If you wish to highlight a full line (which includes indentations), you can use the line-slice syntax as well, +but with `start` *and* `end` omitted. We call this the empty line-slices. -For ranges, you only need to use line-slices on either ends. +To do a full-line range highlighting, you only need to use empty line-slices on either ends of the usual +text-only range highlight. From 52c65138471af875ea46d0c035c80f0e0120181d Mon Sep 17 00:00:00 2001 From: Ryo Armanda Date: Sat, 13 Feb 2021 02:29:49 +0800 Subject: [PATCH 04/19] Update codeblock docs example --- docs/userGuide/syntax/code.mbdf | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/docs/userGuide/syntax/code.mbdf b/docs/userGuide/syntax/code.mbdf index c72b7b02f5..12f781d484 100644 --- a/docs/userGuide/syntax/code.mbdf +++ b/docs/userGuide/syntax/code.mbdf @@ -107,7 +107,7 @@ text-only range highlight. -```java {highlight-lines="1,3[:],6-8,10[:]-12"} +```java {highlight-lines="1[:],3,4[8:18],6[:]-8,10-12,14[12:],18[12:]-20"} import java.util.List; public class Inventory { @@ -121,7 +121,13 @@ public class Inventory { return items.isEmpty(); } - //... + public Item getItem(idx: int) { + return items.get(idx); + } + + public void addItem(item: Item) { + return items.add(item); + } } ``` From f08167de1b474c4913d64baebd9f3ea3f948dcd0 Mon Sep 17 00:00:00 2001 From: Ryo Armanda Date: Sat, 13 Feb 2021 02:42:12 +0800 Subject: [PATCH 05/19] Update tests --- .../test_site/expected/testCodeBlocks.html | 18 ++++++++++++++- .../functional/test_site/testCodeBlocks.md | 22 ++++++++++++++++++- 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/packages/cli/test/functional/test_site/expected/testCodeBlocks.html b/packages/cli/test/functional/test_site/expected/testCodeBlocks.html index 38ac04cb40..b8afcde087 100644 --- a/packages/cli/test/functional/test_site/expected/testCodeBlocks.html +++ b/packages/cli/test/functional/test_site/expected/testCodeBlocks.html @@ -223,13 +223,29 @@

Test 19 20 -

**highlight-lines attr with line-slice syntax of empty indices should highlight leading/trailing spaces

+

highlight-lines attr with empty line-slice syntax should highlight leading/trailing spaces

<foo>
   <bar type="name">goo</bar>
   <baz type="name">goo</baz>
   <qux type="name">goo</qux>
   <quux type="name">goo</quux>
 </foo>
+
+

highlight-lines attr with full line-slice syntax should highlight only at specified range

+
<foo>
+  <bar type="name">goo</bar>
+  <baz type="name">goo</baz>
+  <qux type="name">goo</qux>
+  <quux type="name">goo</quux>
+</foo>
+
+

highlight-lines attr with partially filled line-slice syntax should defaults highlight to start/end of line

+
<foo>
+  <bar type="name">goo</bar>
+  <baz type="name">goo</baz>
+  <qux type="name">goo</qux>
+  <quux type="name">goo</quux>
+</foo>
 

Should render correctly with heading

diff --git a/packages/cli/test/functional/test_site/testCodeBlocks.md b/packages/cli/test/functional/test_site/testCodeBlocks.md index 4efb30b03b..ba9d86eda8 100644 --- a/packages/cli/test/functional/test_site/testCodeBlocks.md +++ b/packages/cli/test/functional/test_site/testCodeBlocks.md @@ -56,7 +56,7 @@ Content in a fenced code block 20 ``` -**`highlight-lines` attr with line-slice syntax of empty indices should highlight leading/trailing spaces +**`highlight-lines` attr with empty line-slice syntax should highlight leading/trailing spaces** ```xml {highlight-lines="2[:],4[:]-5[:]"} goo @@ -66,6 +66,26 @@ Content in a fenced code block ``` +**`highlight-lines` attr with full line-slice syntax should highlight only at specified range** +```xml {highlight-lines="1[1:4],2[5:13],3[2:10]-4,5-6[1:4]"} + + goo + goo + goo + goo + +``` + +**`highlight-lines` attr with partially filled line-slice syntax should defaults highlight to start/end of line** +```xml {highlight-lines="1[1:],2[:13],3[2:]-4,5-6[:2]"} + + goo + goo + goo + goo + +``` + **Should render correctly with heading** ```{heading="A heading"} From cd7c0ea85074f78f71b101023a37246ad39a2545 Mon Sep 17 00:00:00 2001 From: Ryo Armanda Date: Sun, 14 Feb 2021 14:44:23 +0800 Subject: [PATCH 06/19] Address reviews --- packages/core/src/html/NodeProcessor.js | 10 +++++----- .../markdown-it/highlight/HighlightRuleComponent.js | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/core/src/html/NodeProcessor.js b/packages/core/src/html/NodeProcessor.js index 70397935b8..ce6038137c 100644 --- a/packages/core/src/html/NodeProcessor.js +++ b/packages/core/src/html/NodeProcessor.js @@ -253,7 +253,7 @@ class NodeProcessor { // It has at least one child (to contain the text content). // It may have more children, such as inner tag nodes. let curr = 0; - const shouldHighlight = node.children.map((child) => { + const highlightData = node.children.map((child) => { const [traversed, data] = this._traverseLinePart( child, hlStart - curr, hlEnd - curr, ); @@ -262,7 +262,7 @@ class NodeProcessor { return data; }); - if (shouldHighlight.every(v => v === true)) { + if (highlightData.every(v => v === true)) { // Every child wants highlight to be applied at node level // For conciseness, ask for the node's parent to highlight, if possible return [true, curr]; @@ -274,7 +274,7 @@ class NodeProcessor { // Essentially, we have to change the text node to become a tag node node.children.forEach((child, idx) => { - if (shouldHighlight[idx] === false) { + if (!highlightData[idx]) { return; } @@ -286,10 +286,10 @@ class NodeProcessor { const text = child.data; let newElement; - if (shouldHighlight[idx] === true) { + if (highlightData[idx] === true) { [newElement] = cheerio.parseHTML(`${text}`); } else { - const [start, end] = shouldHighlight[idx]; + const [start, end] = highlightData[idx]; const cleaned = utils.unescapeHtml(text); const split = [cleaned.substring(0, start), cleaned.substring(start, end), cleaned.substring(end)]; const [pre, highlighted, post] = split.map(md.utils.escapeHtml); diff --git a/packages/core/src/lib/markdown-it/highlight/HighlightRuleComponent.js b/packages/core/src/lib/markdown-it/highlight/HighlightRuleComponent.js index 034023c4c3..1071b77502 100644 --- a/packages/core/src/lib/markdown-it/highlight/HighlightRuleComponent.js +++ b/packages/core/src/lib/markdown-it/highlight/HighlightRuleComponent.js @@ -64,8 +64,8 @@ class HighlightRuleComponent { } const [boundStart, boundEnd] = this.bounds; - const start = lineStart <= boundStart && boundStart <= lineEnd ? boundStart : lineStart; - const end = lineStart <= boundEnd && boundEnd <= lineEnd ? boundEnd : lineEnd; + const start = (lineStart <= boundStart) && (boundStart <= lineEnd) ? boundStart : lineStart; + const end = (lineStart <= boundEnd) && (boundEnd <= lineEnd) ? boundEnd : lineEnd; return [start, end]; } } From 761066c67505e452f6c2d287a8e3aa8188dea2a5 Mon Sep 17 00:00:00 2001 From: Ryo Armanda Date: Mon, 15 Feb 2021 03:24:58 +0800 Subject: [PATCH 07/19] Add more convenience syntax for partial text highlight --- .../markdown-it/highlight/HighlightRule.js | 25 ++++++++- .../highlight/HighlightRuleComponent.js | 53 +++++++++++++++---- packages/core/src/lib/markdown-it/index.js | 48 ++++++++++------- packages/core/src/utils/index.js | 1 + 4 files changed, 95 insertions(+), 32 deletions(-) diff --git a/packages/core/src/lib/markdown-it/highlight/HighlightRule.js b/packages/core/src/lib/markdown-it/highlight/HighlightRule.js index dbeeae37ca..3f5a8d35c9 100644 --- a/packages/core/src/lib/markdown-it/highlight/HighlightRule.js +++ b/packages/core/src/lib/markdown-it/highlight/HighlightRule.js @@ -10,13 +10,30 @@ class HighlightRule { static parseRule(ruleString) { const components = ruleString.split('-').map(HighlightRuleComponent.parseRuleComponent); + if (components.some(c => !c)) { + // Not all components are properly parsed, which means + // the rule itself is not proper + return null; + } + return new HighlightRule(components); } offsetLines(offset) { this.ruleComponents.forEach(comp => comp.offsetLineNumber(offset)); } - + + convertLinePartToLineSlice(lines) { + if (!this.isLinePart()) { + return; + } + + const [part] = this.ruleComponents; + const line = lines[part.lineNumber - 1]; // line numbers are 1-based + const {1: content} = HighlightRule._splitCodeAndIndentation(line); + part.convertPartToSlice(content); + } + shouldApplyHighlight(lineNumber) { const compares = this.ruleComponents.map(comp => comp.compareLine(lineNumber)); if (this.isLineRange()) { @@ -84,7 +101,7 @@ class HighlightRule { } static _highlightPartOfText(codeStr, bounds) { - const [indents,] = HighlightRule._splitCodeAndIndentation(codeStr); + const {0: indents} = HighlightRule._splitCodeAndIndentation(codeStr); const [start, end] = bounds; // Note: As part-of-text highlighting requires walking over the node of the generated // html by highlight.js, highlighting will be applied in NodeProcessor instead. @@ -92,6 +109,10 @@ class HighlightRule { return `${codeStr}\n`; } + isLinePart() { + return (this.ruleComponents.length === 1) && (this.ruleComponents[0].linePart !== ""); + } + isLineRange() { return this.ruleComponents.length === 2; } diff --git a/packages/core/src/lib/markdown-it/highlight/HighlightRuleComponent.js b/packages/core/src/lib/markdown-it/highlight/HighlightRuleComponent.js index 1071b77502..4e5be45714 100644 --- a/packages/core/src/lib/markdown-it/highlight/HighlightRuleComponent.js +++ b/packages/core/src/lib/markdown-it/highlight/HighlightRuleComponent.js @@ -1,17 +1,19 @@ const LINESLICE_REGEX = new RegExp('(\\d+)\\[(\\d*):(\\d*)]'); +const LINEPART_REGEX = new RegExp('(\\d+)\\[(["\'])((?:\\\\.|[^\\\\])*?)\\2]'); class HighlightRuleComponent { - constructor(lineNumber, isSlice, bounds) { + constructor(lineNumber, isSlice = false, bounds = [], linePart = "") { this.lineNumber = lineNumber; - this.isSlice = isSlice || false; - this.bounds = bounds || []; + this.isSlice = isSlice; + this.bounds = bounds; + this.linePart = linePart; } static parseRuleComponent(compString) { // tries to match with the line slice pattern - const matches = compString.match(LINESLICE_REGEX); - if (matches) { - const groups = matches.slice(1); // keep the capturing group matches only + const linesliceMatch = compString.match(LINESLICE_REGEX); + if (linesliceMatch) { + const groups = linesliceMatch.slice(1); // discard full match const lineNumber = parseInt(groups.shift(), 10); const isUnbounded = groups.every(x => x === ''); @@ -23,9 +25,23 @@ class HighlightRuleComponent { return new HighlightRuleComponent(lineNumber, true, bounds); } - // match fails, so it is just line numbers - const lineNumber = parseInt(compString, 10); - return new HighlightRuleComponent(lineNumber); + const linepartMatch = compString.match(LINEPART_REGEX); + if (linepartMatch) { + const groups = linepartMatch.slice(1); // discard full match + const lineNumber = parseInt(groups.shift(), 10); + groups.shift(); // discard quote group match + const part = groups.shift().replace(/\\'/g, '\'').replace(/\\"/g, '"'); // unescape quotes + + return new HighlightRuleComponent(lineNumber, false, [], part); + } + + if (!isNaN(compString)) { // ensure the whole string can be converted to number + const lineNumber = parseInt(compString, 10); + return new HighlightRuleComponent(lineNumber); + } + + // the string is an improperly written rule + return null; } offsetLineNumber(offset) { @@ -58,8 +74,12 @@ class HighlightRuleComponent { * @returns {[number, number]} The actual bounds computed */ computeLineBounds(line) { + if (!this.isSlice) { + return [0, 0]; + } + const [lineStart, lineEnd] = [0, line.length - 1]; - if (!this.isSlice || this.isUnboundedSlice()) { + if (this.isUnboundedSlice()) { return [lineStart, lineEnd]; } @@ -68,6 +88,19 @@ class HighlightRuleComponent { const end = (lineStart <= boundEnd) && (boundEnd <= lineEnd) ? boundEnd : lineEnd; return [start, end]; } + + convertPartToSlice(content) { + if (!this.linePart) { + return [0, 0]; + } + + const start = content.indexOf(this.linePart); + const bounds = start === -1 ? [0, 0] : [start, start + this.linePart.length]; + + this.isSlice = true; + this.bounds = bounds; + this.linePart = ""; + } } module.exports = { diff --git a/packages/core/src/lib/markdown-it/index.js b/packages/core/src/lib/markdown-it/index.js index 5addd6e5a3..116708942b 100644 --- a/packages/core/src/lib/markdown-it/index.js +++ b/packages/core/src/lib/markdown-it/index.js @@ -56,7 +56,33 @@ markdownIt.renderer.rules.fence = (tokens, idx, options, env, slf) => { const lang = token.info || ''; let str = token.content; let highlighted = false; - let lines; + let lines = str.split('\n'); + + const startFromOneBased = Math.max(1, parseInt(getAttributeAndDelete(token, 'start-from'), 10) || 1); + const startFromZeroBased = startFromOneBased - 1; + + if (startFromOneBased > 1) { + // counter is incremented on each span, so we need to subtract 1 + token.attrJoin('style', `counter-reset: line ${startFromZeroBased};`); + } + + const highlightLinesInput = getAttributeAndDelete(token, 'highlight-lines'); + let highlightRules = []; + if (highlightLinesInput) { + const highlightLines = highlightLinesInput.split(','); + highlightRules = highlightLines.map(HighlightRule.parseRule).filter(rule => rule); + } + highlightRules.forEach(rule => { + // Note: authors provide line numbers based on the 'start-from' attribute if it exists, + // so we need to shift line numbers back down to start at 0 + rule.offsetLines(-startFromZeroBased); + + // Convert line-part rules to line-slice + if (rule.isLinePart()) { + rule.convertLinePartToLineSlice(lines); + } + }); + if (lang && hljs.getLanguage(lang)) { try { /* We cannot syntax highlight THEN split by lines. For eg: @@ -73,7 +99,7 @@ markdownIt.renderer.rules.fence = (tokens, idx, options, env, slf) => { So we have to split by lines THEN syntax highlight. */ let state = null; // state stores the current parse state of hljs, so that we can pass it on line by line - lines = str.split('\n').map((line) => { + lines = lines.map((line) => { const highlightedLine = hljs.highlight(lang, line, true, state); state = highlightedLine.top; return highlightedLine.value; @@ -85,24 +111,6 @@ markdownIt.renderer.rules.fence = (tokens, idx, options, env, slf) => { lines = markdownIt.utils.escapeHtml(str).split('\n'); } - const startFromOneBased = Math.max(1, parseInt(getAttributeAndDelete(token, 'start-from'), 10) || 1); - const startFromZeroBased = startFromOneBased - 1; - - if (startFromOneBased > 1) { - // counter is incremented on each span, so we need to subtract 1 - token.attrJoin('style', `counter-reset: line ${startFromZeroBased};`); - } - - const highlightLinesInput = getAttributeAndDelete(token, 'highlight-lines'); - let highlightRules = []; - if (highlightLinesInput) { - const highlightLines = highlightLinesInput.split(','); - highlightRules = highlightLines.map(HighlightRule.parseRule); - // Note: authors provide line numbers based on the 'start-from' attribute if it exists, - // so we need to shift line numbers back down to start at 0 - highlightRules.forEach(rule => rule.offsetLines(-startFromZeroBased)); - } - lines.pop(); // last line is always a single '\n' newline, so we remove it // wrap all lines with so we can number them str = lines.map((line, index) => { diff --git a/packages/core/src/utils/index.js b/packages/core/src/utils/index.js index fe8da93f61..92a092c5b5 100644 --- a/packages/core/src/utils/index.js +++ b/packages/core/src/utils/index.js @@ -14,6 +14,7 @@ const htmlUnescapedMapping = { '<': '<', '>': '>', '"': '"', + ''': '\'', }; module.exports = { From e0ad241228181af75eeff311296ed75ef676dd1e Mon Sep 17 00:00:00 2001 From: Ryo Armanda Date: Mon, 15 Feb 2021 03:40:08 +0800 Subject: [PATCH 08/19] Generalize parts to slices conversion to multiple components --- .../markdown-it/highlight/HighlightRule.js | 30 +++++++++++-------- packages/core/src/lib/markdown-it/index.js | 4 +-- 2 files changed, 20 insertions(+), 14 deletions(-) diff --git a/packages/core/src/lib/markdown-it/highlight/HighlightRule.js b/packages/core/src/lib/markdown-it/highlight/HighlightRule.js index 3f5a8d35c9..94aa343b24 100644 --- a/packages/core/src/lib/markdown-it/highlight/HighlightRule.js +++ b/packages/core/src/lib/markdown-it/highlight/HighlightRule.js @@ -23,15 +23,20 @@ class HighlightRule { this.ruleComponents.forEach(comp => comp.offsetLineNumber(offset)); } - convertLinePartToLineSlice(lines) { - if (!this.isLinePart()) { + convertPartsToSlices(lines) { + if (!this.hasLinePart()) { return; } - const [part] = this.ruleComponents; - const line = lines[part.lineNumber - 1]; // line numbers are 1-based - const {1: content} = HighlightRule._splitCodeAndIndentation(line); - part.convertPartToSlice(content); + this.ruleComponents.forEach(comp => { + if (!comp.linePart) { + return; + } + + const line = lines[comp.lineNumber - 1]; // line numbers are 1-based + const {1 : content} = HighlightRule._splitCodeAndIndentation(line); + comp.convertPartToSlice(content); + }); } shouldApplyHighlight(lineNumber) { @@ -102,20 +107,21 @@ class HighlightRule { static _highlightPartOfText(codeStr, bounds) { const {0: indents} = HighlightRule._splitCodeAndIndentation(codeStr); - const [start, end] = bounds; + const [start, end] = bounds.map(x => x + indents.length) // Note: As part-of-text highlighting requires walking over the node of the generated // html by highlight.js, highlighting will be applied in NodeProcessor instead. // hl-start and hl-end is used to pass over the bounds. - return `${codeStr}\n`; - } - - isLinePart() { - return (this.ruleComponents.length === 1) && (this.ruleComponents[0].linePart !== ""); + console.log([start, end]); + return `${codeStr}\n`; } isLineRange() { return this.ruleComponents.length === 2; } + + hasLinePart() { + return this.ruleComponents.some(rule => rule.linePart); + } } module.exports = { diff --git a/packages/core/src/lib/markdown-it/index.js b/packages/core/src/lib/markdown-it/index.js index 116708942b..b22733890f 100644 --- a/packages/core/src/lib/markdown-it/index.js +++ b/packages/core/src/lib/markdown-it/index.js @@ -78,8 +78,8 @@ markdownIt.renderer.rules.fence = (tokens, idx, options, env, slf) => { rule.offsetLines(-startFromZeroBased); // Convert line-part rules to line-slice - if (rule.isLinePart()) { - rule.convertLinePartToLineSlice(lines); + if (rule.hasLinePart()) { + rule.convertPartsToSlices(lines); } }); From 61bd56991ce412f739502f8d5d5609d4089c19af Mon Sep 17 00:00:00 2001 From: Ryo Armanda Date: Mon, 15 Feb 2021 03:49:38 +0800 Subject: [PATCH 09/19] Tidy up and fix mistakes --- packages/core/src/html/NodeProcessor.js | 2 +- packages/core/src/lib/markdown-it/highlight/HighlightRule.js | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/core/src/html/NodeProcessor.js b/packages/core/src/html/NodeProcessor.js index ce6038137c..7fae1a124a 100644 --- a/packages/core/src/html/NodeProcessor.js +++ b/packages/core/src/html/NodeProcessor.js @@ -265,7 +265,7 @@ class NodeProcessor { if (highlightData.every(v => v === true)) { // Every child wants highlight to be applied at node level // For conciseness, ask for the node's parent to highlight, if possible - return [true, curr]; + return [curr, true]; } // If node level highlighting is not possible, highlight the individual children as needed diff --git a/packages/core/src/lib/markdown-it/highlight/HighlightRule.js b/packages/core/src/lib/markdown-it/highlight/HighlightRule.js index 94aa343b24..c41ce8e0b9 100644 --- a/packages/core/src/lib/markdown-it/highlight/HighlightRule.js +++ b/packages/core/src/lib/markdown-it/highlight/HighlightRule.js @@ -111,7 +111,6 @@ class HighlightRule { // Note: As part-of-text highlighting requires walking over the node of the generated // html by highlight.js, highlighting will be applied in NodeProcessor instead. // hl-start and hl-end is used to pass over the bounds. - console.log([start, end]); return `${codeStr}\n`; } From ef72888cc9dd73b014d32c43f54759d2b6d2d7ba Mon Sep 17 00:00:00 2001 From: Ryo Armanda Date: Mon, 15 Feb 2021 03:54:45 +0800 Subject: [PATCH 10/19] Update tests --- .../test_site/expected/testCodeBlocks.html | 18 +++++++++++++----- .../functional/test_site/testCodeBlocks.md | 10 ++++++++++ 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/packages/cli/test/functional/test_site/expected/testCodeBlocks.html b/packages/cli/test/functional/test_site/expected/testCodeBlocks.html index b8afcde087..6bc932ecdb 100644 --- a/packages/cli/test/functional/test_site/expected/testCodeBlocks.html +++ b/packages/cli/test/functional/test_site/expected/testCodeBlocks.html @@ -232,20 +232,28 @@

Test </foo>

highlight-lines attr with full line-slice syntax should highlight only at specified range

-
<foo>
-  <bar type="name">goo</bar>
-  <baz type="name">goo</baz>
+        
<foo>
+  <bar type="name">goo</bar>
+  <baz type="name">goo</baz>
   <qux type="name">goo</qux>
   <quux type="name">goo</quux>
 </foo>
 

highlight-lines attr with partially filled line-slice syntax should defaults highlight to start/end of line

<foo>
-  <bar type="name">goo</bar>
-  <baz type="name">goo</baz>
+  <bar type="name">goo</bar>
+  <baz type="name">goo</baz>
   <qux type="name">goo</qux>
   <quux type="name">goo</quux>
 </foo>
+
+

highlight-lines attr with line-part syntax should highlight only at specified text

+
<foo>
+  <bar type="name">goo</bar>
+  <baz type="name">goo</baz>
+  <qux type="name">goo</qux>
+  <quux type="name">go'o</quux>
+</foo>
 

Should render correctly with heading

diff --git a/packages/cli/test/functional/test_site/testCodeBlocks.md b/packages/cli/test/functional/test_site/testCodeBlocks.md index ba9d86eda8..271e80f983 100644 --- a/packages/cli/test/functional/test_site/testCodeBlocks.md +++ b/packages/cli/test/functional/test_site/testCodeBlocks.md @@ -86,6 +86,16 @@ Content in a fenced code block ``` +**`highlight-lines` attr with line-part syntax should highlight only at specified text** +```xml {highlight-lines="2['type'],3[''],4['goo'],5['go\'o']"} + + goo + goo + goo + go'o + +``` + **Should render correctly with heading** ```{heading="A heading"} From 2499138e36d4841708cbbc05ebc7c9b5d1472f93 Mon Sep 17 00:00:00 2001 From: Ryo Armanda Date: Mon, 15 Feb 2021 04:06:41 +0800 Subject: [PATCH 11/19] Update docs --- docs/userGuide/syntax/code.mbdf | 33 ++++++++++++++++++++------------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/docs/userGuide/syntax/code.mbdf b/docs/userGuide/syntax/code.mbdf index 6b6322cb46..e3f06821f0 100644 --- a/docs/userGuide/syntax/code.mbdf +++ b/docs/userGuide/syntax/code.mbdf @@ -67,7 +67,7 @@ These rules dictate where and how MarkBind should highlight your code block. You can specify the rules in many different ways, depending on how you want it to be. There are three main variants: full text, partial text, or full line highlighting. -**Full text highlighting** +###### Full text highlighting To highlight the entirety of the text portion of the line, you can just use the line numbers as is, such as `3`. Note that the numbers should correspond with the line numbers that will be displayed, so if you have changed the @@ -75,27 +75,34 @@ numbering via the `start-from` attribute, you will have to follow that numbering For text-only range highlighting, join the two line numbers with a dash sign (`-`). -**Partial text highlighting** +###### Partial text highlighting -You can also highlight just a part of the text portion of the line. MarkBind has a _line-slice_ syntax to help you -with that. +You can also highlight just a part of the text portion of the line. MarkBind has two syntax that can help you, an +easy and convenient _line-part_ syntax and more concise and powerful _line-slice_ syntax. -The line-slice syntax is of the form `lineNumber[start:end]`, where `lineNumber` is the line number (subject to the -numbering conditions as explained above), `start` and `end` denote the range of character positions -that will be highlighted. +The **line-part** syntax is of the form `lineNumber[part]`, where `lineNumber` is the line number (subject to the +numbering settings as stated above), and the `part` is the exact part of line wrapped in quotes. The exact quote to +be used **must** be different than the one used to specify the `highlight-lines` attribute value. -Character positions start from `0`, which denotes the first non-whitespace character in the line, upwards. -Note that the highlight includes character at `start` but does not include character at `end`. +However, there are limitations to the line-part syntax. One of which is that if you have quotes inside `part`, you +have to manually escape it with backslashes (`\`). Secondly, the line-part syntax will not work if the quote +inside `part` is the same as the one used to specify the `highlight-lines` attribute value, even with escaping. + +The more powerful **line-slice** syntax that can overcome these limitations is of the form `lineNumber[start:end]`, +where `lineNumber` is the line number, `start` and `end` denote the range of character positions that will be +highlighted. This is similar to Python's slice syntax, if you are familiar. -You can omit either `start` or `end` to tell MarkBind either to highlight from the start or until the end, respectively. +Character positions start from `0`, which denotes the first non-whitespace character in the line, upwards. +Note that the highlight includes character at `start` but does not include character at `end`. You can omit either +`start` or `end` to tell MarkBind either to highlight from the start or until the end, respectively. Also, you can start or end a text-only range highlight with a line-slice syntax to indicate partial text highlight at the beginning or end of the range, respectively. -**Full line highlighting** +###### Full line highlighting If you wish to highlight a full line (which includes indentations), you can use the line-slice syntax as well, -but with `start` *and* `end` omitted. We call this the empty line-slices. +but with `start` *and* `end` omitted. We call this the **empty line-slices**. To do a full-line range highlighting, you only need to use empty line-slices on either ends of the usual text-only range highlight. @@ -103,7 +110,7 @@ text-only range highlight. -```java {highlight-lines="1[:],3,4[8:18],6[:]-8,10-12,14[12:],18[12:]-20"} +```java {highlight-lines="1[:],3['Inventory'],4['It\'s designed'],5,6[8:18],8[:]-10,12-14,16[12:],20[12:]-22"} import java.util.List; public class Inventory { From 26e9d253f005b6db293c0855beaf164f4c33e6ae Mon Sep 17 00:00:00 2001 From: Ryo Armanda Date: Mon, 15 Feb 2021 04:13:23 +0800 Subject: [PATCH 12/19] Update docs again --- docs/userGuide/syntax/code.mbdf | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/docs/userGuide/syntax/code.mbdf b/docs/userGuide/syntax/code.mbdf index e3f06821f0..a31deb7a57 100644 --- a/docs/userGuide/syntax/code.mbdf +++ b/docs/userGuide/syntax/code.mbdf @@ -69,23 +69,23 @@ full text, partial text, or full line highlighting. ###### Full text highlighting -To highlight the entirety of the text portion of the line, you can just use the line numbers as is, such as `3`. +To highlight the entirety of the text portion of the line, you can just use the line numbers as is. Note that the numbers should correspond with the line numbers that will be displayed, so if you have changed the numbering via the `start-from` attribute, you will have to follow that numbering as well. -For text-only range highlighting, join the two line numbers with a dash sign (`-`). +For **text-only range highlighting**, join the two line numbers with a dash sign (`-`). ###### Partial text highlighting You can also highlight just a part of the text portion of the line. MarkBind has two syntax that can help you, an -easy and convenient _line-part_ syntax and more concise and powerful _line-slice_ syntax. +easy and convenient **_line-part_** syntax and more concise and powerful **_line-slice_** syntax. The **line-part** syntax is of the form `lineNumber[part]`, where `lineNumber` is the line number (subject to the numbering settings as stated above), and the `part` is the exact part of line wrapped in quotes. The exact quote to be used **must** be different than the one used to specify the `highlight-lines` attribute value. However, there are limitations to the line-part syntax. One of which is that if you have quotes inside `part`, you -have to manually escape it with backslashes (`\`). Secondly, the line-part syntax will not work if the quote +have to manually escape it with backslashes (`\`). Secondly, the line-part syntax **will not work** if the quote inside `part` is the same as the one used to specify the `highlight-lines` attribute value, even with escaping. The more powerful **line-slice** syntax that can overcome these limitations is of the form `lineNumber[start:end]`, @@ -96,15 +96,15 @@ Character positions start from `0`, which denotes the first non-whitespace chara Note that the highlight includes character at `start` but does not include character at `end`. You can omit either `start` or `end` to tell MarkBind either to highlight from the start or until the end, respectively. -Also, you can start or end a text-only range highlight with a line-slice syntax to indicate partial text +Also, you can start or end a **partial text-only range highlight** with a line-slice syntax to indicate partial text highlight at the beginning or end of the range, respectively. ###### Full line highlighting If you wish to highlight a full line (which includes indentations), you can use the line-slice syntax as well, -but with `start` *and* `end` omitted. We call this the **empty line-slices**. +but with `start` **_and_** `end` omitted. We call this the **empty line-slices**. -To do a full-line range highlighting, you only need to use empty line-slices on either ends of the usual +To do a **full-line range highlighting**, you only need to use empty line-slices on either ends of the usual text-only range highlight. @@ -113,6 +113,8 @@ text-only range highlight. ```java {highlight-lines="1[:],3['Inventory'],4['It\'s designed'],5,6[8:18],8[:]-10,12-14,16[12:],20[12:]-22"} import java.util.List; +// Inventory is a class that stores inventory items in a list. +// It's designed as a thin wrapper on the List interface. public class Inventory { private List items; From ce67bf91d236a44d601b7a6ab0c1a389f0d00453 Mon Sep 17 00:00:00 2001 From: Ryo Armanda Date: Sun, 21 Feb 2021 18:45:40 +0800 Subject: [PATCH 13/19] Improve line part syntax to highlight multiple occurrences --- docs/userGuide/syntax/code.mbdf | 33 +++++++----- .../test_site/expected/testCodeBlocks.html | 4 +- .../functional/test_site/testCodeBlocks.md | 4 +- packages/core/src/html/NodeProcessor.js | 18 ++++--- .../markdown-it/highlight/HighlightRule.js | 7 +-- .../highlight/HighlightRuleComponent.js | 50 +++++++++++++++---- 6 files changed, 78 insertions(+), 38 deletions(-) diff --git a/docs/userGuide/syntax/code.mbdf b/docs/userGuide/syntax/code.mbdf index a31deb7a57..2eed64e7a2 100644 --- a/docs/userGuide/syntax/code.mbdf +++ b/docs/userGuide/syntax/code.mbdf @@ -65,7 +65,7 @@ The value of `highlight-lines` is composed of *highlight rules*, separated by co These rules dictate where and how MarkBind should highlight your code block. You can specify the rules in many different ways, depending on how you want it to be. There are three main variants: -full text, partial text, or full line highlighting. +full text, substring, bounded, or full line highlighting. ###### Full text highlighting @@ -75,22 +75,29 @@ numbering via the `start-from` attribute, you will have to follow that numbering For **text-only range highlighting**, join the two line numbers with a dash sign (`-`). -###### Partial text highlighting +###### Substring highlighting -You can also highlight just a part of the text portion of the line. MarkBind has two syntax that can help you, an -easy and convenient **_line-part_** syntax and more concise and powerful **_line-slice_** syntax. +You can also highlight occurrences of a substring in the line with MarkBind's **line-part** syntax. Note that this rule +will highlight **all** occurrences of the substring, and substring is case-sensitive. -The **line-part** syntax is of the form `lineNumber[part]`, where `lineNumber` is the line number (subject to the -numbering settings as stated above), and the `part` is the exact part of line wrapped in quotes. The exact quote to -be used **must** be different than the one used to specify the `highlight-lines` attribute value. +The line-part syntax is of the form `lineNumber[part]`, where `lineNumber` is the line number (subject to the numbering +settings as stated above), and `part` is the substring wrapped in quotes. The exact quote to be used **must** be +different than the one used to specify the `highlight-lines` attribute value. As an example, one can write +`highlight-lines="2['void main']"`. -However, there are limitations to the line-part syntax. One of which is that if you have quotes inside `part`, you -have to manually escape it with backslashes (`\`). Secondly, the line-part syntax **will not work** if the quote -inside `part` is the same as the one used to specify the `highlight-lines` attribute value, even with escaping. +However, there are limitations to the line-part syntax. One of which is that if you have quotes inside the substring +in `part`, you have to manually escape it with backslashes (`\`). Secondly, the line-part syntax **will not work** +if the quote inside `part` is the same as the one used to specify the `highlight-lines` attribute value, +even with escaping. -The more powerful **line-slice** syntax that can overcome these limitations is of the form `lineNumber[start:end]`, -where `lineNumber` is the line number, `start` and `end` denote the range of character positions that will be -highlighted. This is similar to Python's slice syntax, if you are familiar. +###### Bounded highlighting + +Bounded highlighting is available if you want to highlight text on a specific boundary. +This highlighting overcomes the limitations of substring highlighting. + +You can use MarkBind's _line-slice_ syntax format `lineNumber[start:end]` to write the bounded rule, where `lineNumber` +is the line number, `start` and `end` denote the range of character positions that will be highlighted, such as +`5[2:10]`. Character positions start from `0`, which denotes the first non-whitespace character in the line, upwards. Note that the highlight includes character at `start` but does not include character at `end`. You can omit either diff --git a/packages/cli/test/functional/test_site/expected/testCodeBlocks.html b/packages/cli/test/functional/test_site/expected/testCodeBlocks.html index 6bc932ecdb..cf97b7e618 100644 --- a/packages/cli/test/functional/test_site/expected/testCodeBlocks.html +++ b/packages/cli/test/functional/test_site/expected/testCodeBlocks.html @@ -247,10 +247,10 @@

Test <quux type="name">goo</quux> </foo>

-

highlight-lines attr with line-part syntax should highlight only at specified text

+

highlight-lines attr with line-part syntax should highlight only at specified substring

<foo>
   <bar type="name">goo</bar>
-  <baz type="name">goo</baz>
+  <baz type="name">goo</baz>
   <qux type="name">goo</qux>
   <quux type="name">go'o</quux>
 </foo>
diff --git a/packages/cli/test/functional/test_site/testCodeBlocks.md b/packages/cli/test/functional/test_site/testCodeBlocks.md
index 271e80f983..775dda9821 100644
--- a/packages/cli/test/functional/test_site/testCodeBlocks.md
+++ b/packages/cli/test/functional/test_site/testCodeBlocks.md
@@ -86,8 +86,8 @@ Content in a fenced code block
 
 ```
 
-**`highlight-lines` attr with line-part syntax should highlight only at specified text**
-```xml {highlight-lines="2['type'],3[''],4['goo'],5['go\'o']"}
+**`highlight-lines` attr with line-part syntax should highlight only at specified substring**
+```xml {highlight-lines="2['type'],3['baz'],4['goo'],5['go\'o']"}
 
   goo
   goo
diff --git a/packages/core/src/html/NodeProcessor.js b/packages/core/src/html/NodeProcessor.js
index 7fae1a124a..0c29c62ccf 100644
--- a/packages/core/src/html/NodeProcessor.js
+++ b/packages/core/src/html/NodeProcessor.js
@@ -193,17 +193,21 @@ class NodeProcessor {
     }
 
     codeNode.children.forEach((line) => {
-      if (!_.has(line.attribs, 'hl-start') || !_.has(line.attribs, 'hl-end')) {
+      if (!_.has(line.attribs, 'hl-data')) {
         return;
       }
 
-      const start = parseInt(line.attribs['hl-start'], 10);
-      const end = parseInt(line.attribs['hl-end'], 10);
-
-      this._traverseLinePart(line, start, end);
+      const data = line.attribs['hl-data'].split(',').map(boundStr => boundStr.split('-'));
+      const bounds = [];
+      data.forEach((boundStr) => {
+        const [start, end] = boundStr.map(str => parseInt(str, 10));
+        if (!Number.isNaN(start) && !Number.isNaN(end)) {
+          bounds.push([start, end]);
+        }
+      });
+      bounds.forEach(([start, end]) => this._traverseLinePart(line, start, end));
 
-      delete line.attribs['hl-start'];
-      delete line.attribs['hl-end'];
+      delete line.attribs['hl-data'];
     });
   }
 
diff --git a/packages/core/src/lib/markdown-it/highlight/HighlightRule.js b/packages/core/src/lib/markdown-it/highlight/HighlightRule.js
index 0ebd41d805..abe10f5005 100644
--- a/packages/core/src/lib/markdown-it/highlight/HighlightRule.js
+++ b/packages/core/src/lib/markdown-it/highlight/HighlightRule.js
@@ -107,11 +107,12 @@ class HighlightRule {
 
   static _highlightPartOfText(codeStr, bounds) {
     const { 0: indents } = HighlightRule._splitCodeAndIndentation(codeStr);
-    const [start, end] = bounds.map(x => x + indents.length);
+    const correctedBounds = bounds.map(bound => bound.map(x => x + indents.length));
     // Note: As part-of-text highlighting requires walking over the node of the generated
     // html by highlight.js, highlighting will be applied in NodeProcessor instead.
-    // hl-start and hl-end is used to pass over the bounds.
-    return `${codeStr}\n`;
+    // hl-data is used to pass over the bounds.
+    const dataStr = correctedBounds.map(bound => bound.join('-')).join(',');
+    return `${codeStr}\n`;
   }
 
   isLineRange() {
diff --git a/packages/core/src/lib/markdown-it/highlight/HighlightRuleComponent.js b/packages/core/src/lib/markdown-it/highlight/HighlightRuleComponent.js
index 765f0b4353..fa4bdfb034 100644
--- a/packages/core/src/lib/markdown-it/highlight/HighlightRuleComponent.js
+++ b/packages/core/src/lib/markdown-it/highlight/HighlightRuleComponent.js
@@ -3,9 +3,21 @@ const LINEPART_REGEX = new RegExp('(\\d+)\\[(["\'])((?:\\\\.|[^\\\\])*?)\\2]');
 
 class HighlightRuleComponent {
   constructor(lineNumber, isSlice = false, bounds = [], linePart = '') {
+    /**
+     * @type {number}
+     */
     this.lineNumber = lineNumber;
+    /**
+     * @type {boolean}
+     */
     this.isSlice = isSlice;
+    /**
+     * @type {Array<[number, number]>}
+     */
     this.bounds = bounds;
+    /**
+     * @type {string}
+     */
     this.linePart = linePart;
   }
 
@@ -21,8 +33,8 @@ class HighlightRuleComponent {
         return new HighlightRuleComponent(lineNumber, true);
       }
 
-      const bounds = groups.map(x => (x !== '' ? parseInt(x, 10) : -1));
-      return new HighlightRuleComponent(lineNumber, true, bounds);
+      const bound = groups.map(x => (x !== '' ? parseInt(x, 10) : -1));
+      return new HighlightRuleComponent(lineNumber, true, [bound]);
     }
 
     const linepartMatch = compString.match(LINEPART_REGEX);
@@ -71,22 +83,24 @@ class HighlightRuleComponent {
    * to the start/end of the line.
    *
    * @param line The line to be checked
-   * @returns {[number, number]} The actual bounds computed
+   * @returns {Array<[number, number]>} The actual bounds computed
    */
   computeLineBounds(line) {
     if (!this.isSlice) {
-      return [0, 0];
+      return [[0, 0]];
     }
 
     const [lineStart, lineEnd] = [0, line.length - 1];
     if (this.isUnboundedSlice()) {
-      return [lineStart, lineEnd];
+      return [[lineStart, lineEnd]];
     }
 
-    const [boundStart, boundEnd] = this.bounds;
-    const start = (lineStart <= boundStart) && (boundStart <= lineEnd) ? boundStart : lineStart;
-    const end = (lineStart <= boundEnd) && (boundEnd <= lineEnd) ? boundEnd : lineEnd;
-    return [start, end];
+    const lineBounds = this.bounds.map(([boundStart, boundEnd]) => {
+      const start = (lineStart <= boundStart) && (boundStart <= lineEnd) ? boundStart : lineStart;
+      const end = (lineStart <= boundEnd) && (boundEnd <= lineEnd) ? boundEnd : lineEnd;
+      return [start, end];
+    });
+    return lineBounds;
   }
 
   convertPartToSlice(content) {
@@ -94,8 +108,22 @@ class HighlightRuleComponent {
       return;
     }
 
-    const start = content.indexOf(this.linePart);
-    const bounds = start === -1 ? [0, 0] : [start, start + this.linePart.length];
+    let contentRemaining = content;
+    let start = contentRemaining.indexOf(this.linePart);
+    const bounds = [];
+
+    if (start === -1) {
+      bounds.push([0, 0]);
+    }
+
+    let curr = 0;
+    while (start !== -1) {
+      const end = start + this.linePart.length;
+      bounds.push([curr + start, curr + end]);
+      curr += end;
+      contentRemaining = contentRemaining.substring(end);
+      start = contentRemaining.indexOf(this.linePart);
+    }
 
     this.isSlice = true;
     this.bounds = bounds;

From 363fd1f288abb4b3131c5db103ada161e0d028ee Mon Sep 17 00:00:00 2001
From: Ryo Armanda 
Date: Thu, 25 Feb 2021 01:40:24 +0800
Subject: [PATCH 14/19] Implement word-variant line-slice syntax

---
 docs/userGuide/syntax/code.mbdf               | 36 ++++++++-----
 .../test_site/expected/testCodeBlocks.html    | 24 +++++++--
 .../functional/test_site/testCodeBlocks.md    | 28 ++++++++--
 .../markdown-it/highlight/HighlightRule.js    | 20 +++++++
 .../highlight/HighlightRuleComponent.js       | 53 ++++++++++++++++---
 packages/core/src/lib/markdown-it/index.js    |  5 ++
 6 files changed, 139 insertions(+), 27 deletions(-)

diff --git a/docs/userGuide/syntax/code.mbdf b/docs/userGuide/syntax/code.mbdf
index 2eed64e7a2..fffc66da63 100644
--- a/docs/userGuide/syntax/code.mbdf
+++ b/docs/userGuide/syntax/code.mbdf
@@ -58,14 +58,15 @@ function add(a, b) {
 
 
 ##### Line highlighting
+
 You can add the `highlight-lines` attribute to add highlighting to your code block. Refer to the example below for
-a typical usage of the attribute.
+all possible ways of highlighting a code block.
 
 The value of `highlight-lines` is composed of *highlight rules*, separated by commas.
-These rules dictate where and how MarkBind should highlight your code block. 
+These rules dictate where and how MarkBind should highlight your code block.
 
 You can specify the rules in many different ways, depending on how you want it to be. There are three main variants:
-full text, substring, bounded, or full line highlighting.
+full text, substring, bounded (character-wise or word-wise), or full line highlighting.
 
 ###### Full text highlighting
 
@@ -93,18 +94,25 @@ even with escaping.
 ###### Bounded highlighting
 
 Bounded highlighting is available if you want to highlight text on a specific boundary.
-This highlighting overcomes the limitations of substring highlighting.
+This highlighting overcomes the limitations of substring highlighting. You can highlight either by characters
+or by words.
 
-You can use MarkBind's _line-slice_ syntax format `lineNumber[start:end]` to write the bounded rule, where `lineNumber`
+**Character-wise**: You can use MarkBind's _line-slice_ syntax format `lineNumber[start:end]`, where `lineNumber`
 is the line number, `start` and `end` denote the range of character positions that will be highlighted, such as
-`5[2:10]`.
+`5[2:10]`. Note that the highlight includes character at `start` but does not include character at `end`.
 
 Character positions start from `0`, which denotes the first non-whitespace character in the line, upwards.
-Note that the highlight includes character at `start` but does not include character at `end`. You can omit either
-`start` or `end` to tell MarkBind either to highlight from the start or until the end, respectively.
+You can omit either `start` or `end` to tell MarkBind either to highlight from the start or until the end, respectively.
+
+**Word-wise**: The syntax for this is very similar to a normal line-slice syntax, but instead of having a single colon
+between `start` and `end`, you write two colons. That is, you can write it as `lineNumber[start::end]`. Now `start` and
+`end` denote the range of word positions that will be highlighted.
+ 
+Similar to the character-wise variant, `0` denotes the first word in the line, upwards. However, we
+define a word here as a _sequence of non-whitespace characters_.
 
-Also, you can start or end a **partial text-only range highlight** with a line-slice syntax to indicate partial text
-highlight at the beginning or end of the range, respectively.
+Also, you can start or end a **partial text-only range highlight** with a line-slice syntax (any variant) to indicate
+partial text highlight at the beginning or end of the range, respectively.
 
 ###### Full line highlighting
 
@@ -117,7 +125,7 @@ text-only range highlight.
 
 
 
-```java {highlight-lines="1[:],3['Inventory'],4['It\'s designed'],5,6[8:18],8[:]-10,12-14,16[12:],20[12:]-22"}
+```java {highlight-lines="1[:],3['Inventory'],4['It\'s designed'],5,6[8:18],8[0::2],12[:]-14,16-18,20[12:]-22,24[1::]-26"}
 import java.util.List;
 
 // Inventory is a class that stores inventory items in a list.
@@ -128,7 +136,7 @@ public class Inventory {
     public int getItemCount(){
         return items.size();
     }
-    
+
     public bool isEmpty() {
         return items.isEmpty();
     }
@@ -140,6 +148,10 @@ public class Inventory {
     public void addItem(item: Item) {
         return items.add(item);
     }
+
+    public void removeItem(item: Item) {
+        return items.remove(item);
+    }
 }
 ```
 
diff --git a/packages/cli/test/functional/test_site/expected/testCodeBlocks.html b/packages/cli/test/functional/test_site/expected/testCodeBlocks.html
index cf97b7e618..a94e0de197 100644
--- a/packages/cli/test/functional/test_site/expected/testCodeBlocks.html
+++ b/packages/cli/test/functional/test_site/expected/testCodeBlocks.html
@@ -223,15 +223,15 @@ 

Test 19 20

-

highlight-lines attr with empty line-slice syntax should highlight leading/trailing spaces

+

highlight-lines attr with empty (any variant) line-slice syntax should highlight leading/trailing spaces

<foo>
   <bar type="name">goo</bar>
-  <baz type="name">goo</baz>
+  <baz type="name">goo</baz>
   <qux type="name">goo</qux>
   <quux type="name">goo</quux>
 </foo>
 
-

highlight-lines attr with full line-slice syntax should highlight only at specified range

+

highlight-lines attr with full character-variant line-slice syntax should highlight only at specified range

<foo>
   <bar type="name">goo</bar>
   <baz type="name">goo</baz>
@@ -239,7 +239,7 @@ 

Test <quux type="name">goo</quux> </foo>

-

highlight-lines attr with partially filled line-slice syntax should defaults highlight to start/end of line

+

highlight-lines attr with partial character-variant line-slice syntax should defaults highlight to start/end of line

<foo>
   <bar type="name">goo</bar>
   <baz type="name">goo</baz>
@@ -254,6 +254,22 @@ 

Test <qux type="name">goo</qux> <quux type="name">go'o</quux> </foo> +

+

highlight-lines attr with full word-variant line-slice syntax should highlight only at specified word ranges

+
<foo>
+  <bar type="name"> goo </bar>
+  <baz type="name"> goo </baz>
+  <qux type="name"> goo </qux>
+  <quux type="name"> goo </quux>
+</foo>
+
+

highlight-lines attr with partial word-variant line-slice syntax should defaults highlight to start/end of line

+
<foo>
+  <bar type="name"> goo </bar>
+  <baz type="name"> goo </baz>
+  <qux type="name"> goo </qux>
+  <quux type="name"> goo </quux>
+</foo>
 

Should render correctly with heading

diff --git a/packages/cli/test/functional/test_site/testCodeBlocks.md b/packages/cli/test/functional/test_site/testCodeBlocks.md index 775dda9821..5bf0336e75 100644 --- a/packages/cli/test/functional/test_site/testCodeBlocks.md +++ b/packages/cli/test/functional/test_site/testCodeBlocks.md @@ -56,8 +56,8 @@ Content in a fenced code block 20 ``` -**`highlight-lines` attr with empty line-slice syntax should highlight leading/trailing spaces** -```xml {highlight-lines="2[:],4[:]-5[:]"} +**`highlight-lines` attr with empty (any variant) line-slice syntax should highlight leading/trailing spaces** +```xml {highlight-lines="2[:],3[::],4[:]-5[:]"} goo goo @@ -66,7 +66,7 @@ Content in a fenced code block ``` -**`highlight-lines` attr with full line-slice syntax should highlight only at specified range** +**`highlight-lines` attr with full character-variant line-slice syntax should highlight only at specified range** ```xml {highlight-lines="1[1:4],2[5:13],3[2:10]-4,5-6[1:4]"} goo @@ -76,7 +76,7 @@ Content in a fenced code block ``` -**`highlight-lines` attr with partially filled line-slice syntax should defaults highlight to start/end of line** +**`highlight-lines` attr with partial character-variant line-slice syntax should defaults highlight to start/end of line** ```xml {highlight-lines="1[1:],2[:13],3[2:]-4,5-6[:2]"} goo @@ -96,6 +96,26 @@ Content in a fenced code block ``` +**`highlight-lines` attr with full word-variant line-slice syntax should highlight only at specified word ranges** +```xml {highlight-lines="1[0::1],2[3::4],3[0::2],4[2::4],5[1::3]"} + + goo + goo + goo + goo + +``` + +**`highlight-lines` attr with partial word-variant line-slice syntax should defaults highlight to start/end of line** +```xml {highlight-lines="1[0::],2[3::],3[::2],4[2::],5[::3]"} + + goo + goo + goo + goo + +``` + **Should render correctly with heading** ```{heading="A heading"} diff --git a/packages/core/src/lib/markdown-it/highlight/HighlightRule.js b/packages/core/src/lib/markdown-it/highlight/HighlightRule.js index abe10f5005..d48c87e4c2 100644 --- a/packages/core/src/lib/markdown-it/highlight/HighlightRule.js +++ b/packages/core/src/lib/markdown-it/highlight/HighlightRule.js @@ -39,6 +39,22 @@ class HighlightRule { }); } + convertWordSliceToCharSlice(lines) { + if (!this.hasWordSlice()) { + return; + } + + this.ruleComponents.forEach((comp) => { + if (!comp.isSlice || !comp.isWordSlice) { + return; + } + + const line = lines[comp.lineNumber - 1]; // line numbers are 1-based + const { 1: content } = HighlightRule._splitCodeAndIndentation(line); + comp.convertWordSliceToCharSlice(content); + }); + } + shouldApplyHighlight(lineNumber) { const compares = this.ruleComponents.map(comp => comp.compareLine(lineNumber)); if (this.isLineRange()) { @@ -122,6 +138,10 @@ class HighlightRule { hasLinePart() { return this.ruleComponents.some(rule => rule.linePart); } + + hasWordSlice() { + return this.ruleComponents.some(rule => rule.isSlice && rule.isWordSlice); + } } module.exports = { diff --git a/packages/core/src/lib/markdown-it/highlight/HighlightRuleComponent.js b/packages/core/src/lib/markdown-it/highlight/HighlightRuleComponent.js index fa4bdfb034..5926c125b0 100644 --- a/packages/core/src/lib/markdown-it/highlight/HighlightRuleComponent.js +++ b/packages/core/src/lib/markdown-it/highlight/HighlightRuleComponent.js @@ -1,8 +1,10 @@ const LINESLICE_REGEX = new RegExp('(\\d+)\\[(\\d*):(\\d*)]'); const LINEPART_REGEX = new RegExp('(\\d+)\\[(["\'])((?:\\\\.|[^\\\\])*?)\\2]'); +const LINESLICE_WORD_REGEX = new RegExp('(\\d+)\\[(\\d*)::(\\d*)]'); +const BOUND_UNSET = -1; class HighlightRuleComponent { - constructor(lineNumber, isSlice = false, bounds = [], linePart = '') { + constructor(lineNumber, isSlice = false, isWordSlice = false, bounds = [], linePart = '') { /** * @type {number} */ @@ -11,6 +13,10 @@ class HighlightRuleComponent { * @type {boolean} */ this.isSlice = isSlice; + /** + * @type {boolean} + */ + this.isWordSlice = isWordSlice; /** * @type {Array<[number, number]>} */ @@ -22,10 +28,14 @@ class HighlightRuleComponent { } static parseRuleComponent(compString) { - // tries to match with the line slice pattern + // Match line-slice (character and word variant) syntax const linesliceMatch = compString.match(LINESLICE_REGEX); - if (linesliceMatch) { - const groups = linesliceMatch.slice(1); // discard full match + const linesliceWordMatch = compString.match(LINESLICE_WORD_REGEX); + const sliceMatch = linesliceMatch || linesliceWordMatch; + if (sliceMatch) { + const isWordSlice = sliceMatch === linesliceWordMatch; + + const groups = sliceMatch.slice(1); const lineNumber = parseInt(groups.shift(), 10); const isUnbounded = groups.every(x => x === ''); @@ -33,10 +43,11 @@ class HighlightRuleComponent { return new HighlightRuleComponent(lineNumber, true); } - const bound = groups.map(x => (x !== '' ? parseInt(x, 10) : -1)); - return new HighlightRuleComponent(lineNumber, true, [bound]); + const bound = groups.map(x => (x !== '' ? parseInt(x, 10) : BOUND_UNSET)); + return new HighlightRuleComponent(lineNumber, true, isWordSlice, [bound]); } + // Match line-part syntax const linepartMatch = compString.match(LINEPART_REGEX); if (linepartMatch) { const groups = linepartMatch.slice(1); // discard full match @@ -44,9 +55,10 @@ class HighlightRuleComponent { groups.shift(); // discard quote group match const part = groups.shift().replace(/\\'/g, '\'').replace(/\\"/g, '"'); // unescape quotes - return new HighlightRuleComponent(lineNumber, false, [], part); + return new HighlightRuleComponent(lineNumber, false, false, [], part); } + // Match line-number syntax if (!Number.isNaN(compString)) { // ensure the whole string can be converted to number const lineNumber = parseInt(compString, 10); return new HighlightRuleComponent(lineNumber); @@ -129,6 +141,33 @@ class HighlightRuleComponent { this.bounds = bounds; this.linePart = ''; } + + convertWordSliceToCharSlice(content) { + if (!this.isSlice || !this.isWordSlice) { + return; + } + + const [contentStart, contentEnd] = [0, content.length]; + const words = content.split(' '); + let curr = 0; + const startPositions = words.map((word) => { + const pos = curr; + curr += word.length + 1; // include space + return pos; + }); + + const bounds = this.bounds.map((wordBound) => { + const [wordStart, wordEnd] = wordBound; + const [isStartUnset, isEndUnset] = wordBound.map(x => x === BOUND_UNSET); + const [isStartInRange, isEndInRange] = wordBound.map(x => x >= 0 && x < words.length); + const charStart = (isStartUnset || !isStartInRange) ? contentStart : startPositions[wordStart]; + const charEnd = (isEndUnset || !isEndInRange) ? contentEnd : startPositions[wordEnd] - 1; + return [charStart, charEnd]; + }); + + this.isWordSlice = false; + this.bounds = bounds; + } } module.exports = { diff --git a/packages/core/src/lib/markdown-it/index.js b/packages/core/src/lib/markdown-it/index.js index c51c66abcc..3e3fa1cdf2 100644 --- a/packages/core/src/lib/markdown-it/index.js +++ b/packages/core/src/lib/markdown-it/index.js @@ -83,6 +83,11 @@ markdownIt.renderer.rules.fence = (tokens, idx, options, env, slf) => { if (rule.hasLinePart()) { rule.convertPartsToSlices(lines); } + + // Convert word variant of line-slice to char + if (rule.hasWordSlice()) { + rule.convertWordSliceToCharSlice(lines); + } }); if (lang && hljs.getLanguage(lang)) { From 9e1d9274386881ab6eac2e944470723186106f5b Mon Sep 17 00:00:00 2001 From: Ryo Armanda Date: Thu, 25 Feb 2021 01:52:58 +0800 Subject: [PATCH 15/19] Move code block processing to new file --- packages/core/src/html/NodeProcessor.js | 143 +------------------ packages/core/src/html/codeblockProcessor.js | 143 +++++++++++++++++++ 2 files changed, 145 insertions(+), 141 deletions(-) create mode 100644 packages/core/src/html/codeblockProcessor.js diff --git a/packages/core/src/html/NodeProcessor.js b/packages/core/src/html/NodeProcessor.js index 0c29c62ccf..202859b9ec 100644 --- a/packages/core/src/html/NodeProcessor.js +++ b/packages/core/src/html/NodeProcessor.js @@ -15,6 +15,7 @@ const { processInclude, processPanelSrc } = require('./includePanelProcessor'); const { Context } = require('./Context'); const linkProcessor = require('./linkProcessor'); const { insertTemporaryStyles } = require('./tempStyleProcessor'); +const { highlightCodeBlock } = require('./codeblockProcessor'); const md = require('../lib/markdown-it'); const utils = require('../utils'); @@ -176,146 +177,6 @@ class NodeProcessor { cheerio(node).remove(); } - /* - * Code blocks - */ - - /** - * Applies pending highlighting to the code block. - * This looks into each line for highlighting data, and if found, - * traverses over the line and applies the highlight. - * @param node Root of the code block element, which is the 'pre' node - */ - static _highlightCodeBlock(node) { - const codeNode = node.children.find(c => c.name === 'code'); - if (!codeNode) { - return; - } - - codeNode.children.forEach((line) => { - if (!_.has(line.attribs, 'hl-data')) { - return; - } - - const data = line.attribs['hl-data'].split(',').map(boundStr => boundStr.split('-')); - const bounds = []; - data.forEach((boundStr) => { - const [start, end] = boundStr.map(str => parseInt(str, 10)); - if (!Number.isNaN(start) && !Number.isNaN(end)) { - bounds.push([start, end]); - } - }); - bounds.forEach(([start, end]) => this._traverseLinePart(line, start, end)); - - delete line.attribs['hl-data']; - }); - } - - /** - * Traverses a line part and applies highlighting if necessary. - * @param node The node of the line part to be traversed - * @param hlStart The highlight start position, relative to the start of the line part - * @param hlEnd The highlight end position, relative to the start of the line part - * @returns {[number, boolean | [number, number]]} An array of two items. - */ - static _traverseLinePart(node, hlStart, hlEnd) { - // Return value is an array of two items: - // 1. The number of characters traversed - // 2. Highlighting data to be used by the node's parent. It can be: - // - true (ask to apply highlighting from parent) - // - false (do not process this node further) - // - array of two numbers (only for text nodes, inform parent to - // highlight the text at specified range) - - if (hlEnd <= 0) { - // Highlight end has passed, no need to traverse further - return [0, false]; - } - - if (node.type === 'text') { - // Node is a text node. It is not an inherent HTML element of its own, - // so to actually highlight this text, we have to ask to apply at its parent. - - const cleanedText = utils.unescapeHtml(node.data); - const textLength = cleanedText.length; - - if (hlStart >= textLength) { - // Highlight start is not in this text - return [textLength, false]; - } - - if (hlStart <= 0 && hlEnd >= textLength) { - // Highlight spans across the entirety of text - return [textLength, true]; - } - - // Partial text highlighting - return [textLength, [hlStart, hlEnd]]; - } - - // The remaining possibility is that node is a tag node. - // It has at least one child (to contain the text content). - // It may have more children, such as inner tag nodes. - let curr = 0; - const highlightData = node.children.map((child) => { - const [traversed, data] = this._traverseLinePart( - child, hlStart - curr, hlEnd - curr, - ); - curr += traversed; - - return data; - }); - - if (highlightData.every(v => v === true)) { - // Every child wants highlight to be applied at node level - // For conciseness, ask for the node's parent to highlight, if possible - return [curr, true]; - } - - // If node level highlighting is not possible, highlight the individual children as needed - // For tag nodes, it is easy, just add the highlight class - // For text nodes, it is trickier, as we have to wrap the text inside a first - // Essentially, we have to change the text node to become a tag node - - node.children.forEach((child, idx) => { - if (!highlightData[idx]) { - return; - } - - if (child.type === 'tag') { - child.attribs.class = child.attribs.class ? `${child.attribs.class} highlighted` : 'highlighted'; - return; - } - - const text = child.data; - let newElement; - - if (highlightData[idx] === true) { - [newElement] = cheerio.parseHTML(`${text}`); - } else { - const [start, end] = highlightData[idx]; - const cleaned = utils.unescapeHtml(text); - const split = [cleaned.substring(0, start), cleaned.substring(start, end), cleaned.substring(end)]; - const [pre, highlighted, post] = split.map(md.utils.escapeHtml); - [newElement] = cheerio.parseHTML( - `${pre}${highlighted}${post}`, - ); - } - - delete newElement.root; - node.children[idx] = newElement; - }); - - // Set the references accordingly - node.children.forEach((child, idx) => { - child.parent = node; - child.prev = idx > 0 ? node.children[idx - 1] : null; - child.next = idx < node.children.length - 1 ? node.children[idx + 1] : null; - }); - - return [curr, false]; - } - /* * Panels */ @@ -887,7 +748,7 @@ class NodeProcessor { try { switch (node.name) { case 'pre': - NodeProcessor._highlightCodeBlock(node); + highlightCodeBlock(node); break; case 'panel': NodeProcessor._assignPanelId(node); diff --git a/packages/core/src/html/codeblockProcessor.js b/packages/core/src/html/codeblockProcessor.js new file mode 100644 index 0000000000..50b5a8949b --- /dev/null +++ b/packages/core/src/html/codeblockProcessor.js @@ -0,0 +1,143 @@ +const cheerio = require('cheerio'); +const lodashHas = require('lodash/has'); +const md = require('../lib/markdown-it'); +const utils = require('../utils'); + +/** + * Traverses a line part and applies highlighting if necessary. + * @param node The node of the line part to be traversed + * @param hlStart The highlight start position, relative to the start of the line part + * @param hlEnd The highlight end position, relative to the start of the line part + * @returns {[number, boolean | [number, number]]} An array of two items. + */ +function traverseLinePart(node, hlStart, hlEnd) { + // Return value is an array of two items: + // 1. The number of characters traversed + // 2. Highlighting data to be used by the node's parent. It can be: + // - true (ask to apply highlighting from parent) + // - false (do not process this node further) + // - array of two numbers (only for text nodes, inform parent to + // highlight the text at specified range) + + if (hlEnd <= 0) { + // Highlight end has passed, no need to traverse further + return [0, false]; + } + + if (node.type === 'text') { + // Node is a text node. It is not an inherent HTML element of its own, + // so to actually highlight this text, we have to ask to apply at its parent. + + const cleanedText = utils.unescapeHtml(node.data); + const textLength = cleanedText.length; + + if (hlStart >= textLength) { + // Highlight start is not in this text + return [textLength, false]; + } + + if (hlStart <= 0 && hlEnd >= textLength) { + // Highlight spans across the entirety of text + return [textLength, true]; + } + + // Partial text highlighting + return [textLength, [hlStart, hlEnd]]; + } + + // The remaining possibility is that node is a tag node. + // It has at least one child (to contain the text content). + // It may have more children, such as inner tag nodes. + let curr = 0; + const highlightData = node.children.map((child) => { + const [traversed, data] = traverseLinePart(child, hlStart - curr, hlEnd - curr); + curr += traversed; + + return data; + }); + + if (highlightData.every(v => v === true)) { + // Every child wants highlight to be applied at node level + // For conciseness, ask for the node's parent to highlight, if possible + return [curr, true]; + } + + // If node level highlighting is not possible, highlight the individual children as needed + // For tag nodes, it is easy, just add the highlight class + // For text nodes, it is trickier, as we have to wrap the text inside a first + // Essentially, we have to change the text node to become a tag node + + node.children.forEach((child, idx) => { + if (!highlightData[idx]) { + return; + } + + if (child.type === 'tag') { + child.attribs.class = child.attribs.class ? `${child.attribs.class} highlighted` : 'highlighted'; + return; + } + + const text = child.data; + let newElement; + + if (highlightData[idx] === true) { + [newElement] = cheerio.parseHTML(`${text}`); + } else { + const [start, end] = highlightData[idx]; + const cleaned = utils.unescapeHtml(text); + const split = [cleaned.substring(0, start), cleaned.substring(start, end), cleaned.substring(end)]; + const [pre, highlighted, post] = split.map(md.utils.escapeHtml); + [newElement] = cheerio.parseHTML( + `${pre}${highlighted}${post}`, + ); + } + + delete newElement.root; + node.children[idx] = newElement; + }); + + // Set the references accordingly + node.children.forEach((child, idx) => { + child.parent = node; + child.prev = idx > 0 ? node.children[idx - 1] : null; + child.next = idx < node.children.length - 1 ? node.children[idx + 1] : null; + }); + + return [curr, false]; +} + +/** + * Applies pending highlighting to the code block. + * This looks into each line for highlighting data, and if found, + * traverses over the line and applies the highlight. + * @param node Root of the code block element, which is the 'pre' node + * @return + */ +function highlightCodeBlock(node) { + const codeNode = node.children.find(c => c.name === 'code'); + if (!codeNode) { + return; + } + + codeNode.children.forEach((line) => { + if (!lodashHas(line.attribs, 'hl-data')) { + return; + } + + const data = line.attribs['hl-data'].split(',').map(boundStr => boundStr.split('-')); + const bounds = []; + data.forEach((boundStr) => { + const [start, end] = boundStr.map(str => parseInt(str, 10)); + if (!Number.isNaN(start) && !Number.isNaN(end)) { + bounds.push([start, end]); + } + }); + bounds.forEach(([start, end]) => traverseLinePart(line, start, end)); + + delete line.attribs['hl-data']; + }); +} + +module.exports = { + highlightCodeBlock, +}; From 2f13afad1d5366395316747850c89826a0fcce7f Mon Sep 17 00:00:00 2001 From: Ryo Armanda Date: Thu, 25 Feb 2021 02:13:36 +0800 Subject: [PATCH 16/19] Move unescape helper to markdown-it --- packages/core/src/html/codeblockProcessor.js | 2 +- .../core/src/lib/markdown-it/utils/index.js | 29 +++++++++++++++++++ packages/core/src/utils/index.js | 15 ---------- 3 files changed, 30 insertions(+), 16 deletions(-) create mode 100644 packages/core/src/lib/markdown-it/utils/index.js diff --git a/packages/core/src/html/codeblockProcessor.js b/packages/core/src/html/codeblockProcessor.js index 50b5a8949b..27332e41eb 100644 --- a/packages/core/src/html/codeblockProcessor.js +++ b/packages/core/src/html/codeblockProcessor.js @@ -1,7 +1,7 @@ const cheerio = require('cheerio'); const lodashHas = require('lodash/has'); const md = require('../lib/markdown-it'); -const utils = require('../utils'); +const utils = require('../lib/markdown-it/utils'); /** * Traverses a line part and applies highlighting if necessary. diff --git a/packages/core/src/lib/markdown-it/utils/index.js b/packages/core/src/lib/markdown-it/utils/index.js new file mode 100644 index 0000000000..df1aae71ec --- /dev/null +++ b/packages/core/src/lib/markdown-it/utils/index.js @@ -0,0 +1,29 @@ +/* + Extra utility functions related to markdown-it. + markdown-it library exposes a utility module in markdown-it/utils, + below are additional functions that can be used as helpers alongside markdown-it/utils + */ + +// This mapping is taken from markdown-it/utils, just flipped. +// Refer to the original file at markdown-it/lib/common/utils.js +const htmlUnescapedMapping = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + ''': '\'', +}; + +// markdown-it/utils have an escapeHtml function, but not the +// complementary un-escaping function +function unescapeHtml(str) { + let unescaped = str; + Object.entries(htmlUnescapedMapping).forEach(([key, value]) => { + unescaped = unescaped.split(key).join(value); + }); + return unescaped; +} + +module.exports = { + unescapeHtml, +}; diff --git a/packages/core/src/utils/index.js b/packages/core/src/utils/index.js index 92a092c5b5..3c4036cdd6 100644 --- a/packages/core/src/utils/index.js +++ b/packages/core/src/utils/index.js @@ -9,14 +9,6 @@ const { markdownFileExts, } = require('../constants'); -const htmlUnescapedMapping = { - '&': '&', - '<': '<', - '>': '>', - '"': '"', - ''': '\'', -}; - module.exports = { getCurrentDirectoryBase() { return path.basename(process.cwd()); @@ -113,11 +105,4 @@ module.exports = { return text.join('').trim(); }, - unescapeHtml(str) { - let unescaped = str; - Object.entries(htmlUnescapedMapping).forEach(([key, value]) => { - unescaped = unescaped.split(key).join(value); - }); - return unescaped; - }, }; From 71fb389962b01da73d1bf50baebe49b23246f9dd Mon Sep 17 00:00:00 2001 From: Ryo Armanda Date: Sat, 27 Feb 2021 06:57:55 +0800 Subject: [PATCH 17/19] Rework highlight rule processing flow --- docs/userGuide/syntax/code.mbdf | 2 +- .../test_site/expected/testCodeBlocks.html | 10 +- .../functional/test_site/testCodeBlocks.md | 4 +- packages/core/src/html/codeblockProcessor.js | 121 +++++----- .../markdown-it/highlight/HighlightRule.js | 87 ++----- .../highlight/HighlightRuleComponent.js | 221 +++++++++++------- .../src/lib/markdown-it/highlight/helper.js | 12 + packages/core/src/lib/markdown-it/index.js | 23 +- 8 files changed, 232 insertions(+), 248 deletions(-) create mode 100644 packages/core/src/lib/markdown-it/highlight/helper.js diff --git a/docs/userGuide/syntax/code.mbdf b/docs/userGuide/syntax/code.mbdf index fffc66da63..d14a9d8961 100644 --- a/docs/userGuide/syntax/code.mbdf +++ b/docs/userGuide/syntax/code.mbdf @@ -65,7 +65,7 @@ all possible ways of highlighting a code block. The value of `highlight-lines` is composed of *highlight rules*, separated by commas. These rules dictate where and how MarkBind should highlight your code block. -You can specify the rules in many different ways, depending on how you want it to be. There are three main variants: +You can specify the rules in many different ways, depending on how you want it to be. There are three main types: full text, substring, bounded (character-wise or word-wise), or full line highlighting. ###### Full text highlighting diff --git a/packages/cli/test/functional/test_site/expected/testCodeBlocks.html b/packages/cli/test/functional/test_site/expected/testCodeBlocks.html index a94e0de197..1632952a41 100644 --- a/packages/cli/test/functional/test_site/expected/testCodeBlocks.html +++ b/packages/cli/test/functional/test_site/expected/testCodeBlocks.html @@ -240,10 +240,10 @@

Test </foo>

highlight-lines attr with partial character-variant line-slice syntax should defaults highlight to start/end of line

-
<foo>
-  <bar type="name">goo</bar>
-  <baz type="name">goo</baz>
-  <qux type="name">goo</qux>
+        
<foo>
+  <bar type="name">goo</bar>
+  <baz type="name">goo</baz>
+  <qux type="name">goo</qux>
   <quux type="name">goo</quux>
 </foo>
 
@@ -251,7 +251,7 @@

Test
<foo>
   <bar type="name">goo</bar>
   <baz type="name">goo</baz>
-  <qux type="name">goo</qux>
+  <qux type="name">go,o</qux>
   <quux type="name">go'o</quux>
 </foo>
 
diff --git a/packages/cli/test/functional/test_site/testCodeBlocks.md b/packages/cli/test/functional/test_site/testCodeBlocks.md index 5bf0336e75..fd2f15f280 100644 --- a/packages/cli/test/functional/test_site/testCodeBlocks.md +++ b/packages/cli/test/functional/test_site/testCodeBlocks.md @@ -87,11 +87,11 @@ Content in a fenced code block ``` **`highlight-lines` attr with line-part syntax should highlight only at specified substring** -```xml {highlight-lines="2['type'],3['baz'],4['goo'],5['go\'o']"} +```xml {highlight-lines="1[''],2['type'],3['baz'],4['go,o'],5['go\'o']"} goo goo - goo + go,o go'o ``` diff --git a/packages/core/src/html/codeblockProcessor.js b/packages/core/src/html/codeblockProcessor.js index 27332e41eb..b040d77a3e 100644 --- a/packages/core/src/html/codeblockProcessor.js +++ b/packages/core/src/html/codeblockProcessor.js @@ -8,67 +8,80 @@ const utils = require('../lib/markdown-it/utils'); * @param node The node of the line part to be traversed * @param hlStart The highlight start position, relative to the start of the line part * @param hlEnd The highlight end position, relative to the start of the line part - * @returns {[number, boolean | [number, number]]} An array of two items. + * @returns {object} An array of two items. */ function traverseLinePart(node, hlStart, hlEnd) { - // Return value is an array of two items: - // 1. The number of characters traversed - // 2. Highlighting data to be used by the node's parent. It can be: - // - true (ask to apply highlighting from parent) - // - false (do not process this node further) - // - array of two numbers (only for text nodes, inform parent to - // highlight the text at specified range) + const resData = { + numCharsTraversed: 0, + shouldParentHighlight: false, + highlightRange: undefined, + }; if (hlEnd <= 0) { // Highlight end has passed, no need to traverse further - return [0, false]; + return resData; } if (node.type === 'text') { - // Node is a text node. It is not an inherent HTML element of its own, - // so to actually highlight this text, we have to ask to apply at its parent. + /* + * Node is a text node. It is not an inherent HTML element of its own, + * so to actually highlight this text, we have to ask to apply at its parent. + */ const cleanedText = utils.unescapeHtml(node.data); const textLength = cleanedText.length; + resData.numCharsTraversed = textLength; if (hlStart >= textLength) { // Highlight start is not in this text - return [textLength, false]; + resData.shouldParentHighlight = false; + return resData; } if (hlStart <= 0 && hlEnd >= textLength) { // Highlight spans across the entirety of text - return [textLength, true]; + resData.shouldParentHighlight = true; + return resData; } // Partial text highlighting - return [textLength, [hlStart, hlEnd]]; + resData.shouldParentHighlight = true; + resData.highlightRange = [hlStart, hlEnd]; + return resData; } - // The remaining possibility is that node is a tag node. - // It has at least one child (to contain the text content). - // It may have more children, such as inner tag nodes. + /* + * The remaining possibility is that node is a tag node. + * It has at least one child (to contain the text content). + * It may have more children, such as inner tag nodes. + */ + let curr = 0; const highlightData = node.children.map((child) => { - const [traversed, data] = traverseLinePart(child, hlStart - curr, hlEnd - curr); - curr += traversed; - + const data = traverseLinePart(child, hlStart - curr, hlEnd - curr); + curr += data.numCharsTraversed; return data; }); - - if (highlightData.every(v => v === true)) { - // Every child wants highlight to be applied at node level - // For conciseness, ask for the node's parent to highlight, if possible - return [curr, true]; + resData.numCharsTraversed = curr; + + if (highlightData.every(data => data.shouldParentHighlight && !data.highlightRange)) { + /* + * Every child wants highlight to be applied to the whole content at node level. + * For conciseness, ask for the node's parent to highlight, if possible + */ + resData.shouldParentHighlight = true; + return resData; } - // If node level highlighting is not possible, highlight the individual children as needed - // For tag nodes, it is easy, just add the highlight class - // For text nodes, it is trickier, as we have to wrap the text inside a first - // Essentially, we have to change the text node to become a tag node + /* + * If node level highlighting is not possible, highlight the individual children as needed. + * For text nodes, it is trickier, as we have to wrap the text inside a first. + * Essentially, we have to change the text node to become a tag node. + */ node.children.forEach((child, idx) => { - if (!highlightData[idx]) { + const data = highlightData[idx]; + if (!data.shouldParentHighlight) { return; } @@ -77,33 +90,20 @@ function traverseLinePart(node, hlStart, hlEnd) { return; } - const text = child.data; - let newElement; - - if (highlightData[idx] === true) { - [newElement] = cheerio.parseHTML(`${text}`); + if (!data.highlightRange) { + cheerio(child).wrap(''); } else { - const [start, end] = highlightData[idx]; - const cleaned = utils.unescapeHtml(text); + const [start, end] = data.highlightRange; + const cleaned = utils.unescapeHtml(child.data); const split = [cleaned.substring(0, start), cleaned.substring(start, end), cleaned.substring(end)]; const [pre, highlighted, post] = split.map(md.utils.escapeHtml); - [newElement] = cheerio.parseHTML( - `${pre}${highlighted}${post}`, - ); + const newElement = cheerio(`${pre}${highlighted}${post}`); + cheerio(child).replaceWith(newElement); } - - delete newElement.root; - node.children[idx] = newElement; }); - // Set the references accordingly - node.children.forEach((child, idx) => { - child.parent = node; - child.prev = idx > 0 ? node.children[idx - 1] : null; - child.next = idx < node.children.length - 1 ? node.children[idx + 1] : null; - }); - - return [curr, false]; + resData.shouldParentHighlight = false; + return resData; } /** @@ -119,22 +119,15 @@ function highlightCodeBlock(node) { return; } - codeNode.children.forEach((line) => { - if (!lodashHas(line.attribs, 'hl-data')) { + codeNode.children.forEach((lineNode) => { + if (!lodashHas(lineNode.attribs, 'hl-data')) { return; } - const data = line.attribs['hl-data'].split(',').map(boundStr => boundStr.split('-')); - const bounds = []; - data.forEach((boundStr) => { - const [start, end] = boundStr.map(str => parseInt(str, 10)); - if (!Number.isNaN(start) && !Number.isNaN(end)) { - bounds.push([start, end]); - } - }); - bounds.forEach(([start, end]) => traverseLinePart(line, start, end)); - - delete line.attribs['hl-data']; + const bounds = lineNode.attribs['hl-data'].split(',').map(boundStr => boundStr.split('-').map(Number)); + bounds.forEach(([start, end]) => traverseLinePart(lineNode, start, end)); + + delete lineNode.attribs['hl-data']; }); } diff --git a/packages/core/src/lib/markdown-it/highlight/HighlightRule.js b/packages/core/src/lib/markdown-it/highlight/HighlightRule.js index d48c87e4c2..dad9a0eb1d 100644 --- a/packages/core/src/lib/markdown-it/highlight/HighlightRule.js +++ b/packages/core/src/lib/markdown-it/highlight/HighlightRule.js @@ -1,4 +1,5 @@ const { HighlightRuleComponent } = require('./HighlightRuleComponent.js'); +const { splitCodeAndIndentation } = require('./helper'); class HighlightRule { constructor(ruleComponents) { @@ -8,8 +9,10 @@ class HighlightRule { this.ruleComponents = ruleComponents; } - static parseRule(ruleString) { - const components = ruleString.split('-').map(HighlightRuleComponent.parseRuleComponent); + static parseRule(ruleString, lineOffset, lines) { + const components = ruleString.split('-') + .map(compString => HighlightRuleComponent.parseRuleComponent(compString, lineOffset, lines)); + if (components.some(c => !c)) { // Not all components are properly parsed, which means // the rule itself is not proper @@ -19,42 +22,6 @@ class HighlightRule { return new HighlightRule(components); } - offsetLines(offset) { - this.ruleComponents.forEach(comp => comp.offsetLineNumber(offset)); - } - - convertPartsToSlices(lines) { - if (!this.hasLinePart()) { - return; - } - - this.ruleComponents.forEach((comp) => { - if (!comp.linePart) { - return; - } - - const line = lines[comp.lineNumber - 1]; // line numbers are 1-based - const { 1: content } = HighlightRule._splitCodeAndIndentation(line); - comp.convertPartToSlice(content); - }); - } - - convertWordSliceToCharSlice(lines) { - if (!this.hasWordSlice()) { - return; - } - - this.ruleComponents.forEach((comp) => { - if (!comp.isSlice || !comp.isWordSlice) { - return; - } - - const line = lines[comp.lineNumber - 1]; // line numbers are 1-based - const { 1: content } = HighlightRule._splitCodeAndIndentation(line); - comp.convertWordSliceToCharSlice(content); - }); - } - shouldApplyHighlight(lineNumber) { const compares = this.ruleComponents.map(comp => comp.compareLine(lineNumber)); if (this.isLineRange()) { @@ -68,9 +35,10 @@ class HighlightRule { } applyHighlight(line, lineNumber) { - if (this.isLineRange()) { - const [startCompare, endCompare] = this.ruleComponents.map(comp => comp.compareLine(lineNumber)); + // Applied rule is the first component until deduced otherwise + let [appliedRule] = this.ruleComponents; + if (this.isLineRange()) { // For cases like 2[:]-3 (or 2-3[:]), the highlight would be line highlight // across all the ranges const shouldWholeLine = this.ruleComponents.some(comp => comp.isUnboundedSlice()); @@ -78,27 +46,21 @@ class HighlightRule { return HighlightRule._highlightWholeLine(line); } + const [startCompare, endCompare] = this.ruleComponents.map(comp => comp.compareLine(lineNumber)); if (startCompare < 0 && endCompare > 0) { // In-between range return HighlightRule._highlightWholeText(line); } // At the range boundaries - const [start, end] = this.ruleComponents; - const appliedRule = startCompare === 0 ? start : end; - - // Instead of redefining how to highlight according to the rule (which is already laid - // out on the next few cases), we create a new HighlightRule consisting of only the applied - // rule and call apply again - return new HighlightRule([appliedRule]).applyHighlight(line, lineNumber); + const [startRule, endRule] = this.ruleComponents; + appliedRule = startCompare === 0 ? startRule : endRule; } - const isLineSlice = this.ruleComponents.length === 1 && this.ruleComponents[0].isSlice; - if (isLineSlice) { - const [slice] = this.ruleComponents; - return slice.isUnboundedSlice() + if (appliedRule.isSlice) { + return appliedRule.isUnboundedSlice() ? HighlightRule._highlightWholeLine(line) - : HighlightRule._highlightPartOfText(line, slice.computeLineBounds(line)); + : HighlightRule._highlightPartOfText(line, appliedRule.bounds); } // Line number only @@ -109,39 +71,22 @@ class HighlightRule { return `${codeStr}\n`; } - static _splitCodeAndIndentation(codeStr) { - const codeStartIdx = codeStr.search(/\S|$/); - const indents = codeStr.substr(0, codeStartIdx); - const content = codeStr.substr(codeStartIdx); - return [indents, content]; - } - static _highlightWholeText(codeStr) { - const [indents, content] = HighlightRule._splitCodeAndIndentation(codeStr); + const [indents, content] = splitCodeAndIndentation(codeStr); return `${indents}${content}\n`; } static _highlightPartOfText(codeStr, bounds) { - const { 0: indents } = HighlightRule._splitCodeAndIndentation(codeStr); - const correctedBounds = bounds.map(bound => bound.map(x => x + indents.length)); // Note: As part-of-text highlighting requires walking over the node of the generated // html by highlight.js, highlighting will be applied in NodeProcessor instead. // hl-data is used to pass over the bounds. - const dataStr = correctedBounds.map(bound => bound.join('-')).join(','); + const dataStr = bounds.map(bound => bound.join('-')).join(','); return `${codeStr}\n`; } isLineRange() { return this.ruleComponents.length === 2; } - - hasLinePart() { - return this.ruleComponents.some(rule => rule.linePart); - } - - hasWordSlice() { - return this.ruleComponents.some(rule => rule.isSlice && rule.isWordSlice); - } } module.exports = { diff --git a/packages/core/src/lib/markdown-it/highlight/HighlightRuleComponent.js b/packages/core/src/lib/markdown-it/highlight/HighlightRuleComponent.js index 5926c125b0..4438f743e9 100644 --- a/packages/core/src/lib/markdown-it/highlight/HighlightRuleComponent.js +++ b/packages/core/src/lib/markdown-it/highlight/HighlightRuleComponent.js @@ -1,10 +1,12 @@ -const LINESLICE_REGEX = new RegExp('(\\d+)\\[(\\d*):(\\d*)]'); -const LINEPART_REGEX = new RegExp('(\\d+)\\[(["\'])((?:\\\\.|[^\\\\])*?)\\2]'); +const { splitCodeAndIndentation } = require('./helper'); + +const LINESLICE_CHAR_REGEX = new RegExp('(\\d+)\\[(\\d*):(\\d*)]'); const LINESLICE_WORD_REGEX = new RegExp('(\\d+)\\[(\\d*)::(\\d*)]'); -const BOUND_UNSET = -1; +const LINEPART_REGEX = new RegExp('(\\d+)\\[(["\'])((?:\\\\.|[^\\\\])*?)\\2]'); +const UNBOUNDED = -1; class HighlightRuleComponent { - constructor(lineNumber, isSlice = false, isWordSlice = false, bounds = [], linePart = '') { + constructor(lineNumber, isSlice = false, bounds = []) { /** * @type {number} */ @@ -13,54 +15,63 @@ class HighlightRuleComponent { * @type {boolean} */ this.isSlice = isSlice; - /** - * @type {boolean} - */ - this.isWordSlice = isWordSlice; /** * @type {Array<[number, number]>} */ this.bounds = bounds; - /** - * @type {string} - */ - this.linePart = linePart; } - static parseRuleComponent(compString) { + static parseRuleComponent(compString, lineNumberOffset, lines) { // Match line-slice (character and word variant) syntax - const linesliceMatch = compString.match(LINESLICE_REGEX); + const linesliceCharMatch = compString.match(LINESLICE_CHAR_REGEX); const linesliceWordMatch = compString.match(LINESLICE_WORD_REGEX); - const sliceMatch = linesliceMatch || linesliceWordMatch; + const sliceMatch = linesliceCharMatch || linesliceWordMatch; if (sliceMatch) { - const isWordSlice = sliceMatch === linesliceWordMatch; + // There are four capturing groups: [full match, line number, start bound, end bound] + const groups = sliceMatch.slice(1); // discard full match - const groups = sliceMatch.slice(1); - const lineNumber = parseInt(groups.shift(), 10); + let lineNumber = parseInt(groups.shift(), 10); + if (Number.isNaN(lineNumber)) { + return null; + } + lineNumber += lineNumberOffset; const isUnbounded = groups.every(x => x === ''); if (isUnbounded) { - return new HighlightRuleComponent(lineNumber, true); + return new HighlightRuleComponent(lineNumber, true, []); } - const bound = groups.map(x => (x !== '' ? parseInt(x, 10) : BOUND_UNSET)); - return new HighlightRuleComponent(lineNumber, true, isWordSlice, [bound]); + let bound = groups.map(x => (x !== '' ? parseInt(x, 10) : UNBOUNDED)); + const isCharSlice = sliceMatch === linesliceCharMatch; + bound = isCharSlice + ? HighlightRuleComponent.computeCharBounds(bound, lines[lineNumber - 1]) + : HighlightRuleComponent.computeWordBounds(bound, lines[lineNumber - 1]); + + return new HighlightRuleComponent(lineNumber, true, [bound]); } // Match line-part syntax const linepartMatch = compString.match(LINEPART_REGEX); if (linepartMatch) { + // There are four capturing groups: [full match, line number, quote type, line part] const groups = linepartMatch.slice(1); // discard full match - const lineNumber = parseInt(groups.shift(), 10); - groups.shift(); // discard quote group match - const part = groups.shift().replace(/\\'/g, '\'').replace(/\\"/g, '"'); // unescape quotes - return new HighlightRuleComponent(lineNumber, false, false, [], part); + let lineNumber = parseInt(groups.shift(), 10); + if (Number.isNaN(lineNumber)) { + return null; + } + lineNumber += lineNumberOffset; + + groups.shift(); // discard quote type + const linePart = groups.shift().replace(/\\'/g, '\'').replace(/\\"/g, '"'); // unescape quotes + const bounds = HighlightRuleComponent.computeLinePartBounds(linePart, lines[lineNumber - 1]); + + return new HighlightRuleComponent(lineNumber, true, bounds); } // Match line-number syntax if (!Number.isNaN(compString)) { // ensure the whole string can be converted to number - const lineNumber = parseInt(compString, 10); + const lineNumber = parseInt(compString, 10) + lineNumberOffset; return new HighlightRuleComponent(lineNumber); } @@ -68,10 +79,6 @@ class HighlightRuleComponent { return null; } - offsetLineNumber(offset) { - this.lineNumber += offset; - } - /** * Compares the component's line number to a given line number. * @@ -88,85 +95,123 @@ class HighlightRuleComponent { } /** - * Computes the actual bounds of the highlight rule given a line, - * comparing the rule's bounds and the line's range. + * Computes the actual character bound given a user-defined character bound and a line, + * comparing the bounds and the line's range. * - * If the rule does not specify a start/end bound, the computed bound will default - * to the start/end of the line. + * If the bound does not specify either the start or the end bound, the computed bound will default + * to the start or end of line, excluding leading whitespaces. * - * @param line The line to be checked - * @returns {Array<[number, number]>} The actual bounds computed + * @param bound The user-defined bound + * @param line The given line + * @returns {[number, number]} The actual bound computed */ - computeLineBounds(line) { - if (!this.isSlice) { - return [[0, 0]]; + static computeCharBounds(bound, line) { + const [indents] = splitCodeAndIndentation(line); + let [start, end] = bound; + + if (start === UNBOUNDED) { + start = indents.length; + } else { + start += indents.length; + // Clamp values + if (start < indents.length) { + start = indents.length; + } else if (start > line.length) { + start = line.length; + } } - const [lineStart, lineEnd] = [0, line.length - 1]; - if (this.isUnboundedSlice()) { - return [[lineStart, lineEnd]]; + if (end === UNBOUNDED) { + end = line.length; + } else { + end += indents.length; + // Clamp values + if (end < indents.length) { + end = indents.length; + } else if (end > line.length) { + end = line.length; + } } - const lineBounds = this.bounds.map(([boundStart, boundEnd]) => { - const start = (lineStart <= boundStart) && (boundStart <= lineEnd) ? boundStart : lineStart; - const end = (lineStart <= boundEnd) && (boundEnd <= lineEnd) ? boundEnd : lineEnd; - return [start, end]; - }); - return lineBounds; + return [start, end]; } - convertPartToSlice(content) { - if (!this.linePart) { - return; + /** + * Computes the actual character bounds given a user-defined word bound and a line, + * comparing the bounds and the line's range. + * + * If the bound does not specify either the start or the end bound, the computed bound will default + * to the start or end of line, excluding leading whitespaces. + * + * @param bound The user-defined bound + * @param line The given line + * @returns {[number, number]} The actual bound computed + */ + static computeWordBounds(bound, line) { + const [indents, content] = splitCodeAndIndentation(line); + const words = content.split(/\s+/); + const wordPositions = []; + let contentRemaining = content; + let curr = indents.length; + words.forEach((word) => { + const start = contentRemaining.indexOf(word); + const end = start + word.length; + wordPositions.push([curr + start, curr + end]); + contentRemaining = contentRemaining.substring(end); + curr += end; + }); + + let [start, end] = bound; + + if (start === UNBOUNDED || start < 0) { + start = indents.length; + } else if (start > words.length) { + start = line.length; + } else { + const [wordStart] = wordPositions[start]; + start = wordStart; + } + + if (end === UNBOUNDED || end > words.length) { + end = line.length; + } else if (end < 0) { + end = indents.length; + } else { + const [, wordEnd] = wordPositions[end - 1]; + end = wordEnd; } + return [start, end]; + } + + /** + * Computes the actual bounds given a user-defined line part and a line. + * + * @param linePart The user-defined line part + * @param line The given line + * @returns {Array<[number, number]>} The bounds computed, each indicates the range of each + * occurrences of the line part in the line + */ + static computeLinePartBounds(linePart, line) { + const [indents, content] = splitCodeAndIndentation(line); let contentRemaining = content; - let start = contentRemaining.indexOf(this.linePart); - const bounds = []; + let start = contentRemaining.indexOf(linePart); - if (start === -1) { - bounds.push([0, 0]); + if (linePart === '' || start === -1) { + return [[0, 0]]; } - let curr = 0; + const bounds = []; + let curr = indents.length; while (start !== -1) { - const end = start + this.linePart.length; + const end = start + linePart.length; bounds.push([curr + start, curr + end]); curr += end; contentRemaining = contentRemaining.substring(end); - start = contentRemaining.indexOf(this.linePart); - } - - this.isSlice = true; - this.bounds = bounds; - this.linePart = ''; - } - - convertWordSliceToCharSlice(content) { - if (!this.isSlice || !this.isWordSlice) { - return; + start = contentRemaining.indexOf(linePart); } - const [contentStart, contentEnd] = [0, content.length]; - const words = content.split(' '); - let curr = 0; - const startPositions = words.map((word) => { - const pos = curr; - curr += word.length + 1; // include space - return pos; - }); - - const bounds = this.bounds.map((wordBound) => { - const [wordStart, wordEnd] = wordBound; - const [isStartUnset, isEndUnset] = wordBound.map(x => x === BOUND_UNSET); - const [isStartInRange, isEndInRange] = wordBound.map(x => x >= 0 && x < words.length); - const charStart = (isStartUnset || !isStartInRange) ? contentStart : startPositions[wordStart]; - const charEnd = (isEndUnset || !isEndInRange) ? contentEnd : startPositions[wordEnd] - 1; - return [charStart, charEnd]; - }); - - this.isWordSlice = false; - this.bounds = bounds; + return bounds; } } diff --git a/packages/core/src/lib/markdown-it/highlight/helper.js b/packages/core/src/lib/markdown-it/highlight/helper.js new file mode 100644 index 0000000000..3ea405455f --- /dev/null +++ b/packages/core/src/lib/markdown-it/highlight/helper.js @@ -0,0 +1,12 @@ +// Common helper functions to be used in HighlightRule or HighlightRuleComponent + +function splitCodeAndIndentation(codeStr) { + const codeStartIdx = codeStr.search(/\S|$/); + const indents = codeStr.substring(0, codeStartIdx); + const content = codeStr.substring(codeStartIdx); + return [indents, content]; +} + +module.exports = { + splitCodeAndIndentation, +}; diff --git a/packages/core/src/lib/markdown-it/index.js b/packages/core/src/lib/markdown-it/index.js index 3e3fa1cdf2..52b5c44d9e 100644 --- a/packages/core/src/lib/markdown-it/index.js +++ b/packages/core/src/lib/markdown-it/index.js @@ -12,6 +12,8 @@ const logger = require('../../utils/logger'); const { HighlightRule } = require('./highlight/HighlightRule.js'); +const HIGHLIGHT_LINES_DELIMITER_REGEX = new RegExp(',(?![^\\[\\]]*])'); + const createDoubleDelimiterInlineRule = require('./plugins/markdown-it-double-delimiter'); // markdown-it plugins @@ -71,24 +73,11 @@ markdownIt.renderer.rules.fence = (tokens, idx, options, env, slf) => { const highlightLinesInput = getAttributeAndDelete(token, 'highlight-lines'); let highlightRules = []; if (highlightLinesInput) { - const highlightLines = highlightLinesInput.split(','); - highlightRules = highlightLines.map(HighlightRule.parseRule).filter(rule => rule); + const highlightLines = highlightLinesInput.split(HIGHLIGHT_LINES_DELIMITER_REGEX); + highlightRules = highlightLines + .map(ruleStr => HighlightRule.parseRule(ruleStr, -startFromZeroBased, lines)) + .filter(rule => rule); // discards invalid rules } - highlightRules.forEach((rule) => { - // Note: authors provide line numbers based on the 'start-from' attribute if it exists, - // so we need to shift line numbers back down to start at 0 - rule.offsetLines(-startFromZeroBased); - - // Convert line-part rules to line-slice - if (rule.hasLinePart()) { - rule.convertPartsToSlices(lines); - } - - // Convert word variant of line-slice to char - if (rule.hasWordSlice()) { - rule.convertWordSliceToCharSlice(lines); - } - }); if (lang && hljs.getLanguage(lang)) { try { From 7e19b932e9c7a0f9060d23ecfdcd3320603bc612 Mon Sep 17 00:00:00 2001 From: Ryo Armanda Date: Sat, 27 Feb 2021 16:11:01 +0800 Subject: [PATCH 18/19] Summarize highlight rule formats documentation in a table --- docs/userGuide/syntax/code.mbdf | 79 +++++++++------------------------ 1 file changed, 21 insertions(+), 58 deletions(-) diff --git a/docs/userGuide/syntax/code.mbdf b/docs/userGuide/syntax/code.mbdf index d14a9d8961..215617cd69 100644 --- a/docs/userGuide/syntax/code.mbdf +++ b/docs/userGuide/syntax/code.mbdf @@ -59,68 +59,31 @@ function add(a, b) { ##### Line highlighting -You can add the `highlight-lines` attribute to add highlighting to your code block. Refer to the example below for -all possible ways of highlighting a code block. +You can add the `highlight-lines` attribute to add highlighting to your code block. Refer to the example code block +below for a visual demonstration of all the possible ways of highlighting a code block. The value of `highlight-lines` is composed of *highlight rules*, separated by commas. These rules dictate where and how MarkBind should highlight your code block. -You can specify the rules in many different ways, depending on how you want it to be. There are three main types: -full text, substring, bounded (character-wise or word-wise), or full line highlighting. - -###### Full text highlighting - -To highlight the entirety of the text portion of the line, you can just use the line numbers as is. -Note that the numbers should correspond with the line numbers that will be displayed, so if you have changed the -numbering via the `start-from` attribute, you will have to follow that numbering as well. - -For **text-only range highlighting**, join the two line numbers with a dash sign (`-`). - -###### Substring highlighting - -You can also highlight occurrences of a substring in the line with MarkBind's **line-part** syntax. Note that this rule -will highlight **all** occurrences of the substring, and substring is case-sensitive. - -The line-part syntax is of the form `lineNumber[part]`, where `lineNumber` is the line number (subject to the numbering -settings as stated above), and `part` is the substring wrapped in quotes. The exact quote to be used **must** be -different than the one used to specify the `highlight-lines` attribute value. As an example, one can write -`highlight-lines="2['void main']"`. - -However, there are limitations to the line-part syntax. One of which is that if you have quotes inside the substring -in `part`, you have to manually escape it with backslashes (`\`). Secondly, the line-part syntax **will not work** -if the quote inside `part` is the same as the one used to specify the `highlight-lines` attribute value, -even with escaping. - -###### Bounded highlighting - -Bounded highlighting is available if you want to highlight text on a specific boundary. -This highlighting overcomes the limitations of substring highlighting. You can highlight either by characters -or by words. - -**Character-wise**: You can use MarkBind's _line-slice_ syntax format `lineNumber[start:end]`, where `lineNumber` -is the line number, `start` and `end` denote the range of character positions that will be highlighted, such as -`5[2:10]`. Note that the highlight includes character at `start` but does not include character at `end`. - -Character positions start from `0`, which denotes the first non-whitespace character in the line, upwards. -You can omit either `start` or `end` to tell MarkBind either to highlight from the start or until the end, respectively. - -**Word-wise**: The syntax for this is very similar to a normal line-slice syntax, but instead of having a single colon -between `start` and `end`, you write two colons. That is, you can write it as `lineNumber[start::end]`. Now `start` and -`end` denote the range of word positions that will be highlighted. - -Similar to the character-wise variant, `0` denotes the first word in the line, upwards. However, we -define a word here as a _sequence of non-whitespace characters_. - -Also, you can start or end a **partial text-only range highlight** with a line-slice syntax (any variant) to indicate -partial text highlight at the beginning or end of the range, respectively. - -###### Full line highlighting - -If you wish to highlight a full line (which includes indentations), you can use the line-slice syntax as well, -but with `start` **_and_** `end` omitted. We call this the **empty line-slices**. - -To do a **full-line range highlighting**, you only need to use empty line-slices on either ends of the usual -text-only range highlight. +You can specify the rules in many different ways, each is detailed as follows: + +Type | Format | Example +-----|--------|-------- +**Full text highlight**
Highlights the entirety of the text portion of the line | The line numbers as-is (subject to the starting line number set in `start-from`). | `3`, `5` +**Substring highlight**
Highlights _all_ occurrences of a substring in the line | `lineNumber[part]`

_Limitations_: `part` must be wrapped in quotes. If `part` contains a quote, escape it with a backslash (`\`). | `3['Inventory']`,`4['It\'s designed']` +**Character-bounded highlight**
Highlights a specific range of characters in the line | `lineNumber[start:end]`, highlights from character position `start` up to (but not including) `end`.

Character positions start from `0` as the first non-whitespace character, upwards.

Omit either `start`/`end` to highlight from the start / up to the end, respectively. | `19[1:5]`,`30[10:]`,`35[:20]` +**Word-bounded highlight**
Highlights a specific range of words in the line | `lineNumber[start::end]`, highlights from word position `start` up to (but not including) `end`.

Word positions start from `0` as the first word (sequence of non-whitespace characters), upwards

Omit either `start`/`end` to highlight from the start / up to the end, respectively. | `5[2::4]`,`9[1::]`,`11[::5]` +**Full line highlight**
Highlights the entirety of the line | `lineNumber[:]` | `7[:]` + +Not only a single line, MarkBind is also capable of highlighting ranges of lines in various ways. In general, the syntax +for range highlighting consists of two single line highlight rules as listed above joined by a dash (`-`). + +Type | Format | Example +-----|--------|-------- +**Ranged full text highlight**
Highlights only the text portion of the lines within the range | `lineStart-lineEnd` | `2-4` +**Ranged full line highlight**
Highlights the entirety of the lines within the range | `lineStart[:]-lineEnd` or `lineStart-lineEnd[:]` | `1[:]-5`,`10-12[:]` +**Ranged character-bounded highlight**
Highlights the text portion of the lines within the range, but starts/ends at an arbitrary character | `lineStart[start:]-lineEnd` or `lineStart-lineEnd[:end]` | `3[2:]-7`, `4-9[:17]` +**Ranged word-bounded highlight**
Highlights the text portion of the lines within the range, but starts/ends at an arbitrary word | `lineStart[start::]-lineEnd` or `lineStart-lineEnd[::end]` | `16[1::]-20`,`22-24[::3]` From cd40d8ddbf9d5240361e7e1c5c58de23dada3b94 Mon Sep 17 00:00:00 2001 From: Ryo Armanda Date: Sat, 27 Feb 2021 22:13:24 +0800 Subject: [PATCH 19/19] Address docs and minor code reviews --- docs/userGuide/syntax/code.mbdf | 46 +++++++++---------- .../test_site/expected/testCodeBlocks.html | 4 +- .../functional/test_site/testCodeBlocks.md | 4 +- packages/core/src/html/codeblockProcessor.js | 10 ++-- .../markdown-it/highlight/HighlightRule.js | 8 ++-- 5 files changed, 36 insertions(+), 36 deletions(-) diff --git a/docs/userGuide/syntax/code.mbdf b/docs/userGuide/syntax/code.mbdf index 215617cd69..dd8a3c38e6 100644 --- a/docs/userGuide/syntax/code.mbdf +++ b/docs/userGuide/syntax/code.mbdf @@ -62,29 +62,6 @@ function add(a, b) { You can add the `highlight-lines` attribute to add highlighting to your code block. Refer to the example code block below for a visual demonstration of all the possible ways of highlighting a code block. -The value of `highlight-lines` is composed of *highlight rules*, separated by commas. -These rules dictate where and how MarkBind should highlight your code block. - -You can specify the rules in many different ways, each is detailed as follows: - -Type | Format | Example ------|--------|-------- -**Full text highlight**
Highlights the entirety of the text portion of the line | The line numbers as-is (subject to the starting line number set in `start-from`). | `3`, `5` -**Substring highlight**
Highlights _all_ occurrences of a substring in the line | `lineNumber[part]`

_Limitations_: `part` must be wrapped in quotes. If `part` contains a quote, escape it with a backslash (`\`). | `3['Inventory']`,`4['It\'s designed']` -**Character-bounded highlight**
Highlights a specific range of characters in the line | `lineNumber[start:end]`, highlights from character position `start` up to (but not including) `end`.

Character positions start from `0` as the first non-whitespace character, upwards.

Omit either `start`/`end` to highlight from the start / up to the end, respectively. | `19[1:5]`,`30[10:]`,`35[:20]` -**Word-bounded highlight**
Highlights a specific range of words in the line | `lineNumber[start::end]`, highlights from word position `start` up to (but not including) `end`.

Word positions start from `0` as the first word (sequence of non-whitespace characters), upwards

Omit either `start`/`end` to highlight from the start / up to the end, respectively. | `5[2::4]`,`9[1::]`,`11[::5]` -**Full line highlight**
Highlights the entirety of the line | `lineNumber[:]` | `7[:]` - -Not only a single line, MarkBind is also capable of highlighting ranges of lines in various ways. In general, the syntax -for range highlighting consists of two single line highlight rules as listed above joined by a dash (`-`). - -Type | Format | Example ------|--------|-------- -**Ranged full text highlight**
Highlights only the text portion of the lines within the range | `lineStart-lineEnd` | `2-4` -**Ranged full line highlight**
Highlights the entirety of the lines within the range | `lineStart[:]-lineEnd` or `lineStart-lineEnd[:]` | `1[:]-5`,`10-12[:]` -**Ranged character-bounded highlight**
Highlights the text portion of the lines within the range, but starts/ends at an arbitrary character | `lineStart[start:]-lineEnd` or `lineStart-lineEnd[:end]` | `3[2:]-7`, `4-9[:17]` -**Ranged word-bounded highlight**
Highlights the text portion of the lines within the range, but starts/ends at an arbitrary word | `lineStart[start::]-lineEnd` or `lineStart-lineEnd[::end]` | `16[1::]-20`,`22-24[::3]` - @@ -120,6 +97,29 @@ public class Inventory { +The value of `highlight-lines` is composed of *highlight rules*, separated by commas. +These rules dictate where and how MarkBind should highlight your code block. + +You can specify the highlight rules in many different ways, each is detailed as follows: + +Type | Format | Example +-----|--------|-------- +**Full text highlight**
Highlights the entirety of the text portion of the line | The line numbers as-is (subject to the starting line number set in `start-from`). | `3`, `5` +**Substring highlight**
Highlights _all_ occurrences of a substring in the line | `lineNumber[part]`

_Limitations_: `part` must be wrapped in quotes. If `part` contains a quote, escape it with a backslash (`\`). | `3['Inventory']`,`4['It\'s designed']` +**Character-bounded highlight**
Highlights a specific range of characters in the line | `lineNumber[start:end]`, highlights from character position `start` up to (but not including) `end`.

Character positions start from `0` as the first non-whitespace character, upwards.

Omit either `start`/`end` to highlight from the start / up to the end, respectively. | `19[1:5]`,`30[10:]`,`35[:20]` +**Word-bounded highlight**
Highlights a specific range of words in the line | `lineNumber[start::end]`, highlights from word position `start` up to (but not including) `end`.

Word positions start from `0` as the first word (sequence of non-whitespace characters), upwards.

Omit either `start`/`end` to highlight from the start / up to the end, respectively. | `5[2::4]`,`9[1::]`,`11[::5]` +**Full line highlight**
Highlights the entirety of the line | `lineNumber[:]` | `7[:]` + +Not only a single line, MarkBind is also capable of highlighting ranges of lines in various ways. In general, the syntax +for range highlighting consists of two single line highlight rules as listed above joined by a dash (`-`). + +Type | Format | Example +-----|--------|-------- +**Ranged full text highlight**
Highlights from the first non-whitespace character to the last non-whitespace character | `lineStart-lineEnd` | `2-4` +**Ranged full line highlight**
Like ranged full text highlight, but highlights the entirety of the lines | `lineStart[:]-lineEnd` or `lineStart-lineEnd[:]` | `1[:]-5`,`10-12[:]` +**Ranged character-bounded highlight**
Highlights the text portion of the lines within the range, but starts/ends at an arbitrary character | `lineStart[start:]-lineEnd` or `lineStart-lineEnd[:end]` | `3[2:]-7`, `4-9[:17]` +**Ranged word-bounded highlight**
Like ranged character-bounded highlight, but starts/ends at an arbitrary word | `lineStart[start::]-lineEnd` or `lineStart-lineEnd[::end]` | `16[1::]-20`,`22-24[::3]` + ##### Heading To add a heading, add the attribute `heading` with the heading text as the value, as shown below. diff --git a/packages/cli/test/functional/test_site/expected/testCodeBlocks.html b/packages/cli/test/functional/test_site/expected/testCodeBlocks.html index 1632952a41..c556323d41 100644 --- a/packages/cli/test/functional/test_site/expected/testCodeBlocks.html +++ b/packages/cli/test/functional/test_site/expected/testCodeBlocks.html @@ -239,7 +239,7 @@

Test <quux type="name">goo</quux> </foo>

-

highlight-lines attr with partial character-variant line-slice syntax should defaults highlight to start/end of line

+

highlight-lines attr with partial character-variant line-slice syntax should default highlight to start/end of line

<foo>
   <bar type="name">goo</bar>
   <baz type="name">goo</baz>
@@ -263,7 +263,7 @@ 

Test <quux type="name"> goo </quux> </foo>

-

highlight-lines attr with partial word-variant line-slice syntax should defaults highlight to start/end of line

+

highlight-lines attr with partial word-variant line-slice syntax should default highlight to start/end of line

<foo>
   <bar type="name"> goo </bar>
   <baz type="name"> goo </baz>
diff --git a/packages/cli/test/functional/test_site/testCodeBlocks.md b/packages/cli/test/functional/test_site/testCodeBlocks.md
index fd2f15f280..43675c14be 100644
--- a/packages/cli/test/functional/test_site/testCodeBlocks.md
+++ b/packages/cli/test/functional/test_site/testCodeBlocks.md
@@ -76,7 +76,7 @@ Content in a fenced code block
 
 ```
 
-**`highlight-lines` attr with partial character-variant line-slice syntax should defaults highlight to start/end of line**
+**`highlight-lines` attr with partial character-variant line-slice syntax should default highlight to start/end of line**
 ```xml {highlight-lines="1[1:],2[:13],3[2:]-4,5-6[:2]"}
 
   goo
@@ -106,7 +106,7 @@ Content in a fenced code block
 
 ```
 
-**`highlight-lines` attr with partial word-variant line-slice syntax should defaults highlight to start/end of line**
+**`highlight-lines` attr with partial word-variant line-slice syntax should default highlight to start/end of line**
 ```xml {highlight-lines="1[0::],2[3::],3[::2],4[2::],5[::3]"}
 
    goo 
diff --git a/packages/core/src/html/codeblockProcessor.js b/packages/core/src/html/codeblockProcessor.js
index b040d77a3e..40c1a63069 100644
--- a/packages/core/src/html/codeblockProcessor.js
+++ b/packages/core/src/html/codeblockProcessor.js
@@ -8,7 +8,7 @@ const utils = require('../lib/markdown-it/utils');
  * @param node The node of the line part to be traversed
  * @param hlStart The highlight start position, relative to the start of the line part
  * @param hlEnd The highlight end position, relative to the start of the line part
- * @returns {object} An array of two items.
+ * @returns {object} An object that contains data to be used by the node's parent.
  */
 function traverseLinePart(node, hlStart, hlEnd) {
   const resData = {
@@ -56,13 +56,12 @@ function traverseLinePart(node, hlStart, hlEnd) {
    * It may have more children, such as inner tag nodes.
    */
 
-  let curr = 0;
   const highlightData = node.children.map((child) => {
-    const data = traverseLinePart(child, hlStart - curr, hlEnd - curr);
-    curr += data.numCharsTraversed;
+    const [relativeHlStart, relativeHlEnd] = [hlStart, hlEnd].map(x => x - resData.numCharsTraversed);
+    const data = traverseLinePart(child, relativeHlStart, relativeHlEnd);
+    resData.numCharsTraversed += data.numCharsTraversed;
     return data;
   });
-  resData.numCharsTraversed = curr;
 
   if (highlightData.every(data => data.shouldParentHighlight && !data.highlightRange)) {
     /*
@@ -111,7 +110,6 @@ function traverseLinePart(node, hlStart, hlEnd) {
  * This looks into each line for highlighting data, and if found,
  * traverses over the line and applies the highlight.
  * @param node Root of the code block element, which is the 'pre' node
- * @return
  */
 function highlightCodeBlock(node) {
   const codeNode = node.children.find(c => c.name === 'code');
diff --git a/packages/core/src/lib/markdown-it/highlight/HighlightRule.js b/packages/core/src/lib/markdown-it/highlight/HighlightRule.js
index dad9a0eb1d..1761dfe225 100644
--- a/packages/core/src/lib/markdown-it/highlight/HighlightRule.js
+++ b/packages/core/src/lib/markdown-it/highlight/HighlightRule.js
@@ -77,9 +77,11 @@ class HighlightRule {
   }
 
   static _highlightPartOfText(codeStr, bounds) {
-    // Note: As part-of-text highlighting requires walking over the node of the generated
-    // html by highlight.js, highlighting will be applied in NodeProcessor instead.
-    // hl-data is used to pass over the bounds.
+    /*
+     * Note: As part-of-text highlighting requires walking over the node of the generated
+     * html by highlight.js, highlighting will be applied in NodeProcessor instead.
+     * hl-data is used to pass over the bounds.
+     */
     const dataStr = bounds.map(bound => bound.join('-')).join(',');
     return `${codeStr}\n`;
   }