mosaic-headless 1.2.1 → 1.3.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.
@@ -140,6 +140,10 @@ class Surface:
140
140
  # string to them is accepted and then silently produces `transform:none`, `box-shadow:none`
141
141
  # or no rule at all - so these shorthands exist to make the correct shape unavoidable.
142
142
  # Each was confirmed against the compiled CSS; see references/styling.md.
143
+
144
+ # A fixed namespace, so a design token's row ID is a function of its name alone and
145
+ # a rebuild rebinds the same row instead of adding a second one.
146
+ VAR_NAMESPACE = uuid.UUID("6d6f7361-6963-4865-6164-6c657373ff01")
143
147
  VAR_IDS = {}
144
148
 
145
149
 
@@ -245,8 +249,27 @@ def theme_records(spec, doc, surface):
245
249
  if not (collection and mode and skin):
246
250
  sys.exit("theme.variables needs a healed collection/mode/skin")
247
251
  recs = []
252
+ # The ID is DERIVED from the token name, not minted.
253
+ #
254
+ # It used to be a fresh `uuid4()` per run, and `_varIDs` starts empty every
255
+ # time, so each build wrote a NEW collectionVariable row and left the old one
256
+ # in place. The rows are parented to the theme's collection, which survives
257
+ # `build_site.py` making a fresh master, so they accumulate - and both end up
258
+ # in `:root`, where the later one wins. Measured: after changing `--mk-faint`
259
+ # from rgb(160,162,168) to rgb(107,109,113), the delivered stylesheet carried
260
+ #
261
+ # --mk-faint: rgb(107, 109, 113)
262
+ # --mk-faint: rgb(160, 162, 168)
263
+ #
264
+ # and the page kept rendering the old grey. Every server-side check passed:
265
+ # the commit succeeded, the new variable existed, its value was correct. Only
266
+ # `verify_browser.py`, reading the computed colour off the element, saw it.
267
+ #
268
+ # A UUIDv5 over the custom property is stable across runs and across machines
269
+ # with no state to carry, so a rebuild REBINDS the row instead of adding one.
248
270
  for custom_property, value in variables.items():
249
- vid = spec.setdefault("_varIDs", {}).setdefault(custom_property, str(uuid.uuid4()))
271
+ vid = str(uuid.uuid5(VAR_NAMESPACE, custom_property))
272
+ spec.setdefault("_varIDs", {})[custom_property] = vid
250
273
  recs.append({"newRevisionRecord": {
251
274
  "ID": vid, "parentType": "collection", "parentID": collection,
252
275
  "ordering": "a0", "status": "publish", "revision": "", "version": "",
@@ -255,6 +278,16 @@ def theme_records(spec, doc, surface):
255
278
  "customProperty": custom_property,
256
279
  "skinsData": {skin: {mode: {"value": value["value"]}}}}},
257
280
  "originalRevisionRecord": None})
281
+ # Reap strays from before the ID became derivable. Anything on this
282
+ # collection that claims a custom property we manage, under an ID we did not
283
+ # derive, is a leftover that can still win the cascade.
284
+ managed = {v["ID"] for v in (r["newRevisionRecord"] for r in recs)}
285
+ for row in (doc.get("collectionVariable") or []):
286
+ cp = (row.get("data") or {}).get("customProperty")
287
+ if cp in variables and row["ID"] not in managed:
288
+ print(" reaping stale variable %s (%s)" % (cp, row["ID"][:8]))
289
+ recs.append({"newRevisionRecord": dict(row, status="delete"),
290
+ "originalRevisionRecord": row})
258
291
  out["collectionVariable"] = recs
259
292
  # register the IDs before element-class styles are expanded - those cite
260
293
  # tokens too, and expand_shorthands resolves {"token": ...} from this map
@@ -78,10 +78,21 @@ def extract(plugin_root, out_dir):
78
78
  relpath = rel(path, plugin_root)
79
79
  # the data class declaring this type's own properties sits beside the factory;
80
80
  # node-properties.csv is keyed by that class name, so record it to make the join
81
- data_class = ""
81
+ #
82
+ # Its OWN parent has to be recorded here too. A data class that declares no
83
+ # properties of its own writes no row into node-properties.csv, so the
84
+ # `extends` column there cannot tell you what it inherits - and 64 of the 122
85
+ # types are exactly that case. Without this column a lookup for
86
+ # `accordion-content` reports zero properties, when in fact it has the nine
87
+ # every element has. An empty answer that looks like a real one is the
88
+ # failure this whole skill argues against, so read the parent off the class.
89
+ data_class, data_extends = "", ""
82
90
  for sibling in sorted(os.listdir(os.path.dirname(path))):
83
91
  if sibling.endswith("MResourceData.php"):
84
92
  data_class = sibling[:-4]
93
+ dcls = RE_CLASS.search(read(os.path.join(os.path.dirname(path),
94
+ sibling)))
95
+ data_extends = dcls.group(3) if dcls else ""
85
96
  break
86
97
  bools = dict(RE_METHOD_BOOL.findall(src))
87
98
  alias = RE_ALIAS.search(src)
@@ -100,15 +111,26 @@ def extract(plugin_root, out_dir):
100
111
  "can_be_parent": bools.get("canBeParentFor", ""),
101
112
  "default_element_class": (dflt.group(1).strip() if dflt else ""),
102
113
  "data_class": data_class,
114
+ "data_extends": data_extends,
103
115
  "file": relpath,
104
116
  }
105
117
  )
106
118
 
119
+ # Every data class, whether or not it declares a property of its own. An abstract
120
+ # that adds nothing still sits in the chain - `ElementMResourceLoopDataAbstract`
121
+ # is one, and five node types inherit through it - so a hierarchy built only from
122
+ # classes that happen to own properties has holes exactly where the plain ones
123
+ # are. This file makes the walk total.
124
+ hierarchy = []
107
125
  props = []
108
126
  for path in sorted(walk(node_root, ("MResourceData.php", "MResourceDataAbstract.php"))):
109
127
  src = read(path)
110
128
  cls = RE_CLASS.search(src)
111
129
  owner = cls.group(2) if cls else os.path.basename(path)[:-4]
130
+ hierarchy.append({"class": owner,
131
+ "extends": cls.group(3) if cls else "",
132
+ "abstract": "yes" if (cls and cls.group(1)) else "no",
133
+ "file": rel(path, plugin_root)})
112
134
  relpath = rel(path, plugin_root)
113
135
  reserved = RE_RESERVED.search(src)
114
136
  reserved_attrs = re.findall(r"'([^']+)'", reserved.group(1)) if reserved else []
@@ -146,6 +168,9 @@ def extract(plugin_root, out_dir):
146
168
  os.makedirs(out_dir, exist_ok=True)
147
169
  write_csv(os.path.join(out_dir, "node-types.csv"), types)
148
170
  write_csv(os.path.join(out_dir, "node-properties.csv"), props)
171
+ write_csv(os.path.join(out_dir, "data-class-hierarchy.csv"),
172
+ sorted(hierarchy, key=lambda r: r["class"]))
173
+ print("data classes: %d" % len(hierarchy))
149
174
  print("node types: %d (%d pro)" % (len(types), sum(t["edition"] == "pro" for t in types)))
150
175
  print("node properties: %d across %d classes" % (len(props), len({p["owner_class"] for p in props})))
151
176