UNPKG

@elderjs/shortcodes

Version:

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

297 lines (235 loc) 7 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 = async function (input) { var self = this; var tokenizer = Tokenizer(input); await 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(); ctx.name = t.clearBuffer(); 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 (t.getBuffer().trim() !== "") { if (ctx.argKey) { ctx.args[ctx.argKey] = t.clearBuffer() || undefined; } else { const v = t.clearBuffer(); ctx.args[v] = v; } } // if (t.getBuffer().trim() !== "") { // if (!ctx.argKey && !Array.isArray(ctx.args) && Object.keys(ctx.args).length === 0) { // ctx.args = []; // } // if (ctx.argKey) { // const v = {}; // v[ctx.argKey] = t.clearBuffer(); // ctx.args.push(v); // } else { // ctx.args.push(t.clearBuffer()); // } // } else { // if (ctx.argKey) ctx.args[ctx.argKey] = 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("^(\"|'|&quot;)", 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 = async 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]) { ctx.restoreOnError = true; return ShortcodeParser.parsers.error; } const shortcodeOutput = await this.shortcodes[ctx.name].call(ctx, ctx.args, ctx.content); t.toBuffer(shortcodeOutput); t.closeContext(); if (ctx.parentParser) { return ctx.parentParser; } else { t.flushBuffer(); return ShortcodeParser.parsers.tagOpen; } }; //Export module.exports = function (opts) { return new ShortcodeParser(opts); };