diff --git a/docs/_sidebar.md b/docs/_sidebar.md index 085375d..70685a3 100644 --- a/docs/_sidebar.md +++ b/docs/_sidebar.md @@ -16,4 +16,5 @@ * **Concepts** * [Tags](api/tags.md) * [Global Store](api/global.md) - * [JSX Style](api/jsx.md) \ No newline at end of file + * [JSX Style](api/jsx.md) + * [HTML converter](convert.md) \ No newline at end of file diff --git a/docs/convert.js b/docs/convert.js new file mode 100644 index 0000000..37942a7 --- /dev/null +++ b/docs/convert.js @@ -0,0 +1,76 @@ +(() => { + // src/convert.js + var { $, div, h1, label, textarea, button, span } = window; + function html2sigpro(h) { + const B = new Set(["allowfullscreen", "async", "autofocus", "autoplay", "checked", "controls", "default", "defer", "disabled", "formnovalidate", "hidden", "ismap", "itemscope", "loop", "multiple", "muted", "nomodule", "novalidate", "open", "playsinline", "readonly", "required", "reversed", "selected", "truespeed"]), esc = (v) => v.replace(/"/g, "\\\""), bP = (el) => { + let a = [...el.attributes].map(({ name: n, value: v }) => /^on/i.test(n) ? `${n}: (e) => { ${v.replace(/\s+/g, " ").trim()} }` : B.has(n.toLowerCase()) && (!v || v == n) ? `${n}: true` : `${n}: "${esc(v)}"`); + return a.length ? `{ ${a.join(", ")} }` : ""; + }, cN = (n, d = 0) => { + let s = " ".repeat(d); + if (n.nodeType == 3) { + let t = n.textContent; + return t.trim() ? `${s}"${esc(t)}"` : ""; + } + if (n.nodeType == 1) { + let t = n.tagName.toLowerCase(), p = bP(n), c = [...n.childNodes].map((i) => cN(i, d + 1)).filter(Boolean); + if (!c.length) + return `${s}${t}(${p})`; + if (c.length == 1 && !c[0].includes(` +`)) + return `${s}${t}(${p ? p + ", " : ""}${c[0].trim()})`; + return `${s}${t}(${p ? p + ", " : ""}[ +${c.join(`, +`)} +${s}])`; + } + return ""; + }, r = [...new DOMParser().parseFromString(h, "text/html").body.childNodes].map((n) => cN(n)).filter(Boolean); + return r.length == 1 ? r[0].trim() : `[ +${r.join(`, +`)} +]`; + } + var converter = () => { + const inH = $(""); + const setInH = (v) => inH(v); + const outS = $(""); + const setOutS = (v) => outS(v); + cnv = () => { + try { + setOutS(html2sigpro(inH())); + } catch (e) { + setOutS("Error: " + e.message); + } + }, txS = "width:100%;height:200px;padding:10px;border:1px solid #ccc;border-radius:4px;font-family:monospace;font-size:14px;box-sizing:border-box;resize:vertical", btS = "padding:8px 16px;border:none;border-radius:4px;cursor:pointer;margin-right:8px;font-size:14px"; + return div({ style: "max-width:900px;margin:20px auto;font-family:sans-serif" }, [ + h1("HTML → SigPro"), + label({ style: "display:block;font-weight:700" }, "HTML:"), + textarea({ + style: txS, + placeholder: "HTML...", + value: inH, + oninput: (e) => { + setInH(e.target.value); + cnv(); + } + }), + div({ style: "margin:10px 0" }, [ + button({ style: btS + ";background:#3b82f6;color:#fff", onclick: cnv }, "Convert"), + button({ style: btS + ";background:#d1d5db", onclick: () => { + setInH(""); + setOutS(""); + } }, "Clear") + ]), + div({ style: "display:flex;justify-content:space-between;align-items:center;margin-bottom:5px" }, [ + span({ style: "font-weight:700" }, "Out:"), + button({ style: btS + ";background:#10b981;color:#fff", onclick: () => { + navigator.clipboard.writeText(outS()); + alert("Copied!"); + } }, "Copy") + ]), + textarea({ style: txS + ";background:#f9fafb", readonly: true, value: outS, placeholder: "Result..." }) + ]); + }; + window.html2sigpro = html2sigpro; + window.converter = converter; +})(); diff --git a/docs/convert.md b/docs/convert.md new file mode 100644 index 0000000..180d54b --- /dev/null +++ b/docs/convert.md @@ -0,0 +1,11 @@ +# HTML to SigPro Converter + +Convert your existing HTML markup directly into SigPro Tag Helper syntax. Paste your HTML in the left panel and get clean, ready-to-use SigPro code on the right. + +## Usage + +
+ +```js +mount(window.converter, '#sigpro-converter'); +``` \ No newline at end of file diff --git a/docs/index.html b/docs/index.html index 2e34b30..2ac02fb 100644 --- a/docs/index.html +++ b/docs/index.html @@ -51,7 +51,7 @@ ); codeBlocks.forEach((code) => { try { - const scriptContent = `(function() { ${code.innerText} })();`; + const scriptContent = `(function() { ${code.innerText} }).call(window);`; const runDemo = new Function(scriptContent); runDemo(); } catch (err) { @@ -65,6 +65,7 @@ + diff --git a/package.json b/package.json index a344788..4c44dcc 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,7 @@ "del": "bun pm cache rm && rm -f bun.lockb && rm -f bun.lock", "clean": "rm -rf dist", "prebuild": "npm run clean", + "build:convert": "bun build ./src/convert.js --bundle --outfile=./docs/convert.js --format=iife", "build:iife": "bun build ./src/build_umd.js --bundle --outfile=./dist/sigpro.js --format=iife --global-name=sp", "build:iife:min": "bun build ./src/build_umd.js --bundle --outfile=./dist/sigpro.min.js --format=iife --global-name=sp --minify", "build:esm": "bun build ./src/sigpro.js --bundle --outfile=./dist/sigpro.esm.js --format=esm", @@ -52,7 +53,7 @@ "build:utils": "bun build ./src/sigpro.utils.js --bundle --outfile=./dist/sigpro.utils.js --format=esm", "build:vite": "bun build ./src/sigpro.vite.js --bundle --outfile=./dist/sigpro.vite.js --format=esm --external:fs --external:path", "build:copy": "cp ./dist/sigpro.js ./docs/sigpro.js", - "build": "bun run build:iife && bun run build:iife:min && bun run build:esm && bun run build:esm:min && bun run build:utils && bun run build:vite && bun run build:copy", + "build": "bun run build:iife && bun run build:iife:min && bun run build:esm && bun run build:esm:min && bun run build:utils && bun run build:vite && bun run build:convert && bun run build:copy", "docs": "bun x serve docs" }, "keywords": [ diff --git a/src/convert.js b/src/convert.js new file mode 100644 index 0000000..c3739dd --- /dev/null +++ b/src/convert.js @@ -0,0 +1,61 @@ +const { $, div, h1, label, textarea, button, span } = window; + +function html2sigpro(h) { + const B = new Set(['allowfullscreen', 'async', 'autofocus', 'autoplay', 'checked', 'controls', 'default', 'defer', 'disabled', 'formnovalidate', 'hidden', 'ismap', 'itemscope', 'loop', 'multiple', 'muted', 'nomodule', 'novalidate', 'open', 'playsinline', 'readonly', 'required', 'reversed', 'selected', 'truespeed']), + esc = v => v.replace(/"/g, '\\"'), + bP = el => { + let a = [...el.attributes].map(({ name: n, value: v }) => + /^on/i.test(n) ? `${n}: (e) => { ${v.replace(/\s+/g, ' ').trim()} }` : + (B.has(n.toLowerCase()) && (!v || v == n)) ? `${n}: true` : `${n}: "${esc(v)}"` + ); + return a.length ? `{ ${a.join(', ')} }` : ''; + }, + cN = (n, d = 0) => { + let s = ' '.repeat(d); + if (n.nodeType == 3) { + let t = n.textContent; + return t.trim() ? `${s}"${esc(t)}"` : ''; + } + if (n.nodeType == 1) { + let t = n.tagName.toLowerCase(), p = bP(n), + c = [...n.childNodes].map(i => cN(i, d + 1)).filter(Boolean); + if (!c.length) return `${s}${t}(${p})`; + if (c.length == 1 && !c[0].includes('\n')) return `${s}${t}(${p ? p + ', ' : ''}${c[0].trim()})`; + return `${s}${t}(${p ? p + ', ' : ''}[\n${c.join(',\n')}\n${s}])`; + } + return ''; + }, + r = [...new DOMParser().parseFromString(h, 'text/html').body.childNodes].map(n => cN(n)).filter(Boolean); + return r.length == 1 ? r[0].trim() : `[\n${r.join(',\n')}\n]`; +} + +const converter = () => { + const inH = $(''); + const setInH = (v) => inH(v); + const outS = $(''); + const setOutS = (v) => outS(v); + cnv = () => { try { setOutS(html2sigpro(inH())) } catch (e) { setOutS('Error: ' + e.message) } }, + txS = "width:100%;height:200px;padding:10px;border:1px solid #ccc;border-radius:4px;font-family:monospace;font-size:14px;box-sizing:border-box;resize:vertical", + btS = "padding:8px 16px;border:none;border-radius:4px;cursor:pointer;margin-right:8px;font-size:14px"; + + return div({ style: "max-width:900px;margin:20px auto;font-family:sans-serif" }, [ + h1("HTML → SigPro"), + label({ style: "display:block;font-weight:700" }, "HTML:"), + textarea({ + style: txS, placeholder: "HTML...", value: inH, + oninput: e => { setInH(e.target.value); cnv() } + }), + div({ style: "margin:10px 0" }, [ + button({ style: btS + ";background:#3b82f6;color:#fff", onclick: cnv }, "Convert"), + button({ style: btS + ";background:#d1d5db", onclick: () => { setInH(''); setOutS('') } }, "Clear") + ]), + div({ style: "display:flex;justify-content:space-between;align-items:center;margin-bottom:5px" }, [ + span({ style: "font-weight:700" }, "Out:"), + button({ style: btS + ";background:#10b981;color:#fff", onclick: () => { navigator.clipboard.writeText(outS()); alert('Copied!') } }, "Copy") + ]), + textarea({ style: txS + ";background:#f9fafb", readonly: true, value: outS, placeholder: "Result..." }) + ]); +} + +window.html2sigpro = html2sigpro; +window.converter = converter; \ No newline at end of file