@webto-id/variant-check 0.1.3 → 0.1.5
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/dist/cli.js +79 -21
- package/dist/index.js +79 -21
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -6,7 +6,7 @@ var __name = (target, value) => __defProp(target, "name", { value, configurable:
|
|
|
6
6
|
import { readFileSync, writeFileSync } from "fs";
|
|
7
7
|
|
|
8
8
|
// ../variant-compiler/src/ir.ts
|
|
9
|
-
var COMPONENT_NAMES = ["Container", "Button", "EditUrlPill", "ViewMoreLink"];
|
|
9
|
+
var COMPONENT_NAMES = ["Container", "Button", "EditUrlPill", "ViewMoreLink", "AddImageButton"];
|
|
10
10
|
|
|
11
11
|
// ../variant-compiler/src/parse/parser.ts
|
|
12
12
|
var ParseError = class extends Error {
|
|
@@ -603,9 +603,10 @@ var HELPER_BY_IMPORT = {
|
|
|
603
603
|
Container: "Container",
|
|
604
604
|
Button: "Button",
|
|
605
605
|
EditUrlPill: "EditUrlPill",
|
|
606
|
-
ViewMoreLink: "ViewMoreLink"
|
|
606
|
+
ViewMoreLink: "ViewMoreLink",
|
|
607
|
+
AddImageButton: "AddImageButton"
|
|
607
608
|
};
|
|
608
|
-
var ALLOWED_SOURCE = /^(webto\/variant|(\.{1,2}\/)+(lib\/(sanitize|image|section-i18n)|(components\/)?(v2\/)?(ui\/)?(Container|Button|ViewMoreLink)\.astro|(components\/)?EditUrlPill\.astro|ui\/(Container|Button)\.astro|ViewMoreLink\.astro))$/;
|
|
609
|
+
var ALLOWED_SOURCE = /^(webto\/variant|(\.{1,2}\/)+(lib\/(sanitize|image|section-i18n)|(components\/)?(v2\/)?(ui\/)?(Container|Button|ViewMoreLink|AddImageButton)\.astro|(components\/)?EditUrlPill\.astro|ui\/(Container|Button)\.astro|ViewMoreLink\.astro))$/;
|
|
609
610
|
function stripComments(src) {
|
|
610
611
|
return src.replace(/\/\*[\s\S]*?\*\//g, (m) => m.replace(/[^\n]/g, " ")).replace(/(^|[^:\\])\/\/[^\n]*/g, (m, pre) => pre + " ".repeat(m.length - pre.length));
|
|
611
612
|
}
|
|
@@ -744,7 +745,7 @@ function parseFrontmatter(raw, lineOffset = 0) {
|
|
|
744
745
|
const [, isType, def, named, source2] = m;
|
|
745
746
|
i += m[0].length;
|
|
746
747
|
if (isType) continue;
|
|
747
|
-
if (!ALLOWED_SOURCE.test(source2)) throw new ParseError(`import from '${source2}' is not allowed \u2014 only "webto/variant" helpers (t, img, url, Container, Button, EditUrlPill, ViewMoreLink)`, line, "import");
|
|
748
|
+
if (!ALLOWED_SOURCE.test(source2)) throw new ParseError(`import from '${source2}' is not allowed \u2014 only "webto/variant" helpers (t, img, url, Container, Button, EditUrlPill, ViewMoreLink, AddImageButton)`, line, "import");
|
|
748
749
|
const names = [];
|
|
749
750
|
if (def) names.push({ local: def, imported: def });
|
|
750
751
|
if (named) {
|
|
@@ -1153,6 +1154,13 @@ function lintNodes(nodes, ctx, ariaHidden = false) {
|
|
|
1153
1154
|
const hidden = ariaHidden || staticValue(n.attrs["aria-hidden"]) === "true";
|
|
1154
1155
|
for (const [k, v] of Object.entries(n.attrs)) lintAttr(k, v, tag, ctx);
|
|
1155
1156
|
if (tag === "img" && !("alt" in n.attrs)) push(ctx, "warning", "img-alt", "<img> without alt");
|
|
1157
|
+
if (/^h[1-4]$/.test(tag)) {
|
|
1158
|
+
const cls = staticValue(n.attrs.class);
|
|
1159
|
+
const style = staticValue(n.attrs.style);
|
|
1160
|
+
const pinned = typeof style === "string" && /font-size\s*:/i.test(style);
|
|
1161
|
+
const inert = typeof cls === "string" && !pinned ? cls.split(/\s+/).filter((c) => HEADING_SIZE_CLASS.test(c)) : [];
|
|
1162
|
+
if (inert.length) push(ctx, "warning", "heading-size-inert", `${inert.join(" ")} on <${tag}> has no effect: h1-h4 sizes follow the site theme (Style tab). Keep it as intent, pin the size with !${inert[0]} (honored everywhere, never rescaled by theme or per-section settings) or an inline style, or use a non-heading tag to follow the body scale`);
|
|
1163
|
+
}
|
|
1156
1164
|
if (tag === "a") {
|
|
1157
1165
|
const target = staticValue(n.attrs.target);
|
|
1158
1166
|
if (target === "_blank" && !("rel" in n.attrs)) n.attrs.rel = "noopener";
|
|
@@ -1207,6 +1215,7 @@ function checkUrl(name, v, tag, ctx) {
|
|
|
1207
1215
|
if (/^https?:\/\//i.test(s) && (name === "src" || name === "poster")) push(ctx, "warning", "hardcoded-image", `<${tag} ${name}> hardcodes an external URL \u2014 images should come from a content field`);
|
|
1208
1216
|
}
|
|
1209
1217
|
__name(checkUrl, "checkUrl");
|
|
1218
|
+
var HEADING_SIZE_CLASS = /^(?:[\w-]+:)*text-(?:xs|sm|base|lg|xl|\dxl|\[[^\]]+\])$/;
|
|
1210
1219
|
function lintHtml(root, sectionType2) {
|
|
1211
1220
|
const ctx = { lint: [], sectionType: sectionType2, inButton: 0, inSvg: 0 };
|
|
1212
1221
|
lintNodes(root, ctx);
|
|
@@ -1512,18 +1521,18 @@ __name(hasBreak, "hasBreak");
|
|
|
1512
1521
|
|
|
1513
1522
|
// ../variant-compiler/src/css/scope.ts
|
|
1514
1523
|
var ROOT_PLACEHOLDER = "__WV_ROOT__";
|
|
1515
|
-
var RECURSE_AT = /^@(media|supports|container|layer|scope|document)\b/i;
|
|
1516
1524
|
var VERBATIM_AT = /^@(keyframes|-webkit-keyframes|property|font-face|page|counter-style|font-feature-values|namespace|charset)\b/i;
|
|
1517
1525
|
function scopeCss(css, root = ROOT_PLACEHOLDER) {
|
|
1518
|
-
return scopeBlock(stripComments2(css), root);
|
|
1526
|
+
return scopeBlock(stripComments2(css), root, null);
|
|
1519
1527
|
}
|
|
1520
1528
|
__name(scopeCss, "scopeCss");
|
|
1521
1529
|
function stripComments2(s) {
|
|
1522
1530
|
return s.replace(/\/\*[\s\S]*?\*\//g, "");
|
|
1523
1531
|
}
|
|
1524
1532
|
__name(stripComments2, "stripComments");
|
|
1525
|
-
function scopeBlock(src, root) {
|
|
1533
|
+
function scopeBlock(src, root, parents) {
|
|
1526
1534
|
let out = "";
|
|
1535
|
+
let decls = "";
|
|
1527
1536
|
let i = 0;
|
|
1528
1537
|
while (i < src.length) {
|
|
1529
1538
|
let j = i;
|
|
@@ -1540,7 +1549,10 @@ function scopeBlock(src, root) {
|
|
|
1540
1549
|
}
|
|
1541
1550
|
const prelude = src.slice(i, j).trim();
|
|
1542
1551
|
if (j >= src.length) {
|
|
1543
|
-
if (prelude)
|
|
1552
|
+
if (prelude) {
|
|
1553
|
+
if (parents) decls += `${prelude};`;
|
|
1554
|
+
else out += prelude;
|
|
1555
|
+
}
|
|
1544
1556
|
break;
|
|
1545
1557
|
}
|
|
1546
1558
|
if (src[j] === "}") {
|
|
@@ -1548,7 +1560,10 @@ function scopeBlock(src, root) {
|
|
|
1548
1560
|
continue;
|
|
1549
1561
|
}
|
|
1550
1562
|
if (src[j] === ";") {
|
|
1551
|
-
if (prelude)
|
|
1563
|
+
if (prelude) {
|
|
1564
|
+
if (parents && !prelude.startsWith("@")) decls += `${prelude};`;
|
|
1565
|
+
else out += `${prelude};`;
|
|
1566
|
+
}
|
|
1552
1567
|
i = j + 1;
|
|
1553
1568
|
continue;
|
|
1554
1569
|
}
|
|
@@ -1558,21 +1573,28 @@ function scopeBlock(src, root) {
|
|
|
1558
1573
|
if (!prelude) continue;
|
|
1559
1574
|
if (prelude.startsWith("@")) {
|
|
1560
1575
|
if (VERBATIM_AT.test(prelude)) out += `${prelude}{${body}}`;
|
|
1561
|
-
else
|
|
1562
|
-
else out += `${prelude}{${scopeBlock(body, root)}}`;
|
|
1576
|
+
else out += `${prelude}{${scopeBlock(body, root, parents)}}`;
|
|
1563
1577
|
continue;
|
|
1564
1578
|
}
|
|
1565
|
-
const
|
|
1579
|
+
const raw = splitSelectors(prelude).map((s) => s.trim()).filter(Boolean);
|
|
1580
|
+
const selectors = parents ? resolveNested(raw, parents) : raw.map((s) => scopeSelector(s, root)).filter(Boolean);
|
|
1566
1581
|
if (!selectors.length) continue;
|
|
1567
|
-
|
|
1568
|
-
out += `${selectors.join(",")}{${scopeBlock(body, root)}}`;
|
|
1569
|
-
} else {
|
|
1570
|
-
out += `${selectors.join(",")}{${body.trim()}}`;
|
|
1571
|
-
}
|
|
1582
|
+
out += scopeBlock(body, root, selectors);
|
|
1572
1583
|
}
|
|
1584
|
+
if (parents && decls) out = `${parents.join(",")}{${decls}}` + out;
|
|
1573
1585
|
return out;
|
|
1574
1586
|
}
|
|
1575
1587
|
__name(scopeBlock, "scopeBlock");
|
|
1588
|
+
function resolveNested(selectors, parents) {
|
|
1589
|
+
const parentRef = parents.length === 1 ? parents[0] : `:is(${parents.join(", ")})`;
|
|
1590
|
+
const out = [];
|
|
1591
|
+
for (const sel of selectors) {
|
|
1592
|
+
if (sel.includes("&")) out.push(sel.replace(/&/g, parentRef));
|
|
1593
|
+
else for (const p of parents) out.push(`${p} ${sel}`);
|
|
1594
|
+
}
|
|
1595
|
+
return out;
|
|
1596
|
+
}
|
|
1597
|
+
__name(resolveNested, "resolveNested");
|
|
1576
1598
|
function matchBrace2(src, open) {
|
|
1577
1599
|
let depth = 0;
|
|
1578
1600
|
let inStr = null;
|
|
@@ -1617,7 +1639,7 @@ function splitSelectors(s) {
|
|
|
1617
1639
|
__name(splitSelectors, "splitSelectors");
|
|
1618
1640
|
function scopeSelector(sel, root) {
|
|
1619
1641
|
if (!sel) return "";
|
|
1620
|
-
if (sel.startsWith("&")) return `${root}
|
|
1642
|
+
if (sel.startsWith("&")) return `${root}${sel.slice(1)}`.trim();
|
|
1621
1643
|
if (sel.includes(ROOT_PLACEHOLDER)) return sel;
|
|
1622
1644
|
const m = /^(:root|html|body)(?![\w-])/i.exec(sel);
|
|
1623
1645
|
if (m) {
|
|
@@ -1693,7 +1715,8 @@ var MACRO_CLASSES = [
|
|
|
1693
1715
|
"text-foreground hover:bg-muted",
|
|
1694
1716
|
"px-4 py-2 text-sm px-6 py-3 text-base px-8 py-4 text-lg",
|
|
1695
1717
|
"gap-1.5 px-2.5 py-1 rounded-md border-dashed text-xs whitespace-nowrap font-mono break-all w-3.5 h-3.5 shrink-0",
|
|
1696
|
-
"mt-10 flex justify-start justify-center justify-end hover:border-primary/30 text-primary hover:underline w-4 h-4 relative"
|
|
1718
|
+
"mt-10 flex justify-start justify-center justify-end hover:border-primary/30 text-primary hover:underline w-4 h-4 relative",
|
|
1719
|
+
"mt-8 absolute bottom-8 right-8 z-30"
|
|
1697
1720
|
];
|
|
1698
1721
|
function pickCandidates(tokens) {
|
|
1699
1722
|
const set = /* @__PURE__ */ new Set();
|
|
@@ -1758,7 +1781,7 @@ function deriveSchema(fields, props, base2) {
|
|
|
1758
1781
|
__name(deriveSchema, "deriveSchema");
|
|
1759
1782
|
|
|
1760
1783
|
// ../variant-compiler/src/index.ts
|
|
1761
|
-
var COMPILER_VERSION = "0.1.
|
|
1784
|
+
var COMPILER_VERSION = "0.1.5";
|
|
1762
1785
|
var IR_MAX_BYTES = 256 * 1024;
|
|
1763
1786
|
var CSS_MAX_BYTES = 64 * 1024;
|
|
1764
1787
|
var STRICT_CODES = /* @__PURE__ */ new Set(["dead-text", "hardcoded-color", "hardcoded-radius", "hardcoded-font", "hardcoded-image", "img-alt", "props-optional", "tailwind-skipped"]);
|
|
@@ -2313,6 +2336,12 @@ function editUrlPillHtml(field, value, anchor = "below", extraClass = "") {
|
|
|
2313
2336
|
return `<div data-edit-reveal data-anchor="${escAttr(anchor)}" class="edit-url-pill${extraClass ? " " + escAttr(extraClass) : ""}" style="display:none;position:absolute;z-index:10;pointer-events:auto;${pos}"><div class="edit-url-pill__inner inline-flex items-center gap-1.5 px-2.5 py-1 rounded-md border border-dashed text-xs whitespace-nowrap shadow-sm" style="color:var(--color-background,#fff);border-color:color-mix(in srgb,var(--color-foreground,#000) 40%,transparent);background-color:var(--color-foreground,#000);text-transform:none;letter-spacing:normal;"><svg class="w-3.5 h-3.5 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" d="M13.19 8.688a4.5 4.5 0 011.242 7.244l-4.5 4.5a4.5 4.5 0 01-6.364-6.364l1.757-1.757m13.35-.622l1.757-1.757a4.5 4.5 0 00-6.364-6.364l-4.5 4.5a4.5 4.5 0 001.242 7.244" /></svg><span data-edit-field="${escAttr(field)}" class="font-mono break-all">${escText(value)}</span></div></div>`;
|
|
2314
2337
|
}
|
|
2315
2338
|
__name(editUrlPillHtml, "editUrlPillHtml");
|
|
2339
|
+
function addImageButtonHtml(path, label = "Tambah Gambar", mode = "inline") {
|
|
2340
|
+
const btn = /* @__PURE__ */ __name((cls) => `<button type="button" class="${escAttr(cls)}" data-edit-add-image="${escAttr(path)}" aria-label="${escAttr(label)}"><span class="webto-inline-add-icon">+</span>${escText(label)}</button>`, "btn");
|
|
2341
|
+
if (mode === "floating") return btn("webto-inline-add-btn absolute bottom-8 right-8 z-30");
|
|
2342
|
+
return `<div class="webto-add-row mt-8">${btn("webto-inline-add-btn")}</div>`;
|
|
2343
|
+
}
|
|
2344
|
+
__name(addImageButtonHtml, "addImageButtonHtml");
|
|
2316
2345
|
function renderComponent(n, scope, ctx, budget) {
|
|
2317
2346
|
const p = {};
|
|
2318
2347
|
for (const [k, v] of Object.entries(n.props)) p[k] = attrValue(v, scope, budget);
|
|
@@ -2327,6 +2356,13 @@ function renderComponent(n, scope, ctx, budget) {
|
|
|
2327
2356
|
}
|
|
2328
2357
|
case "EditUrlPill":
|
|
2329
2358
|
return editUrlPillHtml(stringify(p.field), stringify(p.value), stringify(p.anchor) || "below", flattenClassList(p.class));
|
|
2359
|
+
case "AddImageButton": {
|
|
2360
|
+
if (ctx.marks) ctx.marks.addImageButton = true;
|
|
2361
|
+
if (!ctx.editChrome || p.visible === false) return "";
|
|
2362
|
+
const path = stringify(p.path);
|
|
2363
|
+
if (!path) return "";
|
|
2364
|
+
return addImageButtonHtml(path, stringify(p.label) || "Tambah Gambar", p.mode === "floating" ? "floating" : "inline");
|
|
2365
|
+
}
|
|
2330
2366
|
case "ViewMoreLink": {
|
|
2331
2367
|
const url = stringify(p.url);
|
|
2332
2368
|
if (!url) return "";
|
|
@@ -2414,12 +2450,34 @@ function renderVariant(ir, content, ctx, opts = {}) {
|
|
|
2414
2450
|
scope[p.name] = v === void 0 && p.def ? evalExpr(p.def, scope, budget) : v;
|
|
2415
2451
|
}
|
|
2416
2452
|
for (const c of ir.consts) scope[c.name] = evalExpr(c.e, scope, budget);
|
|
2417
|
-
const
|
|
2453
|
+
const rctx = { ...ctx, marks: { addImageButton: false } };
|
|
2454
|
+
const html = withAddImageFallback(renderNodes(ir.root, scope, rctx, budget), content, rctx);
|
|
2418
2455
|
const css = ir.css ? layerize(ir.css.replace(/__WV_ROOT__/g, `[data-wv-inst="${uid}"]`)) : "";
|
|
2419
2456
|
const script = ir.script ? wrapScript(ir.script, uid) : "";
|
|
2420
2457
|
return { html, css, script, uid };
|
|
2421
2458
|
}
|
|
2422
2459
|
__name(renderVariant, "renderVariant");
|
|
2460
|
+
var EDIT_IMAGE_ITEM_RE = /data-edit-image="([A-Za-z_]\w*)\.(\d+)\.([A-Za-z_]\w*)"/g;
|
|
2461
|
+
function withAddImageFallback(html, content, ctx) {
|
|
2462
|
+
if (!ctx.editChrome || ctx.marks?.addImageButton || html.includes("data-edit-add-image=")) return html;
|
|
2463
|
+
const lists = /* @__PURE__ */ new Map();
|
|
2464
|
+
for (const m of html.matchAll(EDIT_IMAGE_ITEM_RE)) {
|
|
2465
|
+
const [, list, index, key] = m;
|
|
2466
|
+
const cur = lists.get(list) ?? { key, hits: 0, maxIndex: -1 };
|
|
2467
|
+
cur.hits += 1;
|
|
2468
|
+
cur.maxIndex = Math.max(cur.maxIndex, Number(index));
|
|
2469
|
+
lists.set(list, cur);
|
|
2470
|
+
}
|
|
2471
|
+
let best = null;
|
|
2472
|
+
for (const [list, v] of lists) if (!best || v.hits > best.hits) best = { list, ...v };
|
|
2473
|
+
if (!best) return html;
|
|
2474
|
+
const stored = content[best.list];
|
|
2475
|
+
const length = Array.isArray(stored) ? stored.length : best.maxIndex + 1;
|
|
2476
|
+
const pill = addImageButtonHtml(`${best.list}.${length}.${best.key}`);
|
|
2477
|
+
const tail = html.lastIndexOf("</section>");
|
|
2478
|
+
return tail >= 0 ? html.slice(0, tail) + pill + html.slice(tail) : html + pill;
|
|
2479
|
+
}
|
|
2480
|
+
__name(withAddImageFallback, "withAddImageFallback");
|
|
2423
2481
|
var STATEMENT_AT = /^\s*@[a-zA-Z-]+[^;{}]*;/;
|
|
2424
2482
|
function layerize(css) {
|
|
2425
2483
|
let rest = css;
|
package/dist/index.js
CHANGED
|
@@ -3,7 +3,7 @@ var __defProp = Object.defineProperty;
|
|
|
3
3
|
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
4
4
|
|
|
5
5
|
// ../variant-compiler/src/ir.ts
|
|
6
|
-
var COMPONENT_NAMES = ["Container", "Button", "EditUrlPill", "ViewMoreLink"];
|
|
6
|
+
var COMPONENT_NAMES = ["Container", "Button", "EditUrlPill", "ViewMoreLink", "AddImageButton"];
|
|
7
7
|
|
|
8
8
|
// ../variant-compiler/src/parse/parser.ts
|
|
9
9
|
var ParseError = class extends Error {
|
|
@@ -600,9 +600,10 @@ var HELPER_BY_IMPORT = {
|
|
|
600
600
|
Container: "Container",
|
|
601
601
|
Button: "Button",
|
|
602
602
|
EditUrlPill: "EditUrlPill",
|
|
603
|
-
ViewMoreLink: "ViewMoreLink"
|
|
603
|
+
ViewMoreLink: "ViewMoreLink",
|
|
604
|
+
AddImageButton: "AddImageButton"
|
|
604
605
|
};
|
|
605
|
-
var ALLOWED_SOURCE = /^(webto\/variant|(\.{1,2}\/)+(lib\/(sanitize|image|section-i18n)|(components\/)?(v2\/)?(ui\/)?(Container|Button|ViewMoreLink)\.astro|(components\/)?EditUrlPill\.astro|ui\/(Container|Button)\.astro|ViewMoreLink\.astro))$/;
|
|
606
|
+
var ALLOWED_SOURCE = /^(webto\/variant|(\.{1,2}\/)+(lib\/(sanitize|image|section-i18n)|(components\/)?(v2\/)?(ui\/)?(Container|Button|ViewMoreLink|AddImageButton)\.astro|(components\/)?EditUrlPill\.astro|ui\/(Container|Button)\.astro|ViewMoreLink\.astro))$/;
|
|
606
607
|
function stripComments(src) {
|
|
607
608
|
return src.replace(/\/\*[\s\S]*?\*\//g, (m) => m.replace(/[^\n]/g, " ")).replace(/(^|[^:\\])\/\/[^\n]*/g, (m, pre) => pre + " ".repeat(m.length - pre.length));
|
|
608
609
|
}
|
|
@@ -741,7 +742,7 @@ function parseFrontmatter(raw, lineOffset = 0) {
|
|
|
741
742
|
const [, isType, def, named, source] = m;
|
|
742
743
|
i += m[0].length;
|
|
743
744
|
if (isType) continue;
|
|
744
|
-
if (!ALLOWED_SOURCE.test(source)) throw new ParseError(`import from '${source}' is not allowed \u2014 only "webto/variant" helpers (t, img, url, Container, Button, EditUrlPill, ViewMoreLink)`, line, "import");
|
|
745
|
+
if (!ALLOWED_SOURCE.test(source)) throw new ParseError(`import from '${source}' is not allowed \u2014 only "webto/variant" helpers (t, img, url, Container, Button, EditUrlPill, ViewMoreLink, AddImageButton)`, line, "import");
|
|
745
746
|
const names = [];
|
|
746
747
|
if (def) names.push({ local: def, imported: def });
|
|
747
748
|
if (named) {
|
|
@@ -1150,6 +1151,13 @@ function lintNodes(nodes, ctx, ariaHidden = false) {
|
|
|
1150
1151
|
const hidden = ariaHidden || staticValue(n.attrs["aria-hidden"]) === "true";
|
|
1151
1152
|
for (const [k, v] of Object.entries(n.attrs)) lintAttr(k, v, tag, ctx);
|
|
1152
1153
|
if (tag === "img" && !("alt" in n.attrs)) push(ctx, "warning", "img-alt", "<img> without alt");
|
|
1154
|
+
if (/^h[1-4]$/.test(tag)) {
|
|
1155
|
+
const cls = staticValue(n.attrs.class);
|
|
1156
|
+
const style = staticValue(n.attrs.style);
|
|
1157
|
+
const pinned = typeof style === "string" && /font-size\s*:/i.test(style);
|
|
1158
|
+
const inert = typeof cls === "string" && !pinned ? cls.split(/\s+/).filter((c) => HEADING_SIZE_CLASS.test(c)) : [];
|
|
1159
|
+
if (inert.length) push(ctx, "warning", "heading-size-inert", `${inert.join(" ")} on <${tag}> has no effect: h1-h4 sizes follow the site theme (Style tab). Keep it as intent, pin the size with !${inert[0]} (honored everywhere, never rescaled by theme or per-section settings) or an inline style, or use a non-heading tag to follow the body scale`);
|
|
1160
|
+
}
|
|
1153
1161
|
if (tag === "a") {
|
|
1154
1162
|
const target = staticValue(n.attrs.target);
|
|
1155
1163
|
if (target === "_blank" && !("rel" in n.attrs)) n.attrs.rel = "noopener";
|
|
@@ -1204,6 +1212,7 @@ function checkUrl(name, v, tag, ctx) {
|
|
|
1204
1212
|
if (/^https?:\/\//i.test(s) && (name === "src" || name === "poster")) push(ctx, "warning", "hardcoded-image", `<${tag} ${name}> hardcodes an external URL \u2014 images should come from a content field`);
|
|
1205
1213
|
}
|
|
1206
1214
|
__name(checkUrl, "checkUrl");
|
|
1215
|
+
var HEADING_SIZE_CLASS = /^(?:[\w-]+:)*text-(?:xs|sm|base|lg|xl|\dxl|\[[^\]]+\])$/;
|
|
1207
1216
|
function lintHtml(root, sectionType) {
|
|
1208
1217
|
const ctx = { lint: [], sectionType, inButton: 0, inSvg: 0 };
|
|
1209
1218
|
lintNodes(root, ctx);
|
|
@@ -1509,18 +1518,18 @@ __name(hasBreak, "hasBreak");
|
|
|
1509
1518
|
|
|
1510
1519
|
// ../variant-compiler/src/css/scope.ts
|
|
1511
1520
|
var ROOT_PLACEHOLDER = "__WV_ROOT__";
|
|
1512
|
-
var RECURSE_AT = /^@(media|supports|container|layer|scope|document)\b/i;
|
|
1513
1521
|
var VERBATIM_AT = /^@(keyframes|-webkit-keyframes|property|font-face|page|counter-style|font-feature-values|namespace|charset)\b/i;
|
|
1514
1522
|
function scopeCss(css, root = ROOT_PLACEHOLDER) {
|
|
1515
|
-
return scopeBlock(stripComments2(css), root);
|
|
1523
|
+
return scopeBlock(stripComments2(css), root, null);
|
|
1516
1524
|
}
|
|
1517
1525
|
__name(scopeCss, "scopeCss");
|
|
1518
1526
|
function stripComments2(s) {
|
|
1519
1527
|
return s.replace(/\/\*[\s\S]*?\*\//g, "");
|
|
1520
1528
|
}
|
|
1521
1529
|
__name(stripComments2, "stripComments");
|
|
1522
|
-
function scopeBlock(src, root) {
|
|
1530
|
+
function scopeBlock(src, root, parents) {
|
|
1523
1531
|
let out = "";
|
|
1532
|
+
let decls = "";
|
|
1524
1533
|
let i = 0;
|
|
1525
1534
|
while (i < src.length) {
|
|
1526
1535
|
let j = i;
|
|
@@ -1537,7 +1546,10 @@ function scopeBlock(src, root) {
|
|
|
1537
1546
|
}
|
|
1538
1547
|
const prelude = src.slice(i, j).trim();
|
|
1539
1548
|
if (j >= src.length) {
|
|
1540
|
-
if (prelude)
|
|
1549
|
+
if (prelude) {
|
|
1550
|
+
if (parents) decls += `${prelude};`;
|
|
1551
|
+
else out += prelude;
|
|
1552
|
+
}
|
|
1541
1553
|
break;
|
|
1542
1554
|
}
|
|
1543
1555
|
if (src[j] === "}") {
|
|
@@ -1545,7 +1557,10 @@ function scopeBlock(src, root) {
|
|
|
1545
1557
|
continue;
|
|
1546
1558
|
}
|
|
1547
1559
|
if (src[j] === ";") {
|
|
1548
|
-
if (prelude)
|
|
1560
|
+
if (prelude) {
|
|
1561
|
+
if (parents && !prelude.startsWith("@")) decls += `${prelude};`;
|
|
1562
|
+
else out += `${prelude};`;
|
|
1563
|
+
}
|
|
1549
1564
|
i = j + 1;
|
|
1550
1565
|
continue;
|
|
1551
1566
|
}
|
|
@@ -1555,21 +1570,28 @@ function scopeBlock(src, root) {
|
|
|
1555
1570
|
if (!prelude) continue;
|
|
1556
1571
|
if (prelude.startsWith("@")) {
|
|
1557
1572
|
if (VERBATIM_AT.test(prelude)) out += `${prelude}{${body}}`;
|
|
1558
|
-
else
|
|
1559
|
-
else out += `${prelude}{${scopeBlock(body, root)}}`;
|
|
1573
|
+
else out += `${prelude}{${scopeBlock(body, root, parents)}}`;
|
|
1560
1574
|
continue;
|
|
1561
1575
|
}
|
|
1562
|
-
const
|
|
1576
|
+
const raw = splitSelectors(prelude).map((s) => s.trim()).filter(Boolean);
|
|
1577
|
+
const selectors = parents ? resolveNested(raw, parents) : raw.map((s) => scopeSelector(s, root)).filter(Boolean);
|
|
1563
1578
|
if (!selectors.length) continue;
|
|
1564
|
-
|
|
1565
|
-
out += `${selectors.join(",")}{${scopeBlock(body, root)}}`;
|
|
1566
|
-
} else {
|
|
1567
|
-
out += `${selectors.join(",")}{${body.trim()}}`;
|
|
1568
|
-
}
|
|
1579
|
+
out += scopeBlock(body, root, selectors);
|
|
1569
1580
|
}
|
|
1581
|
+
if (parents && decls) out = `${parents.join(",")}{${decls}}` + out;
|
|
1570
1582
|
return out;
|
|
1571
1583
|
}
|
|
1572
1584
|
__name(scopeBlock, "scopeBlock");
|
|
1585
|
+
function resolveNested(selectors, parents) {
|
|
1586
|
+
const parentRef = parents.length === 1 ? parents[0] : `:is(${parents.join(", ")})`;
|
|
1587
|
+
const out = [];
|
|
1588
|
+
for (const sel of selectors) {
|
|
1589
|
+
if (sel.includes("&")) out.push(sel.replace(/&/g, parentRef));
|
|
1590
|
+
else for (const p of parents) out.push(`${p} ${sel}`);
|
|
1591
|
+
}
|
|
1592
|
+
return out;
|
|
1593
|
+
}
|
|
1594
|
+
__name(resolveNested, "resolveNested");
|
|
1573
1595
|
function matchBrace2(src, open) {
|
|
1574
1596
|
let depth = 0;
|
|
1575
1597
|
let inStr = null;
|
|
@@ -1614,7 +1636,7 @@ function splitSelectors(s) {
|
|
|
1614
1636
|
__name(splitSelectors, "splitSelectors");
|
|
1615
1637
|
function scopeSelector(sel, root) {
|
|
1616
1638
|
if (!sel) return "";
|
|
1617
|
-
if (sel.startsWith("&")) return `${root}
|
|
1639
|
+
if (sel.startsWith("&")) return `${root}${sel.slice(1)}`.trim();
|
|
1618
1640
|
if (sel.includes(ROOT_PLACEHOLDER)) return sel;
|
|
1619
1641
|
const m = /^(:root|html|body)(?![\w-])/i.exec(sel);
|
|
1620
1642
|
if (m) {
|
|
@@ -1690,7 +1712,8 @@ var MACRO_CLASSES = [
|
|
|
1690
1712
|
"text-foreground hover:bg-muted",
|
|
1691
1713
|
"px-4 py-2 text-sm px-6 py-3 text-base px-8 py-4 text-lg",
|
|
1692
1714
|
"gap-1.5 px-2.5 py-1 rounded-md border-dashed text-xs whitespace-nowrap font-mono break-all w-3.5 h-3.5 shrink-0",
|
|
1693
|
-
"mt-10 flex justify-start justify-center justify-end hover:border-primary/30 text-primary hover:underline w-4 h-4 relative"
|
|
1715
|
+
"mt-10 flex justify-start justify-center justify-end hover:border-primary/30 text-primary hover:underline w-4 h-4 relative",
|
|
1716
|
+
"mt-8 absolute bottom-8 right-8 z-30"
|
|
1694
1717
|
];
|
|
1695
1718
|
function pickCandidates(tokens) {
|
|
1696
1719
|
const set = /* @__PURE__ */ new Set();
|
|
@@ -1755,7 +1778,7 @@ function deriveSchema(fields, props, base) {
|
|
|
1755
1778
|
__name(deriveSchema, "deriveSchema");
|
|
1756
1779
|
|
|
1757
1780
|
// ../variant-compiler/src/index.ts
|
|
1758
|
-
var COMPILER_VERSION = "0.1.
|
|
1781
|
+
var COMPILER_VERSION = "0.1.5";
|
|
1759
1782
|
var IR_MAX_BYTES = 256 * 1024;
|
|
1760
1783
|
var CSS_MAX_BYTES = 64 * 1024;
|
|
1761
1784
|
var STRICT_CODES = /* @__PURE__ */ new Set(["dead-text", "hardcoded-color", "hardcoded-radius", "hardcoded-font", "hardcoded-image", "img-alt", "props-optional", "tailwind-skipped"]);
|
|
@@ -2310,6 +2333,12 @@ function editUrlPillHtml(field, value, anchor = "below", extraClass = "") {
|
|
|
2310
2333
|
return `<div data-edit-reveal data-anchor="${escAttr(anchor)}" class="edit-url-pill${extraClass ? " " + escAttr(extraClass) : ""}" style="display:none;position:absolute;z-index:10;pointer-events:auto;${pos}"><div class="edit-url-pill__inner inline-flex items-center gap-1.5 px-2.5 py-1 rounded-md border border-dashed text-xs whitespace-nowrap shadow-sm" style="color:var(--color-background,#fff);border-color:color-mix(in srgb,var(--color-foreground,#000) 40%,transparent);background-color:var(--color-foreground,#000);text-transform:none;letter-spacing:normal;"><svg class="w-3.5 h-3.5 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" d="M13.19 8.688a4.5 4.5 0 011.242 7.244l-4.5 4.5a4.5 4.5 0 01-6.364-6.364l1.757-1.757m13.35-.622l1.757-1.757a4.5 4.5 0 00-6.364-6.364l-4.5 4.5a4.5 4.5 0 001.242 7.244" /></svg><span data-edit-field="${escAttr(field)}" class="font-mono break-all">${escText(value)}</span></div></div>`;
|
|
2311
2334
|
}
|
|
2312
2335
|
__name(editUrlPillHtml, "editUrlPillHtml");
|
|
2336
|
+
function addImageButtonHtml(path, label = "Tambah Gambar", mode = "inline") {
|
|
2337
|
+
const btn = /* @__PURE__ */ __name((cls) => `<button type="button" class="${escAttr(cls)}" data-edit-add-image="${escAttr(path)}" aria-label="${escAttr(label)}"><span class="webto-inline-add-icon">+</span>${escText(label)}</button>`, "btn");
|
|
2338
|
+
if (mode === "floating") return btn("webto-inline-add-btn absolute bottom-8 right-8 z-30");
|
|
2339
|
+
return `<div class="webto-add-row mt-8">${btn("webto-inline-add-btn")}</div>`;
|
|
2340
|
+
}
|
|
2341
|
+
__name(addImageButtonHtml, "addImageButtonHtml");
|
|
2313
2342
|
function renderComponent(n, scope, ctx, budget) {
|
|
2314
2343
|
const p = {};
|
|
2315
2344
|
for (const [k, v] of Object.entries(n.props)) p[k] = attrValue(v, scope, budget);
|
|
@@ -2324,6 +2353,13 @@ function renderComponent(n, scope, ctx, budget) {
|
|
|
2324
2353
|
}
|
|
2325
2354
|
case "EditUrlPill":
|
|
2326
2355
|
return editUrlPillHtml(stringify(p.field), stringify(p.value), stringify(p.anchor) || "below", flattenClassList(p.class));
|
|
2356
|
+
case "AddImageButton": {
|
|
2357
|
+
if (ctx.marks) ctx.marks.addImageButton = true;
|
|
2358
|
+
if (!ctx.editChrome || p.visible === false) return "";
|
|
2359
|
+
const path = stringify(p.path);
|
|
2360
|
+
if (!path) return "";
|
|
2361
|
+
return addImageButtonHtml(path, stringify(p.label) || "Tambah Gambar", p.mode === "floating" ? "floating" : "inline");
|
|
2362
|
+
}
|
|
2327
2363
|
case "ViewMoreLink": {
|
|
2328
2364
|
const url = stringify(p.url);
|
|
2329
2365
|
if (!url) return "";
|
|
@@ -2411,12 +2447,34 @@ function renderVariant(ir, content, ctx, opts = {}) {
|
|
|
2411
2447
|
scope[p.name] = v === void 0 && p.def ? evalExpr(p.def, scope, budget) : v;
|
|
2412
2448
|
}
|
|
2413
2449
|
for (const c of ir.consts) scope[c.name] = evalExpr(c.e, scope, budget);
|
|
2414
|
-
const
|
|
2450
|
+
const rctx = { ...ctx, marks: { addImageButton: false } };
|
|
2451
|
+
const html = withAddImageFallback(renderNodes(ir.root, scope, rctx, budget), content, rctx);
|
|
2415
2452
|
const css = ir.css ? layerize(ir.css.replace(/__WV_ROOT__/g, `[data-wv-inst="${uid}"]`)) : "";
|
|
2416
2453
|
const script = ir.script ? wrapScript(ir.script, uid) : "";
|
|
2417
2454
|
return { html, css, script, uid };
|
|
2418
2455
|
}
|
|
2419
2456
|
__name(renderVariant, "renderVariant");
|
|
2457
|
+
var EDIT_IMAGE_ITEM_RE = /data-edit-image="([A-Za-z_]\w*)\.(\d+)\.([A-Za-z_]\w*)"/g;
|
|
2458
|
+
function withAddImageFallback(html, content, ctx) {
|
|
2459
|
+
if (!ctx.editChrome || ctx.marks?.addImageButton || html.includes("data-edit-add-image=")) return html;
|
|
2460
|
+
const lists = /* @__PURE__ */ new Map();
|
|
2461
|
+
for (const m of html.matchAll(EDIT_IMAGE_ITEM_RE)) {
|
|
2462
|
+
const [, list, index, key] = m;
|
|
2463
|
+
const cur = lists.get(list) ?? { key, hits: 0, maxIndex: -1 };
|
|
2464
|
+
cur.hits += 1;
|
|
2465
|
+
cur.maxIndex = Math.max(cur.maxIndex, Number(index));
|
|
2466
|
+
lists.set(list, cur);
|
|
2467
|
+
}
|
|
2468
|
+
let best = null;
|
|
2469
|
+
for (const [list, v] of lists) if (!best || v.hits > best.hits) best = { list, ...v };
|
|
2470
|
+
if (!best) return html;
|
|
2471
|
+
const stored = content[best.list];
|
|
2472
|
+
const length = Array.isArray(stored) ? stored.length : best.maxIndex + 1;
|
|
2473
|
+
const pill = addImageButtonHtml(`${best.list}.${length}.${best.key}`);
|
|
2474
|
+
const tail = html.lastIndexOf("</section>");
|
|
2475
|
+
return tail >= 0 ? html.slice(0, tail) + pill + html.slice(tail) : html + pill;
|
|
2476
|
+
}
|
|
2477
|
+
__name(withAddImageFallback, "withAddImageFallback");
|
|
2420
2478
|
var STATEMENT_AT = /^\s*@[a-zA-Z-]+[^;{}]*;/;
|
|
2421
2479
|
function layerize(css) {
|
|
2422
2480
|
let rest = css;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webto-id/variant-check",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"description": "Lint, compile and preview a Webto Variant Format (.astro subset) section variant with the exact compiler the webto.id marketplace uses.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|