structscript 1.4.1 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/editor.html +1079 -500
- package/lib/interpreter.js +326 -243
- package/package.json +1 -1
package/lib/interpreter.js
CHANGED
|
@@ -841,249 +841,332 @@ class Interpreter {
|
|
|
841
841
|
}
|
|
842
842
|
}
|
|
843
843
|
|
|
844
|
-
// ── Web Engine
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
// Build a full HTML document from StructScript web commands
|
|
850
|
-
function buildWebDoc(code) {
|
|
851
|
-
let pageTitle = 'StructScript Page';
|
|
852
|
-
let pageStyles = {};
|
|
853
|
-
let cssBlocks = []; // raw CSS rule strings from `css` blocks
|
|
854
|
-
const elements = [];
|
|
855
|
-
const elStack = [];
|
|
856
|
-
let i = 0;
|
|
857
|
-
|
|
858
|
-
const lines = code.split('\n');
|
|
859
|
-
|
|
860
|
-
function getIndent(l) { let n=0; while(n<l.length&&l[n]===' ')n++; return n; }
|
|
861
|
-
|
|
862
|
-
function parseStr(s) {
|
|
863
|
-
s = s.trim();
|
|
864
|
-
if((s.startsWith('"')&&s.endsWith('"'))||(s.startsWith("'")&&s.endsWith("'"))) return s.slice(1,-1);
|
|
865
|
-
return s;
|
|
866
|
-
}
|
|
867
|
-
|
|
868
|
-
// camelCase → kebab-case
|
|
869
|
-
function toKebab(s) { return s.replace(/([A-Z])/g,'-$1').toLowerCase(); }
|
|
870
|
-
// kebab-case or camelCase → camelCase (for object keys)
|
|
871
|
-
function toCamel(s) { return s.replace(/-([a-z])/g,(_,c)=>c.toUpperCase()); }
|
|
872
|
-
|
|
873
|
-
while (i < lines.length) {
|
|
874
|
-
const raw = lines[i], t = raw.trim(), ind = getIndent(raw);
|
|
875
|
-
i++;
|
|
876
|
-
if (!t || t.startsWith('//')) continue;
|
|
877
|
-
|
|
878
|
-
// ── page "title": ──────────────────────────────────────
|
|
879
|
-
if (/^page\b/.test(t)) {
|
|
880
|
-
const m = t.match(/^page\s+"([^"]*)"|^page\s+\'([^']*)\'/);
|
|
881
|
-
if (m) pageTitle = m[1]||m[2];
|
|
882
|
-
const el = { _type:'page', styles:{}, attrs:{} };
|
|
883
|
-
elStack.length = 0;
|
|
884
|
-
elStack.push({ el, indent: ind });
|
|
885
|
-
continue;
|
|
886
|
-
}
|
|
887
|
-
|
|
888
|
-
// ── css: (raw CSS block) ────────────────────────────────
|
|
889
|
-
// css:
|
|
890
|
-
// .btn { background: red }
|
|
891
|
-
// @media (max-width: 600px) { ... }
|
|
892
|
-
if (/^css\s*:?$/.test(t)) {
|
|
893
|
-
const cssLines = [];
|
|
894
|
-
while (i < lines.length) {
|
|
895
|
-
const nr = lines[i], ni = getIndent(nr);
|
|
896
|
-
if (nr.trim() === '' || ni > ind) { cssLines.push(nr.slice(ind+2||0)); i++; }
|
|
897
|
-
else break;
|
|
898
|
-
}
|
|
899
|
-
cssBlocks.push(cssLines.join('\n'));
|
|
900
|
-
continue;
|
|
901
|
-
}
|
|
902
|
-
|
|
903
|
-
// ── add TAG "id": ───────────────────────────────────────
|
|
904
|
-
const addM = t.match(/^add\s+(\w+)(?:\s+"([^"]*)")?(?:\s+\'([^']*)\')?\s*:?$/);
|
|
905
|
-
if (addM) {
|
|
906
|
-
while (elStack.length > 1 && elStack[elStack.length-1].indent >= ind) elStack.pop();
|
|
907
|
-
const parent = elStack[elStack.length-1]?.el || null;
|
|
908
|
-
const rawId = addM[2]||addM[3]||'';
|
|
909
|
-
const el = {
|
|
910
|
-
tag: addM[1].toLowerCase(),
|
|
911
|
-
id: rawId.startsWith('#') ? rawId.slice(1) : (rawId.includes(' ')||rawId.startsWith('.')?'':rawId),
|
|
912
|
-
classes: rawId.startsWith('.') ? [rawId.slice(1)] : (rawId.includes(' ') ? rawId.split(' ') : []),
|
|
913
|
-
text:'', html:'', styles:{}, hoverStyles:{}, focusStyles:{}, attrs:{}, events:[], children:[],
|
|
914
|
-
_indent: ind, _anim: null, _cssRules: [],
|
|
915
|
-
};
|
|
916
|
-
if (parent && parent._type !== 'page') parent.children.push(el);
|
|
917
|
-
else elements.push(el);
|
|
918
|
-
elStack.push({ el, indent: ind });
|
|
919
|
-
continue;
|
|
920
|
-
}
|
|
921
|
-
|
|
922
|
-
// ── Properties inside an element ───────────────────────
|
|
923
|
-
const parentEntry = elStack[elStack.length-1];
|
|
924
|
-
if (parentEntry && ind > parentEntry.indent) {
|
|
925
|
-
const el = parentEntry.el;
|
|
926
|
-
|
|
927
|
-
// text / html
|
|
928
|
-
const textM = t.match(/^text\s+(.+)$/);
|
|
929
|
-
if (textM) { el.text = parseStr(textM[1]); continue; }
|
|
930
|
-
const htmlM = t.match(/^html\s+(.+)$/);
|
|
931
|
-
if (htmlM) { el.html = parseStr(htmlM[1]); continue; }
|
|
932
|
-
|
|
933
|
-
// style prop "value" — any valid CSS property (camel or kebab)
|
|
934
|
-
const styleM = t.match(/^style\s+([\w-]+)\s*:?\s+(.+)$/);
|
|
935
|
-
if (styleM) {
|
|
936
|
-
const key = toCamel(styleM[1]);
|
|
937
|
-
const val = parseStr(styleM[2]);
|
|
938
|
-
el.styles[key] = val;
|
|
939
|
-
if (el._type === 'page') pageStyles[key] = val;
|
|
940
|
-
continue;
|
|
941
|
-
}
|
|
942
|
-
|
|
943
|
-
// hover prop "value" — generates :hover CSS rule
|
|
944
|
-
const hoverM = t.match(/^hover\s+([\w-]+)\s*:?\s+(.+)$/);
|
|
945
|
-
if (hoverM) {
|
|
946
|
-
el.hoverStyles[toCamel(hoverM[1])] = parseStr(hoverM[2]);
|
|
947
|
-
continue;
|
|
948
|
-
}
|
|
949
|
-
|
|
950
|
-
// focus prop "value" — generates :focus CSS rule
|
|
951
|
-
const focusM = t.match(/^focus\s+([\w-]+)\s*:?\s+(.+)$/);
|
|
952
|
-
if (focusM) {
|
|
953
|
-
el.focusStyles[toCamel(focusM[1])] = parseStr(focusM[2]);
|
|
954
|
-
continue;
|
|
955
|
-
}
|
|
956
|
-
|
|
957
|
-
// transition "prop duration easing"
|
|
958
|
-
const transM = t.match(/^transition\s+(.+)$/);
|
|
959
|
-
if (transM) { el.styles.transition = parseStr(transM[1]); continue; }
|
|
960
|
-
|
|
961
|
-
// attr / class
|
|
962
|
-
const attrM = t.match(/^attr\s+(\w+)\s+(.+)$/);
|
|
963
|
-
if (attrM) { el.attrs[attrM[1]] = parseStr(attrM[2]); continue; }
|
|
964
|
-
const classM = t.match(/^class\s+(.+)$/);
|
|
965
|
-
if (classM) { el.classes.push(parseStr(classM[1])); continue; }
|
|
966
|
-
|
|
967
|
-
// animate TYPE DURATION EASING
|
|
968
|
-
const animM = t.match(/^animate\s+(\w+)(?:\s+([\d.]+))?(?:\s+(\w+))?$/);
|
|
969
|
-
if (animM) {
|
|
970
|
-
el._anim = { type: animM[1], dur: parseFloat(animM[2]||0.4), easing: animM[3]||'ease' };
|
|
971
|
-
continue;
|
|
972
|
-
}
|
|
973
|
-
|
|
974
|
-
// on EVENT: (collect indented handler lines)
|
|
975
|
-
const onM = t.match(/^on\s+(\w+)\s*:?$/);
|
|
976
|
-
if (onM) {
|
|
977
|
-
const handlerLines = [];
|
|
978
|
-
while (i < lines.length) {
|
|
979
|
-
const nr = lines[i], ni = getIndent(nr);
|
|
980
|
-
if (!nr.trim() || ni <= ind) break;
|
|
981
|
-
handlerLines.push(nr.slice(ni)); i++;
|
|
982
|
-
}
|
|
983
|
-
el.events.push({ event: onM[1], code: handlerLines.join('\n') });
|
|
984
|
-
continue;
|
|
985
|
-
}
|
|
986
|
-
}
|
|
987
|
-
}
|
|
988
|
-
|
|
989
|
-
// ── Render ──────────────────────────────────────────────────
|
|
990
|
-
const pseudoRules = []; // accumulated :hover, :focus rules
|
|
991
|
-
|
|
992
|
-
function renderEl(el) {
|
|
993
|
-
if (el._type === 'page') return '';
|
|
994
|
-
const tag = el.tag;
|
|
995
|
-
const idAttr = el.id ? ` id="${el.id}"` : '';
|
|
996
|
-
const clsAttr = el.classes.length ? ` class="${el.classes.join(' ')}"` : '';
|
|
997
|
-
|
|
998
|
-
// Inline styles
|
|
999
|
-
let styleStr = Object.entries(el.styles).map(([k,v]) => `${toKebab(k)}:${v}`).join(';');
|
|
1000
|
-
|
|
1001
|
-
// Animation
|
|
1002
|
-
if (el._anim) {
|
|
1003
|
-
const animMap = {
|
|
1004
|
-
fadeIn: `fadeIn ${el._anim.dur}s ${el._anim.easing} forwards`,
|
|
1005
|
-
fadeOut: `fadeOut ${el._anim.dur}s ${el._anim.easing} forwards`,
|
|
1006
|
-
slideIn: `slideIn ${el._anim.dur}s ${el._anim.easing} forwards`,
|
|
1007
|
-
slideUp: `slideUp ${el._anim.dur}s ${el._anim.easing} forwards`,
|
|
1008
|
-
bounce: `bounce ${el._anim.dur}s ${el._anim.easing} infinite`,
|
|
1009
|
-
pulse: `pulse ${el._anim.dur}s ${el._anim.easing} infinite`,
|
|
1010
|
-
spin: `spin ${el._anim.dur}s linear infinite`,
|
|
1011
|
-
shake: `shake ${el._anim.dur}s ${el._anim.easing}`,
|
|
1012
|
-
pop: `pop ${el._anim.dur}s ${el._anim.easing} forwards`,
|
|
1013
|
-
};
|
|
1014
|
-
styleStr += (styleStr?';':'') + `animation:${animMap[el._anim.type]||`${el._anim.type} ${el._anim.dur}s ${el._anim.easing}`}`;
|
|
1015
|
-
}
|
|
1016
|
-
|
|
1017
|
-
// Generate :hover and :focus rules (needs a selector — use id if available, else generate one)
|
|
1018
|
-
if (Object.keys(el.hoverStyles).length || Object.keys(el.focusStyles).length) {
|
|
1019
|
-
// Ensure element has an id for targeting
|
|
1020
|
-
if (!el.id) { el.id = '_ss_' + Math.random().toString(36).slice(2,8); }
|
|
1021
|
-
if (Object.keys(el.hoverStyles).length) {
|
|
1022
|
-
const hoverStr = Object.entries(el.hoverStyles).map(([k,v])=>`${toKebab(k)}:${v}`).join(';');
|
|
1023
|
-
pseudoRules.push(`#${el.id}:hover{${hoverStr}}`);
|
|
1024
|
-
}
|
|
1025
|
-
if (Object.keys(el.focusStyles).length) {
|
|
1026
|
-
const focusStr = Object.entries(el.focusStyles).map(([k,v])=>`${toKebab(k)}:${v}`).join(';');
|
|
1027
|
-
pseudoRules.push(`#${el.id}:focus{${focusStr}}`);
|
|
1028
|
-
}
|
|
1029
|
-
}
|
|
1030
|
-
|
|
1031
|
-
const styleAttr = styleStr ? ` style="${styleStr}"` : '';
|
|
1032
|
-
const extraAttrs = Object.entries(el.attrs).map(([k,v])=>` ${k}="${v}"`).join('');
|
|
1033
|
-
const evAttrs = el.events.map(ev=>{
|
|
1034
|
-
const escaped = ev.code.replace(/"/g,'"').replace(/\n/g,' ');
|
|
1035
|
-
return ` data-ss-on${ev.event}="${escaped}"`;
|
|
1036
|
-
}).join('');
|
|
1037
|
-
|
|
1038
|
-
const selfClose = ['img','input','br','hr','meta','link'].includes(tag);
|
|
1039
|
-
if (selfClose) return `<${tag}${idAttr}${clsAttr}${styleAttr}${extraAttrs}/>
|
|
1040
|
-
`;
|
|
1041
|
-
const inner = el.html || el.text || el.children.map(renderEl).join('');
|
|
1042
|
-
return `<${tag}${idAttr}${clsAttr}${styleAttr}${extraAttrs}${evAttrs}>${inner}</${tag}>
|
|
1043
|
-
`;
|
|
1044
|
-
}
|
|
1045
|
-
|
|
1046
|
-
const bodyStyleStr = Object.entries(pageStyles).map(([k,v])=>`${toKebab(k)}:${v}`).join(';');
|
|
1047
|
-
const bodyHtml = elements.map(renderEl).join('');
|
|
1048
|
-
|
|
1049
|
-
const KEYFRAMES = `
|
|
1050
|
-
@keyframes fadeIn { from{opacity:0;transform:translateY(16px)} to{opacity:1;transform:none} }
|
|
1051
|
-
@keyframes fadeOut { from{opacity:1} to{opacity:0} }
|
|
1052
|
-
@keyframes slideIn { from{transform:translateX(-40px);opacity:0} to{transform:none;opacity:1} }
|
|
1053
|
-
@keyframes slideUp { from{transform:translateY(40px);opacity:0} to{transform:none;opacity:1} }
|
|
1054
|
-
@keyframes bounce { 0%,100%{transform:translateY(0)} 50%{transform:translateY(-16px)} }
|
|
1055
|
-
@keyframes pulse { 0%,100%{opacity:1} 50%{opacity:0.4} }
|
|
1056
|
-
@keyframes spin { from{transform:rotate(0deg)} to{transform:rotate(360deg)} }
|
|
1057
|
-
@keyframes shake { 0%,100%{transform:translateX(0)} 25%{transform:translateX(-8px)} 75%{transform:translateX(8px)} }
|
|
1058
|
-
@keyframes pop { 0%{transform:scale(0.5);opacity:0} 70%{transform:scale(1.1)} 100%{transform:scale(1);opacity:1} }
|
|
1059
|
-
`;
|
|
1060
|
-
|
|
1061
|
-
const EVENT_TYPES = ['click','mouseover','mouseout','mouseenter','mouseleave','keydown','keyup','change','focus','blur','dblclick'];
|
|
1062
|
-
const evScript = EVENT_TYPES.map(ev =>
|
|
1063
|
-
`document.querySelectorAll('[data-ss-on${ev}]').forEach(function(el){el.addEventListener('${ev}',function(){try{(new Function(this.getAttribute('data-ss-on${ev}')))()}catch(e){console.error(e)}}.bind(el));});`
|
|
1064
|
-
).join('\n');
|
|
1065
|
-
|
|
1066
|
-
const allPseudoCSS = pseudoRules.join('\n');
|
|
1067
|
-
const allCustomCSS = cssBlocks.join('\n');
|
|
1068
|
-
|
|
1069
|
-
return `<!DOCTYPE html>
|
|
1070
|
-
<html lang="en">
|
|
1071
|
-
<head>
|
|
1072
|
-
<meta charset="UTF-8">
|
|
1073
|
-
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
1074
|
-
<title>${pageTitle}</title>
|
|
1075
|
-
<style>
|
|
1076
|
-
*{box-sizing:border-box;margin:0;padding:0}
|
|
1077
|
-
${KEYFRAMES}
|
|
1078
|
-
${allPseudoCSS}
|
|
1079
|
-
${allCustomCSS}
|
|
1080
|
-
</style>
|
|
1081
|
-
</head>
|
|
1082
|
-
<body${bodyStyleStr ? ` style="${bodyStyleStr}"` : ''}>
|
|
1083
|
-
${bodyHtml}
|
|
1084
|
-
<script>(function(){${evScript}})()</\/script>
|
|
1085
|
-
</body>
|
|
1086
|
-
</html>`;
|
|
844
|
+
// ── Web Engine ──────────────────────────────────────────────────────────────
|
|
845
|
+
|
|
846
|
+
function isWebCode(code) {
|
|
847
|
+
return /^\s*(page\b|add\s+\w|css\s*:?$|script\s*:?$)/m.test(code);
|
|
1087
848
|
}
|
|
1088
849
|
|
|
850
|
+
function buildWebDoc(rawCode) {
|
|
851
|
+
// Normalise line endings
|
|
852
|
+
const code = rawCode.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
|
853
|
+
|
|
854
|
+
let pageTitle = 'StructScript Page';
|
|
855
|
+
let pageLang = 'en';
|
|
856
|
+
let pageStyles = {};
|
|
857
|
+
let headTags = [];
|
|
858
|
+
let cssBlocks = [];
|
|
859
|
+
let jsBlocks = [];
|
|
860
|
+
let bodyAttrs = {};
|
|
861
|
+
const elements = [];
|
|
862
|
+
const elStack = [];
|
|
863
|
+
let i = 0;
|
|
864
|
+
let _uid = 0;
|
|
865
|
+
const lines = code.split('\n');
|
|
866
|
+
|
|
867
|
+
function indent(l) { let n=0; while(n<l.length&&l[n]===' ')n++; return n; }
|
|
868
|
+
function str(s) { s=s.trim(); return (s[0]==='"'&&s[s.length-1]==='"')||(s[0]==="'"&&s[s.length-1]==="'") ? s.slice(1,-1) : s; }
|
|
869
|
+
function kebab(s) { return s.replace(/([A-Z])/g,'-$1').toLowerCase(); }
|
|
870
|
+
function camel(s) { return s.replace(/-([a-z])/g,(_,c)=>c.toUpperCase()); }
|
|
871
|
+
function uid() { return '_ss'+(++_uid); }
|
|
872
|
+
|
|
873
|
+
// ── parse one line, advancing i if it consumes more ──────────
|
|
874
|
+
while (i < lines.length) {
|
|
875
|
+
const raw = lines[i], t = raw.trim(), ind = indent(raw);
|
|
876
|
+
i++;
|
|
877
|
+
if (!t || t.startsWith('//')) continue;
|
|
878
|
+
|
|
879
|
+
// page "Title" lang? :
|
|
880
|
+
if (/^page\b/.test(t)) {
|
|
881
|
+
const m = t.match(/^page\s+"([^"]*)"|^page\s+'([^']*)'/);
|
|
882
|
+
if (m) pageTitle = m[1]||m[2];
|
|
883
|
+
const lm = t.match(/\blang\s+"([^"]+)"/);
|
|
884
|
+
if (lm) pageLang = lm[1];
|
|
885
|
+
// consume indented page-level directives
|
|
886
|
+
while (i < lines.length) {
|
|
887
|
+
const nr = lines[i].trim(), ni = indent(lines[i]);
|
|
888
|
+
if (!nr || nr.startsWith('//')) { i++; continue; }
|
|
889
|
+
if (ni <= ind) break;
|
|
890
|
+
i++;
|
|
891
|
+
let m2;
|
|
892
|
+
if ((m2=nr.match(/^style\s+([\w-]+)\s+(.+)$/))) { pageStyles[camel(m2[1])]=str(m2[2]); continue; }
|
|
893
|
+
if ((m2=nr.match(/^charset\s+(.+)$/))) { headTags.push(`<meta charset="${str(m2[1])}">`); continue; }
|
|
894
|
+
if ((m2=nr.match(/^viewport\s+(.+)$/))) { headTags.push(`<meta name="viewport" content="${str(m2[1])}">`); continue; }
|
|
895
|
+
if ((m2=nr.match(/^meta\s+([\w-]+)\s+(.+)$/))) { headTags.push(`<meta name="${m2[1]}" content="${str(m2[2])}">`); continue; }
|
|
896
|
+
if ((m2=nr.match(/^metahttp\s+([\w-]+)\s+(.+)$/))) { headTags.push(`<meta http-equiv="${m2[1]}" content="${str(m2[2])}">`); continue; }
|
|
897
|
+
if ((m2=nr.match(/^link\s+(.+)$/))) { headTags.push(`<link ${str(m2[1])}>`); continue; }
|
|
898
|
+
if ((m2=nr.match(/^favicon\s+(.+)$/))) { headTags.push(`<link rel="icon" href="${str(m2[1])}">`); continue; }
|
|
899
|
+
if ((m2=nr.match(/^font\s+(.+)$/))) { headTags.push(`<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=${str(m2[1]).replace(/ /g,'+')}:wght@300;400;600;700;800&display=swap">`); continue; }
|
|
900
|
+
if ((m2=nr.match(/^script\s+src\s+(.+)$/))) { headTags.push(`<script src="${str(m2[1])}"><\/script>`); continue; }
|
|
901
|
+
if ((m2=nr.match(/^bodyattr\s+([\w-]+)\s+(.+)$/))) { bodyAttrs[m2[1]]=str(m2[2]); continue; }
|
|
902
|
+
if ((m2=nr.match(/^(og|twitter):([\w:]+)\s+(.+)$/))){ headTags.push(`<meta property="${m2[1]}:${m2[2]}" content="${str(m2[3])}">`); continue; }
|
|
903
|
+
if ((m2=nr.match(/^canonical\s+(.+)$/))) { headTags.push(`<link rel="canonical" href="${str(m2[1])}">`); continue; }
|
|
904
|
+
if ((m2=nr.match(/^base\s+(.+)$/))) { headTags.push(`<base href="${str(m2[1])}">`); continue; }
|
|
905
|
+
}
|
|
906
|
+
continue;
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
// css: raw CSS block
|
|
910
|
+
if (/^css\s*:?$/.test(t)) {
|
|
911
|
+
const buf=[];
|
|
912
|
+
while(i<lines.length){ const nr=lines[i],ni=indent(nr); if(!nr.trim()||ni>ind){buf.push(nr.slice(Math.min(ni,ind+2)));i++;}else break; }
|
|
913
|
+
cssBlocks.push(buf.join('\n'));
|
|
914
|
+
continue;
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
// script: raw JS block
|
|
918
|
+
if (/^script\s*:?$/.test(t)) {
|
|
919
|
+
const buf=[];
|
|
920
|
+
while(i<lines.length){ const nr=lines[i],ni=indent(nr); if(!nr.trim()||ni>ind){buf.push(nr.slice(Math.min(ni,ind+2)));i++;}else break; }
|
|
921
|
+
jsBlocks.push(buf.join('\n'));
|
|
922
|
+
continue;
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
// add TAG "id/classes": — the workhorse
|
|
926
|
+
// Supports: add div "myId" add div ".btn.active" add div "#main .hero big"
|
|
927
|
+
const addM = t.match(/^add\s+([\w-]+)(?:\s+"([^"]*)")?(?:\s+'([^']*)')?\s*:?$/);
|
|
928
|
+
if (addM) {
|
|
929
|
+
while (elStack.length>1 && elStack[elStack.length-1].indent>=ind) elStack.pop();
|
|
930
|
+
const parent = elStack.length ? elStack[elStack.length-1].el : null;
|
|
931
|
+
const rawSel = (addM[2]||addM[3]||'').trim();
|
|
932
|
+
const el = {
|
|
933
|
+
tag:addM[1].toLowerCase(), id:'', classes:[],
|
|
934
|
+
text:'', html:'',
|
|
935
|
+
styles:{}, pseudo:{}, mediaRules:[],
|
|
936
|
+
beforeContent:null, afterContent:null,
|
|
937
|
+
attrs:{}, events:[], children:[],
|
|
938
|
+
_indent:ind, _anim:null,
|
|
939
|
+
};
|
|
940
|
+
// parse selector tokens: #id .class plain-word→id
|
|
941
|
+
rawSel.split(/\s+/).filter(Boolean).forEach(tok => {
|
|
942
|
+
if (tok.startsWith('#')) el.id = tok.slice(1);
|
|
943
|
+
else if (tok.startsWith('.')) el.classes.push(tok.slice(1));
|
|
944
|
+
else if (!el.id) el.id = tok;
|
|
945
|
+
else el.classes.push(tok);
|
|
946
|
+
});
|
|
947
|
+
if (parent && !parent._isPage) parent.children.push(el);
|
|
948
|
+
else elements.push(el);
|
|
949
|
+
elStack.push({el, indent:ind});
|
|
950
|
+
continue;
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
// ── properties inside an element ─────────────────────────
|
|
954
|
+
const pe = elStack[elStack.length-1];
|
|
955
|
+
if (!pe || ind <= pe.indent) continue;
|
|
956
|
+
const el = pe.el;
|
|
957
|
+
|
|
958
|
+
let m2;
|
|
959
|
+
|
|
960
|
+
// content
|
|
961
|
+
if ((m2=t.match(/^text\s+(.+)$/))) { el.text=str(m2[1]); continue; }
|
|
962
|
+
if ((m2=t.match(/^html\s+(.+)$/))) { el.html=str(m2[1]); continue; }
|
|
963
|
+
|
|
964
|
+
// common attrs — shorthand keywords
|
|
965
|
+
const ATTR_KW = {
|
|
966
|
+
src:1,href:1,alt:1,title:1,type:1,name:1,value:1,
|
|
967
|
+
for:1,action:1,method:1,target:1,rel:1,rows:1,cols:1,
|
|
968
|
+
min:1,max:1,step:1,role:1,tabindex:1,colspan:1,rowspan:1,
|
|
969
|
+
width:1,height:1,loading:1,decoding:1,crossorigin:1,
|
|
970
|
+
enctype:1,accept:1,pattern:1,maxlength:1,minlength:1,
|
|
971
|
+
download:1,sizes:1,srcset:1,poster:1,preload:1,
|
|
972
|
+
sandbox:1,allow:1,frameborder:1,scrolling:1,
|
|
973
|
+
};
|
|
974
|
+
if ((m2=t.match(/^([\w-]+)\s+(.+)$/)) && ATTR_KW[m2[1]]) { el.attrs[m2[1]]=str(m2[2]); continue; }
|
|
975
|
+
|
|
976
|
+
// boolean attrs
|
|
977
|
+
if (/^(checked|disabled|readonly|required|autoplay|loop|controls|muted|multiple|autofocus|hidden|open|selected|novalidate|async|defer|reversed|ismap|allowfullscreen|default|formnovalidate|spellcheck)$/.test(t))
|
|
978
|
+
{ el.attrs[t]=''; continue; }
|
|
979
|
+
|
|
980
|
+
// aria-*, data-*
|
|
981
|
+
if ((m2=t.match(/^aria-([\w-]+)\s+(.+)$/))) { el.attrs[`aria-${m2[1]}`]=str(m2[2]); continue; }
|
|
982
|
+
if ((m2=t.match(/^data-([\w-]+)\s+(.+)$/))) { el.attrs[`data-${m2[1]}`]=str(m2[2]); continue; }
|
|
983
|
+
|
|
984
|
+
// generic attr fallback
|
|
985
|
+
if ((m2=t.match(/^attr\s+([\w-]+)\s+(.+)$/))) { el.attrs[m2[1]]=str(m2[2]); continue; }
|
|
986
|
+
|
|
987
|
+
// class
|
|
988
|
+
if ((m2=t.match(/^class\s+(.+)$/))) { str(m2[1]).split(/\s+/).forEach(c=>c&&el.classes.push(c)); continue; }
|
|
989
|
+
|
|
990
|
+
// style — any CSS property, camelCase or kebab
|
|
991
|
+
if ((m2=t.match(/^style\s+([\w-]+)\s+(.+)$/))) { el.styles[camel(m2[1])]=str(m2[2]); continue; }
|
|
992
|
+
|
|
993
|
+
// pseudo-class styles: hover / focus / active / visited / checked / disabled / focusVisible / focusWithin / placeholder / selection
|
|
994
|
+
if ((m2=t.match(/^(hover|focus|active|visited|checked|disabled|focus-visible|focus-within|placeholder|first-child|last-child|nth-child|not|invalid|valid)\s+([\w-]+)\s+(.+)$/)))
|
|
995
|
+
{ if(!el.pseudo[m2[1]])el.pseudo[m2[1]]={}; el.pseudo[m2[1]][camel(m2[2])]=str(m2[3]); continue; }
|
|
996
|
+
|
|
997
|
+
// pseudo-elements: before / after / first-line / first-letter / marker / selection / backdrop
|
|
998
|
+
if ((m2=t.match(/^before\s+(.+)$/))) { el.beforeContent=str(m2[1]); continue; }
|
|
999
|
+
if ((m2=t.match(/^after\s+(.+)$/))) { el.afterContent=str(m2[1]); continue; }
|
|
1000
|
+
|
|
1001
|
+
// shorthand style helpers
|
|
1002
|
+
if ((m2=t.match(/^transition\s+(.+)$/))) { el.styles.transition=str(m2[1]); continue; }
|
|
1003
|
+
if ((m2=t.match(/^transform\s+(.+)$/))) { el.styles.transform=str(m2[1]); continue; }
|
|
1004
|
+
if ((m2=t.match(/^filter\s+(.+)$/))) { el.styles.filter=str(m2[1]); continue; }
|
|
1005
|
+
if ((m2=t.match(/^grid\s+(.+)$/))) { el.styles.gridTemplateColumns=str(m2[1]); continue; }
|
|
1006
|
+
if ((m2=t.match(/^flex\s+(.+)$/))) { el.styles.flex=str(m2[1]); continue; }
|
|
1007
|
+
if ((m2=t.match(/^gap\s+(.+)$/))) { el.styles.gap=str(m2[1]); continue; }
|
|
1008
|
+
if ((m2=t.match(/^bg\s+(.+)$/))) { el.styles.background=str(m2[1]); continue; }
|
|
1009
|
+
if ((m2=t.match(/^shadow\s+(.+)$/))) { el.styles.boxShadow=str(m2[1]); continue; }
|
|
1010
|
+
if ((m2=t.match(/^clip\s+(.+)$/))) { el.styles.clipPath=str(m2[1]); continue; }
|
|
1011
|
+
if ((m2=t.match(/^mask\s+(.+)$/))) { el.styles.mask=str(m2[1]); continue; }
|
|
1012
|
+
if ((m2=t.match(/^outline\s+(.+)$/))) { el.styles.outline=str(m2[1]); continue; }
|
|
1013
|
+
if ((m2=t.match(/^overflow\s+(.+)$/))) { el.styles.overflow=str(m2[1]); continue; }
|
|
1014
|
+
if ((m2=t.match(/^cursor\s+(.+)$/))) { el.styles.cursor=str(m2[1]); continue; }
|
|
1015
|
+
if ((m2=t.match(/^opacity\s+(.+)$/))) { el.styles.opacity=str(m2[1]); continue; }
|
|
1016
|
+
if ((m2=t.match(/^zindex\s+(.+)$/))) { el.styles.zIndex=str(m2[1]); continue; }
|
|
1017
|
+
if ((m2=t.match(/^var\s+(--[\w-]+)\s+(.+)$/))) { el.styles[m2[1]]=str(m2[2]); continue; }
|
|
1018
|
+
|
|
1019
|
+
// @media per-element: media "max-width:600px" prop value
|
|
1020
|
+
if ((m2=t.match(/^media\s+"([^"]+)"\s+([\w-]+)\s+(.+)$/)))
|
|
1021
|
+
{ el.mediaRules.push({q:m2[1], p:camel(m2[2]), v:str(m2[3])}); continue; }
|
|
1022
|
+
// media "value" (HTML attribute, e.g. on <link> or <source>)
|
|
1023
|
+
if ((m2=t.match(/^media\s+(.+)$/)) && !m2[1].trim().match(/^"[^"]*"\s+[\w-]+/))
|
|
1024
|
+
{ el.attrs.media=str(m2[1]); continue; }
|
|
1025
|
+
// placeholder "text" as HTML attribute (not pseudo-class)
|
|
1026
|
+
if ((m2=t.match(/^placeholder\s+(.+)$/)) && !m2[1].match(/^[\w-]/))
|
|
1027
|
+
{ el.attrs.placeholder=str(m2[1]); continue; }
|
|
1028
|
+
|
|
1029
|
+
// animate NAME DURATION? EASING?
|
|
1030
|
+
if ((m2=t.match(/^animate\s+(\w+)(?:\s+([\d.]+))?(?:\s+([\w-]+))?$/)))
|
|
1031
|
+
{ el._anim={name:m2[1], dur:parseFloat(m2[2]||0.4), ease:m2[3]||'ease'}; continue; }
|
|
1032
|
+
|
|
1033
|
+
// on EVENT: — raw JS handler, indented body
|
|
1034
|
+
if ((m2=t.match(/^on\s+([\w]+)\s*:?$/))) {
|
|
1035
|
+
const buf=[];
|
|
1036
|
+
while(i<lines.length){ const nr=lines[i],ni=indent(nr); if(!nr.trim()||ni<=ind)break; buf.push(nr.slice(ni)); i++; }
|
|
1037
|
+
el.events.push({ev:m2[1], code:buf.join('\n')});
|
|
1038
|
+
continue;
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
// ── Render ────────────────────────────────────────────────
|
|
1043
|
+
const pseudoCSS = [];
|
|
1044
|
+
const mediaCSS = {};
|
|
1045
|
+
|
|
1046
|
+
const VOID = new Set(['area','base','br','col','embed','hr','img','input','link','meta','param','source','track','wbr']);
|
|
1047
|
+
|
|
1048
|
+
function ensureId(el) { if (!el.id) el.id=uid(); return el.id; }
|
|
1049
|
+
|
|
1050
|
+
function renderEl(el) {
|
|
1051
|
+
const tag = el.tag;
|
|
1052
|
+
|
|
1053
|
+
// Build inline style string
|
|
1054
|
+
let styleStr = Object.entries(el.styles).map(([k,v])=>`${kebab(k)}:${v}`).join(';');
|
|
1055
|
+
|
|
1056
|
+
// Animations
|
|
1057
|
+
if (el._anim) {
|
|
1058
|
+
const ANIMS = {
|
|
1059
|
+
fadeIn:'fadeIn',fadeOut:'fadeOut',
|
|
1060
|
+
slideIn:'slideIn',slideInRight:'slideInRight',
|
|
1061
|
+
slideUp:'slideUp',slideDown:'slideDown',
|
|
1062
|
+
pop:'pop',bounce:'bounce',spin:'spin',
|
|
1063
|
+
pulse:'pulse',shake:'shake',flip:'flip',
|
|
1064
|
+
zoom:'zoom',wiggle:'wiggle',typewriter:'typewriter',
|
|
1065
|
+
};
|
|
1066
|
+
const aname = ANIMS[el._anim.name]||el._anim.name;
|
|
1067
|
+
const fill = /^(bounce|spin|pulse)$/.test(el._anim.name)?'infinite':'both';
|
|
1068
|
+
const ease = el._anim.name==='spin'?'linear':el._anim.ease;
|
|
1069
|
+
styleStr += (styleStr?';':'')+`animation:${aname} ${el._anim.dur}s ${ease} ${fill}`;
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
// Pseudo-class rules
|
|
1073
|
+
Object.entries(el.pseudo).forEach(([pseudo, styles]) => {
|
|
1074
|
+
const r = Object.entries(styles).map(([k,v])=>`${kebab(k)}:${v}`).join(';');
|
|
1075
|
+
if (r) { ensureId(el); pseudoCSS.push(`#${el.id}:${pseudo}{${r}}`); }
|
|
1076
|
+
});
|
|
1077
|
+
|
|
1078
|
+
// before/after pseudo-elements
|
|
1079
|
+
if (el.beforeContent!==null) { ensureId(el); pseudoCSS.push(`#${el.id}::before{content:"${el.beforeContent.replace(/"/g,'\\"')}"}`); }
|
|
1080
|
+
if (el.afterContent!==null) { ensureId(el); pseudoCSS.push(`#${el.id}::after{content:"${el.afterContent.replace(/"/g,'\\"')}"}`); }
|
|
1081
|
+
|
|
1082
|
+
// @media rules
|
|
1083
|
+
el.mediaRules.forEach(({q,p,v}) => {
|
|
1084
|
+
const s = el.id ? `#${el.id}` : (el.classes[0] ? `.${el.classes[0]}` : tag);
|
|
1085
|
+
if (!mediaCSS[q]) mediaCSS[q]=[];
|
|
1086
|
+
mediaCSS[q].push(`${s}{${kebab(p)}:${v}}`);
|
|
1087
|
+
});
|
|
1088
|
+
|
|
1089
|
+
// Build attribute string
|
|
1090
|
+
const idA = el.id ? ` id="${el.id}"` : '';
|
|
1091
|
+
const clsA = el.classes.length ? ` class="${el.classes.join(' ')}"` : '';
|
|
1092
|
+
const styA = styleStr ? ` style="${styleStr}"` : '';
|
|
1093
|
+
const xtra = Object.entries(el.attrs).map(([k,v])=>v===''?` ${k}`:` ${k}="${v}"`).join('');
|
|
1094
|
+
const evts = el.events.map(({ev,code})=>{
|
|
1095
|
+
return ` data-ss-${ev}="${code.replace(/"/g,'"').replace(/\n/g,' ')}"`;
|
|
1096
|
+
}).join('');
|
|
1097
|
+
|
|
1098
|
+
if (VOID.has(tag)) return `<${tag}${idA}${clsA}${styA}${xtra}>\n`;
|
|
1099
|
+
const inner = el.html || el.text || el.children.map(renderEl).join('');
|
|
1100
|
+
return `<${tag}${idA}${clsA}${styA}${xtra}${evts}>${inner}</${tag}>\n`;
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
const bodyStyleStr = Object.entries(pageStyles).map(([k,v])=>`${kebab(k)}:${v}`).join(';');
|
|
1104
|
+
const bodyXtra = Object.entries(bodyAttrs).map(([k,v])=>` ${k}="${v}"`).join('');
|
|
1105
|
+
const bodyHtml = elements.map(renderEl).join('');
|
|
1106
|
+
|
|
1107
|
+
const mediaCSSStr = Object.entries(mediaCSS)
|
|
1108
|
+
.map(([q,rules])=>`@media (${q}){${rules.join('')}}`).join('\n');
|
|
1109
|
+
|
|
1110
|
+
const KEYFRAMES = `
|
|
1111
|
+
@keyframes fadeIn {0%{opacity:0;transform:translateY(16px)}100%{opacity:1;transform:none}}
|
|
1112
|
+
@keyframes fadeOut {0%{opacity:1}100%{opacity:0}}
|
|
1113
|
+
@keyframes slideIn {0%{transform:translateX(-40px);opacity:0}100%{transform:none;opacity:1}}
|
|
1114
|
+
@keyframes slideInRight {0%{transform:translateX(40px);opacity:0}100%{transform:none;opacity:1}}
|
|
1115
|
+
@keyframes slideUp {0%{transform:translateY(40px);opacity:0}100%{transform:none;opacity:1}}
|
|
1116
|
+
@keyframes slideDown {0%{transform:translateY(-40px);opacity:0}100%{transform:none;opacity:1}}
|
|
1117
|
+
@keyframes bounce {0%,100%{transform:translateY(0)}50%{transform:translateY(-18px)}}
|
|
1118
|
+
@keyframes pulse {0%,100%{opacity:1}50%{opacity:0.4}}
|
|
1119
|
+
@keyframes spin {to{transform:rotate(360deg)}}
|
|
1120
|
+
@keyframes shake {0%,100%{transform:translateX(0)}20%{transform:translateX(-8px)}40%{transform:translateX(8px)}60%{transform:translateX(-5px)}80%{transform:translateX(5px)}}
|
|
1121
|
+
@keyframes pop {0%{transform:scale(0.5);opacity:0}70%{transform:scale(1.1)}100%{transform:scale(1);opacity:1}}
|
|
1122
|
+
@keyframes flip {0%{transform:rotateY(-90deg);opacity:0}100%{transform:rotateY(0);opacity:1}}
|
|
1123
|
+
@keyframes zoom {0%{transform:scale(0);opacity:0}100%{transform:scale(1);opacity:1}}
|
|
1124
|
+
@keyframes wiggle {0%,100%{transform:rotate(0)}25%{transform:rotate(-8deg)}75%{transform:rotate(8deg)}}
|
|
1125
|
+
@keyframes typewriter {from{clip-path:inset(0 100% 0 0)}to{clip-path:inset(0 0 0 0)}}`;
|
|
1126
|
+
|
|
1127
|
+
// All DOM events wired up
|
|
1128
|
+
const ALL_EVENTS = [
|
|
1129
|
+
'click','dblclick','contextmenu',
|
|
1130
|
+
'mousedown','mouseup','mouseover','mouseout','mouseenter','mouseleave','mousemove',
|
|
1131
|
+
'keydown','keyup','keypress',
|
|
1132
|
+
'input','change','focus','blur','submit','reset','select',
|
|
1133
|
+
'scroll','resize','wheel',
|
|
1134
|
+
'dragstart','drag','dragend','dragover','dragenter','dragleave','drop',
|
|
1135
|
+
'touchstart','touchmove','touchend','touchcancel',
|
|
1136
|
+
'pointerdown','pointermove','pointerup','pointercancel','pointerenter','pointerleave',
|
|
1137
|
+
'animationstart','animationend','animationiteration',
|
|
1138
|
+
'transitionstart','transitionend',
|
|
1139
|
+
'load','error','abort','canplay','play','pause','ended','timeupdate','volumechange',
|
|
1140
|
+
'copy','cut','paste','beforeinput',
|
|
1141
|
+
'fullscreenchange','visibilitychange',
|
|
1142
|
+
];
|
|
1143
|
+
const evScript = ALL_EVENTS.map(ev =>
|
|
1144
|
+
`document.querySelectorAll('[data-ss-${ev}]').forEach(function(el){el.addEventListener('${ev}',function(event){try{(new Function('event',this.getAttribute('data-ss-${ev}'))).call(this,event)}catch(e){console.error(e)}}.bind(el));});`
|
|
1145
|
+
).join('\n');
|
|
1146
|
+
|
|
1147
|
+
return `<!DOCTYPE html>
|
|
1148
|
+
<html lang="${pageLang}">
|
|
1149
|
+
<head>
|
|
1150
|
+
<meta charset="UTF-8">
|
|
1151
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
1152
|
+
<title>${pageTitle}</title>
|
|
1153
|
+
${headTags.join('\n')}
|
|
1154
|
+
<style>
|
|
1155
|
+
*{box-sizing:border-box;margin:0;padding:0}
|
|
1156
|
+
${KEYFRAMES}
|
|
1157
|
+
${pseudoCSS.join('\n')}
|
|
1158
|
+
${mediaCSSStr}
|
|
1159
|
+
${cssBlocks.join('\n')}
|
|
1160
|
+
</style>
|
|
1161
|
+
</head>
|
|
1162
|
+
<body${bodyStyleStr?` style="${bodyStyleStr}"`:''}>
|
|
1163
|
+
${bodyHtml}<script>(function(){
|
|
1164
|
+
${evScript}
|
|
1165
|
+
${jsBlocks.join('\n')}
|
|
1166
|
+
})()</script>
|
|
1167
|
+
</body>
|
|
1168
|
+
</html>`;
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
|
|
1089
1172
|
module.exports = { Interpreter, SSError, Environment, isWebCode, buildWebDoc };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "structscript",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"description": "The StructScript programming language — clean, readable scripting for everyone. Includes a built-in visual editor.",
|
|
5
5
|
"keywords": ["structscript", "language", "interpreter", "scripting", "programming-language"],
|
|
6
6
|
"author": "StructScript",
|