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.
455 lines (444 loc) • 22.2 kB
HTML
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/6.65.7/codemirror.min.css" integrity="sha512-uf06llspW44/LZpHzHT6qBOIVODjWtv4MxCricRxkzvopAlSWnTf6hpZTFxuuZcuNE9CBQhqE0Seu1CoRk84nQ==" crossorigin="anonymous" referrerpolicy="no-referrer" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/6.65.7/codemirror.min.js" integrity="sha512-8RnEqURPUc5aqFEN04aQEiPlSAdE0jlFS/9iGgUyNtwFnSKCXhmB6ZTNl7LnDtDWKabJIASzXrzD0K+LYexU9g==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/pegjs/0.9.0/peg.min.js"></script>
<script src="./parser.js"></script>
<script type="text/javascript" async="" src="https://cdn.jsdelivr.net/npm/@anywhichway/quick-component@0.0.12" component="https://cdn.jsdelivr.net/npm/@anywhichway/math-science-formula@0.0.5"></script>
</head>
<body>
<style>
.CodeMirror {
flex: auto;
height: auto;
width: auto;
border: solid black 1px;
margin-left: 18px;
max-width: 33%;
float:left;
}
</style>
<textarea id="code">
</textarea>
<div id="output" style="float:right">
</div>
<script>
let markup;
document.addEventListener("DOMContentLoaded",async ()=> {
const styleAllowed = "*",
code = document.getElementById("code");
code.innerHTML = markup = await fetch("./markup.txt").then((response) => response.text());
const editor = CodeMirror.fromTextArea(code,{scrollbarStyle:"native",lineWrapping:true,lineNumbers:true});
editor.on("change",async () => {
await transform(editor.getValue(),{styleAllowed})
});
document.getElementById("output").addEventListener("change",(event) => {
const template = event.target.getAttribute("data-template");
if(event.target.tagName==="INPUT" && template!==null && (event.target.value+"")!=="[object Promise]") {
if(event.target.type==="checkbox") {
if(event.target.value!==event.target.checked+"") {
event.target.value = event.target.checked+"";
}
}
event.target.setAttribute("data-template",extractValue(event.target));
[...event.target.dependents||[]].forEach(async (el) => {
el.rawValue = await resolveDataTemplate(el.getAttribute("data-template"));
el.value = formatValue(el);
if(el.hasAttribute("data-autosize")) {
el.style.width = Math.min(80,Math.max(1,el.value.length))+"ch";
}
})
}
});
document.getElementById("output").addEventListener("click",(event) => {
if(event.target.tagName==="INPUT" && event.target.type==="checkbox") {
if(event.target.value!==event.target.checked+"") {
event.target.value = event.target.checked+"";
}
}
});
document.getElementById("output").addEventListener("input",(event) => {
const template = event.target.getAttribute("data-template");
if(event.target.tagName==="INPUT" && template!==null) {
event.target.setAttribute("data-template",extractValue(event.target));
event.target.style.width = Math.min(80,Math.max(1,event.target.value.length))+"ch";
[...event.target.dependents||[]].forEach(async (el) => {
el.rawValue = await resolveDataTemplate(el.getAttribute("data-template"));
el.value = formatValue(el);
if(el.hasAttribute("data-autosize")) {
el.style.width = Math.min(80,Math.max(1,el.value.length))+"ch";
}
})
}
});
await transform(code.value,{styleAllowed});
setInterval(async () => {
const newmarkup = await fetch("./markup.txt").then((response) => response.text());
if(markup!==newmarkup) {
code.innerHTML = markup = markup;
}
},1000)
})
</script>
<script type="module">
import replaceAsync from "https://cdn.jsdelivr.net/npm/string-replace-async@3.0.2";
import {tags, universalAttributes} from "./tags.js";
window.tagupObserver = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
const target = mutation.target;
if (mutation.type === "attributes") {
const event = new Event("attributeChanged");
event.attributeName = mutation.attributeName;
event.attributeNamespace = mutation.attributeNamespace;
event.oldValue = mutation.oldValue;
event.value = target.getAttribute(event.attributeName);
target.dispatchEvent(event);
} else if(mutation.type==="childList") {
[...mutation.removedNodes].forEach((el) => {
const event = new Event("disconnected");
el.dispatchEvent(event);
})
}
});
});
window.extractValue = (el) => {
const extract = el.getAttribute("data-extract");
if(extract) {
const match = [...el.value.matchAll(new RegExp(extract,"g"))][0];
if(match) {
return match[1];
}
}
return el.value;
}
window.formatValue = (el) => {
const template = el.getAttribute("data-format");
if(template) {
return (new Function("value","return `" + template + "`"))(el.rawValue)
}
return el.rawValue;
};
const patchTopLevel = (tree) => {
let previous;
return tree.reduce((result,node) => {
if(typeof(node)==="string") {
const paragraphs = node.split("\n\n");
paragraphs.forEach((paragraph,i) => {
if(i===0 && previous) {
previous.content.push(paragraph);
} else {
previous = {tag:"p",content:[paragraph]};
result.push(previous)
}
})
} else if(previous && !tags[node.tag]?.allowAsRoot) {
previous.content.push(node);
} else {
previous = null;
result.push(node);
}
return result;
},[]);
}
const validateNode = async ({node,path=[],errors=[]}) => {
if(!node || typeof(node)!=="object") {
return;
}
const tag = node.tag;
let config = tags[node.tag];
if(!config) {
node.drop = true;
errors.push(new parser.SyntaxError(`Dropping unknown tag ${tag}`,null,null,node.location));
return errors;
}
if(path.length===0 && !config.allowAsRoot) {
node.drop = true;
errors.push(new parser.SyntaxError(`${tag} is not permitted as a root level element`,null,null,node.location));
return errors;
}
if(config.parentRequired && (path.length==0 || !config.parentRequired.includes(path[path.length-1].tag))) {
node.drop = true;
errors.push(new parser.SyntaxError(`${tag} required parent to be one of`,JSON.stringify(config.parentRequired),path[path.length-1].tag,node.location));
return errors;
}
let ancestorIndex;
if(path.length>0 && !config.indirectChildAllowed && Array.isArray(config.contentAllowed) && !config.contentAllowed.includes(tag) && (ancestorIndex = path.findIndex((ancestor) => ancestor.tag===tag)!==-1)) {
node.drop = true;
const ancestor = path[ancestorIndex+1];
ancestor.content.splice(ancestor.content.findIndex((node) => node.tag===tag),1,...node.content);
errors.push(new parser.SyntaxError(`${tag} is not permitted as a nested element of self. Elevating content.`,null,null,node.location));
}
node.options ||= {};
node.options.attributes ||= {};
if(config.transform) {
await config.transform(node);
}
if(node.content.length>0 && !config.contentAllowed) {
while(node.content.length) { // try remove whitespace
const item = node.content[0];
if(typeof(item)!=="string" || item.trim().length!==0) {
break;
}
node.content.shift();
}
if(node.content.length>0) {
errors.push(new parser.SyntaxError(`${tag} is not permitted to have any content. Dropping content.`,null,JSON.stringify(node.content),node.location));
node.content = [];
}
}
node.content = await node.content.reduce(async (content,child) => {
content = await content;
const type = typeof(child);
if(type==="string") {
if(config.contentAllowed) {
content.push(child);
return content;
}
errors.push(new parser.SyntaxError(`${tag} does not allow string child. Dropping child.`,null,null,node.location))
}
if(child && type==="object") {
if(!config.contentAllowed.includes(child.tag)) {
errors.push(new parser.SyntaxError(`${tag} does not allow child ${child.tag}. Dropping child.`,null,null,node.location))
} else {
await validateNode({node:child,path:[...path,node],errors})
if(!child.drop) {
content.push(child);
// elevate trailing space to parent
if(child.content[child.content.length-1]===" ") {
content.push(child.content.pop());
}
}
}
return content;
}
errors.push(new parser.SyntaxError(`${tag} has unexpected child type ${type} ${child}. Dropping child.`,null,null,node.location));
return content;
},[]);
config.attributesAllowed ||= {};
Object.entries(node.options?.attributes||{}).forEach(([key,value]) => {
const attributeAllowed = universalAttributes[key] || config.attributesAllowed[key],
type = typeof(attributeAllowed);
if(type==="function") {
const result = attributeAllowed(value,node);
delete node.options.attributes[key];
if(result) {
Object.assign(node.options.attributes,result);
}
} else if(attributeAllowed && type==="object") {
if(attributeAllowed.transform) {
const result = attributeAllowed.transform(value,node);
delete node.options.attributes[key];
if(result) {
Object.assign(node.options.attributes,result);
}
}
if(attributeAllowed.default && node.options?.attributes[key] == null) {
node.options.attributes[key] = attributeAllowed.default;
}
if (attributeAllowed.required && node.options?.attributes[key] == null) {
errors.push(new parser.SyntaxError(`${tag} is required to have attribute '${key}'`,null,null,node.location));
return;
}
if(attributeAllowed.validate) {
let valid;
try {
valid = attributeAllowed.validate(value,node);
} catch(e) {
valid = e+"";
}
if(valid!==true) {
delete node.options.attributes[key];
errors.push(new parser.SyntaxError(`${tag} the value of attribute '${key}' is invalid`,valid,value,node.location));
}
}
} else if(attributeAllowed!==true) {
delete node.options.attributes[key];
errors.push(new parser.SyntaxError(`${tag} does not allow attribute ${key}`,null,JSON.stringify(value),node.location))
}
})
if(node.options?.attributes.style) {
const styleAllowed = config.styleAllowed;
if(typeof(styleAllowed)==="function") {
node.options.style = styleAllowed(node.options.style,node);
} else if(!styleAllowed) {
delete node.options.style;
errors.push(new parser.SyntaxError(`${tag} does not allow styling. Dropping style`,null,null,node.location))
}
}
if(tag!==node.tag) { // node was transformed to a different node type, so validate that also
await validateNode({node,path,errors});
}
return errors;
};
const toDOMNodes = (nodes,parentConfig) => {
return nodes.reduce((domNodes,node) => {
if(typeof(node)==="string") {
if(parentConfig && parentConfig.breakOnNewline) {
const lines = node.split("\n");
lines.forEach((line,i) => {
domNodes.push(new Text(line));
if(i<lines.length-1) {
domNodes.push(document.createElement("br"))
}
})
} else {
domNodes.push(new Text(node));
}
} else if(!node.drop) {
const config = tags[node.tag],
el = document.createElement(node.tag),
{id,classes,attributes} = node.options||{};
if(id) el.id = id;
(classes||[]).forEach((className) => el.classList.add(className));
Object.entries(attributes||{}).forEach(([key,value]) => { // style mapping done here so that it bypasses earlier sanitation
const attributeAllowed = config.attributesAllowed[key];
if(key==="style" && value && typeof(value)==="object") {
Object.entries(value).forEach(([key,value]) => {
key.includes("-") ? el.style.setProperty(key,value) : el.style[key] = value;
})
} else if(attributeAllowed?.styleMap) {
const styleName = attributeAllowed.styleMap;
styleName.includes("-") ? el.style.setProperty(styleName,value) : el.style[styleName] = value;
} else {
el.setAttribute(key,value);
}
});
if(node.tag==="script") {
el.setAttribute("type","module");
}
toDOMNodes(node.content,config).forEach((node) => {
el.appendChild(node);
});
domNodes.push(el);
const listeners = Object.entries(config.listeners||{});
if(listeners.length>0) {
const script = document.createElement("script");
script.innerHTML = listeners.reduce((string,[name,f]) => {
let fstring = f +"";
if(fstring.startsWith(`${name}(`)) {
fstring = fstring.replace(`${name}(`,"function(")
}
string += `document.currentScript.previousElementSibling.addEventListener("${name}",${fstring});\n`;
if(name==="attributeChanged") {
string += "tagupObserver.observe(document.currentScript.previousElementSibling,{attributes:true,attributeOldValue:true}});\n";
} else if(name==="disconnected") {
string += "tagupObserver.observe(document.currentScript.previousElementSibling.parentElement,{childList:true}});\n";
}
return string;
},"");
domNodes.push(script);
}
}
return domNodes;
},[])
},
configureStyles = (tags,styleAllowed) => {
if(styleAllowed==="*") {
Object.values(tags).forEach((config) => {
config.styleAllowed ||= true;
})
} else if(typeof(styleAllowed)==="function") {
Object.values(tags).forEach((config) => {
config.styleAllowed ||= styleAllowed;
})
} else {
Object.entries(styleAllowed||[]).forEach(([key,value]) => {
let config;
if(typeof(value)==="function") {
config = tags[key];
config.styleAllowed ||= value;
} else {
config = tags[value];
config.styleAllowed ||= true;
}
if(!config) {
console.error(new parser.SyntaxError(`${key} is not a defined tag and can't be styled`,null,null,node.location))
}
})
}
};
window.transform = async (text,{styleAllowed}={}) => {
if(styleAllowed) {
configureStyles(tags,styleAllowed);
}
const tree = patchTopLevel(parser.parse(text,{
tags,
styleAllowed: {
img(style) { return style }
}
//img(https://www.google.com)[]
}));
const errors = await tree.reduce(async (errors,node) => {
return [...await errors,...await validateNode({node})]
},[]);
console.log(tree,errors)
const output = document.getElementById("output");
output.innerHTML = "";
toDOMNodes(tree).forEach((node) => {
output.appendChild(node);
})
const resolveValueElements = async (node=document.body) => {
const valueEls = [...node.querySelectorAll("input[data-template]")];
for(const el of valueEls) {
const template = el.getAttribute("data-template");
el.value = resolveDataTemplate(template,el).then((value) => {
el.rawValue = value;
el.value = formatValue(el);
if(el.hasAttribute("data-autosize")) {
el.style.width = Math.min(80,Math.max(1,el.value.length))+"ch";
}
if(value===template) {
el.removeAttribute("disabled");
} else {
el.setAttribute("disabled","");
}
});
await el.value; // awaiting after the assignment prevent getting stuck in an Inifnite wait due to recursive resolves
}
};
const AsyncFunction = (async function () {}).constructor;
window.resolveDataTemplate = async (string,requestor) => {
if(!string) return;
const text = await replaceAsync(string,/\$\(([^)]*)\)/g,async (match,selector) => {
let els,
expectsArray;
if(selector.endsWith("[]")) {
expectsArray = true;
els = [...document.querySelectorAll(selector)].filter((el) => {
if(el.tagName==="INPUT" && el.hasAttribute("data-template")) {
return true;
}
});
} else {
const el = document.querySelector(selector);
if(el.tagName==="INPUT" && el.hasAttribute("data-template")) {
els = [el];
}
}
for(const el of els) {
el.dependents ||= new Set();
if(requestor) el.dependents.add(requestor);
if(el.rawValue==null) {
el.rawValue = "";
}
if(el.value==="" || !requestor) {
el.rawValue = await resolveDataTemplate(el.getAttribute("data-template"),el);
el.value = formatValue(el);
if(el.hasAttribute("data-autosize")) {
el.style.width = Math.min(80,Math.max(1,el.value.length))+"ch";
}
}
}
const result = expectsArray ? els.map(el => el.rawValue) : els[0].rawValue
return result && typeof(result)==="object" && !(result instanceof Promise) ? JSON.stringify(result) : result;
});
return (new AsyncFunction("return `${" + text + "}`"))();
};
await resolveValueElements();
}
</script>
</body>
</html>