mosaic-headless 1.2.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.
Files changed (77) hide show
  1. package/LICENSE +21 -0
  2. package/README.ja.md +94 -0
  3. package/README.md +133 -0
  4. package/README.zh-CN.md +87 -0
  5. package/README.zh-TW.md +87 -0
  6. package/SKILL.md +353 -0
  7. package/assets/templates/platforms/claude-ai.json +34 -0
  8. package/assets/templates/platforms/claude-code.json +28 -0
  9. package/assets/templates/platforms/codex-cli.json +33 -0
  10. package/assets/templates/platforms/continue.json +31 -0
  11. package/assets/templates/platforms/copilot.json +46 -0
  12. package/assets/templates/platforms/cursor.json +36 -0
  13. package/assets/templates/platforms/gemini-cli.json +33 -0
  14. package/assets/templates/platforms/windsurf.json +34 -0
  15. package/bin/check-release.mjs +143 -0
  16. package/bin/install.mjs +452 -0
  17. package/bin/sync-version.mjs +63 -0
  18. package/data/animatable-properties.csv +23 -0
  19. package/data/condition-comparators.csv +13 -0
  20. package/data/condition-subjects.csv +60 -0
  21. package/data/db-columns.csv +207 -0
  22. package/data/default-children.csv +11 -0
  23. package/data/dynamic-variables.csv +75 -0
  24. package/data/element-classes.csv +152 -0
  25. package/data/evaluator-functions.csv +20 -0
  26. package/data/interaction-types.csv +13 -0
  27. package/data/node-properties.csv +182 -0
  28. package/data/node-property-verification.csv +182 -0
  29. package/data/node-types.csv +123 -0
  30. package/data/node-verification.csv +123 -0
  31. package/data/placement-rules.csv +123 -0
  32. package/data/pluggables.csv +208 -0
  33. package/data/property-verification.csv +171 -0
  34. package/data/rest-routes.csv +115 -0
  35. package/data/rwd-verification.csv +570 -0
  36. package/data/style-properties.csv +99 -0
  37. package/data/style-states.csv +54 -0
  38. package/data/style-value-shapes.csv +23 -0
  39. package/data/style-verification.csv +99 -0
  40. package/package.json +59 -0
  41. package/references/data-model.md +95 -0
  42. package/references/design-system.md +118 -0
  43. package/references/dynamic-content.md +113 -0
  44. package/references/failure-modes.md +182 -0
  45. package/references/interactions.md +126 -0
  46. package/references/placement.md +117 -0
  47. package/references/responsive.md +174 -0
  48. package/references/styling.md +172 -0
  49. package/references/templates-and-conditions.md +122 -0
  50. package/references/vs-elementor-gutenberg.md +73 -0
  51. package/references/write-protocol.md +79 -0
  52. package/sites/_moksa.py +1165 -0
  53. package/sites/moksa.json +8685 -0
  54. package/tools/bootstrap_probe_theme.php +68 -0
  55. package/tools/build_all.py +55 -0
  56. package/tools/build_page.py +352 -0
  57. package/tools/build_report.py +221 -0
  58. package/tools/build_site.py +174 -0
  59. package/tools/capture_live.py +130 -0
  60. package/tools/check_placement_predicts.py +72 -0
  61. package/tools/copy_styles.py +204 -0
  62. package/tools/extract_default_children.py +94 -0
  63. package/tools/extract_dynamic_variables.py +104 -0
  64. package/tools/extract_interactions.py +98 -0
  65. package/tools/extract_node_types.py +165 -0
  66. package/tools/extract_placement.py +135 -0
  67. package/tools/extract_pluggables.py +90 -0
  68. package/tools/extract_style_properties.py +163 -0
  69. package/tools/mint_session.php +52 -0
  70. package/tools/probe.py +144 -0
  71. package/tools/sweep_node_properties.py +271 -0
  72. package/tools/sweep_node_types.py +318 -0
  73. package/tools/sweep_properties.py +215 -0
  74. package/tools/sweep_style_properties.py +254 -0
  75. package/tools/theme_export.php +91 -0
  76. package/tools/theme_import.php +113 -0
  77. package/tools/verify_rwd.py +278 -0
@@ -0,0 +1,278 @@
1
+ #!/usr/bin/env python3
2
+ """Assert that every responsive value a site spec declares actually compiled.
3
+
4
+ python verify_rwd.py --config sweep.json --site ../sites/moksa.json
5
+ python verify_rwd.py --config sweep.json --site ../sites/moksa.json --csv rwd.csv
6
+
7
+ Why this exists
8
+ ---------------
9
+ A Mosaic breakpoint value is silently discarded in at least three ways, none of
10
+ which change the commit response, the page size, or anything you would notice by
11
+ looking at the page on a wide screen:
12
+
13
+ * the property name is not one Mosaic knows, so the whole declaration vanishes
14
+ * the value shape is wrong for that property, so the rule compiles to nothing
15
+ * the value is fine but you wrote it under a state where a breakpoint belongs
16
+
17
+ "I resized the browser and it looked right" catches none of those on the parts of
18
+ the page that happened to be off screen. So the check is mechanical: read what the
19
+ spec declares, read what the site actually served, and compare them key by key.
20
+
21
+ How it works
22
+ ------------
23
+ Mosaic compiles each breakpoint into its own inline stylesheet in the head:
24
+
25
+ <style id="mosaic-theme-block-editor-styles_-inline-css"> base, no media query
26
+ <style id="mosaic-theme-block-editor-styles_t-inline-css"> @media (max-width:1079px)
27
+ <style id="mosaic-theme-block-editor-styles_m-inline-css"> @media (max-width:767px)
28
+
29
+ Rules inside them hang off the generated `.M_EL<n>` class, never off the `attrID`
30
+ you wrote - so the tool reads `id="<attrID>" class="M_EL<n>"` out of the delivered
31
+ HTML to bridge the two. That mapping is the only reason a declaration in the spec
32
+ can be tied to a rule in the stylesheet.
33
+
34
+ What a row means
35
+ ----------------
36
+ verified the property is present in that breakpoint's rule for that element
37
+ MISSING declared in the spec, absent from the compiled breakpoint block
38
+ no-element the attrID never reached the HTML (the node did not render)
39
+ unmapped the tool has no CSS name for that style key, so it did not judge it
40
+
41
+ `unmapped` is reported, never counted as a pass. A tool that quietly scores its own
42
+ blind spots as successes is worse than no tool.
43
+
44
+ The second pass is a lint for one specific bug that has bitten this codebase twice:
45
+ a breakpoint override can only CHANGE a property, never REMOVE one the base
46
+ breakpoint declared. Writing narrow-screen `customStyles` that simply omit a border
47
+ leaves the wide-screen border standing, drawing rules in the middle of nowhere on a
48
+ collapsed layout. Every such omission is reported as RESET-RISK.
49
+ """
50
+ import argparse
51
+ import csv
52
+ import json
53
+ import os
54
+ import re
55
+ import sys
56
+ import urllib.request
57
+
58
+ BREAKPOINTS = {"_t": ("t", "max-width: 1079px"), "_m": ("m", "max-width: 767px")}
59
+
60
+ # Style keys whose CSS name is not just the kebab-case of the key. Anything absent
61
+ # from here is tried as kebab-case and reported `unmapped` if that finds nothing.
62
+ ALIASES = {
63
+ "gridCols": "grid-template-columns",
64
+ "radius": "border-radius",
65
+ "move": "transform",
66
+ "shadow": "box-shadow",
67
+ "transitionAll": "transition",
68
+ "objectFitStyle": "object-fit",
69
+ "backgroundStyle": "background-image",
70
+ }
71
+
72
+ # Keys that are not single CSS declarations and are handled separately.
73
+ RAW = {"customStyles"}
74
+
75
+
76
+ def kebab(name):
77
+ return re.sub(r"(?<!^)(?=[A-Z])", "-", name).lower()
78
+
79
+
80
+ def css_name(key):
81
+ return ALIASES.get(key, kebab(key))
82
+
83
+
84
+ def walk(node, out):
85
+ """Collect (attrID, breakpoint, key, value) for every responsive declaration."""
86
+ if isinstance(node, dict):
87
+ data = node.get("data")
88
+ attr = data.get("attrID") if isinstance(data, dict) else None
89
+ style = node.get("style")
90
+ state = style.get("&") if isinstance(style, dict) else None
91
+ state = state if isinstance(state, dict) else {}
92
+ if attr:
93
+ for bp_key, decls in state.items():
94
+ if bp_key in BREAKPOINTS and isinstance(decls, dict):
95
+ for key, value in decls.items():
96
+ out.append((attr, bp_key, key, value))
97
+ for v in node.values():
98
+ walk(v, out)
99
+ elif isinstance(node, list):
100
+ for v in node:
101
+ walk(v, out)
102
+ return out
103
+
104
+
105
+ def base_customstyles(node, out):
106
+ """Collect base-breakpoint customStyles per attrID, for the reset lint."""
107
+ if isinstance(node, dict):
108
+ data = node.get("data")
109
+ attr = data.get("attrID") if isinstance(data, dict) else None
110
+ style = node.get("style")
111
+ state = style.get("&") if isinstance(style, dict) else None
112
+ state = state if isinstance(state, dict) else {}
113
+ if attr and isinstance(state.get("_"), dict):
114
+ cs = state["_"].get("customStyles")
115
+ if cs:
116
+ out[attr] = cs
117
+ for v in node.values():
118
+ base_customstyles(v, out)
119
+ elif isinstance(node, list):
120
+ for v in node:
121
+ base_customstyles(v, out)
122
+ return out
123
+
124
+
125
+ def fetch(url):
126
+ req = urllib.request.Request(url, headers={
127
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/140.0 Safari/537.36"})
128
+ with urllib.request.urlopen(req, timeout=60) as r:
129
+ return r.read().decode("utf-8", "replace")
130
+
131
+
132
+ def class_map(html):
133
+ """attrID -> generated M_EL class. The stylesheet targets the class, not the id."""
134
+ out = {}
135
+ for m in re.finditer(r'id="([^"]+)"[^>]*class="(M_EL\d+)', html):
136
+ out[m.group(1)] = m.group(2)
137
+ for m in re.finditer(r'class="(M_EL\d+)[^"]*"[^>]*id="([^"]+)"', html):
138
+ out.setdefault(m.group(2), m.group(1))
139
+ return out
140
+
141
+
142
+ def breakpoint_rules(html, suffix):
143
+ """{M_EL class: {css property: value}} for one breakpoint's inline stylesheet."""
144
+ m = re.search(
145
+ r'<style id="mosaic-theme-block-editor-styles_%s-inline-css">(.*?)</style>'
146
+ % suffix, html, re.S)
147
+ if not m:
148
+ return None
149
+ css = m.group(1)
150
+ inner = re.search(r"@media[^{]*\{(.*)\}\s*$", css, re.S)
151
+ body = inner.group(1) if inner else css
152
+ rules = {}
153
+ for sel, decls in re.findall(r"([^{}]+)\{([^{}]*)\}", body):
154
+ props = {}
155
+ for decl in decls.split(";"):
156
+ if ":" in decl:
157
+ k, v = decl.split(":", 1)
158
+ props[k.strip()] = v.strip()
159
+ for one in sel.split(","):
160
+ one = one.strip()
161
+ cls = re.match(r"^\.(M_EL\d+)$", one)
162
+ if cls:
163
+ rules.setdefault(cls.group(1), {}).update(props)
164
+ return rules
165
+
166
+
167
+ def check_page(url, tree, rows):
168
+ html = fetch(url)
169
+ classes = class_map(html)
170
+ compiled = {}
171
+ for bp_key, (suffix, _q) in BREAKPOINTS.items():
172
+ r = breakpoint_rules(html, suffix)
173
+ if r is None:
174
+ print(" !! no %s stylesheet in the delivered page" % bp_key)
175
+ compiled[bp_key] = r or {}
176
+
177
+ for attr, bp_key, key, value in walk(tree, []):
178
+ cls = classes.get(attr)
179
+ if not cls:
180
+ rows.append([url, attr, bp_key, key, "", "", "no-element"])
181
+ continue
182
+ props = compiled[bp_key].get(cls, {})
183
+ if key in RAW:
184
+ # customStyles is raw CSS: every declaration in it must be present
185
+ for decl in str(value).split(";"):
186
+ if ":" not in decl:
187
+ continue
188
+ k, v = decl.split(":", 1)
189
+ k, v = k.strip(), v.strip()
190
+ got = props.get(k)
191
+ rows.append([url, attr, bp_key, k, v, got or "",
192
+ "verified" if got is not None else "MISSING"])
193
+ continue
194
+ name = css_name(key)
195
+ if name in props:
196
+ rows.append([url, attr, bp_key, name, _flat(value), props[name],
197
+ "verified"])
198
+ elif any(p.startswith(name) for p in props):
199
+ hit = next(p for p in props if p.startswith(name))
200
+ rows.append([url, attr, bp_key, name, _flat(value), props[hit],
201
+ "verified"])
202
+ elif isinstance(value, (dict, list)):
203
+ rows.append([url, attr, bp_key, name, _flat(value), "", "unmapped"])
204
+ else:
205
+ rows.append([url, attr, bp_key, name, _flat(value), "", "MISSING"])
206
+
207
+
208
+ def _flat(v):
209
+ return json.dumps(v, ensure_ascii=False) if isinstance(v, (dict, list)) else str(v)
210
+
211
+
212
+ def reset_lint(tree, rows):
213
+ """A breakpoint override changes a property; it cannot remove one.
214
+
215
+ If the base breakpoint's customStyles declares a border and the narrow-screen
216
+ customStyles for the same element does not mention it, the wide-screen border
217
+ survives into the collapsed layout. That is not a style question - it is the
218
+ single most repeated bug in this codebase.
219
+ """
220
+ base = base_customstyles(tree, {})
221
+ for attr, bp_key, key, value in walk(tree, []):
222
+ if key not in RAW or attr not in base:
223
+ continue
224
+ declared = {d.split(":", 1)[0].strip()
225
+ for d in str(base[attr]).split(";") if ":" in d}
226
+ overridden = {d.split(":", 1)[0].strip()
227
+ for d in str(value).split(";") if ":" in d}
228
+ for prop in sorted(declared - overridden):
229
+ if prop.startswith("border") or prop in ("padding-left", "padding-right"):
230
+ rows.append(["-", attr, bp_key, prop, "set at base", "not reset",
231
+ "RESET-RISK"])
232
+
233
+
234
+ def main():
235
+ ap = argparse.ArgumentParser()
236
+ ap.add_argument("--config", required=True)
237
+ ap.add_argument("--site", required=True)
238
+ ap.add_argument("--csv")
239
+ a = ap.parse_args()
240
+
241
+ cfg = json.load(open(a.config, encoding="utf-8"))
242
+ site = json.load(open(a.site, encoding="utf-8"))
243
+ base = cfg["base"].rstrip("/")
244
+
245
+ rows = []
246
+ for page in site["pages"]:
247
+ url = "%s/%s/" % (base, page["slug"])
248
+ print("checking", url)
249
+ check_page(url, page["tree"], rows)
250
+ check_page(url, site.get("shell", {}), rows)
251
+ reset_lint(page["tree"], rows)
252
+ reset_lint(site.get("shell", {}), rows)
253
+
254
+ counts = {}
255
+ for r in rows:
256
+ counts[r[6]] = counts.get(r[6], 0) + 1
257
+ print()
258
+ for k in ("verified", "MISSING", "no-element", "unmapped", "RESET-RISK"):
259
+ if counts.get(k):
260
+ print(" %-11s %d" % (k, counts[k]))
261
+
262
+ if a.csv:
263
+ path = a.csv if os.path.isabs(a.csv) else a.csv
264
+ with open(path, "w", newline="", encoding="utf-8") as fh:
265
+ w = csv.writer(fh)
266
+ w.writerow(["url", "attrID", "breakpoint", "property",
267
+ "declared", "compiled", "status"])
268
+ w.writerows(rows)
269
+ print("\nwrote", path)
270
+
271
+ bad = counts.get("MISSING", 0) + counts.get("RESET-RISK", 0)
272
+ if bad:
273
+ print("\n%d responsive declaration(s) did not survive the compile." % bad)
274
+ sys.exit(1 if bad else 0)
275
+
276
+
277
+ if __name__ == "__main__":
278
+ main()