UNPKG

meta-shortcodes

Version:

Shortcode parser with support of arguments, key-value attributes and nesting.

335 lines (211 loc) 6.02 kB
/* * META Platform library * Shortcode parser * * @author META Platform <www.meta-platform.com> * @license See LICENSE file distributed with this source code */ var Tokenizer = require("./tokenizer.js"); /** * Parser constructor */ var ShortcodeParser = function(opts){ if(!opts) opts = {}; this.shortcodes = {}; this.options = { openPattern: opts.openPattern || '\\[', closePattern: opts.closePattern || '\\]' }; }; /** * Add shortcode * * @param string shortcode * @param function handler */ ShortcodeParser.prototype.add = function(shortcode, handler){ this.shortcodes[shortcode] = handler; }; /** * Parse string and process shortcodes * * @param string input */ ShortcodeParser.prototype.parse = function(input){ var self = this; var tokenizer = Tokenizer(input); tokenizer.tokenize(ShortcodeParser.parsers.tagOpen, this); return tokenizer.getOutput(); }; /** * Tokenizers */ ShortcodeParser.parsers = {}; ShortcodeParser.parsers.error = function(t, ctx){ if(ctx && ctx.name){ if(ctx.name) t.toBuffer(this.options.openPattern.replace(/\\/g, "") + "!" + ctx.name); if(ctx.errorUnclosed) t.toBuffer("!" + this.options.closePattern.replace(/\\/g, "") + ctx.content); else if(ctx.content !== "") t.toBuffer("!" + this.options.closePattern.replace(/\\/g, "") + ctx.content + this.options.openPattern.replace(/\\/g, "") + "/!" + ctx.name + "!" + this.options.closePattern.replace(/\\/g, "")); else t.toBuffer("!/" + this.options.closePattern.replace(/\\/g, "")); } else { t.toBuffer(this.options.openPattern.replace(/\\/g, "") + "^!" + this.options.closePattern.replace(/\\/g, "")); } t.closeContext(); if(ctx.parentParser){ return ctx.parentParser; } else if(ctx.restoreOnError){ t.flushBuffer(); return ShortcodeParser.parsers.tagOpen; } else { t.flushBuffer(); t.flushRest(); } return false; }; ShortcodeParser.parsers.tagOpen = function(t){ if(!t.match(this.options.openPattern + "(?!\\/)", true)){ t.flushBuffer(); t.flushRest(); return false; } t.flushBuffer(); t.openContext({ name: null, args: [], content: '' }); return ShortcodeParser.parsers.tagName; }; ShortcodeParser.parsers.tagName = function(t, ctx){ if(!t.match("[a-zA-Z0-9\\-_]+", false, true)) return ShortcodeParser.parsers.error; ctx.name = t.clearBuffer().toLowerCase(); return ShortcodeParser.parsers.tagBody; }; ShortcodeParser.parsers.tagBody = function(t, ctx){ t.clearBuffer(); if(t.isEnd()) return ShortcodeParser.parsers.error; //Attributes? if(t.match(".*?(?=(\\/)?" + this.options.closePattern + "|$)", false, false, false)){ t.skip(1); t.clearBuffer(); return ShortcodeParser.parsers.params; //Self closing? } else if(t.match("^\\/" + this.options.closePattern, true)){ return ShortcodeParser.parsers.process; //Pair closing } else if(t.match("^" + this.options.closePattern, true)){ return ShortcodeParser.parsers.content; } return ShortcodeParser.parsers.error; }; ShortcodeParser.parsers.params = function(t, ctx){ var addParam = function(){ if(ctx.argKey) ctx.args[ctx.argKey] = t.clearBuffer(); else if(t.getBuffer().trim() !== "") ctx.args.push(t.clearBuffer()); delete ctx.argKey; }; if(!t.match("^\\/" + this.options.closePattern, false, false, false) && !t.match("^" + this.options.closePattern, false, false, false) && !t.isEnd()){ //Escape if(t.match("^\\\\")){ t.next(true, true); //Quotes? } else if(t.match("^(\"|')", false, true)){ ctx.quoteType = t.clearBuffer(); return ShortcodeParser.parsers.quote; //Space? } else if(t.match("^ ")){ addParam(); //key? } else if(t.match("^=")){ if(ctx.argKey) t.toBuffer("="); else ctx.argKey = t.clearBuffer(); } else { t.next(true, true); } return ShortcodeParser.parsers.params; } addParam(); return ShortcodeParser.parsers.tagBody; }; ShortcodeParser.parsers.quote = function(t, ctx){ while(!t.match("^" + ctx.quoteType) && !t.isEnd()){ t.match("^\\\\"); t.next(true, true); } delete ctx.quoteType; return ShortcodeParser.parsers.params; }; ShortcodeParser.parsers.content = function(t, ctx){ //Subtag? if(t.match(this.options.openPattern, true)){ //Close if(t.match("^\\/", false, false, false)){ if(t.match("^\\/" + ctx.name + this.options.closePattern, true)){ ctx.content+= t.getBuffer(); t.skip(-this.options.closePattern.replace(/\\/g, "").length); return ShortcodeParser.parsers.close; } else { t.skip(-this.options.openPattern.replace(/\\/g, "").length); t.eat(this.options.openPattern.replace(/\\/g, "").length + 1); return ShortcodeParser.parsers.content; } //Open } else { ctx.content+= t.clearBuffer(); t.openContext({ name: null, args: [], content: '', parentParser: ShortcodeParser.parsers.content }); return ShortcodeParser.parsers.tagName; } return ShortcodeParser.parsers.content; } ctx.content+= t.clearBuffer(); ctx.errorUnclosed = true; return ShortcodeParser.parsers.error; }; ShortcodeParser.parsers.close = function(t, ctx){ t.clearBuffer(); if(!t.match(this.options.closePattern, true)){ return ShortcodeParser.parsers.error; } //if(t.getBuffer() != ctx.name) // return ShortcodeParser.parsers.error; return ShortcodeParser.parsers.process; }; ShortcodeParser.parsers.process = function(t, ctx){ //if not shortchode if(!ctx || !ctx.name || ctx.name === "") return false; t.clearBuffer(); //if shortcode not registered if(this.shortcodes[ctx.name]){ t.toBuffer(this.shortcodes[ctx.name].call(ctx, ctx.args, ctx.content)); } else { ctx.restoreOnError = true; return ShortcodeParser.parsers.error; } t.closeContext(); if(ctx.parentParser){ return ctx.parentParser; } else { t.flushBuffer(); return ShortcodeParser.parsers.tagOpen; } }; //Export module.exports = function(opts){ return new ShortcodeParser(opts); };