shortcode-tree
Advanced tools
Comparing version 1.0.4 to 1.1.0
module.exports.default = require('./src/index'); |
{ | ||
"name": "shortcode-tree", | ||
"version": "1.0.4", | ||
"version": "1.1.0", | ||
"description": "Parser library for reading short codes (BB codes) into a tree structure", | ||
@@ -5,0 +5,0 @@ "main": "src/index.js", |
110
README.md
@@ -38,18 +38,29 @@ # shortcode-tree | ||
## Usage | ||
### Parsing a single short code | ||
Start with the raw text you want to process, and feed it to the `parse` function: | ||
To parse **one** individual short code, use the `ShortcodeParser`: | ||
var parser = require('shortcode-tree').ShortcodeParser; | ||
var ShortcodeTree = require('shortcode-tree').ShortcodeTree; | ||
var shortcode = parser.parseShortcode("[b]example content[/b]"); | ||
console.log(shortcode.content); // example content | ||
var rootNode = ShortcodeTree.parse(inputText); | ||
console.log(rootNode.children.length); | ||
The `parseShortcode` method returns a `Shortcode` object. | ||
This function will return a `ShortcodeNode` object. Each `ShortcodeNode` may contain a set of child nodes. | ||
If you have regular text or HTML mixed in at the same level as a shortcode, a `ShortcodeNode` may also contain `TextNode` children to hold these items. Text nodes themselves cannot hold children, only text. | ||
### The `ShortcodeNode` object | ||
A shortcode node contains the following properties: | ||
| Property | Type | Description | | ||
| --- | --- | --- | | ||
| `text` | string | The raw text content (code) of this node. | | ||
| `shortcode` | `Shortcode` or null | Information about the parsed Shortcode represented by this node. Will be `null` if this is the root node. | | ||
| `children` | array | List of children. May contain `ShortcodeNode` and `TextNode` items. | | ||
### The `Shortcode` object | ||
A parsed shortcode. Typically available through the `shortcode` property of a shortcode node. | ||
| Property | Type | Description | | ||
@@ -62,8 +73,81 @@ | --- | --- | --- | | ||
| `codeText` | string | The raw shortcode text, as it was parsed. | | ||
| `offset` | integer | Offset index, relative to the original input string. | | ||
| `offset` | integer | Offset index, relative to the original input string. | | ||
### Extracting multiple shortcodes from text | ||
### The `TextNode` object | ||
If there are multiple shortcodes within one piece of text, you can extract them using the `ShortcodeExtractor`: | ||
A piece of raw text or HTML that was placed on the same level as another shortcode. This is always a child of a shortcode node. | ||
| Property | Type | Description | | ||
| --- | --- | --- | | ||
| `text` | string | Raw text or code | | ||
### Dumping tree structure | ||
By calling `traceTree()` on a node, you can dump a simple visualisation of the parsed tree structure. | ||
var sampleInput = | ||
"[row]" + | ||
"[col]" + | ||
"<h1>My article</h1>" + | ||
"[img src=\"image.jpg\"/]" + | ||
"[/col]" + | ||
"[col]" + | ||
"<p>Just a boring text sample column</p>" + | ||
"[/col]" + | ||
"[/row]" | ||
var rootNode = ShortcodeTree.parse(sampleInput); | ||
rootNode.traceTree(); | ||
The above example would generate the following console output: | ||
+++++ Root Node +++++ | ||
[Shortcode Node: row], content: [col]<h1>My article</h1>[img src="image.jpg"/][/col][col]<p>Just a boring text sample column</p>[/col] | ||
--- [Shortcode Node: col], content: <h1>My article</h1>[img src="image.jpg"/] | ||
------ [Text Node], content: <h1>My article</h1> | ||
------ [Shortcode Node: img], content: null | ||
--- [Shortcode Node: col], content: <p>Just a boring text sample column</p> | ||
## Advanced usage | ||
### Parsing a single short code fragment | ||
To parse **one** individual short code, use the `ShortcodeParser`: | ||
var parser = require('shortcode-tree').ShortcodeParser; | ||
var shortcode = parser.parseShortcode("[b]example content[/b]"); | ||
console.log(shortcode.content); // example content | ||
The `parseShortcode` method returns a `Shortcode` object. | ||
### Custom parser options | ||
When calling `parseShortcode`, you can add an `options` object as a second argument. | ||
parser.parseShortcode("[b]example content[/b]", { /** insert options **/ }) | ||
The following options are available: | ||
| Option | Type | Default | Description | | ||
| --- | --- | --- | --- | | ||
| `mode` | string | `normal` | Parser mode to operate in. See table below. | | ||
| `offset` | integer | `0` | Offset from the start of the input string, where parsing should begin. | | ||
| `throwErrors` | boolean | `true` | If enabled: On shortcode parse error, an `Error` is thrown. If disabled: `false` is returned on parse error. | | ||
| `precise` | boolean | `false` | If things aren't working as expected, enable this for deep recursive parsing. Reduces performance exponentially. | | ||
The default options are defined in `ShortcodeParser.DEFAULT_OPTIONS`. | ||
#### Parser modes | ||
| Constant | Value | Description | | ||
| --- | --- | --- | | ||
| `MODE_NORMAL` | `normal` | Parses text into a `Shortcode` object. | | ||
| `MODE_GET_OPENING_TAG_NAME` | `tag_name` | Halts parsing once the tag name is found, and returns it as a string. | | ||
### Extracting one level of shortcodes from text | ||
The `ShortcodeExtractor` is a component that can extract a set of Shortcodes from a single piece of text, without traversing deeper than one level. | ||
var extractor = require('shortcode-tree').ShortcodeExtractor; | ||
@@ -89,4 +173,2 @@ | ||
This method will only extract shortcodes from one level, and does not process any child tags. | ||
The `extractShortcodes` method returns an array of `Shortcode` objects. | ||
The `extractShortcodes` method returns an array of `Shortcode` objects. |
@@ -5,5 +5,7 @@ module.exports = { | ||
"ShortcodeExtractor": require('./shortcode-extractor'), | ||
"Shortcode": require('./shortcode') | ||
"Shortcode": require('./shortcode'), | ||
"ShortcodeNode": require('./shortcode-node'), | ||
"TextNode": require('./text-node') | ||
}; | ||
module.exports.default = module.exports.ShortcodeTree; |
@@ -11,5 +11,6 @@ let ShortcodeParser = require('./shortcode-parser'); | ||
* @param {string} text | ||
* @param {object} options (Optional) Parser options | ||
* @returns {Shortcode[]|array} | ||
*/ | ||
extractShortcodes(text) { | ||
extractShortcodes(text, options) { | ||
// Perform a generic regex match that will match any block spanning [XXX] to [/XXX]. | ||
@@ -28,3 +29,3 @@ // Because it is a generic regex, this may also result in a matching multiple blocks on the same level; e.g. [XXX]bla[/XXX][YYY]bla2[/YYY]. | ||
resultSet = resultSet.concat(this.reduceShortcodeMatch(text, match.index)); | ||
resultSet = resultSet.concat(this.reduceShortcodeMatch(text, match.index, options)); | ||
} | ||
@@ -40,10 +41,22 @@ | ||
* @param {int} offset | ||
* @param {object} options (Optional) Parser options | ||
* @returns {array} | ||
*/ | ||
reduceShortcodeMatch(text, offset) { | ||
reduceShortcodeMatch(text, offset, options) { | ||
let results = []; | ||
while (offset < text.length) { | ||
let parsedShortcode = ShortcodeParser.parseShortcode(text, {offset: offset, throwErrors: false}); | ||
let optionsMerged = Object.assign( | ||
{ | ||
precise: true | ||
}, | ||
options, | ||
{ | ||
offset: offset, | ||
throwErrors: false, | ||
mode: ShortcodeParser.MODE_NORMAL | ||
}); | ||
let parsedShortcode = ShortcodeParser.parseShortcode(text, optionsMerged); | ||
if (!parsedShortcode) { | ||
@@ -50,0 +63,0 @@ // Parse error |
@@ -1,3 +0,1 @@ | ||
let fs = require('fs'); | ||
let util = require('util'); | ||
let Shortcode = require('./shortcode'); | ||
@@ -15,3 +13,3 @@ | ||
// Step 0: Apply offset from options, then increase offset as needed by looking for an opening tag | ||
// Step 1: Apply offset from options, then increase offset as needed by looking for an opening tag | ||
input = input.substr(options.offset); | ||
@@ -32,9 +30,9 @@ | ||
// Step 1: Read the opening block without enclosing []'s | ||
// Step 2: Read the opening block without enclosing []'s | ||
let openBlockStartIdx = 0; | ||
let openBlockEndIdx = input.indexOf(ShortcodeParser.T_TAG_BLOCK_END); | ||
let openBlockTextFull = input.substr(openBlockStartIdx, openBlockEndIdx + 1); | ||
let openBlockText = openBlockTextFull.substr(1, openBlockTextFull.length - 2).trim(); | ||
let openBlockInner = openBlockTextFull.substr(1, openBlockTextFull.length - 2).trim(); | ||
if (!openBlockText || !openBlockText.length) { | ||
if (!openBlockInner || !openBlockInner.length) { | ||
if (options.throwErrors) { | ||
@@ -47,13 +45,127 @@ throw new Error(`Malformatted shortcode: Invalid or missing opening tag in ${input}`); | ||
// Step 2: Determine if block is self closing or not | ||
let selfCloseIdx = openBlockText.lastIndexOf(ShortcodeParser.T_TAG_CLOSER); | ||
// Step 3: Read the block's name and properties by tokenizing it | ||
if (this.tokenizeOpenBlock(shortcode, openBlockInner, options) === false) { | ||
return false; | ||
} | ||
if (selfCloseIdx === openBlockEndIdx - 2) { | ||
if (options.mode === ShortcodeParser.MODE_GET_OPENING_TAG_NAME) { | ||
return shortcode.name; | ||
} | ||
// Step 4: If we are in fast mode, try reading forward until we find the closing tag. | ||
// Otherwise, if we are in precise mode, keep parsing everything past our tag until we find *our* closing tag. | ||
let closingTagExpected = `[/${shortcode.name}]`; | ||
if (options.precise && !shortcode.isSelfClosing) { | ||
let stackLevel = 0; | ||
let bufferRemainder = input.substr(openBlockTextFull.length); | ||
let bufferContent = ""; | ||
let closingTagFound = null; | ||
let subParseOptions = Object.assign({}, options, { | ||
throwErrors: false, | ||
mode: ShortcodeParser.MODE_NORMAL | ||
}); | ||
while (bufferRemainder.length > 0) { | ||
// Keep iterating the remainder of the input buffer, looking for any other closing or opening tags | ||
let nextBlockMatch = /\[(.*?)\]/g.exec(bufferRemainder); | ||
if (!nextBlockMatch) { | ||
// No more tags found, end of input | ||
break; | ||
} | ||
let nextBlockIdx = nextBlockMatch.index; | ||
let nextBlockText = nextBlockMatch[0]; | ||
let nextBlockTextInner = nextBlockText.substr(1, nextBlockText.length - 2).trim(); | ||
let nextBlockLen = nextBlockText.length; | ||
if (nextBlockTextInner.startsWith("/")) { | ||
// Closing tag | ||
if (stackLevel === 0) { | ||
if (nextBlockText !== closingTagExpected) { | ||
if (options.throwErrors) { | ||
throw new Error(`Malformatted shortcode: Inconsistent closing tag. Expected closing tag: ${closingTagExpected}, but found ${nextBlockText}`); | ||
} else { | ||
return false; | ||
} | ||
} | ||
closingTagFound = nextBlockText; | ||
bufferContent += bufferRemainder.substr(0, nextBlockIdx); | ||
break; | ||
} else { | ||
stackLevel--; | ||
} | ||
} else { | ||
// Open tag | ||
let nextBlockInfo = new Shortcode(); | ||
if (this.tokenizeOpenBlock(nextBlockInfo, nextBlockTextInner, subParseOptions) !== false) { | ||
let blockName = nextBlockInfo.name; | ||
// console.log(nextBlockInfo); | ||
if (!nextBlockInfo.isSelfClosing) { | ||
stackLevel++; | ||
} | ||
} | ||
} | ||
// Modify the buffer to subtract this tag, and add it to the tag's content buffer | ||
bufferContent += bufferRemainder.substr(0, nextBlockIdx + nextBlockLen); | ||
bufferRemainder = bufferRemainder.substr(nextBlockIdx + nextBlockLen); | ||
} | ||
if (!closingTagFound) { | ||
if (options.throwErrors) { | ||
throw new Error(`Malformatted shortcode: Unexpected end of input. Expected closing tag: ${closingTagExpected}`); | ||
} else { | ||
return false; | ||
} | ||
} | ||
shortcode.content = bufferContent; | ||
shortcode.codeText = openBlockTextFull + shortcode.content + closingTagExpected; | ||
} else { | ||
let offsetFromEnd = 0; | ||
if (!shortcode.isSelfClosing) { | ||
let closingTagIdx = input.indexOf(closingTagExpected); | ||
if (closingTagIdx === -1) { | ||
if (options.throwErrors) { | ||
throw new Error(`Malformatted shortcode: Expected closing tag: ${closingTagExpected}`); | ||
} else { | ||
return false; | ||
} | ||
} | ||
offsetFromEnd = (input.length - closingTagExpected.length) - closingTagIdx; | ||
shortcode.content = input.substr(openBlockStartIdx + openBlockTextFull.length, (input.length - openBlockTextFull.length - closingTagExpected.length - offsetFromEnd)); | ||
shortcode.codeText = input.substr(openBlockStartIdx, input.length - offsetFromEnd); | ||
} else { | ||
shortcode.content = null; | ||
shortcode.codeText = input.substr(openBlockStartIdx, openBlockTextFull.length); | ||
} | ||
} | ||
return shortcode; | ||
}, | ||
tokenizeOpenBlock(shortcode, openBlockInner, options) { | ||
// First, determine if block is self closing or not | ||
let selfCloseIdx = openBlockInner.lastIndexOf(ShortcodeParser.T_TAG_CLOSER); | ||
if (selfCloseIdx === openBlockInner.length - 1) { | ||
// Last character before closing tag is the self-closing indicator | ||
// Mark shortcode as self closing, and remove the closer token from our buffer | ||
shortcode.isSelfClosing = true; | ||
openBlockText = openBlockText.substr(0, openBlockText.length - 1).trim(); | ||
openBlockInner = openBlockInner.substr(0, openBlockInner.length - 1).trim(); | ||
} | ||
// Step 3: Read the block's name and properties by tokenizing it | ||
// Start tokenization process | ||
let buffer = ""; | ||
@@ -71,4 +183,4 @@ | ||
for (let index = 0; index <= openBlockText.length; index++) { | ||
let nextToken = openBlockText[index]; | ||
for (let index = 0; index <= openBlockInner.length; index++) { | ||
let nextToken = openBlockInner[index]; | ||
let nothingLeft = (typeof nextToken === "undefined"); | ||
@@ -88,3 +200,3 @@ | ||
if (options.mode === ShortcodeParser.MODE_GET_OPENING_TAG_NAME) { | ||
return shortcode.name; | ||
return; | ||
} | ||
@@ -177,33 +289,3 @@ | ||
if (options.mode === ShortcodeParser.MODE_GET_OPENING_TAG_NAME) { | ||
return shortcode.name; | ||
} | ||
shortcode.properties = properties; | ||
// Step 4: If this is not a self closing tag; verify end tag is here as expected, and read the content | ||
let offsetFromEnd = 0; | ||
if (!shortcode.isSelfClosing) { | ||
let closingTagExpected = `[/${shortcode.name}]`; | ||
let closingTagIdx = input.lastIndexOf(closingTagExpected); | ||
if (closingTagIdx === -1) { | ||
if (options.throwErrors) { | ||
throw new Error(`Malformatted shortcode: Expected closing tag: ${closingTagExpected}`); | ||
} else { | ||
return false; | ||
} | ||
} | ||
offsetFromEnd = (input.length - closingTagExpected.length) - closingTagIdx; | ||
shortcode.content = input.substr(openBlockStartIdx + openBlockTextFull.length, (input.length - openBlockTextFull.length - closingTagExpected.length - offsetFromEnd)); | ||
shortcode.codeText = input.substr(openBlockStartIdx, input.length - offsetFromEnd); | ||
} else { | ||
shortcode.content = null; | ||
shortcode.codeText = input.substr(openBlockStartIdx, openBlockTextFull.length); | ||
} | ||
return shortcode; | ||
} | ||
@@ -226,5 +308,6 @@ }; | ||
offset: 0, | ||
throwErrors: true | ||
throwErrors: true, | ||
precise: false | ||
}; | ||
module.exports = ShortcodeParser; |
@@ -1,6 +0,65 @@ | ||
let ShortcodeParser = require('./shortcode-parser'); | ||
let ShortcodeExtractor = require('./shortcode-extractor'); | ||
let ShortcodeNode = require('./shortcode-node'); | ||
let TextNode = require('./text-node'); | ||
/** | ||
* Utility for parsing text containing Shortcodes into a tree structure. | ||
*/ | ||
let ShortcodeTree = { | ||
/** | ||
* Parses input text into a tree structure. | ||
* | ||
* @param {string} input | ||
* @return {ShortcodeNode} | ||
*/ | ||
parse(input) { | ||
let rootNode = new ShortcodeNode(input); | ||
this.traverseNode(rootNode); | ||
return rootNode; | ||
}, | ||
/** | ||
* Traverses a tree node: extracts short codes from the node text, and traverses its child nodes. | ||
* | ||
* @param {ShortcodeNode} node | ||
*/ | ||
traverseNode(node) { | ||
// Extract shortcodes from this node's raw text | ||
let shortcodesExtracted = ShortcodeExtractor.extractShortcodes(node.text); | ||
let lastEndIndex = 0; | ||
let anyChildNodes = false; | ||
// Iterate each shortcode, and add it as a child node | ||
for (let key in shortcodesExtracted) { | ||
let _shortcode = shortcodesExtracted[key]; | ||
let _shortcodeNode = new ShortcodeNode(_shortcode); | ||
// Determine if we have skipped any text to reach this node; if so, add it as text node | ||
let skippedLen = _shortcodeNode.shortcode.offset - lastEndIndex; | ||
if (skippedLen > 0) { | ||
node.addChild(new TextNode(node.text.substr(lastEndIndex, skippedLen))); | ||
} | ||
// Add the new shortcode node as a child node to the one we're traversing | ||
node.addChild(_shortcodeNode); | ||
lastEndIndex = _shortcodeNode.shortcode.getEndOffset(); | ||
anyChildNodes = true; | ||
// Recursive loop: Traverse the child node as well | ||
this.traverseNode(_shortcodeNode); | ||
} | ||
// Text node any remaining length, if there were any child nodes | ||
// (Otherwise this node's text property / shortcode content property will do just fine) | ||
if (anyChildNodes && node.text) { | ||
let remainingLen = node.text.length - lastEndIndex; | ||
if (remainingLen > 0) { | ||
node.addChild(new TextNode(node.text.substr(lastEndIndex, remainingLen))); | ||
} | ||
} | ||
} | ||
@@ -7,0 +66,0 @@ }; |
@@ -10,4 +10,8 @@ class Shortcode { | ||
} | ||
getEndOffset() { | ||
return this.offset + this.codeText.length; | ||
} | ||
} | ||
module.exports = Shortcode; |
@@ -59,2 +59,51 @@ let ShortcodeExtractor = require('../src').ShortcodeExtractor; | ||
}); | ||
it('extracts a tag with nested sub-tags', function () { | ||
let testInput = "Hi [row]A[row][cell]B[/cell][/row]C[/row] Bye"; | ||
let expectedOutput = [new Shortcode("row", "A[row][cell]B[/cell][/row]C", {}, false, "[row]A[row][cell]B[/cell][/row]C[/row]", 3)]; | ||
let actualOutput = ShortcodeExtractor.extractShortcodes(testInput) || null; | ||
expect(actualOutput).to.deep.equal(expectedOutput); | ||
}); | ||
it('extracts multiple tags with the same name at the same level individually, without mixing them up', function () { | ||
let testInput = "[column]1[/column][column]2[/column]"; | ||
let expectedOutput = [ | ||
new Shortcode("column", "1", {}, false, "[column]1[/column]", 0), | ||
new Shortcode("column", "2", {}, false, "[column]2[/column]", 18) | ||
]; | ||
let actualOutput = ShortcodeExtractor.extractShortcodes(testInput) || null; | ||
expect(actualOutput).to.deep.equal(expectedOutput); | ||
}); | ||
it('extracts multiple tags with the same name from one level, while ignoring ones with the same name at a deeper level', function () { | ||
let testInput = | ||
"[column]" + | ||
"[column]subA1[/column]" + | ||
"[column]subA2[/column]" + | ||
"[/column]" + | ||
"[column]" + | ||
"[column]subB1[/column]" + | ||
"[column]subB2[/column]" + | ||
"[/column]"; | ||
let expectedOutput = [ | ||
new Shortcode("column", "[column]subA1[/column][column]subA2[/column]", {}, false, "[column]" + | ||
"[column]subA1[/column]" + | ||
"[column]subA2[/column]" + | ||
"[/column]", 0), | ||
new Shortcode("column", "[column]subB1[/column][column]subB2[/column]", {}, false, "[column]" + | ||
"[column]subB1[/column]" + | ||
"[column]subB2[/column]" + | ||
"[/column]", 61) | ||
]; | ||
let actualOutput = ShortcodeExtractor.extractShortcodes(testInput) || null; | ||
expect(actualOutput).to.deep.equal(expectedOutput); | ||
}); | ||
}); |
@@ -6,3 +6,3 @@ let ShortcodeParser = require('../src').ShortcodeParser; | ||
describe('ShortcodeParser.parseShortcode() in normal mode', function () { | ||
describe('ShortcodeParser.parseShortcode() with defaults (fast mode)', function () { | ||
let options = ShortcodeParser.DEFAULT_OPTIONS; | ||
@@ -177,2 +177,40 @@ | ||
}); | ||
}); | ||
describe('ShortcodeParser.parseShortcode() in "precise" mode', function () { | ||
it('correctly parses a tag with same-name tags on that level', function () { | ||
let testInput = "[col]a[/col][col]b[/col][col]c[/col]"; | ||
let expectedOutput = new Shortcode('col', 'a', {}, false, "[col]a[/col]", 0); | ||
let actualOutput = ShortcodeParser.parseShortcode(testInput, { precise: true }) || null; | ||
expect(actualOutput).to.deep.equal(expectedOutput); | ||
}); | ||
it('correctly parses a tag with same-name tags as its children', function () { | ||
let testInput = "[col]text [col]deeper col[/col] content[/col]"; | ||
let expectedOutput = new Shortcode('col', 'text [col]deeper col[/col] content', {}, false, "[col]text [col]deeper col[/col] content[/col]", 0); | ||
let actualOutput = ShortcodeParser.parseShortcode(testInput, { precise: true }) || null; | ||
expect(actualOutput).to.deep.equal(expectedOutput); | ||
}); | ||
it('correctly parses self-closing short tags', function () { | ||
let testInput = "blah [img bla=123/] argh"; | ||
let expectedOutput = new Shortcode('img', null, {"bla": "123"}, true, "[img bla=123/]", 5); | ||
let actualOutput = ShortcodeParser.parseShortcode(testInput, { precise: true }) || null; | ||
expect(actualOutput).to.deep.equal(expectedOutput); | ||
}); | ||
it('correctly parses self-closing short tags as children', function () { | ||
let testInput = "[col][img bla=123/][/col]"; | ||
let expectedOutput = new Shortcode('col', '[img bla=123/]', {}, false, "[col][img bla=123/][/col]", 0); | ||
let actualOutput = ShortcodeParser.parseShortcode(testInput, { precise: true }) || null; | ||
expect(actualOutput).to.deep.equal(expectedOutput); | ||
}); | ||
}); |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
48872
24
778
171
0