├── .gitignore ├── .travis.yml ├── Makefile ├── .tm_properties ├── package.json ├── jshint.json ├── LICENSE ├── test ├── fixtures │ ├── shortcodes.txt │ └── output.txt └── unit.js ├── README.md └── lib └── shortcode-parser.js /.gitignore: -------------------------------------------------------------------------------- 1 | *.DS_Store 2 | /node_modules 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - 0.10 5 | 6 | before_script: 7 | - "npm install" 8 | 9 | script: 10 | - "make test" 11 | 12 | notifications: 13 | email: false -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | default: 3 | @echo; echo "Actions: test | lint | all"; echo 4 | 5 | all: lint test 6 | 7 | test: 8 | @./node_modules/.bin/vows --spec test/unit.js 9 | 10 | lint: 11 | @./node_modules/.bin/jshint -c jshint.json lib/shortcode-parser.js test/unit.js 12 | 13 | .PHONY: test -------------------------------------------------------------------------------- /.tm_properties: -------------------------------------------------------------------------------- 1 | 2 | # Files to include 3 | 4 | myExtraIncludes = ".tm_properties,.gitignore,.travis.yml" 5 | fileBrowserGlob = "{*,$myExtraIncludes}" 6 | include = "{$include,$myExtraIncludes}" 7 | 8 | # Files to exclude 9 | 10 | myExtraExcludes = "node_modules" 11 | myExtraExcludes = "$myExtraExcludes" 12 | 13 | excludeInFileChooser = "{$excludeInFileChooser,$myExtraExcludes}" 14 | excludeInFolderSearch = "{$excludeInFolderSearch,$myExtraExcludes}" 15 | excludeInBrowser = "{$excludeInBrowser,$myExtraExcludes}" 16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "shortcode-parser", 3 | "description": "A Shortcode parser", 4 | "version": "0.0.1", 5 | "homepage": "https://github.com/derdesign/shortcode-parser", 6 | "repository": "https://github.com/derdesign/shortcode-parser", 7 | "author": "Ernesto Méndez ", 8 | "license": "MIT", 9 | "main": "lib/shortcode-parser", 10 | "preferGlobal": "false", 11 | "engines": { 12 | "node": "0.10.x" 13 | }, 14 | "dependencies": {}, 15 | "devDependencies": { 16 | "vows": "0.7.0", 17 | "jshint": "2.3.0" 18 | } 19 | } -------------------------------------------------------------------------------- /jshint.json: -------------------------------------------------------------------------------- 1 | { 2 | 3 | "predef": [], 4 | 5 | "bitwise": true, 6 | "curly": false, 7 | "eqeqeq": false, 8 | "forin": false, 9 | "immed": true, 10 | "latedef": false, 11 | "newcap": true, 12 | "noarg": true, 13 | "noempty": true, 14 | "nonew": false, 15 | "plusplus": false, 16 | "regexp": false, 17 | "undef": true, 18 | "trailing": false, 19 | 20 | "asi": true, 21 | "eqnull": true, 22 | "strict": false, 23 | "funcscope": true, 24 | "lastsemic": false, 25 | "multistr": true, 26 | "supernew": true, 27 | "regexdash": false, 28 | "laxbreak": true, 29 | "expr": true, 30 | "loopfunc": true, 31 | "proto": true, 32 | 33 | "devel": true, 34 | "node": true, 35 | 36 | "passfail": false 37 | 38 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012, Ernesto Méndez (der@der-design.com) 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. -------------------------------------------------------------------------------- /test/fixtures/shortcodes.txt: -------------------------------------------------------------------------------- 1 | # Inline 2 | 3 | [inline] 4 | [inline/] 5 | [inline one two three/] 6 | [inline alpha beta=false gamma one=true two=false three=true /] 7 | [inline foo=bar bar="baz" /] 8 | 9 | # Block Level > Same Line 10 | 11 | [container] ... On the same line, spacing is adjusted ... [/container] 12 | 13 | # Block Level > Multiple Lines 14 | 15 | [container hello=world name=John age=30 float=55.5 slider=null boolean=true other=undefined] 16 | 17 | ... On different lines, spacing is preserved ... 18 | 19 | [/container] 20 | 21 | # Inline within content 22 | 23 | Lorem ipsum dolor sit amet, [bold size=4em font="Helvetica Neue"]consectetur[/bold] adipisicing elit, sed do eiusmod tempor 24 | incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut 25 | aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla 26 | pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. 27 | 28 | # Parameters & Typecasting 29 | 30 | [params_test hello=world foo=a-B-c-D name=John1234 some='value' another="Lorem Ipsum Dolor Sit Amet" age=30 float=55.5 slider=null boolean=true other=undefined /] 31 | 32 | # Unrecognized Shortcodes 33 | 34 | [unrecognized] 35 | [unrecognized1234/] 36 | [unrecognized asdfasdfasdf /] 37 | [abcdefg] 38 | [12345] 39 | [some_shortcode] [/some_shortcode] 40 | 41 | # 42 | 43 | Lorem ipsum dolor sit amet, [bold size=10px font="Inconsolata"]Hello World[/bold] adipisicing elit, sed do eiusmod tempor 44 | incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut 45 | aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla 46 | pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. -------------------------------------------------------------------------------- /test/fixtures/output.txt: -------------------------------------------------------------------------------- 1 | # Inline 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | # Block Level > Same Line 10 | 11 | 12 | ... On the same line, spacing is adjusted ... 13 | 14 | 15 | # Block Level > Multiple Lines 16 | 17 | 18 | 19 | ... On different lines, spacing is preserved ... 20 | 21 | 22 | 23 | # Inline within content 24 | 25 | Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor 26 | incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut 27 | aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla 28 | pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. 29 | 30 | # Parameters & Typecasting 31 | 32 | 33 | 34 | # Unrecognized Shortcodes 35 | 36 | [unrecognized] 37 | [unrecognized1234/] 38 | [unrecognized asdfasdfasdf /] 39 | [abcdefg] 40 | [12345] 41 | [some_shortcode] [/some_shortcode] 42 | 43 | # 44 | 45 | Lorem ipsum dolor sit amet, Hello World adipisicing elit, sed do eiusmod tempor 46 | incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut 47 | aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla 48 | pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # shortcode-parser [![Build Status](https://secure.travis-ci.org/derdesign/shortcode-parser.png)](http://travis-ci.org/derdesign/shortcode-parser) 3 | 4 | Shortcode parser written in JavaScript. 5 | 6 | 7 | ## Features 8 | 9 | - Supports self closing and enclosing shortcodes 10 | - Supports shortcode attributes 11 | - Automatic typecasting to JavaScript native objects 12 | - Clean & Linted JavaScript source 13 | - Unit tests included 14 | 15 | 16 | ## Usage 17 | 18 | Install: 19 | 20 | ```shell 21 | npm install shortcode-parser 22 | ``` 23 | 24 | Install from git Repository: 25 | 26 | ```shell 27 | git clone https://github.com/derdesign/shortcode-parser.git 28 | ``` 29 | 30 | Running tests: 31 | 32 | ```shell 33 | make test 34 | ``` 35 | 36 | Require the library: 37 | 38 | ```javascript 39 | var shortcode = require('shortcode-parser'); 40 | ``` 41 | 42 | Adding shortcodes: 43 | 44 | ```javascript 45 | shortcode.add('bold', function(buf, opts) { 46 | if (opts.upper) buf = buf.toUpperCase(); 47 | return '' + buf + ''; 48 | }); 49 | ``` 50 | 51 | Parsing shortcodes: 52 | 53 | ```javascript 54 | var str = "This is [bold upper=true]Bold Text[/bold]!!!"; 55 | var out = shortcode.parse(out); 56 | ``` 57 | 58 | Parsing shortcodes with context: 59 | 60 | ```javascript 61 | var str = "[markdown gfm tables breaks sanitize]## Hello World[/markdown]"; 62 | var out = shortcode.parse(str, { 63 | charcodes: function(buf, opts) { 64 | return marked(buf, opts); 65 | } 66 | }); 67 | ``` 68 | 69 | 70 | ## API 71 | 72 | ### shortcode.add(name, callback) 73 | 74 | Adds a handler to the shortcode `name`. The handler receives `(str, params, data)`. When using an enclosing 75 | shortcode, `str` will contain the wrapped content (empty string if it's a self closing shortcode). 76 | 77 | The `params` object contains the parameters for to the shortcode. The `data` object is passed by `shortcode.parse()` if provided. 78 | 79 | 80 | ### shortcode.remove(name) 81 | 82 | Removes a shortcode handler. 83 | 84 | ### shortcode.parse(str, data, context) 85 | 86 | Performs shortcode replacements on `str`. If `context` is specified, its methods will be used as shortcode handlers instead 87 | of the registered ones. The `data` parameter is optional and will be passed to all shortcode handlers. 88 | 89 | ### shortcode.parseInContext(str, context, data) 90 | 91 | Same as `shortcode.parse` but with the second and third parameters swapped. Added for convenience and readability. 92 | 93 | 94 | ## License 95 | 96 | `shortcode-parser` is [MIT Licensed](https://github.com/derdesign/shortcode-parser/blob/master/LICENSE) -------------------------------------------------------------------------------- /lib/shortcode-parser.js: -------------------------------------------------------------------------------- 1 | 2 | /* lib/shortcode-parser.js */ 3 | 4 | var fs = require('fs'); 5 | var util = require('util'); 6 | 7 | var shortcodes = {}; 8 | 9 | var SHORTCODE_ATTRS = /(\s+([a-z0-9\-_]+|([a-z0-9\-_]+)\s*=\s*([a-z0-9\-_]+|\d+\.\d+|'[^']*'|"[^"]*")))*/.toString().slice(1,-1); 10 | var SHORTCODE_SLASH = /\s*\/?\s*/.toString().slice(1,-1); 11 | var SHORTCODE_OPEN = /\[\s*%s/.toString().slice(1,-1); 12 | var SHORTCODE_RIGHT_BRACKET = '\\]'; 13 | var SHORTCODE_CLOSE = /\[\s*\/\s*%s\s*\]/.toString().slice(1,-1); 14 | var SHORTCODE_CONTENT = /(.|\n|)*?/.toString().slice(1,-1); 15 | var SHORTCODE_SPACE = /\s*/.toString().slice(1,-1); 16 | 17 | function typecast(val) { 18 | val = val.trim().replace(/(^['"]|['"]$)/g, ''); 19 | if (/^\d+$/.test(val)) { 20 | return parseInt(val, 10); 21 | } else if (/^\d+\.\d+$/.test(val)) { 22 | return parseFloat(val); 23 | } else if (/^(true|false)$/.test(val)) { 24 | return (val === 'true'); 25 | } else if (/^undefined$/.test(val)) { 26 | return undefined; 27 | } else if (/^null$/i.test(val)) { 28 | return null; 29 | } else { 30 | return val; 31 | } 32 | } 33 | 34 | function closeTagString(name) { 35 | return /^[^a-z0-9]/.test(name) ? util.format('[%s]?%s', name[0].replace('$', '\\$'), name.slice(1)) : name; 36 | } 37 | 38 | function parseShortcode(name, buf, inline) { 39 | 40 | var regex, match, data = {}, attr = {}; 41 | 42 | if (inline) { 43 | regex = new RegExp('^' + util.format(SHORTCODE_OPEN, name) 44 | + SHORTCODE_ATTRS 45 | + SHORTCODE_SPACE 46 | + SHORTCODE_SLASH 47 | + SHORTCODE_RIGHT_BRACKET, 'i'); 48 | } else { 49 | regex = new RegExp('^' + util.format(SHORTCODE_OPEN, name) 50 | + SHORTCODE_ATTRS 51 | + SHORTCODE_SPACE 52 | + SHORTCODE_RIGHT_BRACKET, 'i'); 53 | } 54 | 55 | while ((match = buf.match(regex)) !== null) { 56 | var key = match[3] || match[2]; 57 | var val = match[4] || match[3]; 58 | var pattern = match[1]; 59 | if (pattern) { 60 | var idx = buf.lastIndexOf(pattern); 61 | attr[key] = (val !== undefined) ? typecast(val) : true; 62 | buf = buf.slice(0, idx) + buf.slice(idx + pattern.length); 63 | } else { 64 | break; 65 | } 66 | } 67 | 68 | attr = Object.keys(attr).reverse().reduce(function(prev, current) { 69 | prev[current] = attr[current]; return prev; 70 | }, {}); 71 | 72 | buf = buf.replace(regex, '').replace(new RegExp(util.format(SHORTCODE_CLOSE, closeTagString(name))), ''); 73 | 74 | return { 75 | attr: attr, 76 | content: inline ? buf : buf.replace(/(^\n|\n$)/g, '') 77 | } 78 | 79 | } 80 | 81 | module.exports = { 82 | 83 | _shortcodes: shortcodes, 84 | 85 | add: function (name, callback) { 86 | if (typeof name == 'object') { 87 | var ob = name; 88 | for (var m in ob) { // Adding methods from instance and prototype 89 | if (ob[m] instanceof Function) { 90 | shortcodes[m] = ob[m]; 91 | } 92 | } 93 | } else { 94 | shortcodes[name] = callback; 95 | } 96 | }, 97 | 98 | remove: function(name) { 99 | delete shortcodes[name]; 100 | }, 101 | 102 | parse: function(buf, extra, context) { 103 | 104 | context = context || shortcodes; 105 | 106 | extra = extra || {}; 107 | 108 | for (var name in context) { 109 | 110 | // Allow absence of first char if not alpha numeric. E.g. [#shortcode]...[/shortcode] 111 | 112 | var regex = { 113 | wrapper: new RegExp(util.format(SHORTCODE_OPEN, name) 114 | + SHORTCODE_ATTRS 115 | + SHORTCODE_RIGHT_BRACKET 116 | + SHORTCODE_CONTENT 117 | + util.format(SHORTCODE_CLOSE, closeTagString(name)), 'gi'), 118 | inline: new RegExp(util.format(SHORTCODE_OPEN, name) 119 | + SHORTCODE_ATTRS 120 | + SHORTCODE_SLASH 121 | + SHORTCODE_RIGHT_BRACKET, 'gi') 122 | } 123 | 124 | var matches = buf.match(regex.wrapper); 125 | 126 | if (matches) { 127 | for (var m,data,i=0,len=matches.length; i < len; i++) { 128 | m = matches[i]; 129 | data = parseShortcode(name, m); 130 | buf = buf.replace(m, context[name].call(null, data.content, data.attr, extra)); 131 | } 132 | } 133 | 134 | matches = buf.match(regex.inline); 135 | 136 | if (matches) { 137 | 138 | while((m = matches.shift()) !== undefined) { 139 | data = parseShortcode(name, m, true); 140 | buf = buf.replace(m, context[name].call(null, data.content, data.attr, extra)); 141 | } 142 | 143 | } 144 | 145 | } 146 | 147 | return buf; 148 | 149 | }, 150 | 151 | parseInContext: function(buf, context, data) { 152 | return this.parse(buf, data, context); 153 | } 154 | 155 | } -------------------------------------------------------------------------------- /test/unit.js: -------------------------------------------------------------------------------- 1 | 2 | var fs = require('fs'); 3 | var vows = require('vows'); 4 | var util = require('util'); 5 | var assert = require('assert'); 6 | var shortcode = require('../lib/shortcode-parser.js'); 7 | 8 | var text = fs.readFileSync('test/fixtures/shortcodes.txt', 'utf8'); 9 | 10 | process.on('uncaughtException', function(err) { 11 | console.log(err.stack); 12 | process.exit(); 13 | }); 14 | 15 | function TestObject() { 16 | 17 | } 18 | 19 | TestObject.prototype.bold = function(buf, opts) { 20 | return util.format('%s', opts.size, opts.font, buf); 21 | }, 22 | 23 | vows.describe("Shortcode Parser").addBatch({ 24 | 25 | 'Shortcode Handlers': { 26 | 27 | topic: function() { 28 | 29 | var result = {}, inline, container, params_test; 30 | 31 | // Using add with single shortcode handler 32 | 33 | shortcode.add('inline', inline = function(buf, opts) { 34 | return util.format('', buf, JSON.stringify(opts)); 35 | }); 36 | 37 | shortcode.add('container', container = function(buf, opts) { 38 | return util.format('\n%s\n', JSON.stringify(opts), buf); 39 | }); 40 | 41 | shortcode.add('params_test', params_test = function(buf, opts) { 42 | 43 | var types = { 44 | hello: (opts.hello === "world") ? util.format('string(%s)', opts.hello) : null, 45 | foo: (opts.foo === 'a-B-c-D') ? util.format('string(%s)', opts.foo) : null, 46 | name: (opts.name === 'John1234') ? util.format('string(%s)', opts.name) : null, 47 | some: (opts.some === 'value') ? util.format('string(%s)', opts.some) : null, 48 | another: (opts.another === 'Lorem Ipsum Dolor Sit Amet') ? util.format('string(%s)', opts.another) : null, 49 | age: (opts.age === 30) ? util.format('number(%d)', opts.age) : null, 50 | float: (opts.float === 55.5) ? util.format('float(%s)', opts.float) : null, 51 | slider: (opts.slider === null) ? util.format('null(%s)', opts.slider) : null, 52 | boolean: (opts.boolean === true) ? util.format('boolean(%s)', opts.boolean) : null, 53 | other: (opts.other === undefined) ? util.format('undefined(%s)', opts.other) : null 54 | } 55 | 56 | for (var key in types) { 57 | if (types[key] === null) throw new Error(util.format("Bad value on param_test: %s=%s", key, opts[key])); 58 | } 59 | 60 | return util.format('', buf, JSON.stringify(opts), JSON.stringify(types)); 61 | 62 | }); 63 | 64 | // Using add with multiple shortcode handlers 65 | 66 | var ob = new TestObject(); // Provides the [bold] handler 67 | 68 | ob.infinite_loop_test = function(buf, opts) { 69 | return ''; 70 | } 71 | 72 | shortcode.add(ob); 73 | 74 | return { 75 | inline: inline, 76 | container: container, 77 | params_test: params_test, 78 | bold: ob.bold, 79 | infinite_loop_test: ob.infinite_loop_test 80 | } 81 | 82 | }, 83 | 84 | "Adds shortcode handlers": function(val) { 85 | assert.deepEqual(val, shortcode._shortcodes); 86 | }, 87 | 88 | "Removes shortcode handlers": function(val) { 89 | var noop = function() {}; 90 | shortcode.add('test', noop); 91 | assert.isFunction(shortcode._shortcodes.test, noop); 92 | shortcode.remove('test', noop); 93 | assert.deepEqual(val, shortcode._shortcodes); 94 | } 95 | 96 | }, 97 | 98 | 'Rendering Shortcodes': { 99 | 100 | topic: function() { 101 | return shortcode.parse(text); 102 | }, 103 | 104 | 'Renders shortcodes property': function(buf) { 105 | 106 | // TODO: Add individual test cases to detect where parser breaks 107 | 108 | var output = fs.readFileSync('test/fixtures/output.txt', 'utf8'); 109 | assert.strictEqual(buf, output); 110 | 111 | }, 112 | 113 | 'Renders using custom context': function() { 114 | var out = shortcode.parseInContext('This is [bold size=2em font="Helvetica"]Some Text[/bold] and [u upper]Some more Text[/u] and [u]Something[/u]...', { 115 | u: function(buf, opts) { 116 | if (opts.upper) buf = buf.toUpperCase(); 117 | return util.format('%s', buf); 118 | } 119 | }); 120 | assert.strictEqual(out, 'This is [bold size=2em font="Helvetica"]Some Text[/bold] and SOME MORE TEXT and Something...'); 121 | }, 122 | 123 | 'Provides data object to handlers': function() { 124 | 125 | var out, str = '... [#data_test][/data_test] ...'; // NOTE: Testing ability to skip first char of shortcode tag 126 | 127 | var context = { 128 | '#data_test': function(buf, params, data) { 129 | return '' 130 | } 131 | } 132 | 133 | out = shortcode.parseInContext(str, context, {user: 'john', blah: true}); 134 | 135 | assert.equal(out, '... ...'); 136 | 137 | out = shortcode.parseInContext(str, context); 138 | 139 | assert.equal(out, '... ...'); // Ensure data is provided as object 140 | 141 | }, 142 | 143 | 'Avoids infinite loops': function() { 144 | var out = shortcode.parse('[infinite_loop_test]'); 145 | assert.strictEqual(out, ''); 146 | } 147 | 148 | } 149 | 150 | }).export(module); --------------------------------------------------------------------------------