dsh-design-studio 0.10.0 → 0.12.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 (98) hide show
  1. package/README.en.md +4 -3
  2. package/README.md +6 -5
  3. package/assets/demo.gif +0 -0
  4. package/assets/hero-sm.jpg +0 -0
  5. package/cordis.patch.yml +5 -0
  6. package/gallery-seed/d/orderg-x__alley-noodles/08e9e40834f9/_shots/menu.jpg +0 -0
  7. package/gallery-seed/d/orderg-x__alley-noodles/08e9e40834f9/app.css +64 -0
  8. package/gallery-seed/d/orderg-x__alley-noodles/08e9e40834f9/cart.html +120 -0
  9. package/gallery-seed/d/orderg-x__alley-noodles/08e9e40834f9/design.json +56 -0
  10. package/gallery-seed/d/orderg-x__alley-noodles/08e9e40834f9/menu.html +200 -0
  11. package/gallery-seed/d/orderg-x__alley-noodles/08e9e40834f9/order.html +135 -0
  12. package/gallery-seed/d/orderg-x__alley-noodles/08e9e40834f9/tokens.css +43 -0
  13. package/gallery-seed/d/orderg-x__autumn-sale/08e9e40834f9/_shots/email.jpg +0 -0
  14. package/gallery-seed/d/orderg-x__autumn-sale/08e9e40834f9/design.json +20 -0
  15. package/gallery-seed/d/orderg-x__autumn-sale/08e9e40834f9/email.html +268 -0
  16. package/gallery-seed/d/orderg-x__little-painter/08e9e40834f9/_shots/courses.jpg +0 -0
  17. package/gallery-seed/d/orderg-x__little-painter/08e9e40834f9/courses.html +364 -0
  18. package/gallery-seed/d/orderg-x__little-painter/08e9e40834f9/design.json +49 -0
  19. package/gallery-seed/d/orderg-x__little-painter/08e9e40834f9/lesson.html +196 -0
  20. package/gallery-seed/d/orderg-x__little-painter/08e9e40834f9/tokens.css +55 -0
  21. package/gallery-seed/d/orderg-x__little-painter/08e9e40834f9/ui.css +255 -0
  22. package/gallery-seed/d/orderg-x__moon-fair/08e9e40834f9/_shots/poster.jpg +0 -0
  23. package/gallery-seed/d/orderg-x__moon-fair/08e9e40834f9/design.json +21 -0
  24. package/gallery-seed/d/orderg-x__moon-fair/08e9e40834f9/poster.html +165 -0
  25. package/gallery-seed/d/orderg-x__moon-fair/08e9e40834f9/tokens.css +35 -0
  26. package/gallery-seed/d/orderg-x__paw-diary/08e9e40834f9/_shots/home.jpg +0 -0
  27. package/gallery-seed/d/orderg-x__paw-diary/08e9e40834f9/app.css +155 -0
  28. package/gallery-seed/d/orderg-x__paw-diary/08e9e40834f9/booking.html +232 -0
  29. package/gallery-seed/d/orderg-x__paw-diary/08e9e40834f9/design.json +57 -0
  30. package/gallery-seed/d/orderg-x__paw-diary/08e9e40834f9/health.html +221 -0
  31. package/gallery-seed/d/orderg-x__paw-diary/08e9e40834f9/home.html +225 -0
  32. package/gallery-seed/d/orderg-x__paw-diary/08e9e40834f9/tokens.css +54 -0
  33. package/gallery-seed/d/orderg-x__pocket-ledger/08e9e40834f9/_shots/overview.jpg +0 -0
  34. package/gallery-seed/d/orderg-x__pocket-ledger/08e9e40834f9/app.css +79 -0
  35. package/gallery-seed/d/orderg-x__pocket-ledger/08e9e40834f9/design.json +51 -0
  36. package/gallery-seed/d/orderg-x__pocket-ledger/08e9e40834f9/entries.html +161 -0
  37. package/gallery-seed/d/orderg-x__pocket-ledger/08e9e40834f9/icons.svg +30 -0
  38. package/gallery-seed/d/orderg-x__pocket-ledger/08e9e40834f9/overview.html +181 -0
  39. package/gallery-seed/d/orderg-x__pocket-ledger/08e9e40834f9/tokens.css +50 -0
  40. package/gallery-seed/d/orderg-x__time-bookshop/08e9e40834f9/_shots/home.jpg +0 -0
  41. package/gallery-seed/d/orderg-x__time-bookshop/08e9e40834f9/book.html +279 -0
  42. package/gallery-seed/d/orderg-x__time-bookshop/08e9e40834f9/design.json +21 -0
  43. package/gallery-seed/d/orderg-x__time-bookshop/08e9e40834f9/home.html +348 -0
  44. package/gallery-seed/d/orderg-x__time-bookshop/08e9e40834f9/style.css +297 -0
  45. package/gallery-seed/d/orderg-x__time-bookshop/08e9e40834f9/tokens.css +49 -0
  46. package/gallery-seed/d/orderg-x__year-review/08e9e40834f9/01-cover.html +133 -0
  47. package/gallery-seed/d/orderg-x__year-review/08e9e40834f9/02-highlights.html +67 -0
  48. package/gallery-seed/d/orderg-x__year-review/08e9e40834f9/03-growth.html +89 -0
  49. package/gallery-seed/d/orderg-x__year-review/08e9e40834f9/04-milestones.html +92 -0
  50. package/gallery-seed/d/orderg-x__year-review/08e9e40834f9/05-next.html +99 -0
  51. package/gallery-seed/d/orderg-x__year-review/08e9e40834f9/06-thanks.html +61 -0
  52. package/gallery-seed/d/orderg-x__year-review/08e9e40834f9/_shots/01-cover.jpg +0 -0
  53. package/gallery-seed/d/orderg-x__year-review/08e9e40834f9/deck.css +67 -0
  54. package/gallery-seed/d/orderg-x__year-review/08e9e40834f9/design.json +25 -0
  55. package/gallery-seed/d/orderg-x__year-review/08e9e40834f9/tokens.css +30 -0
  56. package/gallery-seed/index.json +1 -0
  57. package/gallery-seed/thumbs/orderg-x__alley-noodles.webp +0 -0
  58. package/gallery-seed/thumbs/orderg-x__autumn-sale.webp +0 -0
  59. package/gallery-seed/thumbs/orderg-x__little-painter.webp +0 -0
  60. package/gallery-seed/thumbs/orderg-x__moon-fair.webp +0 -0
  61. package/gallery-seed/thumbs/orderg-x__paw-diary.webp +0 -0
  62. package/gallery-seed/thumbs/orderg-x__pocket-ledger.webp +0 -0
  63. package/gallery-seed/thumbs/orderg-x__time-bookshop.webp +0 -0
  64. package/gallery-seed/thumbs/orderg-x__year-review.webp +0 -0
  65. package/lib/browser.js +23 -9
  66. package/lib/bundle.js +12 -3
  67. package/lib/client.js +3253 -483
  68. package/lib/core.js +231 -7
  69. package/lib/draft-page.js +763 -0
  70. package/lib/draft.js +151 -0
  71. package/lib/gallery/css.js +230 -0
  72. package/lib/gallery/html.js +313 -0
  73. package/lib/gallery/index.js +22 -0
  74. package/lib/gallery/market-routes.js +306 -0
  75. package/lib/gallery/market.js +623 -0
  76. package/lib/gallery/pack.js +311 -0
  77. package/lib/gallery/policy.js +1560 -0
  78. package/lib/gallery/qrfind.js +181 -0
  79. package/lib/gallery/render.js +148 -0
  80. package/lib/gallery/routes.js +360 -0
  81. package/lib/gallery/tar.js +118 -0
  82. package/lib/gallery/use.js +125 -0
  83. package/lib/gallery/zip.js +212 -0
  84. package/lib/handoff.js +190 -36
  85. package/lib/history.js +3 -1
  86. package/lib/index.js +123 -22
  87. package/lib/inspire.js +142 -0
  88. package/lib/offline.js +17 -0
  89. package/lib/picker.js +260 -1
  90. package/lib/serve.js +264 -11
  91. package/lib/tweaks.js +58 -24
  92. package/lib/works.js +238 -0
  93. package/locale/en.json +2 -1
  94. package/locale/offline/en.json +6 -0
  95. package/locale/offline/zh.json +6 -0
  96. package/locale/zh.json +2 -1
  97. package/package.json +9 -2
  98. package/skills/ui-design/SKILL.md +9 -0
package/lib/draft.js ADDED
@@ -0,0 +1,151 @@
1
+ /**
2
+ * 边写边看:模型写一整页 HTML 时,write 工具的参数要流好几分钟才到。这里把流到一半的页面内容攒着,
3
+ * 预览服务器(serve.js:某屏的地址带 ?__ddp_draft=)一边收一边原样吐给 iframe,浏览器就一段段画出来。
4
+ *
5
+ * createDrafts() 文件(绝对路径)→ 正在写的内容;订阅的人随到随收
6
+ * createJsonStringDecoder() 参数是 JSON:把流过来的「字符串里面那一截」一块块还原(转义可能被切在两块中间)
7
+ */
8
+
9
+ const MAX_TEXT = 4 * 1024 * 1024 // 一页最多攒这么多(再大的也不是正常的设计稿)
10
+ const KEEP = 6 // 最多记几份(每轮开始清一次,这里只是兜底)
11
+ // 「整段一下子到的」:最后一阵(中间没停过 GAP_MS)在 LUMP_MS 里就到齐了,有 LUMP_CHARS 字以上、占全文一半以上。
12
+ // 这种才值得事后补放一遍(serve.js 晚来的也一截截放、右侧栏放完再换真文件);一路流着写的看过了,不重放
13
+ const LUMP_CHARS = 1500
14
+ const LUMP_MS = 1500
15
+ const GAP_MS = 1000
16
+
17
+ export function createDrafts({ log = () => {} } = {}) {
18
+ const map = new Map() // abs → { id, text, done, subs }
19
+ let ids = 0
20
+
21
+ const emit = (d, ev) => {
22
+ for (const fn of [...d.subs]) {
23
+ try { fn(ev) } catch (e) { log(e) }
24
+ }
25
+ }
26
+
27
+ return {
28
+ /** 开始写(同一个文件重写也从头来);返回这一份的编号,地址里带上,iframe 才会重载 */
29
+ begin(abs) {
30
+ const old = map.get(abs)
31
+ if (old) emit(old, { done: true, gone: true })
32
+ const d = { id: ++ids, text: '', done: false, subs: new Set() }
33
+ map.delete(abs)
34
+ map.set(abs, d)
35
+ while (map.size > KEEP) {
36
+ const [k, v] = map.entries().next().value
37
+ emit(v, { done: true, gone: true })
38
+ map.delete(k)
39
+ }
40
+ return d.id
41
+ },
42
+ // push / end / drop 带上 begin 给的编号时只认那一份(同一个文件后来又开始重写了,旧的不碰新的)
43
+ push(abs, text, id) {
44
+ const d = map.get(abs)
45
+ if (!d || d.done || !text || (id && d.id !== id)) return
46
+ const room = MAX_TEXT - d.text.length
47
+ if (room <= 0) return
48
+ const s = text.length > room ? text.slice(0, room) : text
49
+ const now = Date.now()
50
+ if (!d.burstAt || now - d.lastAt > GAP_MS) {
51
+ d.burstAt = now
52
+ d.burst = 0
53
+ }
54
+ d.burst += s.length
55
+ d.lastAt = now
56
+ d.text += s
57
+ emit(d, { text: s })
58
+ },
59
+ /** 参数流完了(或者这次作废了):在看的那头收尾 */
60
+ end(abs, id) {
61
+ const d = map.get(abs)
62
+ if (!d || d.done || (id && d.id !== id)) return
63
+ d.done = true
64
+ d.endAt = Date.now()
65
+ d.lump = !!d.burstAt && d.burst >= LUMP_CHARS && d.burst * 2 >= d.text.length && d.endAt - d.burstAt < LUMP_MS
66
+ emit(d, { done: true })
67
+ },
68
+ /** 作废:在看的那头收尾,之后这个地址读不到 */
69
+ drop(abs, id) {
70
+ const d = map.get(abs)
71
+ if (!d || (id && d.id !== id)) return
72
+ d.done = true
73
+ emit(d, { done: true, gone: true })
74
+ map.delete(abs)
75
+ },
76
+ get(abs) {
77
+ return map.get(abs) || null
78
+ },
79
+ /** 订阅:先拿 get() 的现有内容,之后的增量走 fn({ text }) / fn({ done }) */
80
+ sub(abs, fn) {
81
+ const d = map.get(abs)
82
+ if (!d) return () => {}
83
+ d.subs.add(fn)
84
+ return () => d.subs.delete(fn)
85
+ },
86
+ clear() {
87
+ for (const d of map.values()) emit(d, { done: true, gone: true })
88
+ map.clear()
89
+ },
90
+ }
91
+ }
92
+
93
+ const ESC = { n: '\n', t: '\t', r: '\r', b: '\b', f: '\f', '/': '/', '\\': '\\', '"': '"' }
94
+
95
+ /** JSON 字符串内容的流式还原:push(原文的一块) → 还原出来的一块;碰到收尾的引号就停(done) */
96
+ export function createJsonStringDecoder() {
97
+ let pend = '' // 切在两块中间的半个转义
98
+ let hi = '' // 半个表情(高位代理),等下一块
99
+ let done = false
100
+ return {
101
+ get done() {
102
+ return done
103
+ },
104
+ push(chunk) {
105
+ if (done) return ''
106
+ const src = pend + String(chunk || '')
107
+ pend = ''
108
+ let out = ''
109
+ let i = 0
110
+ while (i < src.length) {
111
+ const c = src[i]
112
+ if (c === '"') {
113
+ done = true
114
+ break
115
+ }
116
+ if (c !== '\\') {
117
+ let j = i + 1
118
+ while (j < src.length && src[j] !== '\\' && src[j] !== '"') j++
119
+ out += src.slice(i, j)
120
+ i = j
121
+ continue
122
+ }
123
+ if (i + 1 >= src.length) {
124
+ pend = src.slice(i)
125
+ break
126
+ }
127
+ const e = src[i + 1]
128
+ if (e === 'u') {
129
+ if (i + 6 > src.length) {
130
+ pend = src.slice(i)
131
+ break
132
+ }
133
+ const code = parseInt(src.slice(i + 2, i + 6), 16)
134
+ if (!Number.isNaN(code)) out += String.fromCharCode(code)
135
+ i += 6
136
+ continue
137
+ }
138
+ out += ESC[e] ?? e
139
+ i += 2
140
+ }
141
+ out = hi + out
142
+ hi = ''
143
+ const last = out.charCodeAt(out.length - 1)
144
+ if (!done && last >= 0xd800 && last <= 0xdbff) {
145
+ hi = out.slice(-1)
146
+ out = out.slice(0, -1)
147
+ }
148
+ return out
149
+ },
150
+ }
151
+ }
@@ -0,0 +1,230 @@
1
+ /**
2
+ * 设计库用的 CSS 读法:照 CSS 规范分词(转义会还原:\75rl( 就是 url(),找出所有会去拿文件的地方。
3
+ * Node 和浏览器里都能跑,不依赖别的包。
4
+ */
5
+
6
+ const isHex = (c) => /[0-9a-fA-F]/.test(c)
7
+ const isNameStart = (c) => /[a-zA-Z_]/.test(c) || (c && c.charCodeAt(0) >= 0x80)
8
+ const isName = (c) => isNameStart(c) || /[0-9-]/.test(c)
9
+ const isWs = (c) => c === ' ' || c === '\t' || c === '\n' || c === '\r' || c === '\f'
10
+ const validEscape = (a, b) => a === '\\' && b !== '\n' && b !== '\r' && b !== '\f' && b !== undefined
11
+
12
+ /**
13
+ * 分词。返回 [{ t, v, s, e }]
14
+ * t:ws comment str bad-str url bad-url func ident at hash num delim ( ) [ ] { } , : ; cdo cdc
15
+ * v:还原过转义的值(func / ident / at / hash 是名字,str / url 是内容)
16
+ */
17
+ export function cssTokens(src, { from = 0, stop = null } = {}) {
18
+ const s = String(src)
19
+ const n = s.length
20
+ const out = []
21
+ let i = from
22
+
23
+ function escape() {
24
+ // 已经越过反斜杠
25
+ const c = s[i]
26
+ if (c === undefined) return '�'
27
+ if (isHex(c)) {
28
+ let hex = ''
29
+ while (hex.length < 6 && i < n && isHex(s[i])) hex += s[i++]
30
+ if (i < n && isWs(s[i])) { if (s[i] === '\r' && s[i + 1] === '\n') i++; i++ }
31
+ const cp = parseInt(hex, 16)
32
+ return cp === 0 || cp > 0x10ffff || (cp >= 0xd800 && cp <= 0xdfff) ? '�' : String.fromCodePoint(cp)
33
+ }
34
+ i++
35
+ return c
36
+ }
37
+ function name() {
38
+ let v = ''
39
+ for (;;) {
40
+ const c = s[i]
41
+ if (c !== undefined && isName(c)) { v += c; i++ } else if (validEscape(c, s[i + 1])) { i++; v += escape() } else return v
42
+ }
43
+ }
44
+ const startsIdent = (a, b, c) => a === '-' ? isNameStart(b) || b === '-' || validEscape(b, c) : isNameStart(a) || validEscape(a, b)
45
+ const startsNumber = (a, b, c) => /[0-9]/.test(a) || (a === '.' && /[0-9]/.test(b)) || ((a === '+' || a === '-') && (/[0-9]/.test(b) || (b === '.' && /[0-9]/.test(c))))
46
+
47
+ function str(q, from) {
48
+ let v = ''
49
+ for (;;) {
50
+ const c = s[i]
51
+ if (c === undefined) return { t: 'str', v, s: from, e: i }
52
+ if (c === q) { i++; return { t: 'str', v, s: from, e: i } }
53
+ if (c === '\n' || c === '\r' || c === '\f') return { t: 'bad-str', v, s: from, e: i }
54
+ if (c === '\\') {
55
+ const d = s[i + 1]
56
+ if (d === undefined) { i++; continue }
57
+ if (d === '\n' || d === '\f') { i += 2; continue }
58
+ if (d === '\r') { i += s[i + 2] === '\n' ? 3 : 2; continue }
59
+ i++
60
+ v += escape()
61
+ continue
62
+ }
63
+ v += c
64
+ i++
65
+ }
66
+ }
67
+
68
+ function url(from) {
69
+ while (i < n && isWs(s[i])) i++
70
+ let v = ''
71
+ for (;;) {
72
+ const c = s[i]
73
+ if (c === undefined) return { t: 'url', v, s: from, e: i }
74
+ if (c === ')') { i++; return { t: 'url', v, s: from, e: i } }
75
+ if (isWs(c)) {
76
+ while (i < n && isWs(s[i])) i++
77
+ if (s[i] === ')' || s[i] === undefined) { if (s[i] === ')') i++; return { t: 'url', v, s: from, e: i } }
78
+ return badUrl(v, from)
79
+ }
80
+ if (c === '"' || c === "'" || c === '(' || /[\x00-\x08\x0b\x0e-\x1f\x7f]/.test(c)) return badUrl(v, from)
81
+ if (c === '\\') {
82
+ if (validEscape(c, s[i + 1])) { i++; v += escape(); continue }
83
+ return badUrl(v, from)
84
+ }
85
+ v += c
86
+ i++
87
+ }
88
+ }
89
+ function badUrl(v, from) {
90
+ for (;;) {
91
+ const c = s[i]
92
+ if (c === undefined) return { t: 'bad-url', v, s: from, e: i }
93
+ if (c === ')') { i++; return { t: 'bad-url', v, s: from, e: i } }
94
+ if (validEscape(c, s[i + 1])) { i++; escape(); continue }
95
+ i++
96
+ }
97
+ }
98
+
99
+ while (i < n) {
100
+ if (stop && stop(i, out)) break
101
+ const from = i
102
+ const c = s[i]
103
+ if (c === '/' && s[i + 1] === '*') {
104
+ const e = s.indexOf('*/', i + 2)
105
+ i = e < 0 ? n : e + 2
106
+ out.push({ t: 'comment', v: '', s: from, e: i })
107
+ continue
108
+ }
109
+ if (isWs(c)) { while (i < n && isWs(s[i])) i++; out.push({ t: 'ws', v: ' ', s: from, e: i }); continue }
110
+ if (c === '"' || c === "'") { i++; out.push(str(c, from)); continue }
111
+ if (c === '#') {
112
+ if (isName(s[i + 1]) || validEscape(s[i + 1], s[i + 2])) { i++; out.push({ t: 'hash', v: name(), s: from, e: i }); continue }
113
+ i++; out.push({ t: 'delim', v: '#', s: from, e: i }); continue
114
+ }
115
+ if ('()[]{},:;'.includes(c)) { i++; out.push({ t: c, v: c, s: from, e: i }); continue }
116
+ if (startsNumber(c, s[i + 1], s[i + 2])) {
117
+ i++
118
+ while (i < n && /[0-9.eE]/.test(s[i])) i++
119
+ if (s[i] === '%') i++
120
+ else if (startsIdent(s[i], s[i + 1], s[i + 2])) name()
121
+ out.push({ t: 'num', v: s.slice(from, i), s: from, e: i })
122
+ continue
123
+ }
124
+ if (c === '-' && s[i + 1] === '-' && s[i + 2] === '>') { i += 3; out.push({ t: 'cdc', v: '-->', s: from, e: i }); continue }
125
+ if (c === '<' && s.startsWith('<!--', i)) { i += 4; out.push({ t: 'cdo', v: '<!--', s: from, e: i }); continue }
126
+ if (c === '@' && startsIdent(s[i + 1], s[i + 2], s[i + 3])) { i++; out.push({ t: 'at', v: name(), s: from, e: i }); continue }
127
+ if (startsIdent(c, s[i + 1], s[i + 2])) {
128
+ const v = name()
129
+ if (s[i] === '(') {
130
+ i++
131
+ if (v.toLowerCase() === 'url') {
132
+ let k = i
133
+ while (k < n && isWs(s[k])) k++
134
+ if (s[k] === '"' || s[k] === "'") { out.push({ t: 'func', v, s: from, e: i }); continue }
135
+ out.push(url(from))
136
+ continue
137
+ }
138
+ out.push({ t: 'func', v, s: from, e: i })
139
+ continue
140
+ }
141
+ out.push({ t: 'ident', v, s: from, e: i })
142
+ continue
143
+ }
144
+ i++
145
+ out.push({ t: 'delim', v: c, s: from, e: i })
146
+ }
147
+ return out
148
+ }
149
+
150
+ // 参数里的字符串会被当成文件地址去拿的函数
151
+ export const URL_FUNCS = new Set(['url', 'src', 'image-set', '-webkit-image-set', 'image', 'cross-fade', '-webkit-cross-fade'])
152
+ const BAD_FUNCS = new Set(['expression', '-moz-element', 'element'])
153
+ const BAD_PROPS = new Set(['behavior', '-ms-behavior', '-moz-binding', 'binding'])
154
+
155
+ /**
156
+ * 查一段 CSS(整份样式表,或 style="" 里的声明)。
157
+ * 返回 { refs: [{ url, at, kind }], problems: [{ code, at, detail }] }
158
+ * refs:所有会去拿的地址(kind:'url' 普通、'font' 在 @font-face 里);at:在这段 CSS 里的位置
159
+ * problems:import(@import)、func(expression() 这类)、prop(behavior / -moz-binding)、bad(写坏的 url / 字符串,可能藏东西)、font-url
160
+ */
161
+ export function scanCss(src) {
162
+ const toks = cssTokens(src)
163
+ const refs = []
164
+ const problems = []
165
+ let depth = 0 // 括号层数(URL_FUNCS 里的字符串)
166
+ const funcStack = []
167
+ let fontFace = -1 // 在 @font-face 块里:块的层数
168
+ let block = 0
169
+ let skipUntilSemi = false // @namespace 的地址不会去拿
170
+ let pendingAt = ''
171
+ for (let k = 0; k < toks.length; k++) {
172
+ const tk = toks[k]
173
+ const low = typeof tk.v === 'string' ? tk.v.toLowerCase() : ''
174
+ if (skipUntilSemi) {
175
+ if (tk.t === ';' || tk.t === '{') skipUntilSemi = false
176
+ continue
177
+ }
178
+ switch (tk.t) {
179
+ case 'at':
180
+ if (low === 'import') problems.push({ code: 'import', at: tk.s })
181
+ else if (low === 'namespace') skipUntilSemi = true
182
+ pendingAt = low
183
+ break
184
+ case '{':
185
+ block++
186
+ if (pendingAt === 'font-face' && fontFace < 0) fontFace = block
187
+ pendingAt = ''
188
+ break
189
+ case '}':
190
+ if (fontFace === block) fontFace = -1
191
+ block = Math.max(0, block - 1)
192
+ break
193
+ case ';':
194
+ pendingAt = ''
195
+ break
196
+ case 'url':
197
+ refs.push({ url: tk.v, at: tk.s, kind: fontFace > 0 ? 'font' : 'url' })
198
+ break
199
+ case 'bad-url':
200
+ case 'bad-str':
201
+ problems.push({ code: 'bad', at: tk.s })
202
+ break
203
+ case 'func':
204
+ if (BAD_FUNCS.has(low)) problems.push({ code: 'func', at: tk.s, detail: tk.v })
205
+ funcStack.push(low)
206
+ depth++
207
+ break
208
+ case '(':
209
+ funcStack.push('')
210
+ depth++
211
+ break
212
+ case ')':
213
+ funcStack.pop()
214
+ depth = Math.max(0, depth - 1)
215
+ break
216
+ case 'str':
217
+ if (funcStack.some((f) => URL_FUNCS.has(f))) refs.push({ url: tk.v, at: tk.s, kind: fontFace > 0 ? 'font' : 'url' })
218
+ break
219
+ case 'ident': {
220
+ // 属性名:后面(跳过空白)是冒号
221
+ let j = k + 1
222
+ while (j < toks.length && (toks[j].t === 'ws' || toks[j].t === 'comment')) j++
223
+ if (toks[j] && toks[j].t === ':' && BAD_PROPS.has(low)) problems.push({ code: 'prop', at: tk.s, detail: tk.v })
224
+ break
225
+ }
226
+ default:
227
+ }
228
+ }
229
+ return { refs, problems }
230
+ }