import { Converter } from 'showdown';
import { sanitizeHTML } from "../utils.js";
import * as functions from "./functions/index.js";
import { isFunction } from 'lodash';
export { resolveComplexTemplate };
// static markdown converter and regex options
let markdownConverter_ = null;
const regex = {
templateContent: /\{(([^\{\}]+|("[^"]*"))+)\}/g,
propertyValidation: /^[^\[\]()"., \n]+(?:(?:\.[^\[\]()"., \n]+)|(?:\["[^"\n]*"\]))*$/,
propertyGroups: /([^\[\]()"., \n]+)|(?:\["([^"\n]*)"\])/g,
functionValidation: /^([^\[\]()"., \n]+)\(([^\n]*)\)$/
};
let idCounter = 0;
/**
* Helper method to output console warings
* @private
* @param {string} property
* @param {object} options
*/
function unresolved_(property, options) {
if (options.verbose) console.warn("could not resolve: " + property);
}
/**
* Helper method to resolve a property
* @private
* @param {string} property
* @param {object} data
* @returns {string} resolved value
*/
function resolveProperty_(property, data, options) {
const matches = property.matchAll(regex.propertyGroups);
const path = [];
for (const match of matches) {
if (typeof match[1] == "string") {
path.push(match[1]);
} else if (typeof match[2] == "string") {
path.push(match[2]);
}
}
let result = data
for (let i = 0; i < path.length; ++i) {
if ((result !== undefined) && (path[i] in result)) {
result = result[path[i]];
} else {
unresolved_(property, options);
return undefined;
}
}
return result;
};
/**
* Helper method to resolve functions
* @private
* @param {string} property
* @param {string} match
* @param {object} data
* @param {object} options
* @returns {string} result
*/
function resolveFunction_(property, match, data, postProcessingQueue, options) {
if (!(match[1] in functions)) {
unresolved_(property, options);
return undefined;
}
const fn = functions[match[1]];
const input = match[2];
const myArgs = [];
if (typeof input == "string") {
let isString = false;
let parenthesisCount = 0;
let currentWord = "";
const addWord = () => {
currentWord = currentWord.trim();
if ((currentWord.length >= 2) && currentWord.startsWith('"')) {
if (currentWord.endsWith('"')) {
currentWord = currentWord.slice(1, -1);
} else {
unresolved_("could not parse argument '" + currentWord + "' for function '" + match[1] + "'", options);
return false;
}
} else if ((currentWord.length >= 2) && currentWord.endsWith(')')) {
const functionMatch = currentWord.match(regex.functionValidation);
if (functionMatch) {
currentWord = resolveFunction_(currentWord, functionMatch, data, postProcessingQueue, options)
} else {
unresolved_(property, options);
return false;
}
} else {
const isProperty = regex.propertyValidation.test(currentWord);
if (isProperty) {
currentWord = resolveProperty_(currentWord, data, options);
} else {
unresolved_(property, options);
return false;
}
}
myArgs.push(currentWord);
currentWord = "";
};
for (let i = 0; i < input.length; ++i) {
switch (input[i]) {
case '"':
if (parenthesisCount == 0) isString = !isString;
currentWord += input[i];
break;
case '(':
++parenthesisCount
currentWord += input[i];
break;
case ')':
--parenthesisCount
currentWord += input[i];
break;
case ',':
if (isString || (parenthesisCount > 0)) {
currentWord += input[i];
} else {
addWord();
}
break;
default:
currentWord += input[i];
}
if (i == (input.length - 1)) {
if (addWord() === false) {
return undefined;
}
}
}
}
let result = undefined;
try {
result = fn(...myArgs);
if (result instanceof HTMLElement) {
const id = "template_element_placeholder_" + idCounter++;
const element = result;
result = `<span id="${id}"></span>`
postProcessingQueue.push(parent => {
const wrapper = parent.querySelector("#" + id);
if (!wrapper) return;
wrapper.insertAdjacentElement("afterend", element);
wrapper.remove();
const event = new Event("append");
element.dispatchEvent(event);
})
} else if (isFunction(result)) {
postProcessingQueue.push(result);
result = "";
}
} catch (msg) {
if (msg == "discard_template") {
throw msg;
} else {
console.error("Error executing function '" + match[1] + "'", msg);
}
}
return result;
};
/**
* Fills key-value pairs from object map into the given template string.
* Supports more complex syntax including objects and basic function calls
*
* @param {string} template
* @param {object} data
* @param {object} options default: { verbose: false, printUndefined: false, markdown: false, html: false, postProcess: false }
*
* @memberof vef.utils.template
* @returns {string} resolved template
*/
function resolveComplexTemplate(template, data, options) {
if (!template) throw Error('Template is not defined!');
options = Object.assign({
verbose: false,
printUndefined: false,
markdown: false,
html: false,
postProcess: false,
returnAsString: false
}, options);
const postProcessingQueue = [];
const resultList = [];
const resultPrefix = "ResultSTART"
const resultSuffix = "ResultEND";
let resultIndex = 0;
// resolve content of curly brackets - nesting is not allowed
let result = template.replace(regex.templateContent, (match, property) => {
property = property.trim();
let resolved = false;
const isProperty = regex.propertyValidation.test(property);
if (isProperty) {
property = resolveProperty_(property, data, options);
resolved = true;
} else {
const functionMatch = property.match(regex.functionValidation);
if (functionMatch) {
property = resolveFunction_(property, functionMatch, data, postProcessingQueue, options);
resolved = true;
}
}
if (!resolved) {
unresolved_(property, options);
property = undefined;
}
if (!options.printUndefined && ((property === null) || (property === undefined))) {
property = "";
}
resultList.push(property);
return resultPrefix + (resultIndex++) + resultSuffix;
});
// apply options after the template was resolved
if (options.markdown) {
if (!markdownConverter_) markdownConverter_ = new Converter({
tables: true,
noHeaderId: true,
literalMidWordUnderscores: true
});
result = markdownConverter_.makeHtml(result);
}
// resolve results after the markdown was parsed to avoid issues with special markdown characters
for (let i = 0; i < resultList.length; ++i) {
result = result.replaceAll(resultPrefix + i + resultSuffix, resultList[i]);
}
if (!options.returnAsString && (options.markdown || options.html)) {
const div = document.createElement("div");
const nodes = sanitizeHTML(result);
div.append(...nodes);
result = div;
}
if (options.postProcess && (result instanceof HTMLElement)) {
for (let i = 0; i < postProcessingQueue.length; ++i) {
try {
postProcessingQueue[i](result);
} catch (e) {
console.warn(e);
}
}
}
return result;
}