@neatui/css 4.2.2 → 4.2.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@neatui/css",
3
- "version": "4.2.2",
3
+ "version": "4.2.3",
4
4
  "description": "NeatUI constrained atomic design system — SCSS source for production themes, plus preview CSS with default light/dark",
5
5
  "main": "./neatui.scss",
6
6
  "sass": "./neatui.scss",
@@ -63,6 +63,7 @@
63
63
  "tokens:generate": "node scripts/generate-vscode-snapshot.mjs && node scripts/generate-docs-index.mjs",
64
64
  "tokens:check": "node scripts/generate-vscode-snapshot.mjs --check",
65
65
  "tokens:docs": "node scripts/generate-docs-index.mjs",
66
+ "validate": "node scripts/validate-grammar.mjs",
66
67
  "prepublishOnly": "yarn build"
67
68
  },
68
69
  "peerDependencies": {},
@@ -0,0 +1,310 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Shared NeatUI grammar validator (Node / CommonJS).
5
+ * Pure: pass tokens or a prebuilt grammar spec — no filesystem I/O.
6
+ */
7
+
8
+ function resolveValueGroup(tokens, groupRef) {
9
+ const [root, key] = groupRef.split('.');
10
+ const group = tokens[root]?.[key];
11
+ if (!Array.isArray(group)) {
12
+ throw new Error(`Unknown value group: ${groupRef}`);
13
+ }
14
+ return group;
15
+ }
16
+
17
+ function collectManagedValues(tokens, attrDef) {
18
+ if (Array.isArray(attrDef.values)) {
19
+ return attrDef.values.map((item) => item.value);
20
+ }
21
+ const values = [];
22
+ for (const ref of attrDef.valueGroups || []) {
23
+ for (const item of resolveValueGroup(tokens, ref)) {
24
+ values.push(item.value);
25
+ }
26
+ }
27
+ return values;
28
+ }
29
+
30
+ function escapeRegExp(value) {
31
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
32
+ }
33
+
34
+ function buildGrammarSpec(tokens) {
35
+ const attributes = {};
36
+ for (const [name, def] of Object.entries(tokens.attributes || {})) {
37
+ if (!def.managed) continue;
38
+ attributes[name] = {
39
+ openEnded: Boolean(def.openEnded),
40
+ expandResponsive: Boolean(def.expandResponsivePrefixes),
41
+ values: collectManagedValues(tokens, def),
42
+ intent: def.intent || '',
43
+ };
44
+ }
45
+
46
+ return {
47
+ schemaVersion: tokens.schemaVersion,
48
+ cssVersion: tokens.cssVersion,
49
+ grades: (tokens.grades || []).map((item) => item.value),
50
+ semanticColors: (tokens.semanticColors || []).map((item) => item.value),
51
+ responsivePrefixes: tokens.breakpoints?.attributeResponsivePrefixes || ['mob', 'pad'],
52
+ classPatterns: (tokens.classPatterns || []).map((item) => ({
53
+ id: item.id,
54
+ intent: item.intent,
55
+ example: item.example,
56
+ pattern: item.pattern,
57
+ })),
58
+ attributes,
59
+ };
60
+ }
61
+
62
+ function compileClassPatterns(spec) {
63
+ const grades = (spec.grades || []).map(escapeRegExp).join('|');
64
+ const colors = (spec.semanticColors || []).map(escapeRegExp).join('|');
65
+
66
+ return (spec.classPatterns || []).map((item) => {
67
+ const source = String(item.pattern)
68
+ .split('({grade})')
69
+ .join(`(${grades})`)
70
+ .split('({semanticColor})')
71
+ .join(`(${colors})`);
72
+ return {
73
+ id: item.id,
74
+ intent: item.intent,
75
+ example: item.example,
76
+ regex: new RegExp(source),
77
+ };
78
+ });
79
+ }
80
+
81
+ function createValidatorFromSpec(spec) {
82
+ const classPatterns = compileClassPatterns(spec);
83
+ const responsivePrefixes = new Set(spec.responsivePrefixes || ['mob', 'pad']);
84
+
85
+ const attributes = {};
86
+ for (const [name, def] of Object.entries(spec.attributes || {})) {
87
+ attributes[name] = {
88
+ openEnded: Boolean(def.openEnded),
89
+ expandResponsive: Boolean(def.expandResponsive),
90
+ values: new Set(def.values || []),
91
+ intent: def.intent || '',
92
+ };
93
+ }
94
+
95
+ function stripResponsivePrefix(token) {
96
+ const match = /^([a-z]+):(.+)$/i.exec(token);
97
+ if (!match) return { prefix: null, base: token };
98
+ const [, prefix, base] = match;
99
+ if (!responsivePrefixes.has(prefix)) {
100
+ return { prefix: null, base: token, unknownPrefix: prefix };
101
+ }
102
+ return { prefix, base };
103
+ }
104
+
105
+ function validateClassToken(raw) {
106
+ const token = String(raw || '').trim();
107
+ if (!token) return null;
108
+
109
+ const { prefix, base, unknownPrefix } = stripResponsivePrefix(token);
110
+ if (unknownPrefix) {
111
+ return {
112
+ kind: 'class',
113
+ token,
114
+ ok: false,
115
+ reason: `未知响应前缀 \`${unknownPrefix}:\`(允许: ${[...responsivePrefixes].join(', ')})`,
116
+ };
117
+ }
118
+
119
+ for (const pattern of classPatterns) {
120
+ if (pattern.regex.test(base)) {
121
+ return {
122
+ kind: 'class',
123
+ token,
124
+ ok: true,
125
+ patternId: pattern.id,
126
+ prefix,
127
+ base,
128
+ };
129
+ }
130
+ }
131
+
132
+ return {
133
+ kind: 'class',
134
+ token,
135
+ ok: false,
136
+ reason: `无法匹配 token schema 中的 classPatterns(示例: ${classPatterns
137
+ .slice(0, 3)
138
+ .map((item) => item.example)
139
+ .join(', ')}…)`,
140
+ base,
141
+ prefix,
142
+ };
143
+ }
144
+
145
+ function validateAttributeToken(attrName, raw) {
146
+ const token = String(raw || '').trim();
147
+ if (!token) return null;
148
+
149
+ const def = attributes[attrName];
150
+ if (!def) {
151
+ return {
152
+ kind: 'attr',
153
+ attr: attrName,
154
+ token,
155
+ ok: false,
156
+ reason: `属性 \`${attrName}\` 不在 token schema 的 managed attributes 中`,
157
+ };
158
+ }
159
+
160
+ const { prefix, base, unknownPrefix } = stripResponsivePrefix(token);
161
+ if (unknownPrefix) {
162
+ return {
163
+ kind: 'attr',
164
+ attr: attrName,
165
+ token,
166
+ ok: false,
167
+ reason: `未知响应前缀 \`${unknownPrefix}:\``,
168
+ };
169
+ }
170
+
171
+ if (prefix && !def.expandResponsive) {
172
+ return {
173
+ kind: 'attr',
174
+ attr: attrName,
175
+ token,
176
+ ok: false,
177
+ reason: `属性 \`${attrName}\` 不支持响应前缀`,
178
+ };
179
+ }
180
+
181
+ if (def.values.has(base) || def.openEnded) {
182
+ return {
183
+ kind: 'attr',
184
+ attr: attrName,
185
+ token,
186
+ ok: true,
187
+ prefix,
188
+ base,
189
+ openEnded: def.openEnded && !def.values.has(base),
190
+ };
191
+ }
192
+
193
+ const sample = [...def.values].slice(0, 8).join(', ');
194
+ return {
195
+ kind: 'attr',
196
+ attr: attrName,
197
+ token,
198
+ ok: false,
199
+ reason: `\`${base}\` 不是 \`${attrName}\` 的合法值(例如: ${sample}${def.values.size > 8 ? '…' : ''})`,
200
+ base,
201
+ prefix,
202
+ };
203
+ }
204
+
205
+ function validateClassString(classString) {
206
+ return String(classString || '')
207
+ .split(/\s+/)
208
+ .map(validateClassToken)
209
+ .filter(Boolean);
210
+ }
211
+
212
+ function validateAttributeString(attrName, valueString) {
213
+ return String(valueString || '')
214
+ .split(/\s+/)
215
+ .map((token) => validateAttributeToken(attrName, token))
216
+ .filter(Boolean);
217
+ }
218
+
219
+ function extractFromMarkup(source) {
220
+ const classes = [];
221
+ const attrs = [];
222
+
223
+ for (const match of String(source).matchAll(
224
+ /\bclass(?:Name)?\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]+))/gi,
225
+ )) {
226
+ classes.push(match[1] ?? match[2] ?? match[3] ?? '');
227
+ }
228
+
229
+ for (const name of Object.keys(attributes)) {
230
+ const re = new RegExp(
231
+ `\\b${name}\\s*=\\s*(?:"([^"]*)"|'([^']*)'|([^\\s>]+))`,
232
+ 'gi',
233
+ );
234
+ for (const match of String(source).matchAll(re)) {
235
+ attrs.push({ name, value: match[1] ?? match[2] ?? match[3] ?? '' });
236
+ }
237
+ }
238
+
239
+ return { classes, attrs };
240
+ }
241
+
242
+ function validateMarkup(source) {
243
+ const extracted = extractFromMarkup(source);
244
+ const results = [];
245
+ for (const classString of extracted.classes) {
246
+ results.push(...validateClassString(classString));
247
+ }
248
+ for (const attr of extracted.attrs) {
249
+ results.push(...validateAttributeString(attr.name, attr.value));
250
+ }
251
+ return results;
252
+ }
253
+
254
+ /**
255
+ * Flag only tokens that look like NeatUI / common drift — not arbitrary custom classes.
256
+ */
257
+ function looksLikeNeatuiClass(token) {
258
+ const base = String(token || '').replace(/^(?:mob|pad):/, '');
259
+ if (!base) return false;
260
+ if (/^(?:co|bg|fs|lh|r|o|z)-/.test(base)) return true;
261
+ if (/^-?(?:n|nx|ny|nt|nr|nb|nl|m|mx|my|mt|mr|mb|ml)-/.test(base)) return true;
262
+ if (/^(?:w|min-w|max-w|h|min-h|max-h)-/.test(base)) return true;
263
+ // Common utility drift that should fail loudly
264
+ if (/^(?:p|px|py|pt|pr|pb|pl|gap|space-[xy])-\d/.test(base)) return true;
265
+ if (/^(?:text|bg|border)-(?:red|blue|green|gray|slate|zinc)-\d{2,3}$/.test(base)) return true;
266
+ if (/^(?:justify|items|content)-/.test(base)) return true;
267
+ return false;
268
+ }
269
+
270
+ function validateClassStringSelective(classString) {
271
+ return validateClassString(classString).filter((item) => looksLikeNeatuiClass(item.token));
272
+ }
273
+
274
+ function validateMarkupSelective(source) {
275
+ const extracted = extractFromMarkup(source);
276
+ const results = [];
277
+ for (const classString of extracted.classes) {
278
+ results.push(...validateClassStringSelective(classString));
279
+ }
280
+ for (const attr of extracted.attrs) {
281
+ results.push(...validateAttributeString(attr.name, attr.value));
282
+ }
283
+ return results;
284
+ }
285
+
286
+ return {
287
+ spec,
288
+ attributes,
289
+ classPatterns,
290
+ validateClassToken,
291
+ validateAttributeToken,
292
+ validateClassString,
293
+ validateAttributeString,
294
+ validateClassStringSelective,
295
+ extractFromMarkup,
296
+ validateMarkup,
297
+ validateMarkupSelective,
298
+ looksLikeNeatuiClass,
299
+ };
300
+ }
301
+
302
+ function createValidator(tokens) {
303
+ return createValidatorFromSpec(buildGrammarSpec(tokens));
304
+ }
305
+
306
+ module.exports = {
307
+ buildGrammarSpec,
308
+ createValidator,
309
+ createValidatorFromSpec,
310
+ };
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "cssVersion": "4.2.1",
3
+ "cssVersion": "4.2.2",
4
4
  "name": "neatui-tokens",
5
5
  "description": "NeatUI constrained design tokens — single source for grades, semantic colors, flex coordinates, breakpoints, and core attribute values. Does not enumerate project theme color names.",
6
6
  "grades": [