@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.
- package/.env +9 -0
- package/LICENSE +15 -0
- package/build.js +3 -0
- package/compile.js +349 -0
- package/copy-files.js +200 -0
- package/customize/account.js +726 -0
- package/customize/data.json +484 -0
- package/customize/functions.js +48 -0
- package/customize/hotReloadInjector.js +25 -0
- package/customize/routes.js +141 -0
- package/customize/users.json +44 -0
- package/customize/variables.js +70 -0
- package/dev-server.js +344 -0
- package/dev.js +4 -0
- package/f-CHANGELOG.md +4 -0
- package/f-README.md +485 -0
- package/index.d.ts +133 -0
- package/index.js +4 -0
- package/package.json +77 -0
- package/restoreDefaults.js +8 -0
- package/services/templateService.js +962 -0
- package/static/about.css +118 -0
- package/static/auth.js +27 -0
- package/static/constants.css +138 -0
- package/static/img/dark.png +0 -0
- package/static/img/favicon.ico +0 -0
- package/static/img/light.png +0 -0
- package/static/img/top.png +0 -0
- package/static/index.css +86 -0
- package/static/mouseOrTouch.js +156 -0
- package/static/public.css +288 -0
- package/static/script.css +318 -0
- package/static/script.js +392 -0
- package/static/styling.css +874 -0
- package/static/styling.js +933 -0
- package/static/themeImg.css +10 -0
- package/static/themeImg.js +19 -0
- package/static/themeModule.js +222 -0
- package/static/topImg.css +19 -0
- package/static/topImg.js +21 -0
- package/static/utils/browser13.js +270 -0
- package/static/utils/closebrackets.js +166 -0
- package/static/utils/css-lint.js +308 -0
- package/static/utils/custom-css-hint.js +876 -0
- package/static/utils/foldgutter.js +141 -0
- package/static/utils/match-highlighter.js +70 -0
- package/templates/about.html +236 -0
- package/templates/account/2fa.html +184 -0
- package/templates/account/forgot-password.html +226 -0
- package/templates/account/login.html +230 -0
- package/templates/account/profile.html +977 -0
- package/templates/account/register.html +224 -0
- package/templates/account/reset-password.html +205 -0
- package/templates/account/verify-email.html +163 -0
- package/templates/base.html +71 -0
- package/templates/footer-content.html +5 -0
- package/templates/index.html +140 -0
- package/templates/script.html +209 -0
- package/templates/test-include.html +11 -0
|
@@ -0,0 +1,876 @@
|
|
|
1
|
+
// 根目录/utils/custom-css-hint.js
|
|
2
|
+
(function () {
|
|
3
|
+
'use strict';
|
|
4
|
+
// ---------- CSS 自动补全数据源 ----------
|
|
5
|
+
const GLOBAL_KEYWORDS = ['inherit', 'initial', 'unset', 'revert', 'revert-layer', 'var()'],
|
|
6
|
+
|
|
7
|
+
// 常用颜色名
|
|
8
|
+
COLOR_KEYWORDS = [
|
|
9
|
+
'aliceblue', 'antiquewhite', 'aqua', 'aquamarine', 'azure', 'beige', 'bisque', 'black', 'blanchedalmond', 'blue',
|
|
10
|
+
'blueviolet', 'brown', 'burlywood', 'cadetblue', 'chartreuse', 'chocolate', 'coral', 'cornflowerblue', 'cornsilk',
|
|
11
|
+
'crimson', 'cyan', 'darkblue', 'darkcyan', 'darkgoldenrod', 'darkgray', 'darkgreen', 'darkgrey', 'darkkhaki',
|
|
12
|
+
'darkmagenta', 'darkolivegreen', 'darkorange', 'darkorchid', 'darkred', 'darksalmon', 'darkseagreen', 'darkslateblue',
|
|
13
|
+
'darkslategray', 'darkslategrey', 'darkturquoise', 'darkviolet', 'deeppink', 'deepskyblue', 'dimgray', 'dimgrey',
|
|
14
|
+
'dodgerblue', 'firebrick', 'floralwhite', 'forestgreen', 'fuchsia', 'gainsboro', 'ghostwhite', 'gold', 'goldenrod',
|
|
15
|
+
'gray', 'green', 'greenyellow', 'grey', 'honeydew', 'hotpink', 'indianred', 'indigo', 'ivory', 'khaki', 'lavender',
|
|
16
|
+
'lavenderblush', 'lawngreen', 'lemonchiffon', 'lightblue', 'lightcoral', 'lightcyan', 'lightgoldenrodyellow',
|
|
17
|
+
'lightgray', 'lightgreen', 'lightgrey', 'lightpink', 'lightsalmon', 'lightseagreen', 'lightskyblue', 'lightslategray',
|
|
18
|
+
'lightslategrey', 'lightsteelblue', 'lightyellow', 'lime', 'limegreen', 'linen', 'magenta', 'maroon', 'mediumaquamarine',
|
|
19
|
+
'mediumblue', 'mediumorchid', 'mediumpurple', 'mediumseagreen', 'mediumslateblue', 'mediumspringgreen', 'mediumturquoise',
|
|
20
|
+
'mediumvioletred', 'midnightblue', 'mintcream', 'mistyrose', 'moccasin', 'navajowhite', 'navy', 'oldlace', 'olive',
|
|
21
|
+
'olivedrab', 'orange', 'orangered', 'orchid', 'palegoldenrod', 'palegreen', 'paleturquoise', 'palevioletred', 'papayawhip',
|
|
22
|
+
'peachpuff', 'peru', 'pink', 'plum', 'powderblue', 'purple', 'rebeccapurple', 'red', 'rosybrown', 'royalblue',
|
|
23
|
+
'saddlebrown', 'salmon', 'sandybrown', 'seagreen', 'seashell', 'sienna', 'silver', 'skyblue', 'slateblue', 'slategray',
|
|
24
|
+
'slategrey', 'snow', 'springgreen', 'steelblue', 'tan', 'teal', 'thistle', 'tomato', 'turquoise', 'violet', 'wheat',
|
|
25
|
+
'white', 'whitesmoke', 'yellow', 'yellowgreen', 'transparent',
|
|
26
|
+
// 系统颜色
|
|
27
|
+
'ActiveText', 'ButtonFace', 'ButtonText', 'Canvas', 'CanvasText', 'Field', 'FieldText', 'GrayText', 'Highlight',
|
|
28
|
+
'HighlightText', 'LinkText', 'Mark', 'MarkText', 'VisitedText'
|
|
29
|
+
],
|
|
30
|
+
|
|
31
|
+
// 常规 CSS 属性名 (约200个)
|
|
32
|
+
CSS_PROPERTIES = [
|
|
33
|
+
'align-content', 'align-items', 'align-self', 'all', 'animation', 'animation-delay', 'animation-direction',
|
|
34
|
+
'animation-duration', 'animation-fill-mode', 'animation-iteration-count', 'animation-name', 'animation-play-state',
|
|
35
|
+
'animation-timing-function', 'backdrop-filter', 'backface-visibility', 'background', 'background-attachment',
|
|
36
|
+
'background-blend-mode', 'background-clip', 'background-color', 'background-image', 'background-origin',
|
|
37
|
+
'background-position', 'background-repeat', 'background-size',
|
|
38
|
+
'border', 'border-bottom', 'border-bottom-color', 'border-bottom-style', 'border-bottom-width', 'border-collapse',
|
|
39
|
+
'border-color', 'border-image', 'border-image-outset', 'border-image-repeat', 'border-image-slice', 'border-image-source',
|
|
40
|
+
'border-image-width', 'border-left', 'border-left-color', 'border-left-style', 'border-left-width', 'border-radius',
|
|
41
|
+
'border-top-left-radius', 'border-top-right-radius', 'border-bottom-left-radius', 'border-bottom-right-radius',
|
|
42
|
+
'border-right', 'border-right-color', 'border-right-style', 'border-right-width', 'border-spacing', 'border-style',
|
|
43
|
+
'border-top', 'border-top-color', 'border-top-style', 'border-top-width', 'border-width', 'bottom',
|
|
44
|
+
'box-shadow', 'box-sizing', 'break-after', 'break-before', 'break-inside',
|
|
45
|
+
'caption-side', 'caret-color', 'clear', 'clip', 'clip-path', 'color', 'column-count', 'column-fill', 'column-gap',
|
|
46
|
+
'column-rule', 'column-rule-color', 'column-rule-style', 'column-rule-width', 'column-span', 'column-width',
|
|
47
|
+
'columns', 'content', 'counter-increment', 'counter-reset', 'cursor', 'direction', 'display', 'empty-cells',
|
|
48
|
+
'filter', 'flex', 'flex-basis', 'flex-direction', 'flex-flow', 'flex-grow', 'flex-shrink', 'flex-wrap', 'float', 'font',
|
|
49
|
+
'font-family', 'font-feature-settings', 'font-variation-settings', 'font-kerning', 'font-size', 'font-size-adjust',
|
|
50
|
+
'font-stretch', 'font-style', 'font-synthesis', 'font-variant', 'font-variant-alternates', 'font-variant-caps',
|
|
51
|
+
'font-variant-east-asian', 'font-variant-ligatures', 'font-variant-numeric', 'font-variant-position', 'font-weight',
|
|
52
|
+
'gap', 'grid', 'grid-area', 'grid-auto-columns', 'grid-auto-flow', 'grid-auto-rows', 'grid-column', 'grid-column-end',
|
|
53
|
+
'grid-column-gap', 'grid-column-start', 'grid-gap', 'grid-row', 'grid-row-end', 'grid-row-gap', 'grid-row-start',
|
|
54
|
+
'grid-template', 'grid-template-areas', 'grid-template-columns', 'grid-template-rows',
|
|
55
|
+
'height', 'hyphens', 'image-rendering', 'isolation', 'justify-content', 'justify-items', 'justify-self',
|
|
56
|
+
'left', 'letter-spacing', 'line-height', 'list-style', 'list-style-image', 'list-style-position', 'list-style-type',
|
|
57
|
+
'margin', 'margin-bottom', 'margin-left', 'margin-right', 'margin-top', 'mask', 'mask-clip', 'mask-composite',
|
|
58
|
+
'mask-image', 'mask-mode', 'mask-origin', 'mask-position', 'mask-repeat', 'mask-size', 'mask-type',
|
|
59
|
+
'max-height', 'max-width', 'min-height', 'min-width', 'mix-blend-mode',
|
|
60
|
+
'object-fit', 'object-position', 'opacity', 'order', 'orphans', 'outline', 'outline-color', 'outline-offset',
|
|
61
|
+
'outline-style', 'outline-width', 'overflow', 'overflow-x', 'overflow-y',
|
|
62
|
+
'padding', 'padding-bottom', 'padding-left', 'padding-right', 'padding-top', 'page-break-after',
|
|
63
|
+
'page-break-before', 'page-break-inside', 'perspective', 'perspective-origin', 'pointer-events', 'position',
|
|
64
|
+
'quotes', 'resize', 'right', 'row-gap', 'scroll-behavior',
|
|
65
|
+
'tab-size', 'table-layout', 'text-align', 'text-align-last', 'text-decoration', 'text-decoration-color',
|
|
66
|
+
'text-decoration-line', 'text-decoration-skip', 'text-decoration-skip-ink', 'text-decoration-style', 'text-indent',
|
|
67
|
+
'text-justify', 'text-overflow', 'text-rendering', 'text-shadow', 'text-transform', 'text-underline-offset',
|
|
68
|
+
'text-underline-position', 'top', 'transform', 'transform-origin', 'transform-style', 'transition', 'transition-delay',
|
|
69
|
+
'transition-duration', 'transition-property', 'transition-timing-function',
|
|
70
|
+
'unicode-bidi', 'user-select', 'vertical-align', 'visibility',
|
|
71
|
+
'white-space', 'widows', 'width', 'will-change', 'word-break', 'word-spacing', 'word-wrap', 'writing-mode', 'z-index'
|
|
72
|
+
],
|
|
73
|
+
|
|
74
|
+
// 常规 CSS公用值数组
|
|
75
|
+
BORDER_STYLE_VALUES = ['none', 'hidden', 'dotted', 'dashed', 'solid', 'double', 'groove', 'ridge', 'inset', 'outset'],
|
|
76
|
+
BORDER_WIDTH_VALUES = ['/* 长度值; 例如: 10px */', 'thin', 'medium', 'thick'],
|
|
77
|
+
LENGTH_AND_PERCENTAGE = ['/* 长度值; 例如: 10px */', '/* 百分比值; 例如: 50% */'],
|
|
78
|
+
MARGIN_VALUES = ['/* 长度值; 例如: 10px */', '/* 百分比值; 例如: 50% */', 'auto'],
|
|
79
|
+
MIN_WIDTH_HEIGHT_VALUES = ['/* 长度值; 例如: 10px */', '/* 百分比值; 例如: 50% */', 'min-content', 'max-content', 'fit-content'],
|
|
80
|
+
MAX_WIDTH_HEIGHT_VALUES = ['/* 长度值; 例如: 10px */', '/* 百分比值; 例如: 50% */', 'min-content', 'max-content', 'fit-content',
|
|
81
|
+
'none'],
|
|
82
|
+
WIDTH_HEIGHT_VALUES = ['/* 长度值; 例如: 10px */', '/* 百分比值; 例如: 50% */', 'min-content', 'max-content', 'fit-content',
|
|
83
|
+
'auto'],
|
|
84
|
+
GRID_TEMPLATE_VALUES = ['/* 长度值; 例如: 10px */', '/* 百分比值; 例如: 50% */', 'min-content', 'max-content', 'auto',
|
|
85
|
+
'minmax(/* 2个参数:最小值,最大值;例如: 100px, 1fr */)', 'repeat(/* 2个参数:重复次数,轨道列表;例如: 3, 1fr */)', 'none'],
|
|
86
|
+
GAP_VALUES = ['/* 长度值; 例如: 10px */', '/* 百分比值; 例如: 50% */', 'normal'],
|
|
87
|
+
OVERFLOW_VALUES = ['visible', 'hidden', 'clip', 'scroll', 'auto'],
|
|
88
|
+
SPACING_VALUES = ['normal', '/* 长度值; 例如: 10px */'],
|
|
89
|
+
PAGE_BREAK_SIDES_VALUES = ['auto', 'always', 'avoid', 'left', 'right', 'recto', 'verso'],
|
|
90
|
+
FILTER_VALUES = ['blur(/* 1个参数:长度值;例如: 5px */)', 'brightness(/* 1个参数:数字或百分比;例如: 0.5 或 50% */)',
|
|
91
|
+
'contrast(/* 1个参数:数字或百分比;例如: 200% */)', 'url(/* 1个参数:字符串;例如: "image.png" */)',
|
|
92
|
+
'drop-shadow(/* 2-4个参数:偏移x, 偏移y, 模糊半径(可选), 颜色(可选);例如: 5px 5px 5px black */)',
|
|
93
|
+
'grayscale(/* 1个参数:数字或百分比;例如: 0.5 */)', 'hue-rotate(/* 1个参数:角度值;例如: 90deg */)',
|
|
94
|
+
'invert(/* 1个参数:数字或百分比;例如: 100% */)', 'none', 'opacity(/* 1个参数:数字或百分比;例如: 0.5 */)',
|
|
95
|
+
'saturate(/* 1个参数:数字或百分比;例如: 200% */)', 'sepia(/* 1个参数:数字或百分比;例如: 0.5 */)'],
|
|
96
|
+
|
|
97
|
+
TIME_VALUES = ['/* 时间值; 例如: 2s */'],
|
|
98
|
+
TIMING_FUNCTION_VALUES = ['ease', 'linear', 'ease-in', 'ease-out', 'ease-in-out', 'step-start', 'step-end',
|
|
99
|
+
'steps(/* 2个参数:步数,方向(可选);例如: 5, start */)', 'cubic-bezier(/* 4个0~1参数:x1,y1,x2,y2;例如: 0.25, 0.1, 0.25, 1 */)'],
|
|
100
|
+
SHADOW_VALUES = ['none', '/* 长度值; 例如: 10px */', 'inset', ...COLOR_KEYWORDS],
|
|
101
|
+
BORDER_SHORTHAND_VALUES = ['none', '/* 长度值; 例如: 10px */', 'inset', 'outset', 'thin', 'medium', 'thick', 'solid',
|
|
102
|
+
'dashed', 'dotted', 'double', 'groove', 'ridge', ...COLOR_KEYWORDS],
|
|
103
|
+
FONT_FEATURE_VALUES = ['normal', '/* OpenType 特性值; 例如: "kern" 1 */'],
|
|
104
|
+
FONT_VARIATION_VALUES = ['normal', '/* 字符串+数字; 例如: "wght" 700 */'],
|
|
105
|
+
|
|
106
|
+
// 常规 CSS 属性值映射
|
|
107
|
+
PROPERTY_VALUES = {
|
|
108
|
+
// 颜色类
|
|
109
|
+
'color': COLOR_KEYWORDS, 'background-color': COLOR_KEYWORDS, 'border-color': COLOR_KEYWORDS, 'outline-color': COLOR_KEYWORDS,
|
|
110
|
+
'text-decoration-color': COLOR_KEYWORDS, 'column-rule-color': COLOR_KEYWORDS, 'border-top-color': COLOR_KEYWORDS,
|
|
111
|
+
'border-right-color': COLOR_KEYWORDS, 'border-bottom-color': COLOR_KEYWORDS, 'border-left-color': COLOR_KEYWORDS,
|
|
112
|
+
|
|
113
|
+
// 边框样式
|
|
114
|
+
'border-style': BORDER_STYLE_VALUES, 'border-top-style': BORDER_STYLE_VALUES, 'border-right-style': BORDER_STYLE_VALUES,
|
|
115
|
+
'border-bottom-style': BORDER_STYLE_VALUES, 'border-left-style': BORDER_STYLE_VALUES, 'outline-style': BORDER_STYLE_VALUES,
|
|
116
|
+
|
|
117
|
+
// 边框宽度
|
|
118
|
+
'border-width': BORDER_WIDTH_VALUES, 'border-top-width': BORDER_WIDTH_VALUES, 'border-right-width': BORDER_WIDTH_VALUES,
|
|
119
|
+
'border-bottom-width': BORDER_WIDTH_VALUES, 'border-left-width': BORDER_WIDTH_VALUES, 'outline-width': BORDER_WIDTH_VALUES,
|
|
120
|
+
|
|
121
|
+
// 外边距
|
|
122
|
+
'margin': MARGIN_VALUES, 'margin-top': MARGIN_VALUES, 'margin-right': MARGIN_VALUES,
|
|
123
|
+
'margin-bottom': MARGIN_VALUES, 'margin-left': MARGIN_VALUES,
|
|
124
|
+
|
|
125
|
+
// 内边距和边框圆角
|
|
126
|
+
'padding': LENGTH_AND_PERCENTAGE, 'padding-top': LENGTH_AND_PERCENTAGE, 'padding-right': LENGTH_AND_PERCENTAGE,
|
|
127
|
+
'padding-bottom': LENGTH_AND_PERCENTAGE, 'padding-left': LENGTH_AND_PERCENTAGE, 'border-radius': LENGTH_AND_PERCENTAGE,
|
|
128
|
+
'border-top-left-radius': LENGTH_AND_PERCENTAGE, 'border-top-right-radius': LENGTH_AND_PERCENTAGE,
|
|
129
|
+
'border-bottom-right-radius': LENGTH_AND_PERCENTAGE, 'border-bottom-left-radius': LENGTH_AND_PERCENTAGE,
|
|
130
|
+
|
|
131
|
+
// 尺寸
|
|
132
|
+
'width': WIDTH_HEIGHT_VALUES, 'height': WIDTH_HEIGHT_VALUES,
|
|
133
|
+
'min-width': MIN_WIDTH_HEIGHT_VALUES, 'min-height': MIN_WIDTH_HEIGHT_VALUES,
|
|
134
|
+
'max-width': MAX_WIDTH_HEIGHT_VALUES, 'max-height': MAX_WIDTH_HEIGHT_VALUES,
|
|
135
|
+
|
|
136
|
+
// 显示与定位
|
|
137
|
+
'display': ['block', 'inline', 'inline-block', 'flex', 'inline-flex', 'grid', 'inline-grid', 'flow-root', 'none',
|
|
138
|
+
'contents', 'table', 'table-row', 'table-cell', 'list-item'],
|
|
139
|
+
'position': ['static', 'relative', 'absolute', 'fixed', 'sticky'],
|
|
140
|
+
'float': ['left', 'right', 'inline-start', 'inline-end', 'none'],
|
|
141
|
+
'clear': ['left', 'right', 'inline-start', 'inline-end', 'none', 'both'],
|
|
142
|
+
'visibility': ['visible', 'hidden', 'collapse'],
|
|
143
|
+
|
|
144
|
+
// 溢出
|
|
145
|
+
'overflow': OVERFLOW_VALUES, 'overflow-x': OVERFLOW_VALUES, 'overflow-y': OVERFLOW_VALUES,
|
|
146
|
+
|
|
147
|
+
// 弹性布局
|
|
148
|
+
'flex-direction': ['row', 'row-reverse', 'column', 'column-reverse'],
|
|
149
|
+
'flex-wrap': ['nowrap', 'wrap', 'wrap-reverse'],
|
|
150
|
+
'flex-basis': ['auto', 'content', 'min-content', 'max-content', 'fit-content', '/* 长度值; 例如: 10px */',
|
|
151
|
+
'/* 百分比值; 例如: 50% */',],
|
|
152
|
+
'justify-content': ['center', 'space-between', 'space-around', 'space-evenly', 'start', 'end', 'left', 'right',
|
|
153
|
+
'flex-start', 'flex-end'],
|
|
154
|
+
'align-content': ['center', 'space-between', 'space-around', 'space-evenly', 'stretch', 'flex-start', 'flex-end',],
|
|
155
|
+
'align-items': ['center', 'baseline', 'stretch', 'start', 'end', 'flex-start', 'flex-end'],
|
|
156
|
+
'align-self': ['center', 'baseline', 'stretch', 'start', 'end', 'flex-start', 'flex-end', 'auto'],
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
// 网格布局
|
|
160
|
+
'grid-template-columns': GRID_TEMPLATE_VALUES, 'grid-template-rows': GRID_TEMPLATE_VALUES,
|
|
161
|
+
'gap': GAP_VALUES, 'row-gap': GAP_VALUES, 'column-gap': GAP_VALUES,
|
|
162
|
+
|
|
163
|
+
// 文本相关
|
|
164
|
+
'text-align': ['left', 'right', 'center', 'justify', 'start', 'end'],
|
|
165
|
+
'vertical-align': ['baseline', 'sub', 'super', 'text-top', 'text-bottom', 'middle', 'top', 'bottom',
|
|
166
|
+
'/* 长度值; 例如: 10px */', '/* 百分比值; 例如: 50% */'],
|
|
167
|
+
'white-space': ['normal', 'nowrap', 'pre', 'pre-wrap', 'pre-line', 'break-spaces'],
|
|
168
|
+
'word-break': ['normal', 'break-all', 'keep-all', 'break-word'],
|
|
169
|
+
'font-style': ['normal', 'italic', 'oblique'],
|
|
170
|
+
'font-weight': ['normal', 'bold', 'lighter', 'bolder', '100', '200', '300', '400', '500', '600', '700', '800', '900'],
|
|
171
|
+
'font-size': ['small', 'x-small', 'xx-small', 'medium', 'large', 'x-large', 'xx-large', 'smaller', 'larger',
|
|
172
|
+
'/* 长度值; 例如: 10px */', '/* 百分比值; 例如: 50% */'],
|
|
173
|
+
'font-family': ['serif', 'sans-serif', 'monospace', 'cursive', 'fantasy', 'system-ui', 'Arial', 'Helvetica',
|
|
174
|
+
'Times New Roman', 'Courier New'],
|
|
175
|
+
'font-feature-settings': FONT_FEATURE_VALUES,
|
|
176
|
+
'font-variation-settings': FONT_VARIATION_VALUES,
|
|
177
|
+
'text-transform': ['none', 'capitalize', 'uppercase', 'lowercase', 'full-width'],
|
|
178
|
+
'text-decoration-line': ['none', 'underline', 'overline', 'line-through', 'blink'],
|
|
179
|
+
'text-decoration-style': ['solid', 'double', 'dotted', 'dashed', 'wavy'],
|
|
180
|
+
'text-decoration-thickness': ['auto', 'from-font', '/* 长度值; 例如: 10px */', '/* 百分比值; 例如: 50% */'],
|
|
181
|
+
'line-height': ['normal', '/* 数字值; 例如: 1.5 */', '/* 长度值; 例如: 10px */', '/* 百分比值; 例如: 50% */'],
|
|
182
|
+
'letter-spacing': SPACING_VALUES, 'word-spacing': SPACING_VALUES,
|
|
183
|
+
'cursor': ['auto', 'default', 'none', 'context-menu', 'help', 'pointer', 'progress', 'wait', 'cell', 'crosshair', 'text',
|
|
184
|
+
'vertical-text', 'alias', 'copy', 'move', 'no-drop', 'not-allowed', 'grab', 'grabbing', 'all-scroll', 'col-resize',
|
|
185
|
+
'row-resize', 'n-resize', 'e-resize', 's-resize', 'w-resize', 'ne-resize', 'nw-resize', 'se-resize', 'sw-resize',
|
|
186
|
+
'ew-resize', 'ns-resize', 'nesw-resize', 'nwse-resize', 'zoom-in', 'zoom-out'],
|
|
187
|
+
|
|
188
|
+
// 列表
|
|
189
|
+
'list-style-type': ['disc', 'circle', 'square', 'decimal', 'decimal-leading-zero', 'lower-roman', 'upper-roman',
|
|
190
|
+
'lower-greek', 'lower-alpha', 'upper-alpha', 'none'],
|
|
191
|
+
|
|
192
|
+
// 盒模型
|
|
193
|
+
'box-sizing': ['content-box', 'border-box'],
|
|
194
|
+
'object-fit': ['fill', 'contain', 'cover', 'none', 'scale-down'],
|
|
195
|
+
|
|
196
|
+
// 背景
|
|
197
|
+
'background-size': ['auto', 'cover', 'contain'],
|
|
198
|
+
'background-attachment': ['scroll', 'fixed', 'local'],
|
|
199
|
+
'background-origin': ['border-box', 'padding-box', 'content-box'],
|
|
200
|
+
'background-clip': ['border-box', 'padding-box', 'content-box', 'text'],
|
|
201
|
+
'background-repeat': ['repeat', 'repeat-x', 'repeat-y', 'no-repeat', 'space', 'round'],
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
// 表格
|
|
205
|
+
'border-collapse': ['collapse', 'separate'],
|
|
206
|
+
'caption-side': ['top', 'bottom'],
|
|
207
|
+
'empty-cells': ['show', 'hide'],
|
|
208
|
+
'table-layout': ['auto', 'fixed'],
|
|
209
|
+
|
|
210
|
+
// 书写模式
|
|
211
|
+
'direction': ['ltr', 'rtl'],
|
|
212
|
+
'unicode-bidi': ['normal', 'embed', 'isolate', 'bidi-override', 'isolate-override', 'plaintext'],
|
|
213
|
+
'writing-mode': ['horizontal-tb', 'vertical-rl', 'vertical-lr', 'sideways-rl', 'sideways-lr'],
|
|
214
|
+
|
|
215
|
+
// 分页
|
|
216
|
+
'page-break-before': PAGE_BREAK_SIDES_VALUES, 'page-break-after': PAGE_BREAK_SIDES_VALUES,
|
|
217
|
+
'page-break-inside': ['auto', 'avoid'],
|
|
218
|
+
|
|
219
|
+
// 效果
|
|
220
|
+
'z-index': ['auto', '/* 整数值; 例如: 5 */'],
|
|
221
|
+
'opacity': ['/* 数字值; 例如: 0.5 */'],
|
|
222
|
+
'transform': ['none', 'matrix(/* 6个参数: a, b, c, d, tx, ty;例如: 1, 0, 0, 1, 0, 0 */)',
|
|
223
|
+
'matrix3d(/* 16个参数: 矩阵值;例如: 1,0,0,0, 0,1,0,0, 0,0,1,0, 0,0,0,1 */)',
|
|
224
|
+
'translate(/* 1-2个参数: 长度值X, 长度值Y(可选);例如: 10px, 20px */)',
|
|
225
|
+
'translate3d(/* 3个参数: tx, ty, tz;例如: 10px, 20px, 30px */)', 'translateX(/* 1个参数: 长度值;例如: 10px */)',
|
|
226
|
+
'translateY(/* 1个参数: 长度值;例如: 10px */)', 'translateZ(/* 1个参数: 长度值;例如: 10px */)',
|
|
227
|
+
'scale(/* 1-2个参数: 缩放倍数X, 缩放倍数Y(可选);例如: 2 */)', 'scale3d(/* 3个参数: sx, sy, sz;例如: 2, 0.5, 1 */)',
|
|
228
|
+
'scaleX(/* 1个参数: 缩放倍数;例如: 2 */)', 'scaleY(/* 1个参数: 缩放倍数;例如: 2 */)',
|
|
229
|
+
'scaleZ(/* 1个参数: 缩放倍数;例如: 2 */)', 'rotate(/* 1个参数: 角度值;例如: 45deg */)',
|
|
230
|
+
'rotate3d(/* 4个参数: x, y, z, angle;例如: 1, 1, 0, 45deg */)', 'rotateX(/* 1个参数: 角度值;例如: 45deg */)',
|
|
231
|
+
'rotateY(/* 1个参数: 角度值;例如: 45deg */)', 'rotateZ(/* 1个参数: 角度值;例如: 45deg */)',
|
|
232
|
+
'skew(/* 1-2个参数: 角度X, 角度Y(可选);例如: 10deg, 20deg */)', 'skewX(/* 1个参数: 角度值;例如: 10deg */)',
|
|
233
|
+
'skewY(/* 1个参数: 角度值;例如: 10deg */)', 'perspective(/* 1个参数: 长度值;例如: 100px */)'],
|
|
234
|
+
'filter': FILTER_VALUES, 'backdrop-filter': FILTER_VALUES,
|
|
235
|
+
'clip-path': ['none', 'inset(/* 1-4个参数: 长度值(上右下左);例如: 10px 20px */)',
|
|
236
|
+
'circle(/* 1-2个参数: 半径值, at 圆心位置(可选);例如: 50% */)',
|
|
237
|
+
'ellipse(/* 1-3个参数: rx ry, at 圆心位置(可选);例如: 50% 30% */)',
|
|
238
|
+
'polygon(/* 2个以上参数: 填充规则(可选) + 坐标对列表;例如: 0% 0%, 100% 0%, 100% 100% */)',
|
|
239
|
+
'path(/* 1个参数: 路径字符串;例如: "M 0 0 L 100 100" */)', 'url(/* 1个参数: 字符串;例如: "clip.svg#id" */)'],
|
|
240
|
+
'mask-mode': ['alpha', 'luminance', 'match-source'],
|
|
241
|
+
'mask-type': ['alpha', 'luminance'],
|
|
242
|
+
'isolation': ['auto', 'isolate'],
|
|
243
|
+
'mix-blend-mode': ['normal', 'multiply', 'screen', 'overlay', 'darken', 'lighten', 'color-dodge', 'color-burn', 'color',
|
|
244
|
+
'hard-light', 'soft-light', 'difference', 'exclusion', 'hue', 'saturation', 'luminosity', 'plus-darker', 'plus-lighter'],
|
|
245
|
+
'background-blend-mode': ['normal', 'multiply', 'screen', 'overlay', 'darken', 'lighten', 'color-dodge', 'color-burn',
|
|
246
|
+
'color', 'hard-light', 'soft-light', 'difference', 'exclusion', 'hue', 'saturation', 'luminosity'],
|
|
247
|
+
|
|
248
|
+
// 过渡与动画
|
|
249
|
+
'transition-property': ['all', 'none', ...CSS_PROPERTIES],
|
|
250
|
+
'animation-name': ['none', ...CSS_PROPERTIES],
|
|
251
|
+
'transition-timing-function': TIMING_FUNCTION_VALUES, 'animation-timing-function': TIMING_FUNCTION_VALUES,
|
|
252
|
+
'transition-duration': TIME_VALUES, 'transition-delay': TIME_VALUES,
|
|
253
|
+
'animation-duration': TIME_VALUES, 'animation-delay': TIME_VALUES,
|
|
254
|
+
'animation-iteration-count': ['infinite', '/* 数字值; 例如: 1.0 */'],
|
|
255
|
+
'animation-direction': ['normal', 'reverse', 'alternate', 'alternate-reverse'],
|
|
256
|
+
'animation-fill-mode': ['none', 'forwards', 'backwards', 'both'],
|
|
257
|
+
'animation-play-state': ['running', 'paused'],
|
|
258
|
+
|
|
259
|
+
// 缩写属性
|
|
260
|
+
'border': BORDER_SHORTHAND_VALUES, 'border-top': BORDER_SHORTHAND_VALUES, 'border-right': BORDER_SHORTHAND_VALUES,
|
|
261
|
+
'border-bottom': BORDER_SHORTHAND_VALUES, 'border-left': BORDER_SHORTHAND_VALUES, 'outline': BORDER_SHORTHAND_VALUES,
|
|
262
|
+
|
|
263
|
+
'background': ['none', 'transparent',
|
|
264
|
+
'url(/* 1个参数: 字符串;例如: "image.png" */)',
|
|
265
|
+
'linear-gradient(/* 2个以上参数: 方向(可选) + 颜色停止列表;例如: to right, red, blue */)',
|
|
266
|
+
'radial-gradient(/* 2个以上参数: 形状/位置(可选) + 颜色停止列表;例如: circle, red, blue */)',
|
|
267
|
+
'repeat', 'no-repeat', 'repeat-x', 'repeat-y', 'scroll', 'fixed', 'local', 'cover', 'contain', 'auto',
|
|
268
|
+
'/* 长度值; 例如: 10px */', '/* 百分比值; 例如: 50% */', 'left', 'center', 'right', 'top', 'bottom', ...COLOR_KEYWORDS],
|
|
269
|
+
'font': ['normal', 'italic', 'oblique', 'bold', 'bolder', 'lighter', '100', '200', '300', '400', '500', '600', '700',
|
|
270
|
+
'800', '900', 'small-caps', '/* 长度值; 例如: 10px */', '/* 百分比值; 例如: 50% */', 'xx-small', 'x-small', 'small',
|
|
271
|
+
'medium', 'large', 'x-large', 'xx-large', 'smaller', 'larger', 'serif', 'sans-serif', 'monospace', 'cursive',
|
|
272
|
+
'fantasy', 'system-ui'],
|
|
273
|
+
'list-style': ['none', 'disc', 'circle', 'square', 'decimal', 'decimal-leading-zero', 'lower-roman', 'upper-roman',
|
|
274
|
+
'lower-greek', 'lower-alpha', 'upper-alpha', 'armenian', 'georgian', 'inside', 'outside',
|
|
275
|
+
'url(/* 1个参数: 图像路径,用于自定义列表标记图像;例如: "marker.png" */)'],
|
|
276
|
+
'text-decoration': ['none', 'underline', 'overline', 'line-through', 'blink', 'solid', 'double', 'dotted', 'dashed',
|
|
277
|
+
'wavy', ...COLOR_KEYWORDS],
|
|
278
|
+
'transition': ['none', 'all', '/* 属性名; 例如: color */', '/* 时间值; 例如: 2s */', 'ease', 'linear', 'ease-in',
|
|
279
|
+
'ease-out', 'ease-in-out', 'step-start', 'step-end'],
|
|
280
|
+
'animation': ['none', '/* 动画名称; 例如: myanimation */', '/* 时间值; 例如: 2s */', 'ease', 'linear', 'ease-in',
|
|
281
|
+
'ease-out', 'ease-in-out', 'step-start', 'step-end', 'infinite', 'normal', 'reverse', 'alternate-reverse',
|
|
282
|
+
'alternate', 'forwards', 'backwards', 'both', 'running', 'paused'],
|
|
283
|
+
'box-shadow': SHADOW_VALUES, 'text-shadow': SHADOW_VALUES
|
|
284
|
+
},
|
|
285
|
+
|
|
286
|
+
// 伪类 (以 : 开头)
|
|
287
|
+
PSEUDO_CLASSES = [':active', ':any-link', ':blank', ':checked', ':current', ':default', ':defined',
|
|
288
|
+
':dir(/* 1个参数: 文字方向;例如: ltr或rtl */)', ':disabled', ':empty', ':enabled',
|
|
289
|
+
':first', ':first-child', ':first-of-type', ':fullscreen', ':future', ':focus', ':focus-visible', ':focus-within',
|
|
290
|
+
':has(/* 1个参数: 选择器;例如: img */)', ':host', ':host(/* 1个参数: 选择器;例如: div */)',
|
|
291
|
+
':host-context(/* 1个参数: 选择器;例如: .theme */)', ':hover', ':indeterminate', ':in-range', ':invalid',
|
|
292
|
+
':is(/* 1个选择器列表参数;例如: h1, h2, h3 */)', ':lang(/* 1个参数: 语言代码;例如: en */)',
|
|
293
|
+
':last-child', ':last-of-type', ':left', ':link', ':local-link', ':not(/* 1个参数: 选择器;例如: .hidden */)',
|
|
294
|
+
':nth-child(/* 1个匹配位置参数: an+b 表达式或关键字;例如: 2n+1 */)', ':nth-col(/* 1个匹配位置参数: an+b 表达式;例如: 2n */)',
|
|
295
|
+
':nth-last-child(/* 1个匹配位置参数: an+b 表达式;例如: 2n+1 */)', ':nth-of-type(/* 1个匹配位置参数: an+b 表达式;例如: 2n+1 */)',
|
|
296
|
+
':nth-last-of-type(/* 1个匹配位置参数: an+b 表达式;例如: 2n+1 */)', ':only-child', ':only-of-type', ':optional',
|
|
297
|
+
':out-of-range', ':past', ':paused', ':picture-in-picture', ':placeholder-shown', ':playing', ':read-only',
|
|
298
|
+
':read-write', ':required', ':right', ':root', ':scope', ':target', ':user-invalid', ':valid', ':visited',
|
|
299
|
+
':where(/* 1个选择器列表参数;例如: h1, h2 */)'
|
|
300
|
+
],
|
|
301
|
+
// @page 专用伪类
|
|
302
|
+
PAGE_PSEUDO_CLASSES = [':first', ':left', ':right', ':blank'],
|
|
303
|
+
|
|
304
|
+
// 伪元素 (以 :: 开头)
|
|
305
|
+
PSEUDO_ELEMENTS = [
|
|
306
|
+
'::after', '::backdrop', '::before', '::cue', '::cue-region', '::first-letter', '::first-line', '::grammar-error',
|
|
307
|
+
'::marker', '::part(/* 1个参数: 部件名;例如: button */)', '::placeholder', '::selection', '::spelling-error',
|
|
308
|
+
'::slotted(/* 1个参数: 选择器;例如: span */)'
|
|
309
|
+
],
|
|
310
|
+
|
|
311
|
+
// HTML 元素名
|
|
312
|
+
HTML_ELEMENTS = [
|
|
313
|
+
'a', 'abbr', 'address', 'area', 'article', 'aside', 'audio', 'b', 'base', 'bdi', 'bdo', 'blockquote', 'body', 'br', 'button',
|
|
314
|
+
'canvas', 'caption', 'cite', 'code', 'col', 'colgroup', 'data', 'datalist', 'dd', 'del', 'details', 'dfn', 'dialog',
|
|
315
|
+
'div', 'dl', 'dt', 'em', 'embed', 'fieldset', 'figcaption', 'figure', 'footer', 'form',
|
|
316
|
+
'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'head', 'header', 'hgroup', 'hr', 'html',
|
|
317
|
+
'i', 'iframe', 'img', 'input', 'ins', 'kbd', 'label', 'legend', 'li', 'link', 'main', 'map', 'mark', 'menu', 'meta', 'meter',
|
|
318
|
+
'nav', 'noscript', 'object', 'ol', 'optgroup', 'option', 'output', 'p', 'picture', 'pre', 'progress', 'q', 'rp', 'rt', 'ruby',
|
|
319
|
+
's', 'samp', 'script', 'section', 'select', 'slot', 'small', 'source', 'span', 'strong', 'style', 'sub', 'summary', 'sup',
|
|
320
|
+
'table', 'tbody', 'td', 'template', 'textarea', 'tfoot', 'th', 'thead', 'time', 'title', 'tr', 'track', 'u', 'ul',
|
|
321
|
+
'var', 'video', 'wbr'
|
|
322
|
+
],
|
|
323
|
+
|
|
324
|
+
// @规则名
|
|
325
|
+
AT_RULES = [
|
|
326
|
+
'@charset', '@color-profile', '@container', '@counter-style', '@document', '@font-face', '@font-feature-values',
|
|
327
|
+
'@import', '@keyframes', '@layer', '@media', '@namespace', '@page', '@property', '@scroll-timeline',
|
|
328
|
+
'@supports', '@viewport', '@scope', '@starting-style', '@view-transition', '@font-palette-values'
|
|
329
|
+
],
|
|
330
|
+
|
|
331
|
+
// @media媒体特性名
|
|
332
|
+
MEDIA_FEATURES = [
|
|
333
|
+
'width', 'min-width', 'max-width', 'height', 'min-height', 'max-height',
|
|
334
|
+
'aspect-ratio', 'min-aspect-ratio', 'max-aspect-ratio', 'orientation', 'color', 'color-index', 'monochrome',
|
|
335
|
+
'resolution', 'min-resolution', 'max-resolution', 'scan', 'grid', 'update', 'overflow-block', 'overflow-inline',
|
|
336
|
+
'hover', 'any-hover', 'pointer', 'any-pointer', 'prefers-color-scheme', 'prefers-reduced-motion', 'prefers-contrast',
|
|
337
|
+
'dynamic-range', 'video-dynamic-range', 'scripting', 'display-mode', 'forced-colors', 'inverted-colors', 'prefers-reduced-data'
|
|
338
|
+
],
|
|
339
|
+
|
|
340
|
+
// @media 媒体类型
|
|
341
|
+
MEDIA_TYPES = ['all', 'print', 'screen', 'speech', 'tv', 'projection', 'handheld', 'braille', 'embossed', 'tty'],
|
|
342
|
+
// @规则运算符
|
|
343
|
+
MEDIA_OPERATORS = ['only', 'not', 'and'],
|
|
344
|
+
SUPPORTS_OPERATORS = ['not', 'and', 'or'],
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
// @media媒体通用值
|
|
348
|
+
MEDIA_GENERIC_VALUES = ['/* 长度值; 例如: 10px */', '/* 百分比值; 例如: 50% */', '/* 分辨率值; 例如: 300dpi */',
|
|
349
|
+
'/* 整数值; 例如: 5 */', '/* 数字值; 例如: 0.5 */', 'portrait', 'landscape', 'progressive', 'interlace', 'infinite'],
|
|
350
|
+
|
|
351
|
+
// ----- 媒体特性共用值 -----
|
|
352
|
+
INTEGER_VALUES = ['/* 整数值; 例如: 5 */'],
|
|
353
|
+
HOVER_VALUES = ['none', 'hover'],
|
|
354
|
+
POINTER_VALUES = ['none', 'coarse', 'fine'],
|
|
355
|
+
DYNAMIC_RANGE_VALUES = ['standard', 'high'],
|
|
356
|
+
|
|
357
|
+
// @media媒体特性值映射
|
|
358
|
+
MEDIA_FEATURE_VALUES = {
|
|
359
|
+
'orientation': ['portrait', 'landscape'],
|
|
360
|
+
'scan': ['progressive', 'interlace'],
|
|
361
|
+
'grid': ['0', '1'],
|
|
362
|
+
'color': INTEGER_VALUES, 'color-index': INTEGER_VALUES, 'monochrome': INTEGER_VALUES,
|
|
363
|
+
'resolution': ['/* 分辨率值; 例如: 300dpi */', 'infinite'],
|
|
364
|
+
'update': ['none', 'slow', 'normal'],
|
|
365
|
+
'overflow-inline': ['none', 'scroll'],
|
|
366
|
+
'overflow-block': ['none', 'scroll', 'optional-paged'],
|
|
367
|
+
'hover': HOVER_VALUES, 'any-hover': HOVER_VALUES,
|
|
368
|
+
'pointer': POINTER_VALUES, 'any-pointer': POINTER_VALUES,
|
|
369
|
+
'prefers-color-scheme': ['light', 'dark'],
|
|
370
|
+
'prefers-reduced-motion': ['no-preference', 'reduce'],
|
|
371
|
+
'prefers-contrast': ['no-preference', 'high', 'low', 'forced'],
|
|
372
|
+
'dynamic-range': DYNAMIC_RANGE_VALUES, 'video-dynamic-range': DYNAMIC_RANGE_VALUES,
|
|
373
|
+
'scripting': ['none', 'initial-only', 'enabled'],
|
|
374
|
+
'display-mode': ['fullscreen', 'standalone', 'minimal-ui', 'browser'],
|
|
375
|
+
'forced-colors': ['none', 'active'],
|
|
376
|
+
'inverted-colors': ['none', 'inverted'],
|
|
377
|
+
'prefers-reduced-data': ['no-preference', 'reduce']
|
|
378
|
+
},
|
|
379
|
+
|
|
380
|
+
// @规则块内专有属性名
|
|
381
|
+
AT_RULE_BLOCK_DESCRIPTORS = {
|
|
382
|
+
'font-face': ['font-family', 'src', 'font-weight', 'font-style', 'font-stretch', 'unicode-range', 'font-variant',
|
|
383
|
+
'font-feature-settings', 'font-variation-settings'],
|
|
384
|
+
'page': ['size', 'margin', 'margin-top', 'margin-right', 'margin-bottom', 'margin-left', 'marks', 'bleed'],
|
|
385
|
+
'counter-style': ['system', 'symbols', 'additive-symbols', 'negative', 'prefix', 'suffix', 'range', 'pad',
|
|
386
|
+
'speak-as', 'fallback'],
|
|
387
|
+
'property': ['syntax', 'inherits', 'initial-value'],
|
|
388
|
+
'font-palette-values': ['font-family', 'base-palette', 'override-colors']
|
|
389
|
+
},
|
|
390
|
+
|
|
391
|
+
// @规则括号内专有特性
|
|
392
|
+
AT_RULE_PARENS_DESCRIPTORS = {
|
|
393
|
+
'media': MEDIA_FEATURES, 'container': MEDIA_FEATURES, 'scope': MEDIA_FEATURES, 'supports': CSS_PROPERTIES
|
|
394
|
+
},
|
|
395
|
+
|
|
396
|
+
// @规则专有属性值映射
|
|
397
|
+
AT_RULE_DESCRIPTOR_VALUES = {
|
|
398
|
+
// @font-face 描述符值
|
|
399
|
+
'src': ['url(/* 1个参数: 字符串;例如: "font.woff2" */)', 'local(/* 1个参数: 字体名称;例如: "Arial" */)'],
|
|
400
|
+
'font-stretch': ['normal', 'ultra-condensed', 'extra-condensed', 'condensed', 'semi-condensed', 'semi-expanded',
|
|
401
|
+
'expanded', 'extra-expanded', 'ultra-expanded'],
|
|
402
|
+
'font-variant': ['normal', 'small-caps'],
|
|
403
|
+
'font-feature-settings': FONT_FEATURE_VALUES,
|
|
404
|
+
'font-variation-settings': FONT_VARIATION_VALUES,
|
|
405
|
+
|
|
406
|
+
// @counter-style 描述符值
|
|
407
|
+
'system': ['cyclic', 'numeric', 'alphabetic', 'symbolic', 'additive', 'fixed', 'extends'],
|
|
408
|
+
'negative': ['/* 字符串序列; 例如: "-" ")" */'],
|
|
409
|
+
'prefix': ['/* 前缀字符串(一个或多个),在计数器符号前添加;例如: "第" 或 "(" */'],
|
|
410
|
+
'suffix': ['/* 后缀字符串(一个或多个),在计数器符号后添加;例如: "、" 或 ". " 或 ")" */'],
|
|
411
|
+
'range': ['auto', '/* 整数或 infinite 范围; 例如: [ 1 5 ] */'],
|
|
412
|
+
'pad': ['/* 整数+字符串; 例如: 5 "★" */'],
|
|
413
|
+
'speak-as': ['auto', 'bullets', 'numbers', 'words', 'spell-out', '/* 计数器样式名; 例如: mycounter */'],
|
|
414
|
+
'fallback': ['/* 计数器样式名; 例如: mycounter */'],
|
|
415
|
+
|
|
416
|
+
// @property 描述符值
|
|
417
|
+
'syntax': [`'<length>'`, `'<number>'`, `'<percentage>'`, `'<color>'`, `'®'`, `'<integer>'`, `'<angle>'`,
|
|
418
|
+
`'<time>'`, `'<resolution>'`, `'<transform-function>'`, `'<custom-ident>'`, `'<string>'`],
|
|
419
|
+
'inherits': ['true', 'false'],
|
|
420
|
+
'initial-value': ['/* 任意合法初始值; 例如: 10px */'],
|
|
421
|
+
|
|
422
|
+
// @page 描述符值
|
|
423
|
+
'size': ['auto', 'landscape', 'portrait', '/* 长度值(1-2个); 例如: 10cm 20cm */', 'A5', 'A4', 'A3', 'B5', 'B4', 'JIS-B5', 'JIS-B4', 'letter',
|
|
424
|
+
'legal', 'ledger'],
|
|
425
|
+
'marks': ['none', 'crop', 'cross', 'crop cross'],
|
|
426
|
+
'bleed': ['auto', '/* 长度值; 例如: 10px */'],
|
|
427
|
+
|
|
428
|
+
// @font-palette-values 描述符值
|
|
429
|
+
'base-palette': ['/* 整数值; 例如: 3 */', 'light', 'dark'],
|
|
430
|
+
'override-colors': ['/* 整数+颜色; 例如: 3 #ff0000 */']
|
|
431
|
+
},
|
|
432
|
+
|
|
433
|
+
// 关键帧选择器
|
|
434
|
+
KEYFRAME_SELECTORS = (() => {
|
|
435
|
+
const selectors = ['from', 'to'];
|
|
436
|
+
for (let i = 0; i <= 100; i += 10) selectors.push(i + '%');
|
|
437
|
+
return selectors;
|
|
438
|
+
})(),
|
|
439
|
+
|
|
440
|
+
// ---------- 集合定义 ----------
|
|
441
|
+
allAtRules = new Set(['media', 'supports', 'font-face', 'keyframes', 'page', 'counter-style', 'property', 'container',
|
|
442
|
+
'scope', 'starting-style', 'view-transition', 'font-palette-values']),
|
|
443
|
+
descriptorAtRules = new Set(['font-face', 'counter-style', 'property', 'page', 'font-palette-values']),
|
|
444
|
+
parensAtRules = new Set(['media', 'supports', 'container', 'scope']),
|
|
445
|
+
whitespace = /\s/, commentRegex = /\/\*[^*]*\*+(?:[^/*][^*]*\*+)*\//g, stringRegex = /(["'])(?:\\.|(?!\1)[^\\])*\1/g,
|
|
446
|
+
PU = /[-\w\p{L}\p{N}]/u;
|
|
447
|
+
|
|
448
|
+
// ---------- 工具函数 ----------
|
|
449
|
+
// 移除CSS中的注释和字符串
|
|
450
|
+
function stripCommentsAndStrings(cssText) {
|
|
451
|
+
return cssText.replace(commentRegex, '').replace(stringRegex, '');
|
|
452
|
+
}
|
|
453
|
+
// 判断字符串是否包含指定模式
|
|
454
|
+
function strIncludesAny(str, substrings) {
|
|
455
|
+
if (str == null) return false;
|
|
456
|
+
return substrings.some(sub => str.includes(sub));
|
|
457
|
+
}
|
|
458
|
+
// 判断字符是否为单词字符(用于值补全中的边界识别)
|
|
459
|
+
function isWordChar(c) {
|
|
460
|
+
return c && /[^\s;:(){}]/.test(c);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// 判断从指定位置开始是否为逻辑运算符
|
|
464
|
+
function isLogicOpStart(line, pos) {
|
|
465
|
+
return SUPPORTS_OPERATORS.some(op => line.slice(pos, pos + op.length).toLowerCase() === op);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// 在字符串中向前或向后查找单词边界
|
|
469
|
+
function findWordBoundary(line, lineLen, start, direction = 'forward', charTest = PU) {
|
|
470
|
+
let pos = start;
|
|
471
|
+
if (direction === 'forward') while (pos < lineLen && charTest.test(line[pos])) pos++;
|
|
472
|
+
else while (pos >= 0 && charTest.test(line[pos])) pos--;
|
|
473
|
+
return pos;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// 获取属性值候选列表
|
|
477
|
+
function getValueCandidates(insideParensWithAtRule, atRuleType, featureName) {
|
|
478
|
+
if (insideParensWithAtRule && atRuleType === 'media') return MEDIA_FEATURE_VALUES[featureName] || MEDIA_GENERIC_VALUES;
|
|
479
|
+
if (descriptorAtRules.has(atRuleType)) return AT_RULE_DESCRIPTOR_VALUES[featureName] || PROPERTY_VALUES[featureName] || [];
|
|
480
|
+
return PROPERTY_VALUES[featureName] || [];
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// 获取非值候选项
|
|
484
|
+
function getNonValueCandidates(atRuleType, insideBlock, insideParensWithAtRule, atMatch) {
|
|
485
|
+
if (insideParensWithAtRule) {
|
|
486
|
+
if (AT_RULE_PARENS_DESCRIPTORS[atRuleType]) return AT_RULE_PARENS_DESCRIPTORS[atRuleType];
|
|
487
|
+
if (atMatch) return atMatch[1].toLowerCase() === 'supports' ? CSS_PROPERTIES : MEDIA_FEATURES;
|
|
488
|
+
return [];
|
|
489
|
+
}
|
|
490
|
+
if (insideBlock) {
|
|
491
|
+
if (atRuleType === 'keyframes') return KEYFRAME_SELECTORS;
|
|
492
|
+
if (descriptorAtRules.has(atRuleType)) return AT_RULE_BLOCK_DESCRIPTORS[atRuleType] || [];
|
|
493
|
+
return HTML_ELEMENTS;
|
|
494
|
+
}
|
|
495
|
+
return HTML_ELEMENTS;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// 获取当前光标所在位置的上下文信息
|
|
499
|
+
function getContextInfo(type, sliceLine, state, posCh, atMatch) {
|
|
500
|
+
const colonIndex = sliceLine.lastIndexOf(':'), afterColon = sliceLine.slice(colonIndex + 1);
|
|
501
|
+
let ctx = state?.context, atRuleType = null, insideBlock = false, insideParensWithAtRule = false, inValue = false;
|
|
502
|
+
|
|
503
|
+
// 追踪上下文栈
|
|
504
|
+
while (ctx) {
|
|
505
|
+
const ctxType = ctx.type, ctxPrev = ctx.prev;
|
|
506
|
+
if (allAtRules.has(ctxType)) atRuleType = ctxType;
|
|
507
|
+
if (ctxType === 'block') insideBlock = true;
|
|
508
|
+
if (ctxType === 'parens' && parensAtRules.has(ctxPrev?.type)) insideParensWithAtRule = true;
|
|
509
|
+
ctx = ctxPrev;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
if (!insideParensWithAtRule && atMatch) insideParensWithAtRule = true;
|
|
513
|
+
if (!atRuleType && atMatch) atRuleType = atMatch[1].toLowerCase();
|
|
514
|
+
|
|
515
|
+
if (strIncludesAny(type, ['def', 'variable-2'])) inValue = true;
|
|
516
|
+
else if (!insideBlock && !insideParensWithAtRule) inValue = false;
|
|
517
|
+
else if (colonIndex === -1) inValue = false;
|
|
518
|
+
else if (insideParensWithAtRule && afterColon.includes(')')) inValue = false;
|
|
519
|
+
else if (strIncludesAny(afterColon, [';', '}'])) inValue = false;
|
|
520
|
+
else inValue = posCh > colonIndex;
|
|
521
|
+
|
|
522
|
+
return { inValue, atRuleType, insideBlock, insideParensWithAtRule };
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// 根据输入前缀过滤候选项,并转换为 CodeMirror 补全项格式
|
|
526
|
+
function filterHints(prefix, candidates, transform = item => item) {
|
|
527
|
+
return candidates.filter(item => {
|
|
528
|
+
const matchStr = transform(item).toLowerCase();
|
|
529
|
+
return !prefix || matchStr.startsWith(prefix.toLowerCase());
|
|
530
|
+
}).map(text => ({ text, displayText: text }));
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// 获取伪类/伪元素上下文信息
|
|
534
|
+
function getPseudoContext(line, lineLen, posCh) {
|
|
535
|
+
// 向左找到最后一个冒号的位置
|
|
536
|
+
let colonPos = -1;
|
|
537
|
+
for (let i = posCh - 1; i >= 0; i--) {
|
|
538
|
+
if (line[i] === ':') {
|
|
539
|
+
colonPos = i;
|
|
540
|
+
break;
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
if (colonPos === -1) return { pType: 'none' };
|
|
544
|
+
|
|
545
|
+
// 从 colonPos 向左找到连续冒号的起始
|
|
546
|
+
let pStart = colonPos;
|
|
547
|
+
while (pStart > 0 && line[pStart - 1] === ':') pStart--;
|
|
548
|
+
|
|
549
|
+
// 判断类型:连续冒号个数 >=2 为伪元素,否则为伪类
|
|
550
|
+
const colonCount = colonPos - pStart + 1, pType = colonCount >= 2 ? 'element' : 'class',
|
|
551
|
+
pPrefix = line.slice(colonPos + 1, posCh);
|
|
552
|
+
|
|
553
|
+
// 括号闭合检查:光标位于已闭合的伪类括号后则不补全
|
|
554
|
+
let parenLevel = 0, hasClosedParen = false;
|
|
555
|
+
for (let i = colonPos + 1; i < posCh; i++) {
|
|
556
|
+
const c = line[i];
|
|
557
|
+
if (c === '(') parenLevel++;
|
|
558
|
+
else if (c === ')') {
|
|
559
|
+
parenLevel--;
|
|
560
|
+
if (parenLevel === 0) hasClosedParen = true;
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
if (hasClosedParen && parenLevel === 0) return { pType: 'none' };
|
|
564
|
+
|
|
565
|
+
// 处理语法结构和表达式内容,边界
|
|
566
|
+
let pEnd = pStart, depth = 0;
|
|
567
|
+
while (pEnd < lineLen && line[pEnd] === ':') pEnd++;
|
|
568
|
+
while (pEnd < lineLen) {
|
|
569
|
+
const c = line[pEnd];
|
|
570
|
+
if (c === '(') depth++;
|
|
571
|
+
else if (c === ')') depth--;
|
|
572
|
+
if (depth === 0) {
|
|
573
|
+
if (c === ':' || /[;{}>,+~\[\]]/.test(c)) break;
|
|
574
|
+
}
|
|
575
|
+
pEnd++;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
if (posCh > pEnd) return { pType: 'none' };
|
|
579
|
+
return { pType, pStart, pPrefix, pEnd };
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// 检测光标是否在var()内部,并返回右括号的位置
|
|
583
|
+
function getVarCallInfo(line, posCh, sliceLine) {
|
|
584
|
+
const lastVarIndex = sliceLine.lastIndexOf('var(');
|
|
585
|
+
if (lastVarIndex === -1) return { inside: false, closed: false, closePos: -1 };
|
|
586
|
+
|
|
587
|
+
let balance = 0, closed = false, closePos = -1;
|
|
588
|
+
for (let i = lastVarIndex + 4; i < line.length; i++) {
|
|
589
|
+
const ch = line[i];
|
|
590
|
+
if (ch === '(') balance++;
|
|
591
|
+
else if (ch === ')') {
|
|
592
|
+
if (balance === 0) {
|
|
593
|
+
closed = true, closePos = i;
|
|
594
|
+
break;
|
|
595
|
+
}
|
|
596
|
+
else balance--;
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
const inside = lastVarIndex + 4 <= posCh && (!closed || posCh <= closePos);
|
|
601
|
+
return { inside, closed, closePos };
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// 获取当前光标所在值单词的范围
|
|
605
|
+
function getReplacementRangeForValue(line, lineLen, posLine, posCh, inside, insideParensWithAtRule) {
|
|
606
|
+
if (inside) {
|
|
607
|
+
let start = posCh;
|
|
608
|
+
while (start > 0 && PU.test(line[start - 1])) start--;
|
|
609
|
+
let end = posCh;
|
|
610
|
+
while (end < lineLen && PU.test(line[end])) end++;
|
|
611
|
+
return { from: CodeMirror.Pos(posLine, start), to: CodeMirror.Pos(posLine, end) };
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
// 普通值逻辑
|
|
615
|
+
let colonIndex = -1;
|
|
616
|
+
for (let i = posCh - 1; i >= 0; i--) {
|
|
617
|
+
if (line[i] === ':') {
|
|
618
|
+
let j = i - 1;
|
|
619
|
+
while (j >= 0 && whitespace.test(line[j])) j--;
|
|
620
|
+
let end = j;
|
|
621
|
+
while (j >= 0 && isWordChar(line[j])) j--;
|
|
622
|
+
if (j < end) {
|
|
623
|
+
colonIndex = i;
|
|
624
|
+
break;
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
if (colonIndex === -1) {
|
|
630
|
+
let end = posCh;
|
|
631
|
+
while (end < lineLen && isWordChar(line[end])) end++;
|
|
632
|
+
return {
|
|
633
|
+
from: CodeMirror.Pos(posLine, Math.min(posCh, end)), to: CodeMirror.Pos(posLine, end)
|
|
634
|
+
};
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
let start = colonIndex + 1;
|
|
638
|
+
while (start < lineLen && whitespace.test(line[start])) start++;
|
|
639
|
+
|
|
640
|
+
let end = start;
|
|
641
|
+
while (end < lineLen) {
|
|
642
|
+
const char = line[end];
|
|
643
|
+
if (insideParensWithAtRule) {
|
|
644
|
+
if (char === ')' || char === ';' || char === '}') break;
|
|
645
|
+
if (isLogicOpStart(line, end) && (end === start || whitespace.test(line[end - 1]))) break;
|
|
646
|
+
}
|
|
647
|
+
else if (char === ';' || char === '}') break;
|
|
648
|
+
end++;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
while (end > start && whitespace.test(line[end - 1])) end--;
|
|
652
|
+
return {
|
|
653
|
+
from: CodeMirror.Pos(posLine, Math.min(start, posCh, end)), to: CodeMirror.Pos(posLine, end)
|
|
654
|
+
};
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
// 提取 CSS 变量名
|
|
658
|
+
function extractCSSVariables(cssText) {
|
|
659
|
+
if (!cssText) return [];
|
|
660
|
+
const cleanCss = stripCommentsAndStrings(cssText), varRegex = /--([-\w\p{L}\p{N}]+)\s*:/gu, varNames = new Set();
|
|
661
|
+
let match;
|
|
662
|
+
while ((match = varRegex.exec(cleanCss)) !== null) varNames.add(`--${match[1]}`);
|
|
663
|
+
return Array.from(varNames);
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
/**
|
|
667
|
+
* 检查从 startPos 开始(含)之后,第一个非空白字符是否为分号
|
|
668
|
+
* @param {string} line 当前行完整字符串
|
|
669
|
+
* @param {number} startPos 起始索引(包含)
|
|
670
|
+
* @returns {boolean} 若找到的第一个非空白字符是分号,返回 true,否则返回 false
|
|
671
|
+
*/
|
|
672
|
+
function hasSemicolonAfter(line, startPos) {
|
|
673
|
+
const after = line.slice(startPos);
|
|
674
|
+
for (let i = 0; i < after.length; i++) {
|
|
675
|
+
const ch = after[i];
|
|
676
|
+
if (ch === ';') return true;
|
|
677
|
+
if (!whitespace.test(ch)) return false;
|
|
678
|
+
}
|
|
679
|
+
return false;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
/**
|
|
683
|
+
* 尝试获取当前光标所在位置的 @规则类型,优先使用传入的 atRuleType,
|
|
684
|
+
* 若不可信则通过向上查找括号匹配来获取真正的规则名。
|
|
685
|
+
* @param {string|null} atRuleType 初始的规则类型
|
|
686
|
+
* @param {boolean} insideBlock 是否在块内
|
|
687
|
+
* @param {CodeMirror} cm CodeMirror 实例
|
|
688
|
+
* @param {number} posLine 光标行号
|
|
689
|
+
* @param {number} posCh 光标列号
|
|
690
|
+
* @returns {string|null} 有效的规则名(小写),若未找到则返回 null
|
|
691
|
+
*/
|
|
692
|
+
function resolveAtRuleType(atRuleType, insideBlock, cm, posLine, posCh) {
|
|
693
|
+
let effective = atRuleType;
|
|
694
|
+
if (insideBlock && (!effective || !descriptorAtRules.has(effective))) {
|
|
695
|
+
let sLine = posLine;
|
|
696
|
+
while (sLine >= 0) {
|
|
697
|
+
const lineText = cm.getLine(sLine), atRegex = /@([-\w\p{L}\p{N}]+)\s*([^{]*)?\{/gu;
|
|
698
|
+
let match;
|
|
699
|
+
while ((match = atRegex.exec(lineText)) !== null) {
|
|
700
|
+
const ruleName = match[1].toLowerCase(), bPos = match.index + match[0].length - 1,
|
|
701
|
+
startPos = { line: sLine, ch: bPos }, endMatch = cm.findMatchingBracket(startPos);
|
|
702
|
+
if (endMatch?.match) {
|
|
703
|
+
const { line: endL, ch: endC } = endMatch.to;
|
|
704
|
+
if ((sLine < posLine || (sLine === posLine && bPos < posCh)) && (endL > posLine
|
|
705
|
+
|| (endL === posLine && endC > posCh))) return ruleName;
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
sLine--;
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
return effective;
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
// ---------- 主 hint 函数 ----------
|
|
715
|
+
CodeMirror.registerHelper('hint', 'css', cm => {
|
|
716
|
+
const pos = cm.getCursor(), posLine = pos.line, line = cm.getLine(posLine), posCh = pos.ch;
|
|
717
|
+
if (posCh > 0 && whitespace.test(line[posCh - 1])) return null;
|
|
718
|
+
|
|
719
|
+
const sliceLine = line.slice(0, posCh), { state, type, start, end, string } = cm.getTokenAt(pos), lineLen = line.length,
|
|
720
|
+
cssText = cm.getValue(), oFrom = CodeMirror.Pos(posLine, start), oTo = CodeMirror.Pos(posLine, end),
|
|
721
|
+
atMatch = sliceLine.match(/@(media|supports|container|scope)\s*\([^)]*$/i),
|
|
722
|
+
{ inValue, atRuleType, insideBlock, insideParensWithAtRule } = getContextInfo(type, sliceLine, state, posCh, atMatch);
|
|
723
|
+
|
|
724
|
+
let prefix = string.trim(), candidates = [];
|
|
725
|
+
// @ 规则补全
|
|
726
|
+
if (prefix.startsWith('@')) {
|
|
727
|
+
const list = filterHints(prefix, AT_RULES);
|
|
728
|
+
if (list.length > 0) return { list, from: oFrom, to: oTo };
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
// 属性值补全
|
|
732
|
+
if (inValue) {
|
|
733
|
+
const { inside, closed, closePos } = getVarCallInfo(line, posCh, sliceLine), range = getReplacementRangeForValue(
|
|
734
|
+
line, lineLen, posLine, posCh, inside, insideParensWithAtRule);
|
|
735
|
+
let from, to, needSemicolon = false;
|
|
736
|
+
if (range) from = range.from, to = range.to, needSemicolon = range.needSemicolon || false;
|
|
737
|
+
else from = to = pos;
|
|
738
|
+
|
|
739
|
+
const match = sliceLine.match(/([-\w\p{L}\p{N}]+)\s*:\s*[^;)]*$/u), featureName = match ? match[1] : null;
|
|
740
|
+
let effectiveAtRuleType = resolveAtRuleType(atRuleType, insideBlock, cm, posLine, posCh),
|
|
741
|
+
specificCandidates = getValueCandidates(insideParensWithAtRule, effectiveAtRuleType, featureName);
|
|
742
|
+
|
|
743
|
+
candidates = [...new Set([...specificCandidates, ...GLOBAL_KEYWORDS])];
|
|
744
|
+
// CSS 变量补全
|
|
745
|
+
const varNames = extractCSSVariables(cssText), shouldAddVarHints = (prefix?.startsWith('--')) || inside;
|
|
746
|
+
if (shouldAddVarHints && varNames.length) {
|
|
747
|
+
const varCandidates = filterHints(prefix, varNames);
|
|
748
|
+
candidates = [...new Set([...candidates, ...varCandidates.map(c => c.text)])];
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
if (candidates.length === 0) return null;
|
|
752
|
+
if (prefix === ':' || prefix === '::' || !prefix) prefix = '';
|
|
753
|
+
let list = filterHints(prefix, candidates);
|
|
754
|
+
if (list.length) {
|
|
755
|
+
let suffix = '';
|
|
756
|
+
if (inside) {
|
|
757
|
+
if (closed) {
|
|
758
|
+
const afterClosePos = closePos + 1, hasSemicolon = hasSemicolonAfter(line, afterClosePos);
|
|
759
|
+
if (!hasSemicolon) to = CodeMirror.Pos(posLine, afterClosePos), suffix = ');';
|
|
760
|
+
} else {
|
|
761
|
+
const hasSemicolon = hasSemicolonAfter(line, posCh);
|
|
762
|
+
suffix = hasSemicolon ? ')' : ');';
|
|
763
|
+
}
|
|
764
|
+
} else suffix = ';';
|
|
765
|
+
if (suffix) list = list.map(item => ({ text: `${item.text}${suffix}`, displayText: item.displayText }));
|
|
766
|
+
return { list, from, to };
|
|
767
|
+
}
|
|
768
|
+
return null;
|
|
769
|
+
}
|
|
770
|
+
// 非值补全
|
|
771
|
+
else {
|
|
772
|
+
// 1. 处理各种 @规则 后面的运算符或名称补全
|
|
773
|
+
const atRuleMatch = sliceLine.match(/@(media|supports|container|layer|keyframes|page)\s+([^;{]*)$/i);
|
|
774
|
+
if (atRuleMatch) {
|
|
775
|
+
const atRule = atRuleMatch[1].toLowerCase(), after = atRuleMatch[2], hasOpenParen = after.includes('('),
|
|
776
|
+
hasOpenBrace = after.includes('{'), commonMatch = after.match(/([\w-]*)$/),
|
|
777
|
+
commonPrefix = commonMatch ? commonMatch[1] : '', commonWordStart = posCh - commonPrefix.length;
|
|
778
|
+
|
|
779
|
+
function buildCompletion(prefix, candidates, wordStart = commonWordStart) {
|
|
780
|
+
const list = filterHints(prefix, candidates);
|
|
781
|
+
if (list.length)
|
|
782
|
+
return { list, from: CodeMirror.Pos(posLine, wordStart), to: CodeMirror.Pos(posLine, posCh) };
|
|
783
|
+
return null;
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
if (atRule === 'media' && !hasOpenParen && !hasOpenBrace) {
|
|
787
|
+
if (after.includes(':')) return null;
|
|
788
|
+
let candidates;
|
|
789
|
+
if (commonPrefix && MEDIA_OPERATORS.some(op => op.startsWith(commonPrefix.toLowerCase())))
|
|
790
|
+
candidates = MEDIA_OPERATORS;
|
|
791
|
+
else candidates = MEDIA_TYPES;
|
|
792
|
+
const result = buildCompletion(commonPrefix, candidates);
|
|
793
|
+
if (result) return result;
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
if ((atRule === 'supports' || atRule === 'container') && !hasOpenParen && !hasOpenBrace) {
|
|
797
|
+
if (commonPrefix && /^\w*$/.test(commonPrefix)) {
|
|
798
|
+
const result = buildCompletion(commonPrefix, SUPPORTS_OPERATORS);
|
|
799
|
+
if (result) return result;
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
if (atRule === 'layer' || atRule === 'keyframes') {
|
|
804
|
+
const cleanCss = stripCommentsAndStrings(cssText), regex = atRule === 'layer'
|
|
805
|
+
? /@layer\s+([-\w\p{L}\p{N}]+)(?=\s*[;{,]|\s*\{)/gu : /@keyframes\s+([-\w\p{L}\p{N}]+)(?=\s*\{)/gu,
|
|
806
|
+
matches = [...cleanCss.matchAll(regex)], names = [...new Set(matches.map(m => m[1]))],
|
|
807
|
+
result = buildCompletion(commonPrefix, names);
|
|
808
|
+
if (result) return result;
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
if (atRule === 'page' && !hasOpenBrace) {
|
|
812
|
+
const colonMatch = after.match(/:([\w-]*)$/);
|
|
813
|
+
if (colonMatch) {
|
|
814
|
+
const prefix = colonMatch[1], wordStart = posCh - prefix.length - 1,
|
|
815
|
+
rawCandidates = PAGE_PSEUDO_CLASSES.map(p => p.slice(1)),
|
|
816
|
+
list = filterHints(prefix, rawCandidates).map(item => ({
|
|
817
|
+
text: `: ${item.text}`, displayText: `: ${item.displayText}`
|
|
818
|
+
}));
|
|
819
|
+
if (list.length)
|
|
820
|
+
return { list, from: CodeMirror.Pos(posLine, wordStart), to: CodeMirror.Pos(posLine, posCh) };
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
// 2. 伪类/伪元素补全
|
|
826
|
+
const { pType, pStart, pPrefix, pEnd } = getPseudoContext(line, lineLen, posCh);
|
|
827
|
+
if (pType !== 'none') {
|
|
828
|
+
const [source, stripCount] = pType === 'element' ? [PSEUDO_ELEMENTS, 2] : [PSEUDO_CLASSES, 1];
|
|
829
|
+
let list = filterHints(pPrefix, source, item => item.slice(stripCount));
|
|
830
|
+
if (list.length) {
|
|
831
|
+
let from = CodeMirror.Pos(posLine, pStart), to = CodeMirror.Pos(posLine, pEnd);
|
|
832
|
+
if (!pPrefix) {
|
|
833
|
+
let wordEnd = findWordBoundary(line, lineLen, pEnd);
|
|
834
|
+
if (wordEnd > pEnd) to = CodeMirror.Pos(posLine, wordEnd);
|
|
835
|
+
}
|
|
836
|
+
return { list, from, to };
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
// 3. ID/类选择器补全
|
|
841
|
+
if (prefix.startsWith('#') || prefix.startsWith('.')) {
|
|
842
|
+
const candidatesSet = new Set(), regex = /([#.](?![#.\d])[-\w\p{L}\p{N}]+)(?=[,\s>+~[:{@(]|$)/gmu,
|
|
843
|
+
cleanCss = stripCommentsAndStrings(cssText);
|
|
844
|
+
let match, braceDepth = 0, lastIndex = 0;
|
|
845
|
+
while ((match = regex.exec(cleanCss)) !== null) {
|
|
846
|
+
const segment = cleanCss.slice(lastIndex, match.index);
|
|
847
|
+
for (let i = 0; i < segment.length; i++) {
|
|
848
|
+
if (segment[i] === '{') braceDepth++;
|
|
849
|
+
else if (segment[i] === '}') braceDepth--;
|
|
850
|
+
}
|
|
851
|
+
lastIndex = match.index;
|
|
852
|
+
if (braceDepth === 0) {
|
|
853
|
+
const fullMatch = match[1];
|
|
854
|
+
if ((prefix.startsWith('#') && fullMatch[0] === '#') || (prefix.startsWith('.') && fullMatch[0] === '.'))
|
|
855
|
+
candidatesSet.add(fullMatch);
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
const candidates = Array.from(candidatesSet), list = filterHints(prefix, candidates);
|
|
859
|
+
return list.length ? { list, from: oFrom, to: oTo } : null;
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
// 4. 属性名/其他元素补全
|
|
863
|
+
if (strIncludesAny(type, ['property', 'variable-2'])) {
|
|
864
|
+
let effectiveAtRuleType = resolveAtRuleType(atRuleType, insideBlock, cm, posLine, posCh);
|
|
865
|
+
|
|
866
|
+
if (insideBlock && descriptorAtRules.has(effectiveAtRuleType))
|
|
867
|
+
candidates = AT_RULE_BLOCK_DESCRIPTORS[effectiveAtRuleType] || [];
|
|
868
|
+
else candidates = CSS_PROPERTIES;
|
|
869
|
+
}
|
|
870
|
+
else candidates = getNonValueCandidates(atRuleType, insideBlock, insideParensWithAtRule, atMatch);
|
|
871
|
+
|
|
872
|
+
let list = filterHints(prefix, candidates);
|
|
873
|
+
return list.length ? { list, from: oFrom, to: oTo } : null;
|
|
874
|
+
}
|
|
875
|
+
});
|
|
876
|
+
})();
|