Repository: zestedesavoir/zmarkdown (js)

Issue:
zmarkdown: inverting line number of a range causes a fatal error
The bug only appears with current zmarkdown configuration, you just have to start the server and send provided markdown

# The minimal code

```
```php hl_lines="3-01"
<?php

namespace App\Validator;
```

# observed behaviour

error is raised in `zmarkdown/utils/code-handler.js` at line 70
```
RangeError

Invalid array length
```

# desired behaviour

two possible behaviour : 

- if left limit is greater than right limit, do nothing, 
- swap the range limits if they are inverted


Pull request:
Allow inverted ranges in hl_lines
The parser won't crash if the range is entered inverted by the user, i.e. `hl_lines=2-1` instead of `hl_lines=1-2`. In this example, lines 1 and 2 will be highlighted.

Fixes #414.

#### Notes

- This fixes the problem in the HTML export but not in the LaTeX one where the range stays inverted. Don't know if it's a problem.
- I'm still very new to zmarkdown, so I don't know if I should add a test somewhere for that. I guess so?

Patch:
diff --git a/packages/rebber/dist/types/code.js b/packages/rebber/dist/types/code.js
index 432cca1e..9092c6cb 100644
--- a/packages/rebber/dist/types/code.js
+++ b/packages/rebber/dist/types/code.js
@@ -1,5 +1,19 @@
 "use strict";
 
+function _slicedToArray(arr, i) { return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _unsupportedIterableToArray(arr, i) || _nonIterableRest(); }
+
+function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); }
+
+function _iterableToArrayLimit(arr, i) { if (typeof Symbol === "undefined" || !(Symbol.iterator in Object(arr))) return; var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"] != null) _i["return"](); } finally { if (_d) throw _e; } } return _arr; }
+
+function _arrayWithHoles(arr) { if (Array.isArray(arr)) return arr; }
+
+function _createForOfIteratorHelper(o, allowArrayLike) { var it; if (typeof Symbol === "undefined" || o[Symbol.iterator] == null) { if (Array.isArray(o) || (it = _unsupportedIterableToArray(o)) || allowArrayLike && o && typeof o.length === "number") { if (it) o = it; var i = 0; var F = function F() {}; return { s: F, n: function n() { if (i >= o.length) return { done: true }; return { done: false, value: o[i++] }; }, e: function e(_e2) { throw _e2; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var normalCompletion = true, didErr = false, err; return { s: function s() { it = o[Symbol.iterator](); }, n: function n() { var step = it.next(); normalCompletion = step.done; return step; }, e: function e(_e3) { didErr = true; err = _e3; }, f: function f() { try { if (!normalCompletion && it["return"] != null) it["return"](); } finally { if (didErr) throw err; } } }; }
+
+function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); }
+
+function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) { arr2[i] = arr[i]; } return arr2; }
+
 /* Expose. */
 module.exports = code;
 var codeBlockParamsMapping = [null, 'hl_lines=', 'linenostart='];
@@ -25,6 +39,45 @@ var defaultMacro = function defaultMacro(content, lang, attrs) {
 
         if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
           value = value.slice(1, -1).trim();
+        } // For hl_lines, we parse the parameters to ensure we don't have
+        // inverted ranges.
+
+
+        if (_param === 'hl_lines=') {
+          var hlItems = value.split(/,| /);
+
+          var _iterator = _createForOfIteratorHelper(hlItems.entries()),
+              _step;
+
+          try {
+            for (_iterator.s(); !(_step = _iterator.n()).done;) {
+              var _step$value = _slicedToArray(_step.value, 2),
+                  _i = _step$value[0],
+                  item = _step$value[1];
+
+              if (!item.includes('-')) {
+                continue;
+              }
+
+              var n = item.split('-').map(function (n) {
+                return parseInt(n);
+              }).filter(function (n) {
+                return !isNaN(n);
+              });
+
+              if (n.length !== 2) {
+                continue;
+              }
+
+              hlItems[_i] = "".concat(Math.min(n[0], n[1]), "-").concat(Math.max(n[0], n[1]));
+            }
+          } catch (err) {
+            _iterator.e(err);
+          } finally {
+            _iterator.f();
+          }
+
+          value = hlItems.join(',');
         }
 
         localCodeBlockParams[i] = value;
diff --git a/packages/rebber/src/types/code.js b/packages/rebber/src/types/code.js
index a5b23285..f4e3f29e 100644
--- a/packages/rebber/src/types/code.js
+++ b/packages/rebber/src/types/code.js
@@ -37,6 +37,27 @@ const defaultMacro = (content, lang, attrs) => {
           value = value.slice(1, -1).trim()
         }
 
+        // For hl_lines, we parse the parameters to ensure we don't have
+        // inverted ranges.
+        if (param === 'hl_lines=') {
+          const hlItems = value.split(/,| /)
+
+          for (const [i, item] of hlItems.entries()) {
+            if (!item.includes('-')) {
+              continue
+            }
+
+            const n = item.split('-').map(n => parseInt(n)).filter(n => !isNaN(n))
+            if (n.length !== 2) {
+              continue
+            }
+
+            hlItems[i] = `${Math.min(n[0], n[1])}-${Math.max(n[0], n[1])}`
+          }
+
+          value = hlItems.join(',')
+        }
+
         localCodeBlockParams[i] = value
       }
     }
diff --git a/packages/zmarkdown/utils/code-handler.js b/packages/zmarkdown/utils/code-handler.js
index 893cd145..98edb939 100644
--- a/packages/zmarkdown/utils/code-handler.js
+++ b/packages/zmarkdown/utils/code-handler.js
@@ -64,10 +64,15 @@ const rangeHandler = range => {
   function insert () {
     if (previousNumber >= 0) {
       const currentInt = parseInt(currentNumber)
-      const rangeLength = (currentInt - previousNumber) + 1
+      const previousInt = parseInt(previousNumber)
+
+      const minLineNumber = Math.min(currentInt, previousInt)
+      const maxLineNumber = Math.max(currentInt, previousInt)
+
+      const rangeLength = (maxLineNumber - minLineNumber) + 1
 
       parsedRange.push(...[...Array(rangeLength).keys()]
-        .map(i => i + previousNumber))
+        .map(i => i + minLineNumber))
     } else {
       parsedRange.push(parseInt(currentNumber))
     }


Test patch:
diff --git a/packages/zmarkdown/__tests__/__snapshots__/html-suite.test.js.snap b/packages/zmarkdown/__tests__/__snapshots__/html-suite.test.js.snap
index 374a9ebb..e1043c54 100644
--- a/packages/zmarkdown/__tests__/__snapshots__/html-suite.test.js.snap
+++ b/packages/zmarkdown/__tests__/__snapshots__/html-suite.test.js.snap
@@ -51,6 +51,15 @@ exports[`code highlight special cases supports hl_lines - interval syntax 1`] =
 </code></pre></div>"
 `;
 
+exports[`code highlight special cases supports hl_lines - interval syntax reversed 1`] = `
+"<div class=\\"hljs-code-div hljs-code-python\\"><div class=\\"hljs-line-numbers\\"><span data-count=\\"1\\" class=\\"hll\\"></span><span data-count=\\"2\\" class=\\"hll\\"></span><span data-count=\\"3\\" class=\\"hll\\"></span><span data-count=\\"4\\"></span><span data-count=\\"5\\"></span></div><pre><code class=\\"hljs language-python\\"><span class=\\"hljs-function\\"><span class=\\"hljs-keyword\\">def</span> <span class=\\"hljs-title\\">main</span>():</span>
+  print(<span class=\\"hljs-string\\">'It\\\\'s amazing!'</span>)
+
+<span class=\\"hljs-function\\"><span class=\\"hljs-keyword\\">def</span> <span class=\\"hljs-title\\">unused</span>():</span>
+  print(<span class=\\"hljs-string\\">'Please use me!'</span>)
+</code></pre></div>"
+`;
+
 exports[`code highlight special cases supports hl_lines - spaced syntax 1`] = `
 "<div class=\\"hljs-code-div hljs-code-python\\"><div class=\\"hljs-line-numbers\\"><span data-count=\\"1\\"></span><span data-count=\\"2\\"></span><span data-count=\\"3\\"></span><span data-count=\\"4\\" class=\\"hll\\"></span><span data-count=\\"5\\" class=\\"hll\\"></span></div><pre><code class=\\"hljs language-python\\"><span class=\\"hljs-function\\"><span class=\\"hljs-keyword\\">def</span> <span class=\\"hljs-title\\">main</span>():</span>
   print(<span class=\\"hljs-string\\">'It\\\\'s amazing!'</span>)
diff --git a/packages/zmarkdown/__tests__/__snapshots__/latex-suite.test.js.snap b/packages/zmarkdown/__tests__/__snapshots__/latex-suite.test.js.snap
index cfca5851..4f6db028 100644
--- a/packages/zmarkdown/__tests__/__snapshots__/latex-suite.test.js.snap
+++ b/packages/zmarkdown/__tests__/__snapshots__/latex-suite.test.js.snap
@@ -56,6 +56,23 @@ a code without lang
 \\\\end{CodeBlock}"
 `;
 
+exports[`code+reversed-hl 1`] = `
+"\\\\begin{CodeBlock}[][2-3]{python}
+print('bla')
+print('bla')
+print('bla')
+\\\\end{CodeBlock}
+
+
+
+\\\\begin{CodeBlock}[][1-2,3-4]{python}
+print('bla')
+print('bla')
+print('bla')
+print('bla')
+\\\\end{CodeBlock}"
+`;
+
 exports[`code+caption 1`] = `
 "\\\\begin{CodeBlock}{python}
 print('bla')
diff --git a/packages/zmarkdown/__tests__/html-suite.test.js b/packages/zmarkdown/__tests__/html-suite.test.js
index a21fee7e..56653b22 100644
--- a/packages/zmarkdown/__tests__/html-suite.test.js
+++ b/packages/zmarkdown/__tests__/html-suite.test.js
@@ -328,13 +328,29 @@ describe('code highlight special cases', () => {
 
   it('supports hl_lines - interval syntax', async () => {
     const input = dedent `
-      \`\`\`python hl_lines=1-3
-      def main():
-        print('It\'s amazing!')
-      
-      def unused():
-        print('Please use me!')
-      \`\`\`
+    \`\`\`python hl_lines=1-3
+    def main():
+      print('It\'s amazing!')
+    
+    def unused():
+      print('Please use me!')
+    \`\`\`
+    `
+
+    const result = await renderString(input)
+    expect((result.match(/class="hll"/g) || []).length).toBe(3)
+    expect(result).toMatchSnapshot()
+  })
+
+  it('supports hl_lines - interval syntax reversed', async () => {
+    const input = dedent `
+    \`\`\`python hl_lines=3-1
+    def main():
+      print('It\'s amazing!')
+    
+    def unused():
+      print('Please use me!')
+    \`\`\`
     `
 
     const result = await renderString(input)
diff --git a/packages/zmarkdown/__tests__/latex-suite.test.js b/packages/zmarkdown/__tests__/latex-suite.test.js
index fd33a998..dceff1ee 100644
--- a/packages/zmarkdown/__tests__/latex-suite.test.js
+++ b/packages/zmarkdown/__tests__/latex-suite.test.js
@@ -192,6 +192,28 @@ test('code', () => {
   return expect(p).resolves.toMatchSnapshot()
 })
 
+test('code+reversed-hl', () => {
+  const p = renderString(dedent`
+    \`\`\`python hl_lines=3-2
+    print('bla')
+    print('bla')
+    print('bla')
+    \`\`\`
+
+    \`\`\`python hl_lines=2-1,4-3
+    print('bla')
+    print('bla')
+    print('bla')
+    print('bla')
+    \`\`\`
+  `)
+
+  return expect(p).resolves.toMatchSnapshot() &&
+    expect(p).not.toContain('3-2') &&
+    expect(p).not.toContain('2-1') &&
+    expect(p).not.toContain('4-3')
+})
+
 test('code+caption', () => {
   const p = renderString(dedent`
     \`\`\`python