secst
Version:
SECST is a semantic, extensible, computational, styleable tagged markup language. You can use it to joyfully create compelling, interactive documents backed by HTML.
143 lines (136 loc) • 5.53 kB
JavaScript
import replaceAsync from "string-replace-async";
import XRegExp from "xregexp";
import JSON5 from "json5";
const AsyncFunction = (async function () {}).constructor;
import {formatValue} from "./format-value.js";
import {stringTemplateEval} from "./string-template-eval.js";
import {updateValueWidths} from "./update-value-widths.js";
const validateSelector = (selector) => {
try {
document.createDocumentFragment().querySelector(selector)
} catch {
return;
}
return selector;
};
const valueOf = async (root,selector,requestor) => {
let els,
expectsArray;
if(selector.endsWith("[]") || selector.includes(",")) {
expectsArray = true;
els = [...root.querySelectorAll(selector)].filter((el) => {
if(["INPUT","TEXTAREA"].includes(el.tagName) && el.hasAttribute("data-template")) {
return true;
}
});
} else {
els = [root.querySelector(selector)];
if(els[0]==null) {
return els[0]
}
}
for(const el of els) {
el.dependents ||= new Set();
if(requestor) el.dependents.add(requestor);
if(el.rawValue==null) {
el.rawValue = "";
}
if(["INPUT","TEXTAREA"].includes(el.tagName)) {
if(el.value==="" || !requestor) {
const template = el.getAttribute("data-template");
el.rawValue = await resolveDataTemplate(root,template,el);
}
const formatted = await formatValue(el);
el.value = formatted;
if(el.tagName==="TEXTAREA") {
el.innerHTML = formatted;
if(el.hasAttribute("data-fitcontent")) {
updateValueWidths([el]);
}
} else {
el.setAttribute("value",formatted);
if(el.hasAttribute("data-fitcontent")) {
updateValueWidths([el]);
}
}
} else {
const template = el.getAttribute("data-template");
if(template) {
el.rawValue = await resolveDataTemplate(root,template,el);
} else {
try {
el.rawValue = JSON5.parse(el.innerText)
} catch(e) {
el.rawValue = el.innerText;
}
}
}
}
const result = expectsArray ? els.map(el => el.rawValue) : els[0].rawValue
return result && typeof(result)==="object" && !(result instanceof Promise) ? result : result===undefined ? "" : result; // JSON.stringify(result)
}
const replaceReferences = async (root,string,requestor) => {
if(string.includes("$(")) {
for(const match of XRegExp.matchRecursive(string, '\\$\\(', '\\)', 'g',{unbalanced:"skip"})) {
string = await replaceAsync(string,new RegExp(`\\$\\((${XRegExp.escape(match)})\\)`,"g"),async (match,selector) => {
let bracketAdded;
if(selector.endsWith("[")) {
selector += "]";
bracketAdded = true;
}
try {
let value = selector==="document.data" ? {...document.data||{}} : await valueOf(root,selector,requestor),
type = typeof(value);
if(type==="string") {
try {
value = JSON5.parse(value);
type = typeof(value);
} catch(e) {
}
if(type==="string") {
return "`" + value + "`";
}
}
if(value && type==="object") {
if(selector==="document.data") {
delete value.urls; // really big and should not need in worker
}
let text = value;
if(bracketAdded && text.endsWith("]")) { // a HACK because matchRecursive drops the last "]"
text = text.substring(0,text.length-1);
}
return "(" + JSON.stringify(text) + ")";
}
return value;
} catch(e) {
return match;
}
})
}
}
return string;
}
const resolveDataTemplate = async (root,string,requestor) => {
if(!string) return;
if(requestor?.hasAttribute("data-literal")) return string;
const text = await replaceReferences(root,string,requestor);
if(typeof(Worker)==="function") {
let result = await stringTemplateEval(text), //"${" + text + "}"
type = typeof(result);
if(result && type==="object" && result.stringTemplateError) {
if(requestor) throw new Error(result.stringTemplateError);
return "";
}
if(type==="string") {
try {
result = JSON5.parse(result);
type = typeof(result);
} catch(e) {
}
}
return result && type==="object" ? JSON.stringify(result,null,2) : result;
} else {
return (new AsyncFunction("return `${" + text + "}`"))();
}
}
export {resolveDataTemplate, resolveDataTemplate as default}