@elderjs/shortcodes
Version:
An Shortcode parser with support of arguments, key-value attributes, nesting, and async.
297 lines (235 loc) • 7 kB
JavaScript
/*
* 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("^(\"|'|")", 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);
};