@flun/html-template 4.0.10

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 (59) hide show
  1. package/.env +9 -0
  2. package/LICENSE +15 -0
  3. package/build.js +3 -0
  4. package/compile.js +349 -0
  5. package/copy-files.js +200 -0
  6. package/customize/account.js +726 -0
  7. package/customize/data.json +484 -0
  8. package/customize/functions.js +48 -0
  9. package/customize/hotReloadInjector.js +25 -0
  10. package/customize/routes.js +141 -0
  11. package/customize/users.json +44 -0
  12. package/customize/variables.js +70 -0
  13. package/dev-server.js +344 -0
  14. package/dev.js +4 -0
  15. package/f-CHANGELOG.md +4 -0
  16. package/f-README.md +485 -0
  17. package/index.d.ts +133 -0
  18. package/index.js +4 -0
  19. package/package.json +77 -0
  20. package/restoreDefaults.js +8 -0
  21. package/services/templateService.js +962 -0
  22. package/static/about.css +118 -0
  23. package/static/auth.js +27 -0
  24. package/static/constants.css +138 -0
  25. package/static/img/dark.png +0 -0
  26. package/static/img/favicon.ico +0 -0
  27. package/static/img/light.png +0 -0
  28. package/static/img/top.png +0 -0
  29. package/static/index.css +86 -0
  30. package/static/mouseOrTouch.js +156 -0
  31. package/static/public.css +288 -0
  32. package/static/script.css +318 -0
  33. package/static/script.js +392 -0
  34. package/static/styling.css +874 -0
  35. package/static/styling.js +933 -0
  36. package/static/themeImg.css +10 -0
  37. package/static/themeImg.js +19 -0
  38. package/static/themeModule.js +222 -0
  39. package/static/topImg.css +19 -0
  40. package/static/topImg.js +21 -0
  41. package/static/utils/browser13.js +270 -0
  42. package/static/utils/closebrackets.js +166 -0
  43. package/static/utils/css-lint.js +308 -0
  44. package/static/utils/custom-css-hint.js +876 -0
  45. package/static/utils/foldgutter.js +141 -0
  46. package/static/utils/match-highlighter.js +70 -0
  47. package/templates/about.html +236 -0
  48. package/templates/account/2fa.html +184 -0
  49. package/templates/account/forgot-password.html +226 -0
  50. package/templates/account/login.html +230 -0
  51. package/templates/account/profile.html +977 -0
  52. package/templates/account/register.html +224 -0
  53. package/templates/account/reset-password.html +205 -0
  54. package/templates/account/verify-email.html +163 -0
  55. package/templates/base.html +71 -0
  56. package/templates/footer-content.html +5 -0
  57. package/templates/index.html +140 -0
  58. package/templates/script.html +209 -0
  59. package/templates/test-include.html +11 -0
@@ -0,0 +1,166 @@
1
+ // 根目录/utils/closebrackets.js
2
+ /**
3
+ * closebrackets.js - CodeMirror 自定义自动括号补全插件(基于6.65.7版本的优化)
4
+ *
5
+ * 本插件为 CodeMirror 编辑器提供智能括号补全功能。当用户输入左括号时,自动插入对应的右括号,
6
+ * 并支持在空括号对中使用退格键同时删除两个括号,以及在括号对中按回车时自动缩进;
7
+ * 可通过配置选项自定义括号对、触发字符等;优化:添加了括号平衡检查,代码现代化;
8
+ *
9
+ * @author flun and others
10
+ * @license ISC
11
+ */
12
+
13
+ (function (mod) {
14
+ // CommonJS, AMD 和普通浏览器环境的模块加载兼容处理
15
+ if (typeof exports === "object" && typeof module === "object") mod(require("../../lib/codemirror"));
16
+ else if (typeof define === "function" && define.amd) define(["../../lib/codemirror"], mod);
17
+ else mod(CodeMirror);
18
+ })(CodeMirror => {
19
+ const { Pos } = CodeMirror, defaults = { pairs: `()[]{}''""`, closeBefore: `)]}'":;>`, triples: "", explode: "[]{}" },
20
+ bstring = /\bstring/,
21
+ getOption = (conf, name) =>
22
+ name === "pairs" && typeof conf === "string" ? conf : conf && conf[name] != null ? conf[name] : defaults[name],
23
+ getConfig = cm => {
24
+ const deflt = cm.state.closeBrackets;
25
+ if (!deflt || deflt.override) return deflt;
26
+ const mode = cm.getModeAt(cm.getCursor());
27
+ return mode.closeBrackets || deflt;
28
+ },
29
+ charsAround = (cm, pos) => {
30
+ const str = cm.getRange(Pos(pos.line, pos.ch - 1), Pos(pos.line, pos.ch + 1));
31
+ return str.length === 2 ? str : null;
32
+ },
33
+ stringStartsAfter = (cm, pos) => {
34
+ const { type, start } = cm.getTokenAt(Pos(pos.line, pos.ch + 1));
35
+ return bstring.test(type) && start === pos.ch && (pos.ch === 0 || !bstring.test(cm.getTokenTypeAt(pos)));
36
+ },
37
+ moveSel = (cm, dir) => {
38
+ const ranges = cm.listSelections(), newRanges = [];
39
+ let primary = 0;
40
+ for (let i = 0; i < ranges.length; i++) {
41
+ const range = ranges[i], { ch, line } = range.head, pos = ch || dir > 0 ? { line, ch: ch + dir } : { line: line - 1 };
42
+ if (range.head === cm.getCursor()) primary = i;
43
+ newRanges.push({ anchor: pos, head: pos });
44
+ }
45
+ cm.setSelections(newRanges, primary);
46
+ },
47
+ contractSelection = sel => {
48
+ const { anchor, head } = sel, dir = CodeMirror.cmpPos(anchor, head) > 0 ? -1 : 1;
49
+ return { anchor: new Pos(anchor.line, anchor.ch + dir), head: new Pos(head.line, head.ch - dir) };
50
+ },
51
+ // 通用按键处理:封装配置、禁用输入、选项存在性、选区合法性检查,通过后执行 action
52
+ handleWithOption = (optionName, action) => cm => {
53
+ const conf = getConfig(cm);
54
+ if (!conf || cm.getOption("disableInput")) return CodeMirror.Pass;
55
+ const optionValue = getOption(conf, optionName);
56
+ if (!optionValue) return CodeMirror.Pass;
57
+
58
+ const ranges = cm.listSelections(),
59
+ isValid = ranges.every(range => {
60
+ if (!range.empty()) return false;
61
+ const around = charsAround(cm, range.head);
62
+ return around && optionValue.indexOf(around) % 2 === 0;
63
+ });
64
+ if (!isValid) return CodeMirror.Pass;
65
+
66
+ return action(cm, ranges);
67
+ },
68
+ // ----- 核心处理 -----
69
+ handleBackspace = handleWithOption("pairs", (cm, ranges) => {
70
+ for (let i = ranges.length - 1; i >= 0; i--) {
71
+ const { line, ch } = ranges[i].head;
72
+ cm.replaceRange("", Pos(line, ch - 1), Pos(line, ch + 1), "+delete");
73
+ }
74
+ }),
75
+ handleEnter = handleWithOption("explode", cm => {
76
+ cm.operation(() => {
77
+ const linesep = cm.lineSeparator() || "\n", newRanges = cm.listSelections();
78
+ cm.replaceSelection(linesep.repeat(2), null), moveSel(cm, -1);
79
+
80
+ for (const range of newRanges) {
81
+ const line = range.head.line;
82
+ cm.indentLine(line, null, true), cm.indentLine(line + 1, null, true);
83
+ }
84
+ });
85
+ }),
86
+ handleChar = (cm, ch) => {
87
+ const conf = getConfig(cm);
88
+ if (!conf || cm.getOption("disableInput")) return CodeMirror.Pass;
89
+
90
+ const pairs = getOption(conf, "pairs"), pos = pairs.indexOf(ch);
91
+ if (pos === -1) return CodeMirror.Pass;
92
+
93
+ const nextChar = pairs.charAt(pos + 1), opening = pos % 2 === 0, left = pos % 2 ? pairs.charAt(pos - 1) : ch,
94
+ right = pos % 2 ? ch : nextChar, bracketPair = `${left}${right}`, closeBefore = getOption(conf, "closeBefore"),
95
+ triples = getOption(conf, "triples"), identical = nextChar === ch, ranges = cm.listSelections();
96
+
97
+ // 基于光标后文本的括号平衡检查
98
+ let suppressBoth = false;
99
+ if (opening && left !== right) {
100
+ const cursor = cm.getCursor(), from = cursor, to = Pos(cm.lastLine(), cm.getLine(cm.lastLine()).length),
101
+ textAfter = cm.getRange(from, to), countChar = (str, ch) => str.split(ch).length - 1,
102
+ leftAfter = countChar(textAfter, left), rightAfter = countChar(textAfter, right);
103
+
104
+ if (rightAfter > leftAfter) suppressBoth = true; // 如果光标后右括号多于左括号,不补全
105
+ }
106
+
107
+ let type;
108
+ const allValid = ranges.every(range => {
109
+ const cur = range.head, { line, ch: cCh } = cur, next = cm.getRange(cur, Pos(line, cCh + 1));
110
+ let curType;
111
+ if (opening && !range.empty()) curType = "surround";
112
+ else if ((identical || !opening) && next === ch) {
113
+ if (identical && stringStartsAfter(cm, cur)) curType = "both";
114
+ else if (triples.includes(ch) && cm.getRange(cur, Pos(line, cCh + 3)) === ch.repeat(3)) curType = "skipThree";
115
+ else curType = "skip";
116
+ } else if (identical && cCh > 1 && triples.includes(ch) && cm.getRange(Pos(line, cCh - 2), cur) === ch.repeat(2)) {
117
+ if (cCh > 2 && bstring.test(cm.getTokenTypeAt(Pos(line, cCh - 2)))) return false;
118
+ curType = "addFour";
119
+ } else if (identical) {
120
+ const prev = cCh === 0 ? " " : cm.getRange(Pos(line, cCh - 1), cur);
121
+ if (!CodeMirror.isWordChar(next) && prev !== ch && !CodeMirror.isWordChar(prev)) curType = "both";
122
+ else return false;
123
+ } else if (opening && (next.length === 0 || /\s/.test(next) || closeBefore.includes(next))) {
124
+ if (suppressBoth) return false; // 如果光标后有多余右括号,则抑制补全
125
+ curType = "both";
126
+ }
127
+ else return false;
128
+
129
+ if (!type) type = curType;
130
+ else if (type !== curType) return false;
131
+ return true;
132
+ });
133
+
134
+ if (!allValid) return CodeMirror.Pass;
135
+
136
+ cm.operation(() => {
137
+ if (type === "skip") moveSel(cm, 1);
138
+ else if (type === "skipThree") moveSel(cm, 3);
139
+ else if (type === "surround") {
140
+ let sels = cm.getSelections();
141
+ for (let i = 0; i < sels.length; i++) sels[i] = `${left}${sels[i]}${right}`;
142
+ cm.replaceSelections(sels, "around");
143
+ const newSels = cm.listSelections().slice();
144
+ for (let i = 0; i < newSels.length; i++) newSels[i] = contractSelection(newSels[i]);
145
+ cm.setSelections(newSels);
146
+ }
147
+ else if (type === "both") cm.replaceSelection(bracketPair, null), cm.triggerElectric(bracketPair), moveSel(cm, -1);
148
+ else if (type === "addFour") cm.replaceSelection(left.repeat(4), "before"), moveSel(cm, 1);
149
+ });
150
+ },
151
+ // 键映射和初始化
152
+ ensureBound = (chars) => {
153
+ for (let i = 0; i < chars.length; i++) {
154
+ const ch = chars.charAt(i), key = `'${ch}'`;
155
+ if (!keyMap[key]) keyMap[key] = (cm) => handleChar(cm, ch);
156
+ }
157
+ },
158
+ keyMap = { Backspace: handleBackspace, Enter: handleEnter };
159
+ ensureBound(`${defaults.pairs}\``);
160
+
161
+ // ----- 选项注册 -----
162
+ CodeMirror.defineOption("autoCloseBrackets", false, (cm, val, old) => {
163
+ if (old && old !== CodeMirror.Init) cm.removeKeyMap(keyMap), cm.state.closeBrackets = null;
164
+ if (val) ensureBound(getOption(val, "pairs")), cm.state.closeBrackets = val, cm.addKeyMap(keyMap);
165
+ });
166
+ });
@@ -0,0 +1,308 @@
1
+ // 根目录/utils/css-lint.js
2
+ /**
3
+ * 自定义 CSS Lint 函数(适配 CSSTree 的中文消息),由 CodeMirror 的 lint 插件自动调用;
4
+ *
5
+ * @param {string} text - 编辑器文本:用于解析和检查语法;
6
+ * @param {function} callback - 回调函数,接收检查结果数组:用于向编辑器反馈错误/警告;
7
+ * @param {object} [_options] - lint 配置选项中传入的自定义属性:用于传递用户偏好、开关、环境变量等;
8
+ * @param {CodeMirror} [_cm] - 当前的编辑器实例:获取光标位置、选中的文本、行信息、编辑器状态等,
9
+ * 实现更智能的检查(如只检查可见区域、根据光标位置优化提示等);
10
+ */
11
+ function customCssLint(text, callback, _options, _cm) {
12
+ const annotations = [], lines = text.split('\n'), syntaxErrors = [];
13
+ let ast = null;
14
+ if (!text.trim()) return callback(annotations);
15
+ if (typeof csstree === 'undefined') return console.error('csstree未加载,lint功能不可用'), callback(annotations);
16
+
17
+ /**
18
+ * 将CSSTree原始的英文错误消息转换为中文提示;
19
+ * @param {string} originalMsg - 原始错误消息;
20
+ * @returns {string} 本地化后的错误消息;
21
+ */
22
+ function localizeLintMessage(originalMsg) {
23
+ if (originalMsg.includes("Unexpected input")) return "意外的输入,波浪线后缺少分号或大括号或存在非法字符";
24
+ if (originalMsg.includes("Identifier is expected")) return "波浪线后存在非法字符或缺少大括号或使用了CSS嵌套";
25
+ if (originalMsg.includes("Declaration is expected")) return "声明错误,波浪线后缺少分号或大括号";
26
+ if (originalMsg.includes("Selector is expected")) return "选择器错误,波浪线后不应为大括号或多余";
27
+ if (originalMsg.includes('")" is expected')) return "当前行缺少闭合小括号或某个单词书写错误或参数中存在注释";
28
+ if (originalMsg.includes('Identifier or parenthesis is expected')) return "当前行缺少标识符或括号";
29
+ if (originalMsg.includes('Semicolon or block is expected')) return "当前行缺少分号或大括号";
30
+ if (originalMsg.includes('<empty string>')) return "输入空字符串无效";
31
+
32
+ const mismatchMatch = originalMsg.match(/Mismatch\s*(?:syntax\s*:)?\s*(.+?)(?:\s*value\s*:|\s*$)/is);
33
+ if (mismatchMatch) {
34
+ let expectedRaw = mismatchMatch[1].trim(), actualRaw = '';
35
+ const valueMatch = originalMsg.match(/value\s*:\s*(.+?)(?:\s*$|\n)/is);
36
+ if (valueMatch) actualRaw = valueMatch[1].trim();
37
+
38
+ // 清理->只取第一行
39
+ expectedRaw = expectedRaw.split('\n')[0].trim(), actualRaw = actualRaw.split('\n')[0].trim();
40
+ const expectedParts = expectedRaw.split(/\s*\|\s*/),
41
+ translatedParts = expectedParts.map(part => {
42
+ part = part.trim();
43
+ if (part === '<length>') return '长度单位';
44
+ if (part === '<percentage>') return '百分比';
45
+ if (part === '<color>') return '颜色值';
46
+ if (part === '<number>') return '数字';
47
+ if (part === '<integer>') return '整数';
48
+ if (part === '<string>') return '字符串';
49
+ if (part === 'auto') return 'auto';
50
+ if (part === 'none') return 'none';
51
+ if (part === 'inherit') return 'inherit';
52
+ if (part === 'initial') return 'initial';
53
+ return part;
54
+ }),
55
+
56
+ expectedDisplay = translatedParts.join(' | ');
57
+ return actualRaw ? `当前行属性无效:${actualRaw}\n 请修正:${expectedDisplay}` : `请修正为:${expectedDisplay}`;
58
+ }
59
+
60
+ if (originalMsg.startsWith('Unknown property')) {
61
+ const prop = originalMsg.replace('Unknown property', '').trim();
62
+ return `未知属性 ${prop}`;
63
+ }
64
+
65
+ // 未知@规则映射
66
+ if (originalMsg.startsWith('Unknown at-rule')) {
67
+ const atRule = originalMsg.replace('Unknown at-rule', '').trim();
68
+ return `未知的 @规则 ${atRule}`;
69
+ }
70
+
71
+ return `CSS 错误:${originalMsg}`;
72
+ }
73
+
74
+ /**
75
+ * 判断 AST 节点中是否包含 var() 函数
76
+ * @param {object} node - CSSTree 节点
77
+ * @returns {boolean}
78
+ */
79
+ function containsVar(node) {
80
+ if (!node) return false;
81
+ let found = false;
82
+ csstree.walk(node, {
83
+ visit: 'Function',
84
+ enter: funcNode => {
85
+ if (funcNode.name === 'var') {
86
+ found = true;
87
+ return csstree.walk.BREAK;
88
+ }
89
+ }
90
+ });
91
+ return found;
92
+ }
93
+
94
+ /**
95
+ * 处理匹配失败的情况,添加本地化错误注解
96
+ * @param {object} match - 匹配结果对象 (包含 matched 和 error)
97
+ * @param {object} location - 错误位置 (如 node.loc)
98
+ * @param {string} defaultMsg - 默认错误消息(当 match.error 不存在时使用)
99
+ * @param {Array} annotations - 存储注解的数组
100
+ */
101
+ function addMatchError(match, location, defaultMsg, annotations) {
102
+ if (!match.matched) {
103
+ let message = match.error ? match.error.message : defaultMsg;
104
+ message = localizeLintMessage(message);
105
+ addAnnotation(annotations, location, message, 'error');
106
+ }
107
+ }
108
+
109
+ try {
110
+ ast = csstree.parse(text, {
111
+ positions: true,
112
+ onParseError: error => {
113
+ if (syntaxErrors.length > 0) return; // 重要设计:只保留第一个解析错误
114
+ const lineText = lines[error.line - 1] || '';
115
+ if (error.message.includes('Identifier is expected') && /^\s*@\w+\s*:/.test(lineText)) return;
116
+
117
+ let line = error.line - 1, ch = error.column - 1;
118
+ const msg = error.message, missingErrors =
119
+ ["Unexpected input", "Identifier is expected", "Declaration is expected", "Selector is expected"];
120
+
121
+ if (missingErrors.some(e => msg.includes(e))) {
122
+ if (line >= lines.length) line = lines.length - 1, ch = lines[line].length;
123
+ if (line > 0) {
124
+ let targetLine = line - 1;
125
+ while (targetLine >= 0 && lines[targetLine].trim() === '') targetLine--;
126
+ if (targetLine >= 0) line = targetLine, ch = lines[line].length;
127
+ }
128
+ }
129
+
130
+ const lineLength = lines[line] ? lines[line].length : 0, toCh = (ch === lineLength) ? ch : ch + 1;
131
+ let fromCh = (ch > 0) ? ch - 1 : ch;
132
+ if (ch === lineLength) fromCh = Math.max(0, lineLength - 1);
133
+ else fromCh = ch;
134
+
135
+ syntaxErrors.push({
136
+ from: { line, ch: fromCh }, to: { line, ch: toCh },
137
+ message: localizeLintMessage(msg), severity: 'error'
138
+ });
139
+ }
140
+ });
141
+ } catch (e) {
142
+ return callback(syntaxErrors);
143
+ }
144
+
145
+ if (syntaxErrors.length > 0) return callback(syntaxErrors);
146
+
147
+ /**
148
+ * 将 CSSTree 的位置信息转换为 CodeMirror 注解并添加到结果数组
149
+ * @param {Array} annotations - 存储注解的数组
150
+ * @param {object} location - 包含 start/end 的位置对象 (如 node.loc)
151
+ * @param {string} message - 提示信息
152
+ * @param {string} severity - 严重级别 ('error' 或 'warning')
153
+ */
154
+ function addAnnotation(annotations, location, message, severity = 'error') {
155
+ if (!location) return;
156
+ const { start, end } = location;
157
+ if (start && end) {
158
+ annotations.push({
159
+ from: { line: start.line - 1, ch: start.column - 1 },
160
+ to: { line: end.line - 1, ch: end.column },
161
+ message, severity
162
+ });
163
+ }
164
+ }
165
+
166
+ /**
167
+ * 遍历CSS AST节点,检查并报告未知的CSS单位;
168
+ * @param {object} node - CSSTree 语法树节点;
169
+ * @param {Array} annotations - 用于存储错误信息的数组;
170
+ */
171
+ function validateUnits(node, annotations) {
172
+ if (containsVar(node)) return;
173
+
174
+ csstree.walk(node, {
175
+ visit: 'Dimension',
176
+ enter: dimNode => {
177
+ const unit = dimNode.unit,
178
+ // 常见 CSS 单位列表
179
+ validUnits = [
180
+ 'px', 'em', 'rem', 'ex', 'ch', 'vw', 'vh', 'vmin', 'vmax', '%', 'cm', 'mm', 'in', 'pt', 'pc', 'q',
181
+ 'dpi', 'dpcm', 'dppx', 's', 'ms', 'hz', 'khz', 'deg', 'grad', 'rad', 'turn'
182
+ ];
183
+ if (!validUnits.includes(unit)) addAnnotation(annotations, dimNode.loc, `未知的单位:${unit}`);
184
+ }
185
+ });
186
+ }
187
+
188
+ // 语义检查
189
+ if (ast) {
190
+ // 收集所有 @规则
191
+ const atRules = [];
192
+ csstree.walk(ast, { visit: 'Atrule', enter: node => atRules.push(node) });
193
+
194
+ // @charset 检查
195
+ const charsetNodes = atRules.filter(node => node.name === 'charset');
196
+ if (charsetNodes.length > 1)
197
+ charsetNodes.forEach(node => addAnnotation(annotations, node.loc, '@charset 只能出现一次', 'error'));
198
+ else if (charsetNodes.length === 1 && atRules[0]?.name !== 'charset')
199
+ addAnnotation(annotations, atRules[0].loc, '@charset 必须位于样式表最前面', 'error');
200
+
201
+ // @import 位置检查
202
+ let seenNonImport = false; // 是否出现过非 @charset 且非 @import 的规则
203
+ for (const node of atRules) {
204
+ if (node.name === 'charset') continue;
205
+ if (node.name === 'import' && seenNonImport)
206
+ addAnnotation(annotations, node.loc, '@import 必须在其他规则(除 @charset 外)之前', 'error');
207
+ else if (node.name !== 'import') seenNonImport = true;
208
+ }
209
+
210
+ // ---------- 上下文感知:维护@规则栈 ----------
211
+ const descriptorAtrules = new Set(['page', 'font-face', 'viewport', 'counter-style', 'property']), atruleStack = [];
212
+
213
+ // 详细检查(带 enter/leave 以维护栈)
214
+ csstree.walk(ast, {
215
+ enter: node => {
216
+ const { type, property, value, loc, name, prelude } = node;
217
+
218
+ // 进入 @规则时压栈
219
+ if (type === 'Atrule') atruleStack.push(name);
220
+ if (type === 'Declaration') {
221
+ const hasVar = containsVar(value);
222
+
223
+ // 处理自定义属性(以 -- 开头)
224
+ if (property.startsWith('--')) {
225
+ if (!hasVar) validateUnits(value, annotations);
226
+ return;
227
+ }
228
+
229
+ // 判断当前所处的@规则上下文(最近嵌套的 @规则)
230
+ const currentAtrule = atruleStack.length > 0 ? atruleStack[atruleStack.length - 1] : null,
231
+ isInsideDescriptorAtrule = currentAtrule && descriptorAtrules.has(currentAtrule);
232
+
233
+ // 位于支持描述符的@规则内,使用 matchAtruleDescriptor
234
+ if (isInsideDescriptorAtrule) {
235
+ const currentAtrule = atruleStack[atruleStack.length - 1];
236
+ let matched = false, match = null;
237
+
238
+ try {
239
+ match = csstree.lexer.matchAtruleDescriptor(currentAtrule, property, value);
240
+ if (match.matched) matched = true;
241
+ } catch (e) { }
242
+
243
+ // 如果描述符匹配失败,且属性不是自定义属性,尝试作为普通属性匹配
244
+ if (!matched && !property.startsWith('--')) {
245
+ const propDef = csstree.lexer.getProperty(property);
246
+ if (propDef) {
247
+ try {
248
+ match = csstree.lexer.matchProperty(property, value);
249
+ if (match.matched) matched = true;
250
+ } catch (e) { }
251
+ }
252
+ }
253
+
254
+ // 根据匹配结果处理
255
+ if (matched && !hasVar) validateUnits(value, annotations);
256
+ if (!matched) {
257
+ const errMsg = match && !match.matched ? match.error?.message
258
+ : `无效的 ${currentAtrule} 描述符 '${property}'`;
259
+ addMatchError({ matched: false, error: { message: errMsg } }, value.loc || loc, errMsg, annotations);
260
+ }
261
+ return;
262
+ }
263
+
264
+ // 普通规则或位于不支持描述符的@规则内,使用标准属性验证
265
+ const propertyDef = csstree.lexer.getProperty(property);
266
+ if (!propertyDef) {
267
+ addAnnotation(annotations, loc, `未知属性 '${property}'`, 'warning');
268
+ if (!hasVar) validateUnits(value, annotations);
269
+ return;
270
+ }
271
+ if (hasVar) return;
272
+
273
+ const match = csstree.lexer.matchProperty(property, value);
274
+ addMatchError(match, value.loc || loc, `无效的属性值 '${property}'`, annotations);
275
+ validateUnits(value, annotations);
276
+ }
277
+ else if (type === 'Atrule') {
278
+ if (prelude) {
279
+ if (name !== 'page') {
280
+ const preludeSyntax = csstree.lexer.getAtrulePrelude(name);
281
+ if (preludeSyntax) {
282
+ const match = csstree.lexer.matchAtrulePrelude(name, prelude);
283
+ addMatchError(match, prelude.loc || loc, `无效的 ${name} 规则条件`, annotations);
284
+ if (!containsVar(prelude)) validateUnits(prelude, annotations);
285
+ }
286
+ }
287
+ if (!containsVar(prelude)) validateUnits(prelude, annotations);
288
+ }
289
+ }
290
+ else if (type === 'KeyframeSelector') {
291
+ csstree.walk(node, {
292
+ visit: 'Identifier',
293
+ enter: idNode => {
294
+ const name = idNode.name;
295
+ if (name !== 'from' && name !== 'to')
296
+ addAnnotation(annotations, idNode.loc, `关键帧选择器只能为 from、to 或百分比`, 'error');
297
+ }
298
+ });
299
+ }
300
+ },
301
+ leave: node => {
302
+ if (node.type === 'Atrule') atruleStack.pop();
303
+ }
304
+ });
305
+ }
306
+
307
+ callback(annotations);
308
+ }