mirror of
https://github.com/tenrok/BBob.git
synced 2026-06-11 18:02:26 +03:00
add eslint, travis config, test tasks
This commit is contained in:
@@ -1,4 +1,3 @@
|
||||
package-lock.json
|
||||
coverage
|
||||
lib
|
||||
dist
|
||||
@@ -1,2 +1,5 @@
|
||||
benchmark
|
||||
*.test.js
|
||||
package-lock.json
|
||||
coverage
|
||||
src
|
||||
dist
|
||||
!lib
|
||||
+155
-152
@@ -1,174 +1,177 @@
|
||||
const {
|
||||
convertTokenToText,
|
||||
getTagName,
|
||||
getTokenColumn,
|
||||
getTokenLine,
|
||||
getTokenValue,
|
||||
isAttrNameToken,
|
||||
isAttrValueToken,
|
||||
isTagStart,
|
||||
isTagToken,
|
||||
isTextToken,
|
||||
isTagEnd
|
||||
} = require("./Tokenizer");
|
||||
const Tokenizer = require("./Tokenizer");
|
||||
convertTokenToText,
|
||||
getTagName,
|
||||
getTokenColumn,
|
||||
getTokenLine,
|
||||
getTokenValue,
|
||||
isAttrNameToken,
|
||||
isAttrValueToken,
|
||||
isTagStart,
|
||||
isTagToken,
|
||||
isTextToken,
|
||||
isTagEnd,
|
||||
} = require('./Tokenizer');
|
||||
|
||||
const Tokenizer = require('./Tokenizer');
|
||||
|
||||
const TokenChar = Tokenizer.CHAR;
|
||||
const getChar = Tokenizer.getChar;
|
||||
|
||||
const createTagNode = (name, attrs = {}, content = []) => ({tag: name, attrs, content});
|
||||
const createTagNode = (name, attrs = {}, content = []) => ({ tag: name, attrs, content });
|
||||
|
||||
/**
|
||||
*
|
||||
{
|
||||
tag: 'div',
|
||||
attrs: {
|
||||
class: 'foo'
|
||||
},
|
||||
content: ['hello world!']
|
||||
}
|
||||
{
|
||||
tag: 'div',
|
||||
attrs: {
|
||||
class: 'foo'
|
||||
},
|
||||
content: ['hello world!']
|
||||
}
|
||||
*/
|
||||
module.exports = class Parser {
|
||||
constructor(tokens, options = {}) {
|
||||
this.tokens = tokens;
|
||||
this.options = options
|
||||
}
|
||||
constructor(tokens, options = {}) {
|
||||
this.tokens = tokens;
|
||||
this.options = options;
|
||||
}
|
||||
|
||||
parse() {
|
||||
const tokens = this.tokens;
|
||||
const nodes = [];
|
||||
const nestedNodes = [];
|
||||
const curTags = [];
|
||||
const curTagsAttrName = [];
|
||||
parse() {
|
||||
const nodes = [];
|
||||
const nestedNodes = [];
|
||||
const curTags = [];
|
||||
const curTagsAttrName = [];
|
||||
|
||||
const closableTags = this.findNestedTags(tokens);
|
||||
const closableTags = this.findNestedTags(this.tokens);
|
||||
|
||||
const isNestedTag = (token) => closableTags.indexOf(getTokenValue(token)) >= 0;
|
||||
const isNestedTag = token => closableTags.indexOf(getTokenValue(token)) >= 0;
|
||||
|
||||
const getCurTag = () => {
|
||||
if (curTags.length) {
|
||||
return curTags[curTags.length - 1]
|
||||
const getCurTag = () => {
|
||||
if (curTags.length) {
|
||||
return curTags[curTags.length - 1];
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const createCurTag = (token) => {
|
||||
curTags.push(createTagNode(getTokenValue(token)));
|
||||
};
|
||||
|
||||
const getCurTagAttrName = () => {
|
||||
if (curTagsAttrName.length) {
|
||||
return curTagsAttrName[curTagsAttrName.length - 1];
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const createCurTagAttrName = (token) => {
|
||||
curTagsAttrName.push(getTokenValue(token));
|
||||
};
|
||||
|
||||
const clearCurTagAttrName = () => {
|
||||
if (curTagsAttrName.length) {
|
||||
curTagsAttrName.pop();
|
||||
}
|
||||
};
|
||||
|
||||
const clearCurTag = () => {
|
||||
if (curTags.length) {
|
||||
curTags.pop();
|
||||
|
||||
clearCurTagAttrName();
|
||||
}
|
||||
};
|
||||
|
||||
const getNodes = () => {
|
||||
if (nestedNodes.length) {
|
||||
const nestedNode = nestedNodes[nestedNodes.length - 1];
|
||||
return nestedNode.content;
|
||||
}
|
||||
|
||||
return nodes;
|
||||
};
|
||||
|
||||
let token;
|
||||
// eslint-disable-next-line no-cond-assign
|
||||
while (token = this.tokens.shift()) {
|
||||
if (!token) {
|
||||
// eslint-disable-next-line no-continue
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isTagToken(token)) {
|
||||
if (this.isAllowedTag(getTagName(token))) {
|
||||
// [tag]
|
||||
if (isTagStart(token)) {
|
||||
createCurTag(token);
|
||||
|
||||
if (isNestedTag(token)) {
|
||||
nestedNodes.push(getCurTag());
|
||||
} else {
|
||||
getNodes().push(getCurTag());
|
||||
clearCurTag();
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
};
|
||||
// [/tag]
|
||||
if (isTagEnd(token)) {
|
||||
clearCurTag();
|
||||
|
||||
const createCurTag = (token) => {
|
||||
curTags.push(createTagNode(getTokenValue(token)))
|
||||
};
|
||||
const lastNestedNode = nestedNodes.pop();
|
||||
|
||||
const clearCurTag = () => {
|
||||
if (curTags.length) {
|
||||
curTags.pop();
|
||||
|
||||
clearCurTagAttrName()
|
||||
}
|
||||
};
|
||||
|
||||
const getCurTagAttrName = () => {
|
||||
if (curTagsAttrName.length) {
|
||||
return curTagsAttrName[curTagsAttrName.length - 1]
|
||||
}
|
||||
|
||||
return null
|
||||
};
|
||||
|
||||
const createCurTagAttrName = (token) => {
|
||||
curTagsAttrName.push(getTokenValue(token))
|
||||
};
|
||||
|
||||
const clearCurTagAttrName = () => {
|
||||
if (curTagsAttrName.length) {
|
||||
curTagsAttrName.pop()
|
||||
}
|
||||
};
|
||||
|
||||
const getNodes = () => {
|
||||
if (nestedNodes.length) {
|
||||
const nestedNode = nestedNodes[nestedNodes.length - 1];
|
||||
return nestedNode.content
|
||||
}
|
||||
|
||||
return nodes
|
||||
};
|
||||
|
||||
let token;
|
||||
while (token = tokens.shift()) {
|
||||
if (!token) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isTagToken(token)) {
|
||||
if (this.isAllowedTag(getTagName(token))) {
|
||||
// [tag]
|
||||
if (isTagStart(token)) {
|
||||
createCurTag(token);
|
||||
|
||||
if (isNestedTag(token)) {
|
||||
nestedNodes.push(getCurTag())
|
||||
} else {
|
||||
getNodes().push(getCurTag());
|
||||
clearCurTag()
|
||||
}
|
||||
}
|
||||
|
||||
// [/tag]
|
||||
if (isTagEnd(token)) {
|
||||
clearCurTag();
|
||||
|
||||
const lastNestedNode = nestedNodes.pop();
|
||||
|
||||
if (lastNestedNode) {
|
||||
getNodes().push(lastNestedNode)
|
||||
} else {
|
||||
console.warn(`Inconsistent tag '${getTokenValue(token)}' on line ${getTokenLine(token)} and column ${getTokenColumn(token)}`);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
getNodes().push(convertTokenToText(token))
|
||||
}
|
||||
}
|
||||
|
||||
if (getCurTag()) {
|
||||
if (isAttrNameToken(token)) {
|
||||
createCurTagAttrName(token);
|
||||
getCurTag().attrs[getCurTagAttrName()] = null
|
||||
} else if (isAttrValueToken(token)) {
|
||||
getCurTag().attrs[getCurTagAttrName()] = getTokenValue(token);
|
||||
clearCurTagAttrName()
|
||||
} else if (isTextToken(token)) {
|
||||
getCurTag().content.push(getTokenValue(token))
|
||||
}
|
||||
} else if (isTextToken(token)) {
|
||||
getNodes().push(getTokenValue(token))
|
||||
if (lastNestedNode) {
|
||||
getNodes().push(lastNestedNode);
|
||||
} else {
|
||||
console.warn(`Inconsistent tag '${getTokenValue(token)}' on line ${getTokenLine(token)} and column ${getTokenColumn(token)}`);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
getNodes().push(convertTokenToText(token));
|
||||
}
|
||||
}
|
||||
|
||||
return nodes
|
||||
}
|
||||
|
||||
findNestedTags(tokens) {
|
||||
const tags = tokens.filter(isTagToken).reduce((acc, token) => {
|
||||
acc[getTokenValue(token)] = true;
|
||||
|
||||
return acc
|
||||
}, {});
|
||||
|
||||
const closeChar = getChar(TokenChar.SLASH);
|
||||
|
||||
return Object.keys(tags).reduce((arr, key) => {
|
||||
if (tags[key] && tags[closeChar + key]) {
|
||||
arr.push(key)
|
||||
}
|
||||
|
||||
return arr;
|
||||
}, [])
|
||||
}
|
||||
|
||||
isAllowedTag(value) {
|
||||
if (this.options.allowOnlyTags && this.options.allowOnlyTags.length) {
|
||||
return this.options.allowOnlyTags.indexOf(value) >= 0
|
||||
if (getCurTag()) {
|
||||
if (isAttrNameToken(token)) {
|
||||
createCurTagAttrName(token);
|
||||
getCurTag().attrs[getCurTagAttrName()] = null;
|
||||
} else if (isAttrValueToken(token)) {
|
||||
getCurTag().attrs[getCurTagAttrName()] = getTokenValue(token);
|
||||
clearCurTagAttrName();
|
||||
} else if (isTextToken(token)) {
|
||||
getCurTag().content.push(getTokenValue(token));
|
||||
}
|
||||
|
||||
return true
|
||||
} else if (isTextToken(token)) {
|
||||
getNodes().push(getTokenValue(token));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return nodes;
|
||||
}
|
||||
|
||||
findNestedTags(tokens) {
|
||||
const tags = tokens.filter(isTagToken).reduce((acc, token) => {
|
||||
acc[getTokenValue(token)] = true;
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
const closeChar = getChar(TokenChar.SLASH);
|
||||
|
||||
return Object.keys(tags).reduce((arr, key) => {
|
||||
if (tags[key] && tags[closeChar + key]) {
|
||||
arr.push(key);
|
||||
}
|
||||
|
||||
return arr;
|
||||
}, []);
|
||||
}
|
||||
|
||||
isAllowedTag(value) {
|
||||
if (this.options.allowOnlyTags && this.options.allowOnlyTags.length) {
|
||||
return this.options.allowOnlyTags.indexOf(value) >= 0;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
const Parser = require('./Parser');
|
||||
const TOKEN = require('./token');
|
||||
|
||||
describe("Parser", () => {
|
||||
test("parse paired tags tokens", () => {
|
||||
const parser = new Parser([
|
||||
[TOKEN.TYPE_TAG, 'ch'],
|
||||
[TOKEN.TYPE_TAG, '/ch']
|
||||
]);
|
||||
|
||||
})
|
||||
});
|
||||
describe('Parser', () => {
|
||||
test('parse paired tags tokens', () => {
|
||||
const parser = new Parser([
|
||||
[TOKEN.TYPE_TAG, 'ch'],
|
||||
[TOKEN.TYPE_TAG, '/ch'],
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -52,7 +52,7 @@ class Tokenizer {
|
||||
this.colPos = 0;
|
||||
this.rowPos = 0;
|
||||
this.index = 0;
|
||||
|
||||
|
||||
this.tokenIndex = -1;
|
||||
this.tokens = [];
|
||||
}
|
||||
|
||||
@@ -1,144 +1,145 @@
|
||||
const Tokenizer = require('./Tokenizer');
|
||||
|
||||
const TYPE = Tokenizer.TYPE;
|
||||
|
||||
describe("Tokenizer", () => {
|
||||
test("tokenize single tag", () => {
|
||||
const input = `[SingleTag]`;
|
||||
const tokens = new Tokenizer(input).tokenize();
|
||||
describe('Tokenizer', () => {
|
||||
test('tokenize single tag', () => {
|
||||
const input = '[SingleTag]';
|
||||
const tokens = new Tokenizer(input).tokenize();
|
||||
|
||||
expect(tokens).toBeInstanceOf(Array);
|
||||
expect(tokens).toEqual([
|
||||
[TYPE.TAG, 'SingleTag', 0, 0]
|
||||
])
|
||||
});
|
||||
expect(tokens).toBeInstanceOf(Array);
|
||||
expect(tokens).toEqual([
|
||||
[TYPE.TAG, 'SingleTag', 0, 0],
|
||||
]);
|
||||
});
|
||||
|
||||
test("tokenize single tag with spaces", () => {
|
||||
const input = `[Single Tag]`;
|
||||
const tokens = new Tokenizer(input).tokenize();
|
||||
test('tokenize single tag with spaces', () => {
|
||||
const input = '[Single Tag]';
|
||||
const tokens = new Tokenizer(input).tokenize();
|
||||
|
||||
expect(tokens).toBeInstanceOf(Array);
|
||||
expect(tokens).toEqual([
|
||||
[TYPE.TAG, 'Single Tag', 0, 0]
|
||||
])
|
||||
});
|
||||
expect(tokens).toBeInstanceOf(Array);
|
||||
expect(tokens).toEqual([
|
||||
[TYPE.TAG, 'Single Tag', 0, 0],
|
||||
]);
|
||||
});
|
||||
|
||||
test("tokenize tag as param", () => {
|
||||
const input = `[color="#ff0000"]Text[/color]`;
|
||||
const tokens = new Tokenizer(input).tokenize();
|
||||
test('tokenize tag as param', () => {
|
||||
const input = '[color="#ff0000"]Text[/color]';
|
||||
const tokens = new Tokenizer(input).tokenize();
|
||||
|
||||
expect(tokens).toBeInstanceOf(Array);
|
||||
expect(tokens).toEqual([
|
||||
[TYPE.TAG, 'color', 0, 0],
|
||||
[TYPE.ATTR_VALUE, '#ff0000', 6, 0],
|
||||
[TYPE.WORD, 'Text', 17, 0],
|
||||
[TYPE.TAG, '/color', 21, 0]
|
||||
])
|
||||
});
|
||||
expect(tokens).toBeInstanceOf(Array);
|
||||
expect(tokens).toEqual([
|
||||
[TYPE.TAG, 'color', 0, 0],
|
||||
[TYPE.ATTR_VALUE, '#ff0000', 6, 0],
|
||||
[TYPE.WORD, 'Text', 17, 0],
|
||||
[TYPE.TAG, '/color', 21, 0],
|
||||
]);
|
||||
});
|
||||
|
||||
test("tokenize tag param without quotemarks", () => {
|
||||
const input = `[style color=#ff0000]Text[/style]`;
|
||||
const tokens = new Tokenizer(input).tokenize();
|
||||
test('tokenize tag param without quotemarks', () => {
|
||||
const input = '[style color=#ff0000]Text[/style]';
|
||||
const tokens = new Tokenizer(input).tokenize();
|
||||
|
||||
expect(tokens).toBeInstanceOf(Array);
|
||||
expect(tokens).toEqual([
|
||||
[TYPE.TAG, 'style', 0, 0],
|
||||
[TYPE.ATTR_NAME, 'color', 6, 0],
|
||||
[TYPE.ATTR_VALUE, '#ff0000', 12, 0],
|
||||
[TYPE.WORD, 'Text', 21, 0],
|
||||
[TYPE.TAG, '/style', 25, 0]
|
||||
])
|
||||
});
|
||||
expect(tokens).toBeInstanceOf(Array);
|
||||
expect(tokens).toEqual([
|
||||
[TYPE.TAG, 'style', 0, 0],
|
||||
[TYPE.ATTR_NAME, 'color', 6, 0],
|
||||
[TYPE.ATTR_VALUE, '#ff0000', 12, 0],
|
||||
[TYPE.WORD, 'Text', 21, 0],
|
||||
[TYPE.TAG, '/style', 25, 0],
|
||||
]);
|
||||
});
|
||||
|
||||
test("tokenize list tag with items", () => {
|
||||
const input = `[list]
|
||||
test('tokenize list tag with items', () => {
|
||||
const input = `[list]
|
||||
[*] Item 1.
|
||||
[*] Item 2.
|
||||
[*] Item 3.
|
||||
[/list]`;
|
||||
|
||||
const tokens = new Tokenizer(input).tokenize();
|
||||
const tokens = new Tokenizer(input).tokenize();
|
||||
|
||||
expect(tokens).toBeInstanceOf(Array);
|
||||
expect(tokens).toEqual([
|
||||
[TYPE.TAG, 'list', 0, 0],
|
||||
[TYPE.NEW_LINE, '\n', 6, 0],
|
||||
[TYPE.SPACE, ' ', 0, 1],
|
||||
[TYPE.SPACE, ' ', 1, 1],
|
||||
[TYPE.SPACE, ' ', 2, 1],
|
||||
[TYPE.TAG, '*', 3, 1],
|
||||
[TYPE.SPACE, ' ', 6, 1],
|
||||
[TYPE.WORD, 'Item', 7, 1],
|
||||
[TYPE.SPACE, ' ', 11, 1],
|
||||
[TYPE.WORD, '1.', 11, 1],
|
||||
[TYPE.NEW_LINE, '\n', 14, 1],
|
||||
[TYPE.SPACE, ' ', 0, 2],
|
||||
[TYPE.SPACE, ' ', 1, 2],
|
||||
[TYPE.SPACE, ' ', 2, 2],
|
||||
[TYPE.TAG, '*', 3, 2],
|
||||
[TYPE.SPACE, ' ', 6, 2],
|
||||
[TYPE.WORD, 'Item', 14, 1],
|
||||
[TYPE.SPACE, ' ', 11, 2],
|
||||
[TYPE.WORD, '2.', 11, 2],
|
||||
[TYPE.NEW_LINE, '\n', 14, 2],
|
||||
[TYPE.SPACE, ' ', 0, 3],
|
||||
[TYPE.SPACE, ' ', 1, 3],
|
||||
[TYPE.SPACE, ' ', 2, 3],
|
||||
[TYPE.TAG, '*', 3, 3],
|
||||
[TYPE.SPACE, ' ', 6, 3],
|
||||
[TYPE.WORD, 'Item', 14, 2],
|
||||
[TYPE.SPACE, ' ', 11, 3],
|
||||
[TYPE.WORD, '3.', 11, 3],
|
||||
[TYPE.NEW_LINE, '\n', 14, 3],
|
||||
[TYPE.TAG, '/list', 0, 4]
|
||||
])
|
||||
expect(tokens).toBeInstanceOf(Array);
|
||||
expect(tokens).toEqual([
|
||||
[TYPE.TAG, 'list', 0, 0],
|
||||
[TYPE.NEW_LINE, '\n', 6, 0],
|
||||
[TYPE.SPACE, ' ', 0, 1],
|
||||
[TYPE.SPACE, ' ', 1, 1],
|
||||
[TYPE.SPACE, ' ', 2, 1],
|
||||
[TYPE.TAG, '*', 3, 1],
|
||||
[TYPE.SPACE, ' ', 6, 1],
|
||||
[TYPE.WORD, 'Item', 7, 1],
|
||||
[TYPE.SPACE, ' ', 11, 1],
|
||||
[TYPE.WORD, '1.', 11, 1],
|
||||
[TYPE.NEW_LINE, '\n', 14, 1],
|
||||
[TYPE.SPACE, ' ', 0, 2],
|
||||
[TYPE.SPACE, ' ', 1, 2],
|
||||
[TYPE.SPACE, ' ', 2, 2],
|
||||
[TYPE.TAG, '*', 3, 2],
|
||||
[TYPE.SPACE, ' ', 6, 2],
|
||||
[TYPE.WORD, 'Item', 14, 1],
|
||||
[TYPE.SPACE, ' ', 11, 2],
|
||||
[TYPE.WORD, '2.', 11, 2],
|
||||
[TYPE.NEW_LINE, '\n', 14, 2],
|
||||
[TYPE.SPACE, ' ', 0, 3],
|
||||
[TYPE.SPACE, ' ', 1, 3],
|
||||
[TYPE.SPACE, ' ', 2, 3],
|
||||
[TYPE.TAG, '*', 3, 3],
|
||||
[TYPE.SPACE, ' ', 6, 3],
|
||||
[TYPE.WORD, 'Item', 14, 2],
|
||||
[TYPE.SPACE, ' ', 11, 3],
|
||||
[TYPE.WORD, '3.', 11, 3],
|
||||
[TYPE.NEW_LINE, '\n', 14, 3],
|
||||
[TYPE.TAG, '/list', 0, 4],
|
||||
]);
|
||||
});
|
||||
|
||||
test('tokenize bad tags as texts', () => {
|
||||
const inputs = [
|
||||
'[]',
|
||||
'[=]',
|
||||
'',
|
||||
'x html([a. title][, alt][, classes]) x',
|
||||
'[/y]',
|
||||
'[sc',
|
||||
// '[sc / [/sc]',
|
||||
// '[sc arg="val',
|
||||
];
|
||||
|
||||
const asserts = [
|
||||
[[TYPE.WORD, '[]', 0, 0]],
|
||||
[[TYPE.WORD, '[=]', 0, 0]],
|
||||
[
|
||||
[TYPE.WORD, '!', 0, 0],
|
||||
[TYPE.WORD, '[](image.jpg)', 1, 0],
|
||||
],
|
||||
[
|
||||
[TYPE.WORD, 'x', 0, 0],
|
||||
[TYPE.SPACE, ' ', 1, 0],
|
||||
[TYPE.WORD, 'html(', 1, 0],
|
||||
[TYPE.TAG, 'a. title', 7, 0],
|
||||
[TYPE.TAG, ', alt', 17, 0],
|
||||
[TYPE.TAG, ', classes', 24, 0],
|
||||
[TYPE.WORD, ')', 7, 0],
|
||||
[TYPE.SPACE, ' ', 36, 0],
|
||||
[TYPE.WORD, 'x', 36, 0],
|
||||
],
|
||||
[[TYPE.TAG, '/y', 0, 0]],
|
||||
[[TYPE.WORD, '[sc', 0, 0]],
|
||||
[
|
||||
[TYPE.WORD, '[sc', 0, 0],
|
||||
[TYPE.SPACE, ' ', 0, 0],
|
||||
[TYPE.WORD, '/', 0, 0],
|
||||
[TYPE.SPACE, ' ', 0, 0],
|
||||
[TYPE.WORD, '[/sc]', 0, 0],
|
||||
],
|
||||
];
|
||||
|
||||
inputs.forEach((input, idx) => {
|
||||
const tokens = new Tokenizer(input).tokenize();
|
||||
|
||||
expect(tokens).toBeInstanceOf(Array);
|
||||
expect(tokens).toEqual(asserts[idx]);
|
||||
});
|
||||
|
||||
test("tokenize bad tags as texts", () => {
|
||||
const inputs = [
|
||||
'[]',
|
||||
'[=]',
|
||||
'',
|
||||
'x html([a. title][, alt][, classes]) x',
|
||||
'[/y]',
|
||||
'[sc',
|
||||
'[sc / [/sc]',
|
||||
'[sc arg="val',
|
||||
];
|
||||
|
||||
const asserts = [
|
||||
[[TYPE.WORD, '[]', 0, 0]],
|
||||
[[TYPE.WORD, '[=]', 0, 0]],
|
||||
[
|
||||
[TYPE.WORD, '!', 0, 0],
|
||||
[TYPE.WORD, '[](image.jpg)', 1, 0]
|
||||
],
|
||||
[
|
||||
[TYPE.WORD, "x", 0, 0],
|
||||
[TYPE.SPACE, " ", 1, 0],
|
||||
[TYPE.WORD, "html(", 1, 0],
|
||||
[TYPE.TAG, "a. title", 7, 0],
|
||||
[TYPE.TAG, ", alt", 17, 0],
|
||||
[TYPE.TAG, ", classes", 24, 0],
|
||||
[TYPE.WORD, ")", 7, 0],
|
||||
[TYPE.SPACE, " ", 36, 0],
|
||||
[TYPE.WORD, "x", 36, 0]
|
||||
],
|
||||
[[TYPE.TAG, "/y", 0, 0]],
|
||||
[[TYPE.WORD, '[sc', 0, 0]],
|
||||
[
|
||||
[TYPE.WORD, '[sc', 0, 0],
|
||||
[TYPE.SPACE, ' ', 0, 0],
|
||||
[TYPE.WORD, '/', 0, 0],
|
||||
[TYPE.SPACE, ' ', 0, 0],
|
||||
[TYPE.WORD, '[/sc]', 0, 0]
|
||||
],
|
||||
];
|
||||
|
||||
inputs.forEach((input, idx) => {
|
||||
const tokens = new Tokenizer(input).tokenize();
|
||||
|
||||
expect(tokens).toBeInstanceOf(Array);
|
||||
expect(tokens).toEqual(asserts[idx])
|
||||
});
|
||||
})
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,215 +0,0 @@
|
||||
|
||||
const attrNameChars = '[a-zA-Z0-9\\.\\-_:;/]'
|
||||
const attrValueChars = '[a-zA-Z0-9\\.\\-_:;#/\\s]'
|
||||
const pattern = `\\[(\/\\w*)\\]|\\[(\\w*)+(=(["])${attrValueChars}*\\4)?( (${attrNameChars}+)?=(["])(${attrValueChars}+)\\7)*\\]`
|
||||
|
||||
const TAG_RE = new RegExp(pattern, 'g')
|
||||
|
||||
const EOL = '\n'
|
||||
const WHITESPACE = ' '
|
||||
const isNode = el => typeof el === 'object' && el.tag
|
||||
const isStringNode = el => typeof el === 'string'
|
||||
const isChordNode = el => el.tag === 'ch'
|
||||
const isTabNode = el => el.tag === 'tab'
|
||||
const isSyllableNode = el => el.tag === 'syllable'
|
||||
const isTextNode = el => el.tag === 'text'
|
||||
const isEOL = el => el === EOL
|
||||
|
||||
const getNodeLength = node => {
|
||||
if (isNode(node)) {
|
||||
node.content.reduce((count, contentNode) => count + getNodeLength(contentNode), 0)
|
||||
} else if (isStringNode(node)) {
|
||||
return node.length
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
const tagsDefinition = {
|
||||
ch: {
|
||||
closable: true,
|
||||
},
|
||||
syllable: {
|
||||
closable: true,
|
||||
},
|
||||
tab: {
|
||||
closable: true,
|
||||
},
|
||||
}
|
||||
|
||||
// @TODO: Разбить на парсер и токенайзер, ноды и токены должны жить отдельно
|
||||
/**
|
||||
* Парсит контент таба с BB кодами в AST дерево [{tag:'ch', attrs:{..}, content:[...]}]
|
||||
*
|
||||
* @example
|
||||
*
|
||||
* textTabParser
|
||||
* .parse('[Intro] [ch app=123]G[/ch] hello world', {ch: {closable: true}})
|
||||
*
|
||||
*/
|
||||
module.exports = {
|
||||
parse(str, tags = tagsDefinition) {
|
||||
this.tags = tags
|
||||
|
||||
const tokens = this.tokenize(str)
|
||||
const ast = this.parseTokens(tokens)
|
||||
|
||||
return ast
|
||||
},
|
||||
|
||||
tokenize(str) {
|
||||
let tokens = []
|
||||
let match
|
||||
let lastIndex = 0
|
||||
|
||||
// console.time('tokenize')
|
||||
while (match = TAG_RE.exec(str)) {
|
||||
const delta = match.index - lastIndex
|
||||
|
||||
if (delta > 0) {
|
||||
tokens = tokens.concat(this.toTextTokens(str.substr(lastIndex, delta)))
|
||||
}
|
||||
|
||||
tokens.push(this.tagToken(match))
|
||||
lastIndex = TAG_RE.lastIndex
|
||||
}
|
||||
|
||||
const delta = str.length - lastIndex
|
||||
|
||||
if (delta > 0) {
|
||||
tokens = tokens.concat(this.toTextTokens(str.substr(lastIndex, delta)))
|
||||
}
|
||||
// console.timeEnd('tokenize')
|
||||
|
||||
return tokens
|
||||
},
|
||||
|
||||
parseTokens(tokens) {
|
||||
const nodes = []
|
||||
let curToken
|
||||
const nestedNodes = []
|
||||
|
||||
function getNodes() {
|
||||
if (nestedNodes.length) {
|
||||
const nestedNode = nestedNodes[nestedNodes.length - 1]
|
||||
return nestedNode.content
|
||||
}
|
||||
|
||||
return nodes
|
||||
}
|
||||
|
||||
// console.time('parseTokens')
|
||||
while (curToken = tokens.shift()) {
|
||||
curToken = this.isTokenSupported(curToken) ? curToken : this.asTextToken(curToken)
|
||||
|
||||
if (curToken.isText) {
|
||||
getNodes().push(curToken.text)
|
||||
}
|
||||
|
||||
if (curToken.isTag) {
|
||||
const node = this.tagNode(curToken.tagName, curToken.attributes)
|
||||
|
||||
if (curToken.isStart) {
|
||||
if (this.isTokenHasCloseTag(curToken)) {
|
||||
nestedNodes.push(node)
|
||||
} else {
|
||||
getNodes().push(node)
|
||||
}
|
||||
}
|
||||
|
||||
if (curToken.isEnd) {
|
||||
const lastNestedNode = nestedNodes.pop()
|
||||
|
||||
if (lastNestedNode) {
|
||||
getNodes().push(lastNestedNode)
|
||||
} else {
|
||||
console.error(`Inconsistent tag '${curToken.tagName}'`)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// console.timeEnd('parseTokens')
|
||||
|
||||
return nodes
|
||||
},
|
||||
|
||||
isTokenSupported(token) {
|
||||
return token.isTag && this.tags && this.tags[token.tagName]
|
||||
},
|
||||
|
||||
isTokenHasCloseTag(token) {
|
||||
return this.tags && this.tags[token.tagName] && this.tags[token.tagName].closable
|
||||
},
|
||||
|
||||
tagNode(name, attrs, content = []) {
|
||||
return { tag: name, attrs, content }
|
||||
},
|
||||
|
||||
toTextTokens(text) {
|
||||
const tokens = []
|
||||
const chars = text.split('')
|
||||
let currText = ''
|
||||
|
||||
const flushText = () => {
|
||||
if (currText) {
|
||||
tokens.push(this.textToken(currText))
|
||||
currText = ''
|
||||
}
|
||||
}
|
||||
|
||||
chars.forEach((char) => {
|
||||
if (char === EOL || char === WHITESPACE) {
|
||||
flushText()
|
||||
tokens.push(this.textToken(char))
|
||||
} else {
|
||||
currText += char
|
||||
}
|
||||
})
|
||||
|
||||
if (currText) {
|
||||
tokens.push(this.textToken(currText))
|
||||
}
|
||||
|
||||
return tokens
|
||||
},
|
||||
|
||||
textToken(text) {
|
||||
return { isText: true, text }
|
||||
},
|
||||
|
||||
tagToken(match) {
|
||||
if (typeof match[1] === 'undefined') { // Start tag
|
||||
const tagName = match[2]
|
||||
const attributes = {}
|
||||
const ATTR_RE = new RegExp(`(${attrNameChars}+)?=(["])(${attrValueChars}+)\\2`, 'g')
|
||||
const attrStr = match[0].substr(1 + tagName.length, match[0].length - 2 - tagName.length)
|
||||
|
||||
let attrMatch
|
||||
|
||||
while (attrMatch = ATTR_RE.exec(attrStr)) {
|
||||
if (typeof attrMatch[1] === 'undefined') { // The tag attribute
|
||||
attributes[tagName] = attrMatch[3]
|
||||
} else { // Normal attribute
|
||||
attributes[attrMatch[1]] = attrMatch[3]
|
||||
}
|
||||
}
|
||||
|
||||
return { isStart: true, isTag: true, tagName, attributes, text: match[0] }
|
||||
}
|
||||
|
||||
// End tag
|
||||
return { isEnd: true, isTag: true, tagName: match[1].substr(1, match[1].length - 1) }
|
||||
},
|
||||
|
||||
asTextToken(token) {
|
||||
if (token.isTag && token.isStart) {
|
||||
return this.textToken(token.text)
|
||||
}
|
||||
|
||||
if (token.isTag && token.isEnd) {
|
||||
return this.textToken(`[/${token.tagName}]`)
|
||||
}
|
||||
|
||||
return token
|
||||
},
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
const parse = require('../index');
|
||||
|
||||
const options = {
|
||||
closableTags: ['ch', 'syllable', 'tab']
|
||||
};
|
||||
|
||||
const textStub = require("./test/stub");
|
||||
|
||||
const count = 0;
|
||||
const parsers3 = [];
|
||||
|
||||
console.time('newParser');
|
||||
for (let i = 0; i <= count; i++) {
|
||||
const parser3 = parse(textStub, options);
|
||||
|
||||
parsers3.push(parser3);
|
||||
}
|
||||
console.timeEnd('newParser');
|
||||
// console.log(JSON.stringify(parsers3));
|
||||
@@ -1,15 +0,0 @@
|
||||
const OldParser = require('./OldParser')
|
||||
|
||||
const textStub = require("./test/stub");
|
||||
|
||||
const count = 0;
|
||||
const oldParsers3 = [];
|
||||
console.time('oldParser');
|
||||
for (let i = 0; i <= count; i++) {
|
||||
const oldParser3 = OldParser.parse(textStub);
|
||||
|
||||
oldParsers3.push(oldParser3);
|
||||
}
|
||||
console.timeEnd('oldParser');
|
||||
// console.log(JSON.stringify(oldParsers3));
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,26 +1,26 @@
|
||||
const N = "\n".charCodeAt(0);
|
||||
const TAB = "\t".charCodeAt(0);
|
||||
const F = "\f".charCodeAt(0);
|
||||
const R = "\r".charCodeAt(0);
|
||||
const N = '\n'.charCodeAt(0);
|
||||
const TAB = '\t'.charCodeAt(0);
|
||||
const F = '\f'.charCodeAt(0);
|
||||
const R = '\r'.charCodeAt(0);
|
||||
|
||||
const EQ = "=".charCodeAt(0);
|
||||
const QUOTEMARK = "\"".charCodeAt(0);
|
||||
const SPACE = " ".charCodeAt(0);
|
||||
const EQ = '='.charCodeAt(0);
|
||||
const QUOTEMARK = '"'.charCodeAt(0);
|
||||
const SPACE = ' '.charCodeAt(0);
|
||||
|
||||
const OPEN_BRAKET = "[".charCodeAt(0);
|
||||
const CLOSE_BRAKET = "]".charCodeAt(0);
|
||||
const OPEN_BRAKET = '['.charCodeAt(0);
|
||||
const CLOSE_BRAKET = ']'.charCodeAt(0);
|
||||
|
||||
const SLASH = "/".charCodeAt(0);
|
||||
const SLASH = '/'.charCodeAt(0);
|
||||
|
||||
module.exports = {
|
||||
N,
|
||||
F,
|
||||
R,
|
||||
TAB,
|
||||
EQ,
|
||||
QUOTEMARK,
|
||||
SPACE,
|
||||
OPEN_BRAKET,
|
||||
CLOSE_BRAKET,
|
||||
SLASH
|
||||
};
|
||||
N,
|
||||
F,
|
||||
R,
|
||||
TAB,
|
||||
EQ,
|
||||
QUOTEMARK,
|
||||
SPACE,
|
||||
OPEN_BRAKET,
|
||||
CLOSE_BRAKET,
|
||||
SLASH,
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "bbob-parser",
|
||||
"name": "@bbob/parser",
|
||||
"version": "1.0.1",
|
||||
"description": "Fast BB Code parser written in pure javascript, no dependencies",
|
||||
"main": "index.js",
|
||||
@@ -7,14 +7,12 @@
|
||||
"test": "test"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "jest"
|
||||
"test": "../../node_modules/.bin/jest --",
|
||||
"cover": "../../node_modules/.bin/jest --coverage",
|
||||
"lint": "../../node_modules/.bin/eslint ."
|
||||
},
|
||||
"author": "Nikolay Kostyurin <jilizart@gmail.com>",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"jest": "^23.1.0",
|
||||
"xbbcode-parser": "^0.1.2"
|
||||
},
|
||||
"publishConfig": {
|
||||
"registry": "https://registry.npmjs.org/"
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
const Tokenizer = require("./Tokenizer");
|
||||
const Parser = require("./Parser");
|
||||
const Tokenizer = require('./Tokenizer');
|
||||
const Parser = require('./Parser');
|
||||
|
||||
module.exports = function parse(input, options) {
|
||||
const tokenizer = new Tokenizer(input);
|
||||
const tokens = tokenizer.tokenize();
|
||||
const parser = new Parser(tokens, options);
|
||||
const ast = parser.parse();
|
||||
const tokenizer = new Tokenizer(input);
|
||||
const tokens = tokenizer.tokenize();
|
||||
const parser = new Parser(tokens, options);
|
||||
const ast = parser.parse();
|
||||
|
||||
return ast
|
||||
};
|
||||
return ast;
|
||||
};
|
||||
|
||||
@@ -1,37 +1,37 @@
|
||||
const parse = require('./index');
|
||||
|
||||
const options = {
|
||||
allowOnlyTags: ['ch', 'syllable', 'tab'],
|
||||
allowOnlyTags: ['ch', 'syllable', 'tab'],
|
||||
};
|
||||
|
||||
describe("parse", () => {
|
||||
test("tag with spaces", () => {
|
||||
const ast = parse(`[Verse 2]`);
|
||||
describe('parse', () => {
|
||||
test('tag with spaces', () => {
|
||||
const ast = parse('[Verse 2]');
|
||||
|
||||
expect(ast).toEqual([{tag: 'Verse 2', attrs: {}, content: []}]);
|
||||
});
|
||||
expect(ast).toEqual([{ tag: 'Verse 2', attrs: {}, content: [] }]);
|
||||
});
|
||||
|
||||
// test("pass invalid tags", () => {
|
||||
// const inputs = [
|
||||
// '[]',
|
||||
// '',
|
||||
// 'x html([a. title][, alt][, classes]) x',
|
||||
// '[/y]',
|
||||
// '[sc',
|
||||
// '[sc / [/sc]',
|
||||
// '[sc arg="val',
|
||||
// ];
|
||||
//
|
||||
// const ast1 = parse(inputs[0]);
|
||||
//
|
||||
//
|
||||
//
|
||||
// console.log('ast1', ast1);
|
||||
//
|
||||
//
|
||||
//
|
||||
// expect(ast1).toEqual([
|
||||
//
|
||||
// ]);
|
||||
// })
|
||||
});
|
||||
// test("pass invalid tags", () => {
|
||||
// const inputs = [
|
||||
// '[]',
|
||||
// '',
|
||||
// 'x html([a. title][, alt][, classes]) x',
|
||||
// '[/y]',
|
||||
// '[sc',
|
||||
// '[sc / [/sc]',
|
||||
// '[sc arg="val',
|
||||
// ];
|
||||
//
|
||||
// const ast1 = parse(inputs[0]);
|
||||
//
|
||||
//
|
||||
//
|
||||
// console.log('ast1', ast1);
|
||||
//
|
||||
//
|
||||
//
|
||||
// expect(ast1).toEqual([
|
||||
//
|
||||
// ]);
|
||||
// })
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user