mirror of
https://github.com/tenrok/BBob.git
synced 2026-06-20 20:00:33 +03:00
feat(parser): custom open and close tags support, html tags tests (#3)
This commit is contained in:
committed by
GitHub
parent
f5fd078eca
commit
790825af30
+3
-2
@@ -7,7 +7,7 @@
|
|||||||
"lint": "lerna run build && lerna run link && lerna run lint"
|
"lint": "lerna run build && lerna run link && lerna run lint"
|
||||||
},
|
},
|
||||||
"author": {
|
"author": {
|
||||||
"name": "Nikolay Kostyurin",
|
"name": "Nikolay Kostyurin <jilizart@gmail.com>",
|
||||||
"url": "https://artkost.ru/"
|
"url": "https://artkost.ru/"
|
||||||
},
|
},
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -15,10 +15,10 @@
|
|||||||
"babel-cli": "^6.26.0",
|
"babel-cli": "^6.26.0",
|
||||||
"babel-core": "^6.26.3",
|
"babel-core": "^6.26.3",
|
||||||
"babel-jest": "^23.4.2",
|
"babel-jest": "^23.4.2",
|
||||||
"babel-plugin-transform-object-rest-spread": "^6.26.0",
|
|
||||||
"babel-plugin-external-helpers": "^6.22.0",
|
"babel-plugin-external-helpers": "^6.22.0",
|
||||||
"babel-plugin-transform-decorators-legacy": "^1.3.5",
|
"babel-plugin-transform-decorators-legacy": "^1.3.5",
|
||||||
"babel-plugin-transform-es2015-modules-commonjs": "^6.26.2",
|
"babel-plugin-transform-es2015-modules-commonjs": "^6.26.2",
|
||||||
|
"babel-plugin-transform-object-rest-spread": "^6.26.0",
|
||||||
"babel-preset-env": "^1.7.0",
|
"babel-preset-env": "^1.7.0",
|
||||||
"babel-preset-es2015": "^6.24.1",
|
"babel-preset-es2015": "^6.24.1",
|
||||||
"babel-preset-react": "^6.24.1",
|
"babel-preset-react": "^6.24.1",
|
||||||
@@ -37,6 +37,7 @@
|
|||||||
"lint-staged": "^7.2.2",
|
"lint-staged": "^7.2.2",
|
||||||
"microtime": "^2.1.8",
|
"microtime": "^2.1.8",
|
||||||
"posthtml-render": "^1.1.4",
|
"posthtml-render": "^1.1.4",
|
||||||
|
"rimraf": "^2.6.2",
|
||||||
"rollup": "^0.62.0",
|
"rollup": "^0.62.0",
|
||||||
"rollup-plugin-babel": "^3.0.7",
|
"rollup-plugin-babel": "^3.0.7",
|
||||||
"rollup-plugin-commonjs": "^9.1.6",
|
"rollup-plugin-commonjs": "^9.1.6",
|
||||||
|
|||||||
@@ -3,16 +3,15 @@ import {render} from '../src';
|
|||||||
|
|
||||||
const process = (input, params) => {
|
const process = (input, params) => {
|
||||||
const ast = parse(input);
|
const ast = parse(input);
|
||||||
const html = render(ast, params);
|
|
||||||
|
|
||||||
return html
|
return render(ast, params)
|
||||||
};
|
};
|
||||||
|
|
||||||
describe('@bbob/html', () => {
|
describe('@bbob/html', () => {
|
||||||
test('render bbcode tag with single param as html tag', () => {
|
test('render bbcode tag with single param as html tag', () => {
|
||||||
const input = '[url=https://ru.wikipedia.org]Text[/url]';
|
const input = '[url=https://ru.wikipedia.org]Text[/url]';
|
||||||
const expected = '<url url="https://ru.wikipedia.org">Text</url>';
|
const expected = '<url https://ru.wikipedia.org="https://ru.wikipedia.org">Text</url>';
|
||||||
const result = process(input)
|
const result = process(input);
|
||||||
|
|
||||||
expect(result).toBe(expected);
|
expect(result).toBe(expected);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -53,14 +53,14 @@ const convertTagToText = (token) => {
|
|||||||
|
|
||||||
class Token {
|
class Token {
|
||||||
constructor(type, value, line, row) {
|
constructor(type, value, line, row) {
|
||||||
this.type = String(type);
|
this[TOKEN_TYPE_ID] = String(type);
|
||||||
this.value = String(value);
|
this[TOKEN_VALUE_ID] = String(value);
|
||||||
this.line = Number(line);
|
this[TOKEN_LINE_ID] = Number(line);
|
||||||
this.row = Number(row);
|
this[TOKEN_COLUMN_ID] = Number(row);
|
||||||
}
|
}
|
||||||
|
|
||||||
isEmpty() {
|
isEmpty() {
|
||||||
return !!this.type;
|
return !!this[TOKEN_TYPE_ID];
|
||||||
}
|
}
|
||||||
|
|
||||||
isText() {
|
isText() {
|
||||||
|
|||||||
@@ -13,13 +13,7 @@ import {
|
|||||||
|
|
||||||
import { Token, TYPE_ATTR_NAME, TYPE_ATTR_VALUE, TYPE_NEW_LINE, TYPE_SPACE, TYPE_TAG, TYPE_WORD } from './Token';
|
import { Token, TYPE_ATTR_NAME, TYPE_ATTR_VALUE, TYPE_NEW_LINE, TYPE_SPACE, TYPE_TAG, TYPE_WORD } from './Token';
|
||||||
|
|
||||||
const RESERVED_CHARS = [CLOSE_BRAKET, OPEN_BRAKET, QUOTEMARK, BACKSLASH, SPACE, TAB, EQ, N];
|
const EM = '!';
|
||||||
const NOT_CHAR_TOKENS = [OPEN_BRAKET, SPACE, TAB, N];
|
|
||||||
const WHITESPACES = [SPACE, TAB];
|
|
||||||
|
|
||||||
const isCharReserved = char => (RESERVED_CHARS.indexOf(char) >= 0);
|
|
||||||
const isWhiteSpace = char => (WHITESPACES.indexOf(char) >= 0);
|
|
||||||
const isCharToken = char => (NOT_CHAR_TOKENS.indexOf(char) === -1);
|
|
||||||
|
|
||||||
const createCharGrabber = (source) => {
|
const createCharGrabber = (source) => {
|
||||||
let idx = 0;
|
let idx = 0;
|
||||||
@@ -69,6 +63,19 @@ function createLexer(buffer, options = {}) {
|
|||||||
|
|
||||||
let tokenIndex = -1;
|
let tokenIndex = -1;
|
||||||
const tokens = new Array(Math.floor(buffer.length));
|
const tokens = new Array(Math.floor(buffer.length));
|
||||||
|
const openTag = options.openTag || OPEN_BRAKET;
|
||||||
|
const closeTag = options.closeTag || CLOSE_BRAKET;
|
||||||
|
|
||||||
|
const RESERVED_CHARS = [closeTag, openTag, QUOTEMARK, BACKSLASH, SPACE, TAB, EQ, N, EM];
|
||||||
|
const NOT_CHAR_TOKENS = [openTag, SPACE, TAB, N];
|
||||||
|
const WHITESPACES = [SPACE, TAB];
|
||||||
|
const SPECIAL_CHARS = [EQ, SPACE, TAB];
|
||||||
|
|
||||||
|
const isCharReserved = char => (RESERVED_CHARS.indexOf(char) >= 0);
|
||||||
|
const isWhiteSpace = char => (WHITESPACES.indexOf(char) >= 0);
|
||||||
|
const isCharToken = char => (NOT_CHAR_TOKENS.indexOf(char) === -1);
|
||||||
|
const isSpecialChar = char => (SPECIAL_CHARS.indexOf(char) >= 0);
|
||||||
|
|
||||||
const emitToken = (token) => {
|
const emitToken = (token) => {
|
||||||
if (options.onToken) {
|
if (options.onToken) {
|
||||||
options.onToken(token);
|
options.onToken(token);
|
||||||
@@ -80,37 +87,46 @@ function createLexer(buffer, options = {}) {
|
|||||||
|
|
||||||
const parseAttrs = (str) => {
|
const parseAttrs = (str) => {
|
||||||
let tagName = null;
|
let tagName = null;
|
||||||
let skipSpaces = false;
|
let skipSpecialChars = false;
|
||||||
|
|
||||||
const attrTokens = [];
|
const attrTokens = [];
|
||||||
const attrCharGrabber = createCharGrabber(str);
|
const attrCharGrabber = createCharGrabber(str);
|
||||||
const validAttr = (val) => {
|
|
||||||
const isEQ = val === EQ;
|
|
||||||
const isWS = isWhiteSpace(val);
|
|
||||||
const isPrevSLASH = attrCharGrabber.getPrev() === SLASH;
|
|
||||||
|
|
||||||
if (tagName === null) {
|
const validAttr = (char) => {
|
||||||
return !(isEQ || isWS || attrCharGrabber.isLast());
|
const isEQ = char === EQ;
|
||||||
|
const isWS = isWhiteSpace(char);
|
||||||
|
const prevChar = attrCharGrabber.getPrev();
|
||||||
|
const nextChar = attrCharGrabber.getNext();
|
||||||
|
const isPrevSLASH = prevChar === BACKSLASH;
|
||||||
|
const isTagNameEmpty = tagName === null;
|
||||||
|
|
||||||
|
if (isTagNameEmpty) {
|
||||||
|
return (isEQ || isWS || attrCharGrabber.isLast()) === false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (skipSpaces && isWS) {
|
if (skipSpecialChars && isSpecialChar(char)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (val === QUOTEMARK && !isPrevSLASH) {
|
if (char === QUOTEMARK && !isPrevSLASH) {
|
||||||
skipSpaces = !skipSpaces;
|
skipSpecialChars = !skipSpecialChars;
|
||||||
|
|
||||||
|
if (!skipSpecialChars && !(nextChar === EQ || isWhiteSpace(nextChar))) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return !(isEQ || isWS);
|
return (isEQ || isWS) === false;
|
||||||
};
|
};
|
||||||
|
|
||||||
const nextAttr = () => {
|
const nextAttr = () => {
|
||||||
const attrStr = attrCharGrabber.grabWhile(validAttr);
|
const attrStr = attrCharGrabber.grabWhile(validAttr);
|
||||||
|
const currChar = attrCharGrabber.getCurr();
|
||||||
|
|
||||||
// first string before space is a tag name
|
// first string before space is a tag name
|
||||||
if (tagName === null) {
|
if (tagName === null) {
|
||||||
tagName = attrStr;
|
tagName = attrStr;
|
||||||
} else if (isWhiteSpace(attrCharGrabber.getCurr()) || !attrCharGrabber.hasNext()) {
|
} else if (isWhiteSpace(currChar) || currChar === QUOTEMARK || !attrCharGrabber.hasNext()) {
|
||||||
const escaped = unquote(trimChar(attrStr, QUOTEMARK));
|
const escaped = unquote(trimChar(attrStr, QUOTEMARK));
|
||||||
attrTokens.push(createToken(TYPE_ATTR_VALUE, escaped, row, col));
|
attrTokens.push(createToken(TYPE_ATTR_VALUE, escaped, row, col));
|
||||||
} else {
|
} else {
|
||||||
@@ -127,29 +143,29 @@ function createLexer(buffer, options = {}) {
|
|||||||
return { tag: tagName, attrs: attrTokens };
|
return { tag: tagName, attrs: attrTokens };
|
||||||
};
|
};
|
||||||
|
|
||||||
const grabber = createCharGrabber(buffer);
|
const bufferGrabber = createCharGrabber(buffer);
|
||||||
|
|
||||||
const next = () => {
|
const next = () => {
|
||||||
const char = grabber.getCurr();
|
const char = bufferGrabber.getCurr();
|
||||||
|
|
||||||
if (char === N) {
|
if (char === N) {
|
||||||
grabber.skip();
|
bufferGrabber.skip();
|
||||||
col = 0;
|
col = 0;
|
||||||
row++;
|
row++;
|
||||||
|
|
||||||
emitToken(createToken(TYPE_NEW_LINE, char, row, col));
|
emitToken(createToken(TYPE_NEW_LINE, char, row, col));
|
||||||
} else if (isWhiteSpace(char)) {
|
} else if (isWhiteSpace(char)) {
|
||||||
const str = grabber.grabWhile(isWhiteSpace);
|
const str = bufferGrabber.grabWhile(isWhiteSpace);
|
||||||
emitToken(createToken(TYPE_SPACE, str, row, col));
|
emitToken(createToken(TYPE_SPACE, str, row, col));
|
||||||
} else if (char === OPEN_BRAKET) {
|
} else if (char === openTag) {
|
||||||
const nextChar = grabber.getNext();
|
const nextChar = bufferGrabber.getNext();
|
||||||
grabber.skip(); // skip [
|
bufferGrabber.skip(); // skip [
|
||||||
|
|
||||||
if (isCharReserved(nextChar)) {
|
if (isCharReserved(nextChar)) {
|
||||||
emitToken(createToken(TYPE_WORD, char, row, col));
|
emitToken(createToken(TYPE_WORD, char, row, col));
|
||||||
} else {
|
} else {
|
||||||
const str = grabber.grabWhile(val => val !== CLOSE_BRAKET);
|
const str = bufferGrabber.grabWhile(val => val !== closeTag);
|
||||||
grabber.skip(); // skip ]
|
bufferGrabber.skip(); // skip ]
|
||||||
|
|
||||||
if (!(str.indexOf(EQ) > 0) || str[0] === SLASH) {
|
if (!(str.indexOf(EQ) > 0) || str[0] === SLASH) {
|
||||||
emitToken(createToken(TYPE_TAG, str, row, col));
|
emitToken(createToken(TYPE_TAG, str, row, col));
|
||||||
@@ -160,19 +176,19 @@ function createLexer(buffer, options = {}) {
|
|||||||
parsed.attrs.map(emitToken);
|
parsed.attrs.map(emitToken);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (char === CLOSE_BRAKET) {
|
} else if (char === closeTag) {
|
||||||
grabber.skip();
|
bufferGrabber.skip();
|
||||||
|
|
||||||
emitToken(createToken(TYPE_WORD, char, row, col));
|
emitToken(createToken(TYPE_WORD, char, row, col));
|
||||||
} else if (isCharToken(char)) {
|
} else if (isCharToken(char)) {
|
||||||
const str = grabber.grabWhile(isCharToken);
|
const str = bufferGrabber.grabWhile(isCharToken);
|
||||||
|
|
||||||
emitToken(createToken(TYPE_WORD, str, row, col));
|
emitToken(createToken(TYPE_WORD, str, row, col));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const tokenize = () => {
|
const tokenize = () => {
|
||||||
while (grabber.hasNext()) {
|
while (bufferGrabber.hasNext()) {
|
||||||
next();
|
next();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -182,7 +198,7 @@ function createLexer(buffer, options = {}) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const isTokenNested = (token) => {
|
const isTokenNested = (token) => {
|
||||||
const value = OPEN_BRAKET + SLASH + token.getValue();
|
const value = openTag + SLASH + token.getValue();
|
||||||
return buffer.indexOf(value) > -1;
|
return buffer.indexOf(value) > -1;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -28,8 +28,6 @@ let tokenizer = null;
|
|||||||
// eslint-disable-next-line no-unused-vars
|
// eslint-disable-next-line no-unused-vars
|
||||||
let tokens = null;
|
let tokens = null;
|
||||||
|
|
||||||
const createTokenizer = (input, onToken) => createLexer(input, { onToken });
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @private
|
* @private
|
||||||
* @param token
|
* @param token
|
||||||
@@ -41,7 +39,7 @@ const isTagNested = token => tokenizer.isTokenNested(token);
|
|||||||
* @private
|
* @private
|
||||||
* @return {TagNode}
|
* @return {TagNode}
|
||||||
*/
|
*/
|
||||||
const getTagNode = () => (tagNodes.length ? tagNodes[tagNodes.length - 1] : null);
|
const getLastTagNode = () => (tagNodes.length ? tagNodes[tagNodes.length - 1] : null);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @private
|
* @private
|
||||||
@@ -61,7 +59,7 @@ const createTagNodeAttrName = token => tagNodesAttrName.push(token.getValue());
|
|||||||
* @return {Array}
|
* @return {Array}
|
||||||
*/
|
*/
|
||||||
const getTagNodeAttrName = () =>
|
const getTagNodeAttrName = () =>
|
||||||
(tagNodesAttrName.length ? tagNodesAttrName[tagNodesAttrName.length - 1] : getTagNode().tag);
|
(tagNodesAttrName.length ? tagNodesAttrName[tagNodesAttrName.length - 1] : null);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @private
|
* @private
|
||||||
@@ -92,6 +90,7 @@ const clearTagNode = () => {
|
|||||||
const getNodes = () => {
|
const getNodes = () => {
|
||||||
if (nestedNodes.length) {
|
if (nestedNodes.length) {
|
||||||
const nestedNode = nestedNodes[nestedNodes.length - 1];
|
const nestedNode = nestedNodes[nestedNodes.length - 1];
|
||||||
|
|
||||||
return nestedNode.content;
|
return nestedNode.content;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,9 +126,9 @@ const handleTagStart = (token) => {
|
|||||||
createTagNode(token);
|
createTagNode(token);
|
||||||
|
|
||||||
if (isTagNested(token)) {
|
if (isTagNested(token)) {
|
||||||
nestedNodes.push(getTagNode());
|
nestedNodes.push(getLastTagNode());
|
||||||
} else {
|
} else {
|
||||||
appendNode(getTagNode());
|
appendNode(getLastTagNode());
|
||||||
clearTagNode();
|
clearTagNode();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -151,6 +150,7 @@ const handleTagEnd = (token) => {
|
|||||||
const tag = token.getValue();
|
const tag = token.getValue();
|
||||||
const line = token.getLine();
|
const line = token.getLine();
|
||||||
const column = token.getColumn();
|
const column = token.getColumn();
|
||||||
|
|
||||||
options.onError({
|
options.onError({
|
||||||
message: `Inconsistent tag '${tag}' on line ${line} and column ${column}`,
|
message: `Inconsistent tag '${tag}' on line ${line} and column ${column}`,
|
||||||
lineNumber: line,
|
lineNumber: line,
|
||||||
@@ -183,15 +183,22 @@ const handleTagToken = (token) => {
|
|||||||
* @param {Token} token
|
* @param {Token} token
|
||||||
*/
|
*/
|
||||||
const handleTagNode = (token) => {
|
const handleTagNode = (token) => {
|
||||||
const tagNode = getTagNode();
|
const tagNode = getLastTagNode();
|
||||||
|
|
||||||
if (tagNode) {
|
if (tagNode) {
|
||||||
if (token.isAttrName()) {
|
if (token.isAttrName()) {
|
||||||
createTagNodeAttrName(token);
|
createTagNodeAttrName(token);
|
||||||
tagNode.attr(getTagNodeAttrName(), null);
|
tagNode.attr(getTagNodeAttrName(), '');
|
||||||
} else if (token.isAttrValue()) {
|
} else if (token.isAttrValue()) {
|
||||||
tagNode.attr(getTagNodeAttrName(), token.getValue());
|
const attrName = getTagNodeAttrName();
|
||||||
clearTagNodeAttrName();
|
const attrValue = token.getValue();
|
||||||
|
|
||||||
|
if (attrName) {
|
||||||
|
tagNode.attr(getTagNodeAttrName(), attrValue);
|
||||||
|
clearTagNodeAttrName();
|
||||||
|
} else {
|
||||||
|
tagNode.attr(attrValue, attrValue);
|
||||||
|
}
|
||||||
} else if (token.isText()) {
|
} else if (token.isText()) {
|
||||||
tagNode.append(token.getValue());
|
tagNode.append(token.getValue());
|
||||||
}
|
}
|
||||||
@@ -215,7 +222,12 @@ const parseToken = (token) => {
|
|||||||
*/
|
*/
|
||||||
const parse = (input, opts = {}) => {
|
const parse = (input, opts = {}) => {
|
||||||
options = opts;
|
options = opts;
|
||||||
tokenizer = (opts.createTokenizer ? opts.createTokenizer : createTokenizer)(input, parseToken);
|
tokenizer = (opts.createTokenizer ? opts.createTokenizer : createLexer)(input, {
|
||||||
|
onToken: parseToken,
|
||||||
|
onlyAllowTags: options.onlyAllowTags,
|
||||||
|
openTag: options.openTag,
|
||||||
|
closeTag: options.closeTag,
|
||||||
|
});
|
||||||
|
|
||||||
nodes = [];
|
nodes = [];
|
||||||
nestedNodes = [];
|
nestedNodes = [];
|
||||||
|
|||||||
@@ -14,15 +14,16 @@ const tokenize = input => (createLexer(input).tokenize());
|
|||||||
|
|
||||||
describe('lexer', () => {
|
describe('lexer', () => {
|
||||||
const expectOutput = (output, tokens) => {
|
const expectOutput = (output, tokens) => {
|
||||||
|
expect(tokens.length).toBe(output.length);
|
||||||
expect(tokens).toBeInstanceOf(Array);
|
expect(tokens).toBeInstanceOf(Array);
|
||||||
output.forEach((token, idx) => {
|
tokens.forEach((token, idx) => {
|
||||||
expect(tokens[idx]).toBeInstanceOf(Object);
|
expect(token).toBeInstanceOf(Object);
|
||||||
expect(tokens[idx].type).toEqual(token[0]);
|
expect(token.type).toEqual(output[idx][0]);
|
||||||
expect(tokens[idx].value).toEqual(token[1]);
|
expect(token.value).toEqual(output[idx][1]);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
test('tokenize single tag', () => {
|
test('single tag', () => {
|
||||||
const input = '[SingleTag]';
|
const input = '[SingleTag]';
|
||||||
const tokens = tokenize(input);
|
const tokens = tokenize(input);
|
||||||
const output = [
|
const output = [
|
||||||
@@ -32,7 +33,7 @@ describe('lexer', () => {
|
|||||||
expectOutput(output, tokens);
|
expectOutput(output, tokens);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('tokenize single tag with spaces', () => {
|
test('single tag with spaces', () => {
|
||||||
const input = '[Single Tag]';
|
const input = '[Single Tag]';
|
||||||
const tokens = tokenize(input);
|
const tokens = tokenize(input);
|
||||||
|
|
||||||
@@ -43,7 +44,7 @@ describe('lexer', () => {
|
|||||||
expectOutput(output, tokens);
|
expectOutput(output, tokens);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('tokenize string with quotemarks', () => {
|
test('string with quotemarks', () => {
|
||||||
const input = '"Someone Like You" by Adele';
|
const input = '"Someone Like You" by Adele';
|
||||||
const tokens = tokenize(input);
|
const tokens = tokenize(input);
|
||||||
|
|
||||||
@@ -62,7 +63,7 @@ describe('lexer', () => {
|
|||||||
expectOutput(output, tokens);
|
expectOutput(output, tokens);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('tokenize tags in brakets', () => {
|
test('tags in brakets', () => {
|
||||||
const input = '[ [h1]G[/h1] ]';
|
const input = '[ [h1]G[/h1] ]';
|
||||||
const tokens = tokenize(input);
|
const tokens = tokenize(input);
|
||||||
|
|
||||||
@@ -79,7 +80,7 @@ describe('lexer', () => {
|
|||||||
expectOutput(output, tokens);
|
expectOutput(output, tokens);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('tokenize tag as param', () => {
|
test('tag as param', () => {
|
||||||
const input = '[color="#ff0000"]Text[/color]';
|
const input = '[color="#ff0000"]Text[/color]';
|
||||||
const tokens = tokenize(input);
|
const tokens = tokenize(input);
|
||||||
const output = [
|
const output = [
|
||||||
@@ -92,7 +93,7 @@ describe('lexer', () => {
|
|||||||
expectOutput(output, tokens);
|
expectOutput(output, tokens);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('tokenize tag with quotemark params with spaces', () => {
|
test('tag with quotemark params with spaces', () => {
|
||||||
const input = '[url text="Foo Bar" text2="Foo Bar 2"]Text[/url]';
|
const input = '[url text="Foo Bar" text2="Foo Bar 2"]Text[/url]';
|
||||||
const tokens = tokenize(input);
|
const tokens = tokenize(input);
|
||||||
const output = [
|
const output = [
|
||||||
@@ -108,7 +109,7 @@ describe('lexer', () => {
|
|||||||
expectOutput(output, tokens);
|
expectOutput(output, tokens);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('tokenize tag with escaped quotemark param', () => {
|
test('tag with escaped quotemark param', () => {
|
||||||
const input = `[url text="Foo \\"Bar"]Text[/url]`;
|
const input = `[url text="Foo \\"Bar"]Text[/url]`;
|
||||||
const tokens = tokenize(input);
|
const tokens = tokenize(input);
|
||||||
const output = [
|
const output = [
|
||||||
@@ -122,7 +123,7 @@ describe('lexer', () => {
|
|||||||
expectOutput(output, tokens);
|
expectOutput(output, tokens);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('tokenize tag param without quotemarks', () => {
|
test('tag param without quotemarks', () => {
|
||||||
const input = '[style color=#ff0000]Text[/style]';
|
const input = '[style color=#ff0000]Text[/style]';
|
||||||
const tokens = tokenize(input);
|
const tokens = tokenize(input);
|
||||||
const output = [
|
const output = [
|
||||||
@@ -136,7 +137,7 @@ describe('lexer', () => {
|
|||||||
expectOutput(output, tokens);
|
expectOutput(output, tokens);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('tokenize list tag with items', () => {
|
test('list tag with items', () => {
|
||||||
const input = `[list]
|
const input = `[list]
|
||||||
[*] Item 1.
|
[*] Item 1.
|
||||||
[*] Item 2.
|
[*] Item 2.
|
||||||
@@ -174,7 +175,7 @@ describe('lexer', () => {
|
|||||||
expectOutput(output, tokens);
|
expectOutput(output, tokens);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('tokenize bad tags as texts', () => {
|
test('bad tags as texts', () => {
|
||||||
const inputs = [
|
const inputs = [
|
||||||
'[]',
|
'[]',
|
||||||
'[=]',
|
'[=]',
|
||||||
@@ -234,4 +235,75 @@ describe('lexer', () => {
|
|||||||
expectOutput(asserts[idx], tokens);
|
expectOutput(asserts[idx], tokens);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/*
|
||||||
|
test('bad unclosed tag', () => {
|
||||||
|
const input = `[Finger tapping; R.H. = Right Hand) Part A [Finger tapping (Right hand -15-, -16-)]`;
|
||||||
|
const tokens = tokenize(input);
|
||||||
|
const output = [];
|
||||||
|
|
||||||
|
expectOutput(output, tokens);
|
||||||
|
});
|
||||||
|
*/
|
||||||
|
|
||||||
|
describe('html', () => {
|
||||||
|
const tokenizeHTML = input => createLexer(input, { openTag: '<', closeTag: '>' }).tokenize();
|
||||||
|
|
||||||
|
test('Normal attributes', () => {
|
||||||
|
const content = `<button id="test0" class="value0" title="value1">class="value0" title="value1"</button>`;
|
||||||
|
const tokens = tokenizeHTML(content);
|
||||||
|
const output = [
|
||||||
|
[TYPE.TAG, 'button', 2, 0],
|
||||||
|
[TYPE.ATTR_NAME, 'id', 2, 0],
|
||||||
|
[TYPE.ATTR_VALUE, 'test0', 2, 0],
|
||||||
|
[TYPE.ATTR_NAME, 'class', 2, 0],
|
||||||
|
[TYPE.ATTR_VALUE, 'value0', 2, 0],
|
||||||
|
[TYPE.ATTR_NAME, 'title', 2, 0],
|
||||||
|
[TYPE.ATTR_VALUE, 'value1', 2, 0],
|
||||||
|
[TYPE.WORD, "class=\"value0\"", 2, 0],
|
||||||
|
[TYPE.SPACE, " ", 2, 0],
|
||||||
|
[TYPE.WORD, "title=\"value1\"", 2, 0],
|
||||||
|
[TYPE.TAG, '/button', 2, 0]
|
||||||
|
];
|
||||||
|
|
||||||
|
expectOutput(output, tokens);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Attributes with no quotes or value', () => {
|
||||||
|
const content = `<button id="test1" class=value2 disabled>class=value2 disabled</button>`;
|
||||||
|
const tokens = tokenizeHTML(content);
|
||||||
|
const output = [
|
||||||
|
[TYPE.TAG, 'button', 2, 0],
|
||||||
|
[TYPE.ATTR_NAME, 'id', 2, 0],
|
||||||
|
[TYPE.ATTR_VALUE, 'test1', 2, 0],
|
||||||
|
[TYPE.ATTR_NAME, 'class', 2, 0],
|
||||||
|
[TYPE.ATTR_VALUE, 'value2', 2, 0],
|
||||||
|
[TYPE.ATTR_VALUE, 'disabled', 2, 0],
|
||||||
|
[TYPE.WORD, "class=value2", 2, 0],
|
||||||
|
[TYPE.SPACE, " ", 2, 0],
|
||||||
|
[TYPE.WORD, "disabled", 2, 0],
|
||||||
|
[TYPE.TAG, '/button', 2, 0]
|
||||||
|
];
|
||||||
|
|
||||||
|
expectOutput(output, tokens);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Attributes with no space between them. No valid, but accepted by the browser', () => {
|
||||||
|
const content = `<button id="test2" class="value4"title="value5">class="value4"title="value5"</button>`;
|
||||||
|
const tokens = tokenizeHTML(content);
|
||||||
|
const output = [
|
||||||
|
[TYPE.TAG, 'button', 2, 0],
|
||||||
|
[TYPE.ATTR_NAME, 'id', 2, 0],
|
||||||
|
[TYPE.ATTR_VALUE, 'test2', 2, 0],
|
||||||
|
[TYPE.ATTR_NAME, 'class', 2, 0],
|
||||||
|
[TYPE.ATTR_VALUE, 'value4', 2, 0],
|
||||||
|
[TYPE.ATTR_NAME, 'title', 2, 0],
|
||||||
|
[TYPE.ATTR_VALUE, 'value5', 2, 0],
|
||||||
|
[TYPE.WORD, "class=\"value4\"title=\"value5\"", 2, 0],
|
||||||
|
[TYPE.TAG, '/button', 2, 0]
|
||||||
|
];
|
||||||
|
|
||||||
|
expectOutput(output, tokens);
|
||||||
|
});
|
||||||
|
})
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
import { parse } from '../src'
|
import { parse } from '../src'
|
||||||
|
|
||||||
describe('Parser', () => {
|
describe('Parser', () => {
|
||||||
|
const expectOutput = (ast, output) => {
|
||||||
|
expect(ast).toBeInstanceOf(Array);
|
||||||
|
expect(ast).toEqual(output);
|
||||||
|
};
|
||||||
|
|
||||||
test('parse paired tags tokens', () => {
|
test('parse paired tags tokens', () => {
|
||||||
const ast = parse('[best name=value]Foo Bar[/best]');
|
const ast = parse('[best name=value]Foo Bar[/best]');
|
||||||
|
|
||||||
expect(ast).toBeInstanceOf(Array);
|
expectOutput(ast, [
|
||||||
expect(ast).toEqual([
|
|
||||||
{
|
{
|
||||||
tag: 'best',
|
tag: 'best',
|
||||||
attrs: {
|
attrs: {
|
||||||
@@ -25,8 +29,7 @@ describe('Parser', () => {
|
|||||||
onlyAllowTags: ['h1']
|
onlyAllowTags: ['h1']
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(ast).toBeInstanceOf(Array);
|
expectOutput(ast, [
|
||||||
expect(ast).toEqual([
|
|
||||||
{
|
{
|
||||||
tag: 'h1',
|
tag: 'h1',
|
||||||
attrs: {
|
attrs: {
|
||||||
@@ -45,36 +48,32 @@ describe('Parser', () => {
|
|||||||
test('parse inconsistent tags', () => {
|
test('parse inconsistent tags', () => {
|
||||||
const ast = parse('[h1 name=value]Foo [Bar] /h1]');
|
const ast = parse('[h1 name=value]Foo [Bar] /h1]');
|
||||||
|
|
||||||
expect(ast).toBeInstanceOf(Array);
|
expectOutput(ast, [
|
||||||
expect(ast).toEqual(
|
{
|
||||||
[
|
attrs: {},
|
||||||
{
|
tag: 'h1',
|
||||||
attrs: {},
|
content: []
|
||||||
tag: 'h1',
|
},
|
||||||
content: []
|
'Foo',
|
||||||
},
|
' ',
|
||||||
'Foo',
|
{
|
||||||
' ',
|
tag: 'Bar',
|
||||||
{
|
attrs: {},
|
||||||
tag: 'Bar',
|
content: []
|
||||||
attrs: {},
|
},
|
||||||
content: []
|
' ',
|
||||||
},
|
'/h1]',
|
||||||
' ',
|
]);
|
||||||
'/h1]',
|
|
||||||
]
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('parse tag with value param', () => {
|
test('parse tag with value param', () => {
|
||||||
const ast = parse('[url=https://github.com/jilizart/bbob]BBob[/url]');
|
const ast = parse('[url=https://github.com/jilizart/bbob]BBob[/url]');
|
||||||
|
|
||||||
expect(ast).toBeInstanceOf(Array);
|
expectOutput(ast, [
|
||||||
expect(ast).toEqual([
|
|
||||||
{
|
{
|
||||||
tag: 'url',
|
tag: 'url',
|
||||||
attrs: {
|
attrs: {
|
||||||
url: 'https://github.com/jilizart/bbob',
|
'https://github.com/jilizart/bbob': 'https://github.com/jilizart/bbob',
|
||||||
},
|
},
|
||||||
content: ['BBob'],
|
content: ['BBob'],
|
||||||
},
|
},
|
||||||
@@ -84,8 +83,7 @@ describe('Parser', () => {
|
|||||||
test('parse tag with quoted param with spaces', () => {
|
test('parse tag with quoted param with spaces', () => {
|
||||||
const ast = parse('[url href=https://ru.wikipedia.org target=_blank text="Foo Bar"]Text[/url]');
|
const ast = parse('[url href=https://ru.wikipedia.org target=_blank text="Foo Bar"]Text[/url]');
|
||||||
|
|
||||||
expect(ast).toBeInstanceOf(Array);
|
expectOutput(ast, [
|
||||||
expect(ast).toEqual([
|
|
||||||
{
|
{
|
||||||
tag: 'url',
|
tag: 'url',
|
||||||
attrs: {
|
attrs: {
|
||||||
@@ -103,5 +101,71 @@ describe('Parser', () => {
|
|||||||
const ast = parse('[c][/c][b]hello[/c][/b][b]', { onError });
|
const ast = parse('[c][/c][b]hello[/c][/b][b]', { onError });
|
||||||
|
|
||||||
expect(onError).toHaveBeenCalled();
|
expect(onError).toHaveBeenCalled();
|
||||||
})
|
});
|
||||||
|
|
||||||
|
describe('html', () => {
|
||||||
|
const parseHTML = input => parse(input, { openTag: '<', closeTag: '>' });
|
||||||
|
|
||||||
|
test('normal attributes', () => {
|
||||||
|
const content = `<button id="test0" class="value0" title="value1">class="value0" title="value1"</button>`;
|
||||||
|
const ast = parseHTML(content);
|
||||||
|
|
||||||
|
expectOutput(ast, [
|
||||||
|
{
|
||||||
|
"tag": "button",
|
||||||
|
"attrs": {
|
||||||
|
"id": "test0",
|
||||||
|
"class": "value0",
|
||||||
|
"title": "value1"
|
||||||
|
},
|
||||||
|
"content": [
|
||||||
|
"class=\"value0\"",
|
||||||
|
" ",
|
||||||
|
"title=\"value1\""
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('attributes with no quotes or value', () => {
|
||||||
|
const content = `<button id="test1" class=value2 disabled required>class=value2 disabled</button>`;
|
||||||
|
const ast = parseHTML(content);
|
||||||
|
|
||||||
|
expectOutput(ast, [
|
||||||
|
{
|
||||||
|
"tag": "button",
|
||||||
|
"attrs": {
|
||||||
|
"id": "test1",
|
||||||
|
"class": "value2",
|
||||||
|
"disabled": "disabled",
|
||||||
|
"required": "required"
|
||||||
|
},
|
||||||
|
"content": [
|
||||||
|
"class=value2",
|
||||||
|
" ",
|
||||||
|
"disabled"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('attributes with no space between them. no valid, but accepted by the browser', () => {
|
||||||
|
const content = `<button id="test2" class="value4"title="value5">class="value4"title="value5"</button>`;
|
||||||
|
const ast = parseHTML(content);
|
||||||
|
|
||||||
|
expectOutput(ast, [
|
||||||
|
{
|
||||||
|
"tag": "button",
|
||||||
|
"attrs": {
|
||||||
|
"id": "test2",
|
||||||
|
"class": "value4",
|
||||||
|
"title": "value5"
|
||||||
|
},
|
||||||
|
"content": [
|
||||||
|
"class=\"value4\"title=\"value5\""
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ describe('posthtml-render', () => {
|
|||||||
const ast = parse('[size=150][b]PostHTML render test[/b][/size]');
|
const ast = parse('[size=150][b]PostHTML render test[/b][/size]');
|
||||||
const html = render(ast);
|
const html = render(ast);
|
||||||
|
|
||||||
expect(html).toBe('<size size="150"><b>PostHTML render test</b></size>')
|
expect(html).toBe('<size 150="150"><b>PostHTML render test</b></size>')
|
||||||
})
|
})
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -56,6 +56,10 @@ const asListItems = (content) => {
|
|||||||
return [].concat(listItems);
|
return [].concat(listItems);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getUniqAttr = attrs => Object
|
||||||
|
.keys(attrs)
|
||||||
|
.reduce((res, key) => (attrs[key] === key ? attrs[key] : null), null);
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
b: node => ({
|
b: node => ({
|
||||||
tag: 'span',
|
tag: 'span',
|
||||||
@@ -88,7 +92,7 @@ export default {
|
|||||||
url: (node, { render }) => ({
|
url: (node, { render }) => ({
|
||||||
tag: 'a',
|
tag: 'a',
|
||||||
attrs: {
|
attrs: {
|
||||||
href: node.attrs.url ? node.attrs.url : render(node.content),
|
href: getUniqAttr(node.attrs) ? getUniqAttr(node.attrs) : render(node.content),
|
||||||
},
|
},
|
||||||
content: node.content,
|
content: node.content,
|
||||||
}),
|
}),
|
||||||
|
|||||||
Reference in New Issue
Block a user