From 13559a41f095b4f74acab67e589ad8bd8c21a248 Mon Sep 17 00:00:00 2001 From: Kinneyzhang Date: Fri, 28 Aug 2026 22:06:17 +0800 Subject: [PATCH] fix: retain only valid cascade winners --- docs/api.en.md | 1 + docs/api.zh.md | 1 + ecss-cascade.el | 34 +++++++++++++++++++++---- tests/ecss-cascade-tests.el | 49 +++++++++++++++++++++++++++++++++++++ 4 files changed, 80 insertions(+), 5 deletions(-) diff --git a/docs/api.en.md b/docs/api.en.md index ca40888..88751ee 100644 --- a/docs/api.en.md +++ b/docs/api.en.md @@ -54,6 +54,7 @@ Do not pass both `:stylesheet` and `:rules`. `:rules` accepts the rule list retu - `(ecss-computed-style-custom-properties STYLE)` returns a deterministic custom-property plist. - `(ecss-computed-style-active-properties STYLE)` returns property IDs supplied by valid declarations or active inheritance, preserving explicit nil versus absence. An invalid winner falling back to the initial value is not presented as an active declaration. - `(ecss-computed-style-specified-properties STYLE)` returns longhand property IDs that have cascade winners, independent of whether provenance was requested. +- `(ecss-computed-style-precedence-winner STYLE PROPERTIES)` returns the property ID whose already-selected declaration winner has the highest cascade precedence. It never reruns cascade or requires public provenance. - `(ecss-computed-style-copy-with-values STYLE VALUES &key active-properties specified-properties)` returns an immutable-style copy for a consumer that has already proved selector/cascade facts unchanged and only needs to replace computed values. - `(ecss-computed-style-provenance STYLE)` returns winner, origin, importance, layer, specificity, scope/source/declaration order, validity, and fallback facts. - `(ecss-computed-style-diagnostics STYLE)` returns deterministic diagnostics such as custom-property cycles. diff --git a/docs/api.zh.md b/docs/api.zh.md index 9901f45..f2c3646 100644 --- a/docs/api.zh.md +++ b/docs/api.zh.md @@ -54,6 +54,7 @@ ORIGIN 是 `ua`、`user`、`author`、`animation` 或 `transition`。Inline decl - `(ecss-computed-style-custom-properties STYLE)` 返回 deterministic custom-property plist。 - `(ecss-computed-style-active-properties STYLE)` 返回由有效 declaration 或 active inheritance 实际提供的 property ids,保留 explicit nil 与 absence 的区别;invalid winner 回退到 initial 时不伪装成 active declaration。 - `(ecss-computed-style-specified-properties STYLE)` 返回存在 cascade winner 的长手 property ids,独立于是否请求 provenance。 +- `(ecss-computed-style-precedence-winner STYLE PROPERTIES)` 返回这些 canonical property 已选 winner 中 cascade 优先级最高的 property ID;不会重跑 cascade,也不要求公开 provenance。 - `(ecss-computed-style-copy-with-values STYLE VALUES &key active-properties specified-properties)` 在 selector/cascade 事实已证明不变、只需替换 computed values 时返回新的 immutable-style copy。 - `(ecss-computed-style-provenance STYLE)` 返回 winner、origin、important、layer、specificity、scope/source/declaration order、valid/fallback 等事实。 - `(ecss-computed-style-diagnostics STYLE)` 返回 custom-property cycle 等 deterministic diagnostics。 diff --git a/ecss-cascade.el b/ecss-cascade.el index ad18c8c..51586c2 100644 --- a/ecss-cascade.el +++ b/ecss-cascade.el @@ -55,7 +55,7 @@ (:conc-name ecss--computed-style-)) "Computed declarations, custom properties, metadata, and diagnostics." schemas values custom-properties active-properties specified-properties - provenance diagnostics) + winners provenance diagnostics) (cl-defstruct (ecss-computed-property-fact (:constructor ecss--make-computed-property-fact)) @@ -1061,7 +1061,7 @@ handle explicit sources, and DIAGNOSTICS records variable failures." PARENT-STYLE and CUSTOM-TABLES supply inherited values. SUBJECT and RESOLVER handle explicit sources. DIAGNOSTICS records failures; PROVENANCE-P retains facts." - (let (values active specified provenance) + (let (values active specified winners provenance) (dolist (property (ecss--schema-set-order schemas)) (let ((schema (gethash property (ecss--schema-set-table schemas)))) (unless (ecss-property-schema-shorthand schema) @@ -1076,13 +1076,15 @@ facts." (ecss--parent-property-active-p parent-style property))) (push property active)) (when winner - (push property specified)) + (push property specified) + (unless invalid + (setq winners (plist-put winners property winner)))) (when provenance-p (setq provenance (plist-put provenance property (ecss--property-provenance winner invalid source)))))))) - (list values (nreverse active) (nreverse specified) provenance))) + (list values (nreverse active) (nreverse specified) winners provenance))) (defun ecss--validate-rule-declarations (schemas rule) "Validate canonical declarations in RULE against SCHEMAS." @@ -1143,7 +1145,7 @@ non-nil, is the only function allowed to evaluate caller-owned value sources; (custom (ecss--resolved-custom-properties (car raw-facts) diagnostics))) (let ((custom-tables (ecss--custom-value-tables custom))) - (pcase-let ((`(,values ,active ,specified ,property-facts) + (pcase-let ((`(,values ,active ,specified ,winners ,property-facts) (ecss--compute-property-values schemas table parent-style custom-tables subject value-resolver diagnostics provenance))) @@ -1151,6 +1153,7 @@ non-nil, is the only function allowed to evaluate caller-owned value sources; :schemas schemas :values values :custom-properties custom :active-properties active :specified-properties specified + :winners winners :provenance (and provenance (append (cadr raw-facts) property-facts)) :diagnostics (nreverse (car diagnostics)))))))) @@ -1207,6 +1210,27 @@ the public boundary." (ecss--copy-boundary-data (ecss--computed-style-specified-properties style))) +(defun ecss-computed-style-precedence-winner (style properties) + "Return the highest-precedence declared property in PROPERTIES for STYLE. +PROPERTIES may use canonical IDs or aliases from STYLE's schema set. Nil is +returned when none has a cascade winner. The comparison reuses ECSS's exact +cascade ordering and does not require public provenance materialization." + (setq style (ecss--computed-style-check style)) + (let ((schemas (ecss--computed-style-schemas style)) + (winners (ecss--computed-style-winners style)) + winner-property winner) + (dolist (property properties winner-property) + (let* ((canonical + (or (ecss-schema-set-canonical-id schemas property) + (signal 'ecss-invalid-declaration + (list :unknown-property property)))) + (candidate (plist-get winners canonical))) + (when (and candidate + (or (null winner) + (ecss--candidate-higher-p candidate winner))) + (setq winner-property canonical + winner candidate)))))) + (defun ecss-computed-style-provenance (style) "Return defensive winner provenance from computed STYLE." (ecss--computed-style-check style) diff --git a/tests/ecss-cascade-tests.el b/tests/ecss-cascade-tests.el index 1549a0b..7bfe000 100644 --- a/tests/ecss-cascade-tests.el +++ b/tests/ecss-cascade-tests.el @@ -751,6 +751,55 @@ (should (equal (ecss-computed-style-specified-properties declared) '(app/color app/width app/payload))))) +(ert-deftest ecss-cascade-test-cross-property-projection-reuses-cascade-order () + "A consumer projection should choose among property winners without recascade." + (let* ((schemas (ecss-cascade-test--schemas)) + (stylesheet (ecss-stylesheet-create)) + (subject (ecss-cascade-test--subject + :id "hero" :classes '("target")))) + (ecss-stylesheet-add-rule + stylesheet schemas ".target" '(app/padding-left 1)) + (ecss-stylesheet-add-rule + stylesheet schemas "#hero" '(app/width 2)) + (let* ((ruled (ecss-compute-style + schemas subject :stylesheet stylesheet)) + (inline (ecss-compute-style + schemas subject :stylesheet stylesheet + :declarations '(app/padding-left 3))) + (copy (ecss-computed-style-copy-with-values + inline (ecss-computed-style-values inline)))) + (should (eq (ecss-computed-style-precedence-winner + ruled '(app/padding-left app/width)) + 'app/width)) + (should (eq (ecss-computed-style-precedence-winner + inline '(app/padding-left app/width)) + 'app/padding-left)) + (should (eq (ecss-computed-style-precedence-winner + copy '(app/padding-left app/width)) + 'app/padding-left)) + (should-not (ecss-computed-style-provenance inline))))) + +(ert-deftest ecss-cascade-test-cross-property-projection-skips-invalid-winner () + "An invalid declaration must not override a valid cross-property fact." + (let* ((schemas (ecss-cascade-test--schemas)) + (stylesheet (ecss-stylesheet-create)) + (subject (ecss-cascade-test--subject + :id "hero" :classes '("target")))) + (ecss-stylesheet-add-rule + stylesheet schemas ".target" '(app/padding-left 5)) + (ecss-stylesheet-add-rule + stylesheet schemas "#hero" '(app/width "invalid")) + (let ((style (ecss-compute-style + schemas subject :stylesheet stylesheet))) + (should (eq (ecss-computed-style-precedence-winner + style '(app/padding-left app/width)) + 'app/padding-left)) + (should (memq 'app/width + (ecss-computed-style-specified-properties style))) + (should-not (memq 'app/width + (ecss-computed-style-active-properties style))) + (should (= (ecss-computed-style-value style 'app/width) 0))))) + (ert-deftest ecss-cascade-test-provenance-getter-copies-selector-data () (let ((schemas (ecss-cascade-test--schemas)) (stylesheet (ecss-stylesheet-create))