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.
- package/README.en.md +4 -3
- package/README.md +6 -5
- package/assets/demo.gif +0 -0
- package/assets/hero-sm.jpg +0 -0
- package/cordis.patch.yml +5 -0
- package/gallery-seed/d/orderg-x__alley-noodles/08e9e40834f9/_shots/menu.jpg +0 -0
- package/gallery-seed/d/orderg-x__alley-noodles/08e9e40834f9/app.css +64 -0
- package/gallery-seed/d/orderg-x__alley-noodles/08e9e40834f9/cart.html +120 -0
- package/gallery-seed/d/orderg-x__alley-noodles/08e9e40834f9/design.json +56 -0
- package/gallery-seed/d/orderg-x__alley-noodles/08e9e40834f9/menu.html +200 -0
- package/gallery-seed/d/orderg-x__alley-noodles/08e9e40834f9/order.html +135 -0
- package/gallery-seed/d/orderg-x__alley-noodles/08e9e40834f9/tokens.css +43 -0
- package/gallery-seed/d/orderg-x__autumn-sale/08e9e40834f9/_shots/email.jpg +0 -0
- package/gallery-seed/d/orderg-x__autumn-sale/08e9e40834f9/design.json +20 -0
- package/gallery-seed/d/orderg-x__autumn-sale/08e9e40834f9/email.html +268 -0
- package/gallery-seed/d/orderg-x__little-painter/08e9e40834f9/_shots/courses.jpg +0 -0
- package/gallery-seed/d/orderg-x__little-painter/08e9e40834f9/courses.html +364 -0
- package/gallery-seed/d/orderg-x__little-painter/08e9e40834f9/design.json +49 -0
- package/gallery-seed/d/orderg-x__little-painter/08e9e40834f9/lesson.html +196 -0
- package/gallery-seed/d/orderg-x__little-painter/08e9e40834f9/tokens.css +55 -0
- package/gallery-seed/d/orderg-x__little-painter/08e9e40834f9/ui.css +255 -0
- package/gallery-seed/d/orderg-x__moon-fair/08e9e40834f9/_shots/poster.jpg +0 -0
- package/gallery-seed/d/orderg-x__moon-fair/08e9e40834f9/design.json +21 -0
- package/gallery-seed/d/orderg-x__moon-fair/08e9e40834f9/poster.html +165 -0
- package/gallery-seed/d/orderg-x__moon-fair/08e9e40834f9/tokens.css +35 -0
- package/gallery-seed/d/orderg-x__paw-diary/08e9e40834f9/_shots/home.jpg +0 -0
- package/gallery-seed/d/orderg-x__paw-diary/08e9e40834f9/app.css +155 -0
- package/gallery-seed/d/orderg-x__paw-diary/08e9e40834f9/booking.html +232 -0
- package/gallery-seed/d/orderg-x__paw-diary/08e9e40834f9/design.json +57 -0
- package/gallery-seed/d/orderg-x__paw-diary/08e9e40834f9/health.html +221 -0
- package/gallery-seed/d/orderg-x__paw-diary/08e9e40834f9/home.html +225 -0
- package/gallery-seed/d/orderg-x__paw-diary/08e9e40834f9/tokens.css +54 -0
- package/gallery-seed/d/orderg-x__pocket-ledger/08e9e40834f9/_shots/overview.jpg +0 -0
- package/gallery-seed/d/orderg-x__pocket-ledger/08e9e40834f9/app.css +79 -0
- package/gallery-seed/d/orderg-x__pocket-ledger/08e9e40834f9/design.json +51 -0
- package/gallery-seed/d/orderg-x__pocket-ledger/08e9e40834f9/entries.html +161 -0
- package/gallery-seed/d/orderg-x__pocket-ledger/08e9e40834f9/icons.svg +30 -0
- package/gallery-seed/d/orderg-x__pocket-ledger/08e9e40834f9/overview.html +181 -0
- package/gallery-seed/d/orderg-x__pocket-ledger/08e9e40834f9/tokens.css +50 -0
- package/gallery-seed/d/orderg-x__time-bookshop/08e9e40834f9/_shots/home.jpg +0 -0
- package/gallery-seed/d/orderg-x__time-bookshop/08e9e40834f9/book.html +279 -0
- package/gallery-seed/d/orderg-x__time-bookshop/08e9e40834f9/design.json +21 -0
- package/gallery-seed/d/orderg-x__time-bookshop/08e9e40834f9/home.html +348 -0
- package/gallery-seed/d/orderg-x__time-bookshop/08e9e40834f9/style.css +297 -0
- package/gallery-seed/d/orderg-x__time-bookshop/08e9e40834f9/tokens.css +49 -0
- package/gallery-seed/d/orderg-x__year-review/08e9e40834f9/01-cover.html +133 -0
- package/gallery-seed/d/orderg-x__year-review/08e9e40834f9/02-highlights.html +67 -0
- package/gallery-seed/d/orderg-x__year-review/08e9e40834f9/03-growth.html +89 -0
- package/gallery-seed/d/orderg-x__year-review/08e9e40834f9/04-milestones.html +92 -0
- package/gallery-seed/d/orderg-x__year-review/08e9e40834f9/05-next.html +99 -0
- package/gallery-seed/d/orderg-x__year-review/08e9e40834f9/06-thanks.html +61 -0
- package/gallery-seed/d/orderg-x__year-review/08e9e40834f9/_shots/01-cover.jpg +0 -0
- package/gallery-seed/d/orderg-x__year-review/08e9e40834f9/deck.css +67 -0
- package/gallery-seed/d/orderg-x__year-review/08e9e40834f9/design.json +25 -0
- package/gallery-seed/d/orderg-x__year-review/08e9e40834f9/tokens.css +30 -0
- package/gallery-seed/index.json +1 -0
- package/gallery-seed/thumbs/orderg-x__alley-noodles.webp +0 -0
- package/gallery-seed/thumbs/orderg-x__autumn-sale.webp +0 -0
- package/gallery-seed/thumbs/orderg-x__little-painter.webp +0 -0
- package/gallery-seed/thumbs/orderg-x__moon-fair.webp +0 -0
- package/gallery-seed/thumbs/orderg-x__paw-diary.webp +0 -0
- package/gallery-seed/thumbs/orderg-x__pocket-ledger.webp +0 -0
- package/gallery-seed/thumbs/orderg-x__time-bookshop.webp +0 -0
- package/gallery-seed/thumbs/orderg-x__year-review.webp +0 -0
- package/lib/browser.js +23 -9
- package/lib/bundle.js +12 -3
- package/lib/client.js +3253 -483
- package/lib/core.js +231 -7
- package/lib/draft-page.js +763 -0
- package/lib/draft.js +151 -0
- package/lib/gallery/css.js +230 -0
- package/lib/gallery/html.js +313 -0
- package/lib/gallery/index.js +22 -0
- package/lib/gallery/market-routes.js +306 -0
- package/lib/gallery/market.js +623 -0
- package/lib/gallery/pack.js +311 -0
- package/lib/gallery/policy.js +1560 -0
- package/lib/gallery/qrfind.js +181 -0
- package/lib/gallery/render.js +148 -0
- package/lib/gallery/routes.js +360 -0
- package/lib/gallery/tar.js +118 -0
- package/lib/gallery/use.js +125 -0
- package/lib/gallery/zip.js +212 -0
- package/lib/handoff.js +190 -36
- package/lib/history.js +3 -1
- package/lib/index.js +123 -22
- package/lib/inspire.js +142 -0
- package/lib/offline.js +17 -0
- package/lib/picker.js +260 -1
- package/lib/serve.js +264 -11
- package/lib/tweaks.js +58 -24
- package/lib/works.js +238 -0
- package/locale/en.json +2 -1
- package/locale/offline/en.json +6 -0
- package/locale/offline/zh.json +6 -0
- package/locale/zh.json +2 -1
- package/package.json +9 -2
- 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
|
+
}
|