Skip to content

Commit e1866d9

Browse files
lwchkgazu
authored andcommitted
fix: Allow using of non-word characters in [import] tag (#52)
* Allow more characters to be used in string fields of [include] tag * Removed unwanted file. * Respond to comments in pull request #68 * Fix errors in test, fix typo, add license for unescape-string.js * Update dependency of gitbook-cli in example builds * Respond to comments again. (Did not read a comment correctly) * Fix missing empty line * Changed the last assert.equal to assert.strictEqual
1 parent 9069a92 commit e1866d9

File tree

13 files changed

+275
-53
lines changed

13 files changed

+275
-53
lines changed

LICENSE

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,34 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
1717
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
1818
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
1919
SOFTWARE.
20+
21+
---
22+
23+
src/unescape-string.js is extracted from https://github.com/jgm/commonmark.js:
24+
25+
Copyright (c) 2014, John MacFarlane
26+
27+
All rights reserved.
28+
29+
Redistribution and use in source and binary forms, with or without
30+
modification, are permitted provided that the following conditions are met:
31+
32+
* Redistributions of source code must retain the above copyright
33+
notice, this list of conditions and the following disclaimer.
34+
35+
* Redistributions in binary form must reproduce the above
36+
copyright notice, this list of conditions and the following
37+
disclaimer in the documentation and/or other materials provided
38+
with the distribution.
39+
40+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
41+
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
42+
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
43+
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
44+
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
45+
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
46+
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
47+
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
48+
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
49+
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
50+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

examples/ace/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
"author": "azu",
1616
"license": "MIT",
1717
"devDependencies": {
18-
"gitbook-cli": "^2.1.3",
18+
"gitbook-cli": "2.3.0 || ^2.3.2",
1919
"gitbook-plugin-include-codeblock": "file:../../"
2020
}
2121
}

examples/custom/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
"author": "azu",
1616
"license": "MIT",
1717
"devDependencies": {
18-
"gitbook-cli": "^2.1.3",
18+
"gitbook-cli": "2.3.0 || ^2.3.2",
1919
"gitbook-plugin-include-codeblock": "file:../../"
2020
}
2121
}

examples/default/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
"author": "azu",
1616
"license": "MIT",
1717
"devDependencies": {
18-
"gitbook-cli": "^2.1.3",
18+
"gitbook-cli": "2.3.0 || ^2.3.2",
1919
"gitbook-plugin-include-codeblock": "file:../../"
2020
}
2121
}

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@
8989
}
9090
},
9191
"dependencies": {
92+
"entities": "^1.1.1",
9293
"language-map": "^1.1.1",
9394
"handlebars": "^4.0.5",
9495
"winston-color": "^1.0.0"

src/parser.js

Lines changed: 76 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,25 @@
33
const path = require("path");
44
const Handlebars = require("handlebars");
55
const logger = require("winston-color");
6-
import { defaultKeyValueMap, initOptions, checkMapTypes, convertValue } from "./options.js";
6+
import { defaultKeyValueMap, initOptions, checkMapTypes } from "./options.js";
7+
import { unescapeString } from "./unescape-string.js";
78
import { getLang } from "./language-detection";
89
import { getMarker, hasMarker, markerSliceCode, removeMarkers } from "./marker";
910
import { sliceCode, hasSliceRange, getSliceRange } from "./slicer";
1011
import { hasTitle } from "./title";
1112
import { getTemplateContent, readFileFromPath } from "./template";
12-
const markdownLinkFormatRegExp = /\[([^\]]*?)\]\(([^\)]*?)\)/gm;
13+
const markdownLinkFormatRegExp = /\[((?:[^\]]|\\.)*?)\]\(((?:[^\)]|\\.)*?)\)/gm;
14+
15+
const keyEx = "\\w+";
16+
const kvsepEx = "[:=]";
17+
const spacesEx = "\\s*";
18+
const quoteEx = "[\"']";
19+
const valEx = "(?:[^'\"\\\\]|\\\\.)*";
20+
const argEx = `${quoteEx}${valEx}${quoteEx}|true|false`;
21+
const expressionEx = `(${keyEx})${kvsepEx}${spacesEx}(${argEx})`;
22+
const expressionRegExp = new RegExp(expressionEx, "g");
23+
24+
const markerRegExp = /^\s*(([-\w\s]*,?)*)$/;
1325

1426
/**
1527
* A counter to count how many code are imported.
@@ -68,6 +80,48 @@ export function containIncludeCommand(commands = []) {
6880
});
6981
}
7082

83+
/**
84+
* Parse the given value to the given type. Returns the value if valid, otherwise returns undefined.
85+
* @param {string} value
86+
* @param {string} type "string", "boolean"
87+
* @param {string} key
88+
* @return {boolean|string|undefined}
89+
*/
90+
export function parseValue(value, type, key) {
91+
if (type === "string") {
92+
const unescapedvalue = unescapeString(value.substring(1, value.length - 1));
93+
if (key === "marker" && !markerRegExp.test(unescapedvalue)) {
94+
logger.error(
95+
"include-codeblock: parseVariablesFromLabel: invalid value " +
96+
`\`${unescapedvalue}\` in key \`marker\``
97+
);
98+
return undefined;
99+
}
100+
return unescapedvalue;
101+
}
102+
103+
if (type === "boolean") {
104+
if (["true", '"true"', "'true'"].indexOf(value) >= 0) {
105+
return true;
106+
}
107+
108+
if (["false", '"false"', "'false'"].indexOf(value) >= 0) {
109+
return false;
110+
}
111+
112+
logger.error(
113+
"include-codeblock: parseVariablesFromLabel: invalid value " +
114+
`\`${value}\` in key \`${key}\`. Expect true or false.`
115+
);
116+
return undefined;
117+
}
118+
119+
logger.error(
120+
`include-codeblock: parseVariablesFromLabel: unknown key type \`${type}\` (see options.js)`
121+
);
122+
return undefined;
123+
}
124+
71125
/** Parse the command label and return a new key-value object
72126
* @example
73127
* [import,title:"<thetitle>",label:"<thelabel>"](path/to/file.ext)
@@ -77,56 +131,29 @@ export function containIncludeCommand(commands = []) {
77131
*/
78132
export function parseVariablesFromLabel(kvMap, label) {
79133
const kv = Object.assign({}, kvMap);
80-
const beginEx = "^.*";
81-
const endEx = ".*$";
82-
const sepEx = ",?";
83-
const kvsepEx = "[:=]";
84-
const spacesEx = "\\s*";
85-
const quotesEx = "[\"']";
86-
87-
Object.keys(kv).forEach(key => {
88-
let keyEx = "(" + key + ")";
89-
let valEx = "([-\\w\\s]*)";
90-
if (key === "marker") {
91-
keyEx = "(import|include)";
92-
valEx = "(([-\\w\\s]*,?)*)";
134+
135+
let match = "";
136+
while ((match = expressionRegExp.exec(label))) {
137+
let key = match[1];
138+
if (key === "include" || key === "import") {
139+
key = "marker";
93140
}
94-
// Add value check here
95-
switch (typeof defaultKeyValueMap[key]) {
96-
case "string":
97-
valEx = quotesEx + valEx + quotesEx;
98-
break;
99-
case "boolean":
100-
// no quotes
101-
valEx = quotesEx + "?(true|false)" + quotesEx + "?";
102-
break;
103-
default:
104-
logger.error(
105-
"include-codeblock: parseVariablesFromLabel: key type `" +
106-
typeof defaultKeyValueMap[key] +
107-
"` unknown (see options.js)"
108-
);
109-
break;
141+
const value = match[2];
142+
143+
if (!kv.hasOwnProperty(key)) {
144+
logger.error(
145+
"include-codeblock: parseVariablesFromLabel: unknown key " +
146+
`\`${key}\` (see options.js)`
147+
);
148+
return;
110149
}
111-
// Val type cast to string.
112-
const regStr =
113-
beginEx +
114-
sepEx +
115-
spacesEx +
116-
keyEx +
117-
spacesEx +
118-
kvsepEx +
119-
spacesEx +
120-
valEx +
121-
spacesEx +
122-
sepEx +
123-
endEx;
124-
const reg = new RegExp(regStr);
125-
const res = label.match(reg);
126-
if (res) {
127-
kv[key] = convertValue(res[2], typeof defaultKeyValueMap[key]);
150+
151+
const parsedValue = parseValue(value, typeof defaultKeyValueMap[key], key);
152+
if (parsedValue !== undefined) {
153+
kv[key] = parsedValue;
128154
}
129-
});
155+
}
156+
130157
return Object.freeze(kv);
131158
}
132159

src/unescape-string.js

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
// The code in this file is extracted from commonmark.js
2+
// (https://github.com/jgm/commonmark.js), which is owned by John MacFarlane.
3+
// LICENSE : BSD-2-Clause
4+
"use strict";
5+
6+
var decodeHTML = require("entities").decodeHTML;
7+
8+
var C_BACKSLASH = 92;
9+
var ENTITY = "&(?:#x[a-f0-9]{1,8}|#[0-9]{1,8}|[a-z][a-z0-9]{1,31});";
10+
var reBackslashOrAmp = /[\\&]/;
11+
var ESCAPABLE = "[!\"#$%&'()*+,./:;<=>?@[\\\\\\]^_`{|}~-]";
12+
var reEntityOrEscapedChar = new RegExp("\\\\" + ESCAPABLE + "|" + ENTITY, "gi");
13+
14+
function unescapeChar(s) {
15+
if (s.charCodeAt(0) === C_BACKSLASH) {
16+
return s.charAt(1);
17+
} else {
18+
return decodeHTML(s);
19+
}
20+
}
21+
22+
// Replace entities and backslash escapes with literal characters.
23+
export function unescapeString(s) {
24+
if (reBackslashOrAmp.test(s)) {
25+
return s.replace(reEntityOrEscapedChar, unescapeChar);
26+
} else {
27+
return s;
28+
}
29+
}

test/parser-test.js

Lines changed: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
containIncludeCommand,
77
splitLabelToCommands,
88
strip,
9+
parseValue,
910
parseVariablesFromLabel
1011
} from "../src/parser";
1112

@@ -45,7 +46,45 @@ describe("parse", function() {
4546
assert(containIncludeCommand(commands));
4647
});
4748
});
48-
context("parseVariablesFromLabel ", function() {
49+
describe("parseValue", function() {
50+
it("should unescape string parameter", function() {
51+
const result = parseValue(
52+
'"\\!\\"\\#\\$\\%\\&\\\'\\(\\)\\*\\+\\,\\-\\.\\/' +
53+
"\\:\\;\\<\\=\\>\\?\\@\\[\\\\\\]\\^\\_\\`\\{\\|\\}\\~" +
54+
'&amp;&lt;&gt;&#65;&#x41;"',
55+
"string",
56+
""
57+
);
58+
assert.strictEqual(result, "!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~&<>AA");
59+
});
60+
it("should backslash unescape commonmark defined characters only", function() {
61+
const result = parseValue('"\\r\\n\\[\\]"', "string", "");
62+
assert.strictEqual(result, "\\r\\n[]"); // \r and \n should not be unescaped.
63+
});
64+
it("should validate markers", function() {
65+
let result = parseValue('" marker0 , marker1 "', "string", "marker");
66+
assert.strictEqual(result, " marker0 , marker1 ");
67+
68+
result = parseValue('"~invalid~"', "string", "marker");
69+
assert.strictEqual(result, undefined);
70+
});
71+
it("should parse boolean values", function() {
72+
let result = parseValue("true", "boolean", "");
73+
assert.strictEqual(result, true);
74+
result = parseValue('"true"', "boolean", "");
75+
assert.strictEqual(result, true);
76+
result = parseValue("'true'", "boolean", "");
77+
assert.strictEqual(result, true);
78+
79+
result = parseValue("false", "boolean", "");
80+
assert.strictEqual(result, false);
81+
result = parseValue('"false"', "boolean", "");
82+
assert.strictEqual(result, false);
83+
result = parseValue("'false'", "boolean", "");
84+
assert.strictEqual(result, false);
85+
});
86+
});
87+
describe("parseVariablesFromLabel ", function() {
4988
it("should retrieve edit boolean", function() {
5089
const resmap = parseVariablesFromLabel(kvmap, "include,edit:true");
5190
const results = resmap;
@@ -113,6 +152,36 @@ describe("parse", function() {
113152
const results = resmap;
114153
assert.equal(results.marker, "");
115154
});
155+
it("should handle characters for string parameter", function() {
156+
const resmap = parseVariablesFromLabel(
157+
kvmap,
158+
'import,title="test+with-special*string"'
159+
);
160+
const results = resmap;
161+
assert.strictEqual(results.title, "test+with-special*string");
162+
});
163+
it("should unescape string parameter", function() {
164+
const resmap = parseVariablesFromLabel(
165+
kvmap,
166+
'import,title="\\!\\"\\#\\$\\%\\&\\\'\\(\\)\\*\\+\\,\\-\\.\\/' +
167+
"\\:\\;\\<\\=\\>\\?\\@\\[\\\\\\]\\^\\_\\`\\{\\|\\}\\~" +
168+
'&amp;&lt;&gt;&#65;&#x41;"'
169+
);
170+
const results = resmap;
171+
assert.strictEqual(results.title, "!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~&<>AA");
172+
});
173+
it("should backslash unescape commonmark defined characters only", function() {
174+
const resmap = parseVariablesFromLabel(kvmap, 'import,title="\\r\\n\\[\\]"');
175+
const results = resmap;
176+
assert.strictEqual(results.title, "\\r\\n[]"); // \r and \n should not be unescaped.
177+
});
178+
it("should not parse string argument into another key-value pair", function() {
179+
assert.strictEqual(kvmap.edit, false);
180+
const resmap = parseVariablesFromLabel(kvmap, 'import,title="edit:true"');
181+
const results = resmap;
182+
assert.strictEqual(results.edit, false);
183+
assert.strictEqual(results.title, "edit:true");
184+
});
116185
});
117186
// inspired from https://github.com/rails/rails/blob/master/activesupport/test/core_ext/string_ext_test.rb
118187
describe("strip", function() {
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
[import, title:"title<1>", id:"edit:true<2>", lang:"javascript+theme:abc<3>", class="class<4>", edit=true, check=true, theme="monokai"](./test&1.js)
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
const path = require("path");
2+
module.exports = {
3+
"pluginsConfig": {
4+
"include-codeblock": {
5+
"template": path.join(__dirname, "dump.hbs")
6+
}
7+
}
8+
};

0 commit comments

Comments
 (0)