tjs-lang 0.8.7 → 0.9.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 (75) hide show
  1. package/CLAUDE.md +12 -4
  2. package/dist/experiments/ambient/probe.d.ts +52 -0
  3. package/dist/index.js +115 -171
  4. package/dist/index.js.map +4 -4
  5. package/dist/scripts/build-editors.d.ts +19 -0
  6. package/dist/src/css/dimensions.d.ts +23 -0
  7. package/dist/src/css/index.d.ts +85 -0
  8. package/dist/src/css/predicates.d.ts +38 -0
  9. package/dist/src/css/shorthands.d.ts +31 -0
  10. package/dist/src/css/style.d.ts +48 -0
  11. package/dist/src/lang/emitters/js.d.ts +9 -1
  12. package/dist/src/lang/index.d.ts +2 -2
  13. package/dist/src/lang/parser-transforms.d.ts +9 -5
  14. package/dist/src/lang/parser.d.ts +2 -0
  15. package/dist/src/lang/predicate.d.ts +60 -0
  16. package/dist/src/lang/transpiler.d.ts +3 -2
  17. package/dist/src/lang/types.d.ts +16 -0
  18. package/dist/src/schema/index.d.ts +12 -0
  19. package/dist/tjs-browser-from-ts.js +20 -20
  20. package/dist/tjs-browser-from-ts.js.map +3 -3
  21. package/dist/tjs-browser.js +100 -90
  22. package/dist/tjs-browser.js.map +4 -4
  23. package/dist/tjs-css.js +193 -0
  24. package/dist/tjs-css.js.map +7 -0
  25. package/dist/tjs-eval.js +43 -42
  26. package/dist/tjs-eval.js.map +4 -4
  27. package/dist/tjs-from-ts.js +13 -13
  28. package/dist/tjs-from-ts.js.map +2 -2
  29. package/dist/tjs-lang.js +102 -92
  30. package/dist/tjs-lang.js.map +4 -4
  31. package/dist/tjs-runtime.js +10 -0
  32. package/dist/tjs-runtime.js.map +7 -0
  33. package/dist/tjs-schema.js +6 -0
  34. package/dist/tjs-schema.js.map +7 -0
  35. package/dist/tjs-vm.js +55 -54
  36. package/dist/tjs-vm.js.map +4 -4
  37. package/docs/ambient-contracts.md +261 -0
  38. package/editors/ace/ajs-mode.js +214 -233
  39. package/editors/codemirror/ajs-language.js +1570 -233
  40. package/editors/editors-build.test.ts +29 -0
  41. package/editors/monaco/ajs-monarch.js +239 -195
  42. package/llms.txt +4 -0
  43. package/package.json +35 -4
  44. package/src/css/css.test.ts +122 -0
  45. package/src/css/dimensions.test.ts +112 -0
  46. package/src/css/dimensions.ts +146 -0
  47. package/src/css/index.ts +243 -0
  48. package/src/css/perf.bench.test.ts +109 -0
  49. package/src/css/predicates.ts +232 -0
  50. package/src/css/property-aware.test.ts +84 -0
  51. package/src/css/shorthands.test.ts +90 -0
  52. package/src/css/shorthands.ts +113 -0
  53. package/src/css/style.test.ts +125 -0
  54. package/src/css/style.ts +134 -0
  55. package/src/index-tsfree.test.ts +58 -0
  56. package/src/lang/emit-verified-predicate.test.ts +95 -0
  57. package/src/lang/emitters/dts.test.ts +31 -0
  58. package/src/lang/emitters/dts.ts +23 -4
  59. package/src/lang/emitters/js.ts +33 -1
  60. package/src/lang/from-ts.test.ts +1 -1
  61. package/src/lang/generic-verified-predicate.test.ts +39 -0
  62. package/src/lang/index.ts +14 -5
  63. package/src/lang/parser-transforms.ts +109 -14
  64. package/src/lang/parser.ts +9 -2
  65. package/src/lang/predicate-evaluator.test.ts +49 -0
  66. package/src/lang/predicate-report.test.ts +54 -0
  67. package/src/lang/predicate.ts +268 -0
  68. package/src/lang/redos-lint.test.ts +81 -0
  69. package/src/lang/transpiler.ts +13 -0
  70. package/src/lang/type-verified-predicate.test.ts +83 -0
  71. package/src/lang/types.ts +17 -0
  72. package/src/lang/typescript-syntax.test.ts +2 -1
  73. package/src/schema/index.ts +62 -0
  74. package/src/schema/schema.test.ts +66 -0
  75. package/src/use-cases/bootstrap.test.ts +14 -4
@@ -0,0 +1,122 @@
1
+ /**
2
+ * `tjs-lang/css` phase 1 — the color grammar. Proves the full vertical slice:
3
+ * the predicate source verifies safe (pure + ReDoS-clean), compiles to correct
4
+ * native validators, and mines valid autocomplete suggestions.
5
+ */
6
+ import { describe, it, expect } from 'bun:test'
7
+ import {
8
+ isColor,
9
+ isColorValue,
10
+ isNamedColor,
11
+ isHexColor,
12
+ verifyCss,
13
+ suggestColor,
14
+ CSS_NAMED_COLORS,
15
+ } from './index'
16
+
17
+ describe('CSS color source is predicate-safe', () => {
18
+ it('verifies pure, synchronous, ReDoS-clean', () => {
19
+ const r = verifyCss()
20
+ if (!r.safe) console.error(r.diagnostics)
21
+ expect(r.safe).toBe(true)
22
+ expect(r.diagnostics).toEqual([])
23
+ })
24
+ })
25
+
26
+ describe('isColor accepts valid CSS colors', () => {
27
+ const valid = [
28
+ 'red',
29
+ 'REBECCAPURPLE', // case-insensitive named
30
+ 'transparent',
31
+ 'currentColor',
32
+ '#f00',
33
+ '#ff0000',
34
+ '#ff0000aa', // 8-digit
35
+ '#abcd', // 4-digit
36
+ 'rgb(255, 0, 0)',
37
+ 'rgb(255 0 0 / 0.5)', // modern slash syntax
38
+ 'rgba(0,0,0,0.2)',
39
+ 'hsl(120, 50%, 50%)',
40
+ 'hsl(120deg 50% 50% / 0.5)',
41
+ 'hwb(194 0% 0%)',
42
+ 'oklch(0.7 0.15 200)',
43
+ 'lab(50% 40 59.5)',
44
+ 'color-mix(in oklch, red, blue)',
45
+ 'color(display-p3 1 0.5 0)',
46
+ 'var(--brand)',
47
+ ]
48
+ for (const c of valid) {
49
+ it(`accepts ${c}`, () => expect(isColor(c)).toBe(true))
50
+ }
51
+ })
52
+
53
+ describe('isColor rejects non-colors', () => {
54
+ const invalid = [
55
+ 'notacolor',
56
+ '#gg0000', // non-hex digits
57
+ '#12345', // 5 digits (not 3/4/6/8)
58
+ 'reddish',
59
+ '',
60
+ 'rgb', // no parens
61
+ 'linear-gradient(red, blue)', // a gradient, not a color
62
+ 42,
63
+ null,
64
+ undefined,
65
+ {},
66
+ ]
67
+ for (const c of invalid) {
68
+ it(`rejects ${JSON.stringify(c)}`, () => expect(isColor(c)).toBe(false))
69
+ }
70
+ })
71
+
72
+ describe('isColorValue tolerates !important', () => {
73
+ it('accepts a color with !important', () => {
74
+ expect(isColorValue('#3a3 !important')).toBe(true)
75
+ expect(isColorValue('red !important')).toBe(true)
76
+ })
77
+ it('still rejects a non-color with !important', () => {
78
+ expect(isColorValue('notacolor !important')).toBe(false)
79
+ })
80
+ it('isColor (strict) does not accept !important', () => {
81
+ expect(isColor('red !important')).toBe(false)
82
+ })
83
+ })
84
+
85
+ describe('leaf predicates', () => {
86
+ it('isNamedColor is case-insensitive and complete-ish', () => {
87
+ expect(isNamedColor('RebeccaPurple')).toBe(true)
88
+ expect(isNamedColor('cornflowerblue')).toBe(true)
89
+ expect(isNamedColor('bluish')).toBe(false)
90
+ // sanity: the full named set is present
91
+ expect(CSS_NAMED_COLORS.length).toBeGreaterThan(140)
92
+ })
93
+ it('isHexColor validates digit counts', () => {
94
+ expect(isHexColor('#abc')).toBe(true)
95
+ expect(isHexColor('#abcd')).toBe(true)
96
+ expect(isHexColor('#aabbcc')).toBe(true)
97
+ expect(isHexColor('#aabbccdd')).toBe(true)
98
+ expect(isHexColor('#ab')).toBe(false)
99
+ expect(isHexColor('#abcde')).toBe(false)
100
+ })
101
+ })
102
+
103
+ describe('suggestColor mines valid completions', () => {
104
+ it('suggests named colors for a prefix, all valid', () => {
105
+ const s = suggestColor('red')
106
+ const values = s.filter((x) => x.kind === 'value').map((x) => x.value)
107
+ expect(values).toContain('red')
108
+ // every suggested value actually passes the predicate (mined + validated)
109
+ for (const v of values) expect(isColor(v)).toBe(true)
110
+ })
111
+ it('offers the open var(-- stub for a relevant prefix', () => {
112
+ // Empty-prefix suggest floods with named colors (correctly); autocomplete is
113
+ // prefix-driven, so the open functional stubs surface once you start typing.
114
+ const s = suggestColor('var')
115
+ const stubs = s.filter((x) => x.kind === 'stub').map((x) => x.value)
116
+ expect(stubs).toContain('var(--')
117
+ })
118
+ it('offers color-mix( for the color- prefix', () => {
119
+ const s = suggestColor('color-')
120
+ expect(s.some((x) => x.value === 'color-mix(')).toBe(true)
121
+ })
122
+ })
@@ -0,0 +1,112 @@
1
+ /**
2
+ * `tjs-lang/css` phase 2 — dimensions / numbers / angles / times / keywords.
3
+ * Same vertical slice as colors: the source verifies safe (via `verifyCss`,
4
+ * which now covers all clusters) and compiles to correct native validators.
5
+ */
6
+ import { describe, it, expect } from 'bun:test'
7
+ import {
8
+ isLength,
9
+ isPercentage,
10
+ isNumber,
11
+ isInteger,
12
+ isAngle,
13
+ isTime,
14
+ isDimension,
15
+ isGlobalKeyword,
16
+ verifyCss,
17
+ } from './index'
18
+
19
+ describe('all CSS clusters are predicate-safe (incl. dimensions)', () => {
20
+ it('verifyCss covers color + dimension, all safe', () => {
21
+ const r = verifyCss()
22
+ if (!r.safe) console.error(r.diagnostics)
23
+ expect(r.safe).toBe(true)
24
+ })
25
+ })
26
+
27
+ describe('isLength', () => {
28
+ const valid = [
29
+ '0', // unitless zero
30
+ '10px',
31
+ '1.5rem',
32
+ '-2em',
33
+ '100vh',
34
+ '50vmax',
35
+ '3ch',
36
+ '2.5cqw', // container query unit
37
+ '1dvh', // dynamic viewport
38
+ '10Q',
39
+ '.5in',
40
+ 'calc(100% - 10px)',
41
+ 'var(--gap)',
42
+ ]
43
+ for (const v of valid)
44
+ it(`accepts ${v}`, () => expect(isLength(v)).toBe(true))
45
+
46
+ const invalid = ['10', '10foo', 'px', '10 px', 'red', '', {}, null]
47
+ for (const v of invalid)
48
+ it(`rejects ${JSON.stringify(v)}`, () => expect(isLength(v)).toBe(false))
49
+
50
+ it('accepts numeric 0 (unitless zero) but not other bare numbers', () => {
51
+ expect(isLength(0)).toBe(true)
52
+ expect(isLength(10)).toBe(false) // a bare number isn't a length
53
+ })
54
+ })
55
+
56
+ describe('isPercentage', () => {
57
+ it('accepts', () => {
58
+ expect(isPercentage('50%')).toBe(true)
59
+ expect(isPercentage('-12.5%')).toBe(true)
60
+ })
61
+ it('rejects', () => {
62
+ expect(isPercentage('50')).toBe(false)
63
+ expect(isPercentage('50px')).toBe(false)
64
+ })
65
+ })
66
+
67
+ describe('isNumber / isInteger', () => {
68
+ it('isNumber accepts numbers and numeric strings', () => {
69
+ expect(isNumber(42)).toBe(true)
70
+ expect(isNumber(3.14)).toBe(true)
71
+ expect(isNumber('42')).toBe(true)
72
+ expect(isNumber('-.5')).toBe(true)
73
+ expect(isNumber('4px')).toBe(false)
74
+ expect(isNumber(Infinity)).toBe(false)
75
+ expect(isNumber(NaN)).toBe(false)
76
+ })
77
+ it('isInteger', () => {
78
+ expect(isInteger(5)).toBe(true)
79
+ expect(isInteger('5')).toBe(true)
80
+ expect(isInteger(5.5)).toBe(false)
81
+ expect(isInteger('5.0')).toBe(false)
82
+ })
83
+ })
84
+
85
+ describe('isAngle / isTime', () => {
86
+ it('angles', () => {
87
+ for (const a of ['45deg', '1turn', '1.5rad', '100grad', '-90deg'])
88
+ expect(isAngle(a)).toBe(true)
89
+ expect(isAngle('45')).toBe(false)
90
+ expect(isAngle('45px')).toBe(false)
91
+ })
92
+ it('times', () => {
93
+ expect(isTime('200ms')).toBe(true)
94
+ expect(isTime('1s')).toBe(true)
95
+ expect(isTime('1.5s')).toBe(true)
96
+ expect(isTime('1')).toBe(false)
97
+ })
98
+ })
99
+
100
+ describe('isDimension (any numeric family) + isGlobalKeyword', () => {
101
+ it('isDimension unifies the families', () => {
102
+ for (const v of ['10px', '50%', '45deg', '1s', '42', 'calc(1px + 2px)'])
103
+ expect(isDimension(v)).toBe(true)
104
+ expect(isDimension('red')).toBe(false)
105
+ })
106
+ it('isGlobalKeyword', () => {
107
+ for (const k of ['inherit', 'initial', 'unset', 'revert', 'revert-layer'])
108
+ expect(isGlobalKeyword(k)).toBe(true)
109
+ expect(isGlobalKeyword('INHERIT')).toBe(true) // case-insensitive
110
+ expect(isGlobalKeyword('auto')).toBe(false)
111
+ })
112
+ })
@@ -0,0 +1,146 @@
1
+ /**
2
+ * CSS dimension / numeric value grammar (phase 2) — the other big family of leaf
3
+ * values: lengths, percentages, angles, times, resolutions, plain numbers, and
4
+ * the CSS-wide global keywords. Same rules as the color cluster: pure,
5
+ * synchronous, ReDoS-clean (flat character classes / bounded alternation only),
6
+ * with `var(--…)`/`calc(…)` as the open escapes.
7
+ *
8
+ * The numeric core `[+-]?(\d*\.\d+|\d+)` is a bounded alternation (each branch a
9
+ * single quantifier — no nesting), so it passes the verifier's ReDoS lint.
10
+ */
11
+
12
+ /** Absolute + relative + container + viewport length units (CSS Values 4). */
13
+ export const CSS_LENGTH_UNITS = [
14
+ // font-relative
15
+ 'cap',
16
+ 'ch',
17
+ 'em',
18
+ 'ex',
19
+ 'ic',
20
+ 'lh',
21
+ 'rcap',
22
+ 'rch',
23
+ 'rem',
24
+ 'rex',
25
+ 'ric',
26
+ 'rlh',
27
+ // viewport
28
+ 'vw',
29
+ 'vh',
30
+ 'vi',
31
+ 'vb',
32
+ 'vmin',
33
+ 'vmax',
34
+ 'svw',
35
+ 'svh',
36
+ 'svi',
37
+ 'svb',
38
+ 'svmin',
39
+ 'svmax',
40
+ 'lvw',
41
+ 'lvh',
42
+ 'lvi',
43
+ 'lvb',
44
+ 'lvmin',
45
+ 'lvmax',
46
+ 'dvw',
47
+ 'dvh',
48
+ 'dvi',
49
+ 'dvb',
50
+ 'dvmin',
51
+ 'dvmax',
52
+ // container query
53
+ 'cqw',
54
+ 'cqh',
55
+ 'cqi',
56
+ 'cqb',
57
+ 'cqmin',
58
+ 'cqmax',
59
+ // absolute
60
+ 'px',
61
+ 'cm',
62
+ 'mm',
63
+ 'q',
64
+ 'in',
65
+ 'pt',
66
+ 'pc',
67
+ ]
68
+
69
+ /** CSS-wide keywords valid on any property. */
70
+ export const CSS_GLOBAL_KEYWORDS = [
71
+ 'inherit',
72
+ 'initial',
73
+ 'unset',
74
+ 'revert',
75
+ 'revert-layer',
76
+ ]
77
+
78
+ /**
79
+ * Dimension predicate cluster (source). Entry points cover each numeric family
80
+ * plus `isDimension` (any of them) and `isGlobalKeyword`. Units are ordered
81
+ * longest-first inside the alternation so e.g. `rem` isn't shadowed by `em`
82
+ * (belt-and-suspenders — the `$` anchor already forces a full match).
83
+ */
84
+ export const CSS_DIMENSION_SOURCE = `
85
+ var CSS_LENGTH_UNITS = ${JSON.stringify(
86
+ // longest-first for safe alternation ordering
87
+ [...CSS_LENGTH_UNITS].sort((a, b) => b.length - a.length)
88
+ )}
89
+ var CSS_GLOBAL_KEYWORDS = ${JSON.stringify(CSS_GLOBAL_KEYWORDS)}
90
+
91
+ function isCssVar(v) {
92
+ return typeof v === 'string' && v.startsWith('var(--') && v.endsWith(')')
93
+ }
94
+ function isCssCalc(v) {
95
+ return typeof v === 'string' && v.startsWith('calc(') && v.endsWith(')')
96
+ }
97
+ function isNumber(v) {
98
+ if (typeof v === 'number') { return isFinite(v) }
99
+ return typeof v === 'string' && /^[+-]?(\\d*\\.\\d+|\\d+)$/.test(v)
100
+ }
101
+ function isInteger(v) {
102
+ if (typeof v === 'number') { return Number.isInteger(v) }
103
+ return typeof v === 'string' && /^[+-]?\\d+$/.test(v)
104
+ }
105
+ function isPercentage(v) {
106
+ return typeof v === 'string' && /^[+-]?(\\d*\\.\\d+|\\d+)%$/.test(v)
107
+ }
108
+ function isLength(v) {
109
+ if (v === 0 || v === '0') { return true }
110
+ if (isCssVar(v) || isCssCalc(v)) { return true }
111
+ if (typeof v !== 'string') { return false }
112
+ var m = v.match(/^[+-]?(\\d*\\.\\d+|\\d+)([a-z%]+)$/i)
113
+ if (m === null) { return false }
114
+ return CSS_LENGTH_UNITS.includes(m[2].toLowerCase())
115
+ }
116
+ function isAngle(v) {
117
+ return typeof v === 'string' && /^[+-]?(\\d*\\.\\d+|\\d+)(deg|grad|rad|turn)$/i.test(v)
118
+ }
119
+ function isTime(v) {
120
+ return typeof v === 'string' && /^[+-]?(\\d*\\.\\d+|\\d+)(ms|s)$/i.test(v)
121
+ }
122
+ function isResolution(v) {
123
+ return typeof v === 'string' && /^[+-]?(\\d*\\.\\d+|\\d+)(dpi|dpcm|dppx|x)$/i.test(v)
124
+ }
125
+ function isGlobalKeyword(v) {
126
+ return typeof v === 'string' && CSS_GLOBAL_KEYWORDS.includes(v.toLowerCase())
127
+ }
128
+ function isDimension(v) {
129
+ return isLength(v) || isPercentage(v) || isAngle(v)
130
+ || isTime(v) || isResolution(v) || isNumber(v)
131
+ || isCssVar(v) || isCssCalc(v)
132
+ }
133
+ `
134
+
135
+ /** Entry predicates exported by the dimension cluster (for compile/suggest). */
136
+ export const CSS_DIMENSION_ENTRIES = [
137
+ 'isNumber',
138
+ 'isInteger',
139
+ 'isPercentage',
140
+ 'isLength',
141
+ 'isAngle',
142
+ 'isTime',
143
+ 'isResolution',
144
+ 'isGlobalKeyword',
145
+ 'isDimension',
146
+ ] as const
@@ -0,0 +1,243 @@
1
+ /**
2
+ * `tjs-lang/css` — CSS validation built from verified-safe predicates.
3
+ *
4
+ * The "computational half" of a CSS schema: precise, total validators for the
5
+ * open value grammars TS/JSON-Schema cave to `string` on (colors here in
6
+ * phase 1; lengths, shorthands and the recursive style-object structure land in
7
+ * later phases — see TODO.md #4). Every validator is compiled from a
8
+ * predicate-safe source cluster (`./predicates`), so it is pure, synchronous,
9
+ * fuel-bounded, and ReDoS-clean — safe to run on untrusted input.
10
+ *
11
+ * Autocomplete rides the same source: `suggestColor()` mines the named-color set
12
+ * and the open functional stubs (`var(--`, `color-mix(`, …), validating mined
13
+ * values through the compiled predicate so completions are guaranteed valid.
14
+ */
15
+ import {
16
+ compilePredicate,
17
+ verifyPredicate,
18
+ suggest,
19
+ type PredicateVerifyResult,
20
+ type Suggestion,
21
+ } from '../lang/predicate'
22
+ import {
23
+ CSS_COLOR_SOURCE,
24
+ CSS_COLOR_ENTRIES,
25
+ CSS_NAMED_COLORS,
26
+ } from './predicates'
27
+ import {
28
+ CSS_DIMENSION_SOURCE,
29
+ CSS_DIMENSION_ENTRIES,
30
+ CSS_LENGTH_UNITS,
31
+ CSS_GLOBAL_KEYWORDS,
32
+ } from './dimensions'
33
+ import { CSS_STYLE_SOURCE, CSS_STYLE_ENTRIES } from './style'
34
+ import { CSS_SHORTHAND_SOURCE, CSS_SHORTHAND_ENTRIES } from './shorthands'
35
+
36
+ export {
37
+ CSS_COLOR_SOURCE,
38
+ CSS_COLOR_ENTRIES,
39
+ CSS_NAMED_COLORS,
40
+ } from './predicates'
41
+ export {
42
+ CSS_DIMENSION_SOURCE,
43
+ CSS_DIMENSION_ENTRIES,
44
+ CSS_LENGTH_UNITS,
45
+ CSS_GLOBAL_KEYWORDS,
46
+ } from './dimensions'
47
+ export {
48
+ CSS_STYLE_SOURCE,
49
+ CSS_STYLE_ENTRIES,
50
+ cssStyleSchema,
51
+ cssColorSchema,
52
+ cssDimensionSchema,
53
+ } from './style'
54
+ export {
55
+ CSS_SHORTHAND_SOURCE,
56
+ CSS_SHORTHAND_ENTRIES,
57
+ cssAnimationSchema,
58
+ cssTransitionSchema,
59
+ } from './shorthands'
60
+
61
+ /** Every predicate-source cluster the library ships, keyed for verify/drift. */
62
+ const CSS_SOURCES: Record<string, string> = {
63
+ color: CSS_COLOR_SOURCE,
64
+ dimension: CSS_DIMENSION_SOURCE,
65
+ style: CSS_STYLE_SOURCE,
66
+ shorthand: CSS_SHORTHAND_SOURCE,
67
+ }
68
+
69
+ type ColorValidators = Record<
70
+ (typeof CSS_COLOR_ENTRIES)[number],
71
+ (v: unknown) => boolean
72
+ >
73
+ type DimensionValidators = Record<
74
+ (typeof CSS_DIMENSION_ENTRIES)[number],
75
+ (v: unknown) => boolean
76
+ >
77
+
78
+ // Compiled once, on first use (verification + `new Function` are one-time).
79
+ let _color: ColorValidators | null = null
80
+ function colorValidators(): ColorValidators {
81
+ if (!_color) {
82
+ _color = compilePredicate(
83
+ CSS_COLOR_SOURCE,
84
+ CSS_COLOR_ENTRIES as unknown as string[]
85
+ ) as ColorValidators
86
+ }
87
+ return _color
88
+ }
89
+
90
+ let _dimension: DimensionValidators | null = null
91
+ function dimensionValidators(): DimensionValidators {
92
+ if (!_dimension) {
93
+ _dimension = compilePredicate(
94
+ CSS_DIMENSION_SOURCE,
95
+ CSS_DIMENSION_ENTRIES as unknown as string[]
96
+ ) as DimensionValidators
97
+ }
98
+ return _dimension
99
+ }
100
+
101
+ type StyleValidators = Record<
102
+ (typeof CSS_STYLE_ENTRIES)[number],
103
+ (v: unknown) => boolean
104
+ >
105
+ let _style: StyleValidators | null = null
106
+ function styleValidators(): StyleValidators {
107
+ if (!_style) {
108
+ _style = compilePredicate(
109
+ CSS_STYLE_SOURCE,
110
+ CSS_STYLE_ENTRIES as unknown as string[]
111
+ ) as StyleValidators
112
+ }
113
+ return _style
114
+ }
115
+
116
+ type ShorthandValidators = Record<
117
+ (typeof CSS_SHORTHAND_ENTRIES)[number],
118
+ (v: unknown) => boolean
119
+ >
120
+ let _shorthand: ShorthandValidators | null = null
121
+ function shorthandValidators(): ShorthandValidators {
122
+ if (!_shorthand) {
123
+ _shorthand = compilePredicate(
124
+ CSS_SHORTHAND_SOURCE,
125
+ CSS_SHORTHAND_ENTRIES as unknown as string[]
126
+ ) as ShorthandValidators
127
+ }
128
+ return _shorthand
129
+ }
130
+
131
+ /** Is `v` a valid CSS color (named, hex, rgb/hsl, modern fn, or `var(--…)`)? */
132
+ export const isColor = (v: unknown): boolean => colorValidators().isColor(v)
133
+ /** Like {@link isColor}, but tolerates a trailing `!important`. */
134
+ export const isColorValue = (v: unknown): boolean =>
135
+ colorValidators().isColorValue(v)
136
+ /** Is `v` a CSS named color (incl. `transparent`/`currentcolor`)? */
137
+ export const isNamedColor = (v: unknown): boolean =>
138
+ colorValidators().isNamedColor(v)
139
+ /** Is `v` a CSS hex color (`#rgb`, `#rgba`, `#rrggbb`, `#rrggbbaa`)? */
140
+ export const isHexColor = (v: unknown): boolean =>
141
+ colorValidators().isHexColor(v)
142
+
143
+ // --- dimensions / numbers / keywords (phase 2) ------------------------------
144
+
145
+ /** Is `v` a CSS `<length>` (unit'd, unitless `0`, or `var()`/`calc()`)? */
146
+ export const isLength = (v: unknown): boolean =>
147
+ dimensionValidators().isLength(v)
148
+ /** Is `v` a CSS `<percentage>` (`50%`)? */
149
+ export const isPercentage = (v: unknown): boolean =>
150
+ dimensionValidators().isPercentage(v)
151
+ /** Is `v` a CSS `<number>` (numeric, or a numeric string)? */
152
+ export const isNumber = (v: unknown): boolean =>
153
+ dimensionValidators().isNumber(v)
154
+ /** Is `v` a CSS `<integer>`? */
155
+ export const isInteger = (v: unknown): boolean =>
156
+ dimensionValidators().isInteger(v)
157
+ /** Is `v` a CSS `<angle>` (`45deg`, `1turn`, `1.5rad`, `100grad`)? */
158
+ export const isAngle = (v: unknown): boolean => dimensionValidators().isAngle(v)
159
+ /** Is `v` a CSS `<time>` (`200ms`, `1s`)? */
160
+ export const isTime = (v: unknown): boolean => dimensionValidators().isTime(v)
161
+ /** Is `v` any CSS dimension/number (length, %, angle, time, resolution, number)? */
162
+ export const isDimension = (v: unknown): boolean =>
163
+ dimensionValidators().isDimension(v)
164
+ /** Is `v` a CSS-wide keyword (`inherit`/`initial`/`unset`/`revert`/`revert-layer`)? */
165
+ export const isGlobalKeyword = (v: unknown): boolean =>
166
+ dimensionValidators().isGlobalKeyword(v)
167
+
168
+ // --- recursive style-object structure (phase 4) -----------------------------
169
+
170
+ /**
171
+ * Is `v` a valid CSS **style object** — an open, recursive map of CSS properties
172
+ * to values and selectors/at-rules to nested style objects? Validates the whole
173
+ * structure (keys are properties or selectors; property values are style values,
174
+ * selector values are nested rules), recursively and fuel-bounded. This is the
175
+ * shape TS/JSON-Schema can't express.
176
+ */
177
+ export const isStyleObject = (v: unknown): boolean =>
178
+ styleValidators().isStyleObject(v)
179
+ /** Is `v` a valid leaf CSS value (a known color/dimension/keyword, else a non-empty string / finite number)? */
180
+ export const isStyleValue = (v: unknown): boolean =>
181
+ styleValidators().isStyleValue(v)
182
+ /**
183
+ * Property-AWARE leaf check: given the property name, tightens the closed value
184
+ * grammars (color/animation/transition) so `isStyleValueFor('color', 'notacolor')`
185
+ * is `false` while `isStyleValueFor('padding', 'anything')` stays permissive.
186
+ * This is what makes {@link isStyleObject} catch real value errors.
187
+ */
188
+ export const isStyleValueFor = (prop: unknown, val: unknown): boolean =>
189
+ (styleValidators().isStyleValueFor as (p: unknown, v: unknown) => boolean)(
190
+ prop,
191
+ val
192
+ )
193
+ /** Is `k` a CSS property name (`color`, `--custom`, `-webkit-foo`)? */
194
+ export const isCssProperty = (k: unknown): boolean =>
195
+ styleValidators().isCssProperty(k)
196
+ /** Is `k` a selector or at-rule key (nests a rule) rather than a property? */
197
+ export const isSelectorOrAtRule = (k: unknown): boolean =>
198
+ styleValidators().isSelectorOrAtRule(k)
199
+
200
+ // --- order-flexible shorthands (phase 3) ------------------------------------
201
+
202
+ /** Is `v` a valid CSS `animation` shorthand (comma-separated, order-free tokens)? */
203
+ export const isAnimation = (v: unknown): boolean =>
204
+ shorthandValidators().isAnimation(v)
205
+ /** Is `v` a valid CSS `transition` shorthand (comma-separated layers)? */
206
+ export const isTransition = (v: unknown): boolean =>
207
+ shorthandValidators().isTransition(v)
208
+ /** Is `v` a CSS `<easing-function>` (keyword, `cubic-bezier(…)`, `steps(…)`, `linear(…)`)? */
209
+ export const isTimingFunction = (v: unknown): boolean =>
210
+ shorthandValidators().isTimingFunction(v)
211
+
212
+ /**
213
+ * Verify every CSS predicate-source cluster is predicate-safe (pure,
214
+ * synchronous, ReDoS-clean). Returns the combined result (safe iff all clusters
215
+ * are; diagnostics carry the cluster name). Always safe for the shipped source —
216
+ * exposed so downstream tools (and the drift test) can assert it, and so a
217
+ * consumer embedding a source in a `$predicate` schema can re-check before
218
+ * trusting it.
219
+ */
220
+ export function verifyCss(): PredicateVerifyResult {
221
+ const predicates: string[] = []
222
+ const diagnostics: PredicateVerifyResult['diagnostics'] = []
223
+ for (const [name, source] of Object.entries(CSS_SOURCES)) {
224
+ const r = verifyPredicate(source)
225
+ predicates.push(...r.predicates)
226
+ for (const d of r.diagnostics)
227
+ diagnostics.push({ ...d, predicate: `${name}:${d.predicate}` })
228
+ }
229
+ return { safe: diagnostics.length === 0, predicates, diagnostics }
230
+ }
231
+
232
+ /**
233
+ * Autocomplete candidates for a CSS color value. Returns concrete named colors
234
+ * (validated through the compiled predicate) plus open-ended stubs mined from
235
+ * the functional forms (`var(--`, `color-mix(`, `oklch(`, …). Prefix-filtered.
236
+ */
237
+ export function suggestColor(prefix = '', limit = 50): Suggestion[] {
238
+ return suggest(CSS_COLOR_SOURCE, {
239
+ entry: 'isColor',
240
+ prefix,
241
+ limit,
242
+ })
243
+ }