dsh-structured 0.9.4 → 0.9.9
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/CHANGELOG.md +130 -108
- package/README.md +274 -273
- package/assets/screenshots/01-title-card.png +0 -0
- package/assets/screenshots/06-status-table.png +0 -0
- package/assets/screenshots/07-code-block.png +0 -0
- package/assets/screenshots/08-narrow.png +0 -0
- package/assets/screenshots/09-theme-light.png +0 -0
- package/assets/screenshots/10-theme-dark.png +0 -0
- package/docs/0.9-FINAL-RELEASE.md +22 -0
- package/docs/1.0-BASELINE.md +38 -0
- package/docs/1.0-GATE.md +39 -0
- package/docs/1.0-PERSONA-SPEC.md +59 -0
- package/lib/client.js +95 -68
- package/lib/index.js +518 -517
- package/package.json +102 -99
- package/scripts/theme-contrast.mjs +2 -2
- package/scripts/theme-hue-distance.mjs +55 -0
- package/src/client.js +93 -66
- package/src/index.js +516 -515
- package/test/client-cards.test.mjs +145 -140
- package/test/client-incremental.test.mjs +105 -0
- package/test/client-visual.test.mjs +299 -290
- package/test/fixtures/first-glance.html +80 -0
- package/test/fixtures/persona-gate/no-persona.md +8 -0
- package/test/fixtures/persona-gate/persona-append.md +9 -0
- package/test/fixtures/persona-gate/style-override.md +8 -0
- package/test/fixtures/style-probe.mjs +74 -0
- package/test/structured.test.mjs +590 -582
- package/test/style-browser.test.mjs +71 -0
- package/test/theme.test.mjs +8 -9
|
@@ -1,290 +1,299 @@
|
|
|
1
|
-
import assert from 'node:assert/strict'
|
|
2
|
-
import { readFileSync } from 'node:fs'
|
|
3
|
-
import test from 'node:test'
|
|
4
|
-
import vm from 'node:vm'
|
|
5
|
-
|
|
6
|
-
function makeDom() {
|
|
7
|
-
const styles = new Map()
|
|
8
|
-
const attrs = new Map()
|
|
9
|
-
const element = () => ({
|
|
10
|
-
children: [],
|
|
11
|
-
appendChild(child) {
|
|
12
|
-
this.children.push(child)
|
|
13
|
-
},
|
|
14
|
-
remove() {},
|
|
15
|
-
setAttribute(name, value) {
|
|
16
|
-
this[name] = value
|
|
17
|
-
},
|
|
18
|
-
removeAttribute(name) {
|
|
19
|
-
delete this[name]
|
|
20
|
-
},
|
|
21
|
-
querySelectorAll() {
|
|
22
|
-
return []
|
|
23
|
-
},
|
|
24
|
-
closest() {
|
|
25
|
-
return null
|
|
26
|
-
},
|
|
27
|
-
tagName: 'P',
|
|
28
|
-
})
|
|
29
|
-
const document = {
|
|
30
|
-
body: element(),
|
|
31
|
-
documentElement: {
|
|
32
|
-
setAttribute(name, value) {
|
|
33
|
-
attrs.set(name, value)
|
|
34
|
-
},
|
|
35
|
-
removeAttribute(name) {
|
|
36
|
-
attrs.delete(name)
|
|
37
|
-
},
|
|
38
|
-
getAttribute(name) {
|
|
39
|
-
return attrs.get(name)
|
|
40
|
-
},
|
|
41
|
-
},
|
|
42
|
-
head: {
|
|
43
|
-
children: [],
|
|
44
|
-
appendChild(style) {
|
|
45
|
-
this.children.push(style)
|
|
46
|
-
styles.set(style.id, style)
|
|
47
|
-
},
|
|
48
|
-
},
|
|
49
|
-
createElement() {
|
|
50
|
-
const style = element()
|
|
51
|
-
style.textContent = ''
|
|
52
|
-
style.id = ''
|
|
53
|
-
style.remove = () => {
|
|
54
|
-
styles.delete(style.id)
|
|
55
|
-
}
|
|
56
|
-
return style
|
|
57
|
-
},
|
|
58
|
-
getElementById(id) {
|
|
59
|
-
return styles.get(id) ?? null
|
|
60
|
-
},
|
|
61
|
-
querySelectorAll() {
|
|
62
|
-
return []
|
|
63
|
-
},
|
|
64
|
-
}
|
|
65
|
-
return { document, styles, attrs }
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function loadClient(dom) {
|
|
69
|
-
const code = readFileSync(new URL('../src/client.js', import.meta.url), 'utf8')
|
|
70
|
-
let module = null
|
|
71
|
-
const window = {
|
|
72
|
-
__ModuleLoader__: {
|
|
73
|
-
load(definition) {
|
|
74
|
-
module = definition.factory()
|
|
75
|
-
},
|
|
76
|
-
},
|
|
77
|
-
}
|
|
78
|
-
const context = {
|
|
79
|
-
window,
|
|
80
|
-
document: dom.document,
|
|
81
|
-
MutationObserver: class {
|
|
82
|
-
observe() {}
|
|
83
|
-
disconnect() {}
|
|
84
|
-
},
|
|
85
|
-
requestAnimationFrame: (fn) => fn(),
|
|
86
|
-
cancelAnimationFrame: () => {},
|
|
87
|
-
setTimeout: (fn) => fn(),
|
|
88
|
-
clearTimeout: () => {},
|
|
89
|
-
console: { info() {}, warn() {}, error() {} },
|
|
90
|
-
}
|
|
91
|
-
vm.runInNewContext(code, context, { filename: 'src/client.js' })
|
|
92
|
-
assert.ok(module, 'client module should register')
|
|
93
|
-
return module
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
function makeContext(form) {
|
|
97
|
-
return {
|
|
98
|
-
inject(services, callback) {
|
|
99
|
-
callback({
|
|
100
|
-
configForms: {
|
|
101
|
-
get(id) {
|
|
102
|
-
return id === 'dsh-structured' ? form : undefined
|
|
103
|
-
},
|
|
104
|
-
},
|
|
105
|
-
effect(fn) {
|
|
106
|
-
return fn()
|
|
107
|
-
},
|
|
108
|
-
})
|
|
109
|
-
},
|
|
110
|
-
effect(fn) {
|
|
111
|
-
return fn()
|
|
112
|
-
},
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
test('client:visual=false 停用视觉层,visual=true 恢复', () => {
|
|
117
|
-
const dom = makeDom()
|
|
118
|
-
const form = {
|
|
119
|
-
value: { visual: false, theme: 'codex' },
|
|
120
|
-
listeners: new Set(),
|
|
121
|
-
getSnapshot() {
|
|
122
|
-
return { value: this.value }
|
|
123
|
-
},
|
|
124
|
-
subscribe(listener) {
|
|
125
|
-
this.listeners.add(listener)
|
|
126
|
-
return () => this.listeners.delete(listener)
|
|
127
|
-
},
|
|
128
|
-
}
|
|
129
|
-
const module = loadClient(dom)
|
|
130
|
-
const dispose = module.apply(makeContext(form))
|
|
131
|
-
|
|
132
|
-
assert.equal(dom.styles.has('dsh-structured-skin-style'), false)
|
|
133
|
-
assert.equal(dom.attrs.has('data-dsh-structured'), false)
|
|
134
|
-
assert.equal(dom.document.body['data-dsh-structured-theme'], undefined)
|
|
135
|
-
|
|
136
|
-
form.value = { visual: true, theme: 'codex' }
|
|
137
|
-
for (const listener of form.listeners) listener()
|
|
138
|
-
assert.equal(dom.styles.has('dsh-structured-skin-style'), true)
|
|
139
|
-
assert.equal(dom.attrs.get('data-dsh-structured'), 'active')
|
|
140
|
-
assert.equal(dom.document.body['data-dsh-structured-theme'], 'codex')
|
|
141
|
-
|
|
142
|
-
form.value = { visual: true, theme: 'anthropic' }
|
|
143
|
-
for (const listener of form.listeners) listener()
|
|
144
|
-
assert.equal(dom.document.body['data-dsh-structured-theme'], 'anthropic')
|
|
145
|
-
|
|
146
|
-
form.value = { visual: true, theme: 'bogus' }
|
|
147
|
-
for (const listener of form.listeners) listener()
|
|
148
|
-
assert.equal(dom.document.body['data-dsh-structured-theme'], 'deepseek')
|
|
149
|
-
|
|
150
|
-
form.value = { visual: false, theme: 'anthropic' }
|
|
151
|
-
for (const listener of form.listeners) listener()
|
|
152
|
-
assert.equal(dom.styles.has('dsh-structured-skin-style'), false)
|
|
153
|
-
assert.equal(dom.attrs.has('data-dsh-structured'), false)
|
|
154
|
-
assert.equal(dom.document.body['data-dsh-structured-theme'], undefined)
|
|
155
|
-
|
|
156
|
-
dispose()
|
|
157
|
-
})
|
|
158
|
-
|
|
159
|
-
test('client:configForms 不可用时视觉层保持默认开启', () => {
|
|
160
|
-
const dom = makeDom()
|
|
161
|
-
const module = loadClient(dom)
|
|
162
|
-
const ctx = {
|
|
163
|
-
inject(services, callback) {
|
|
164
|
-
// 模拟服务缺失:不调用 callback
|
|
165
|
-
},
|
|
166
|
-
effect(fn) {
|
|
167
|
-
return fn()
|
|
168
|
-
},
|
|
169
|
-
}
|
|
170
|
-
const dispose = module.apply(ctx)
|
|
171
|
-
assert.equal(dom.styles.has('dsh-structured-skin-style'), true)
|
|
172
|
-
assert.equal(dom.attrs.get('data-dsh-structured'), 'active')
|
|
173
|
-
dispose()
|
|
174
|
-
})
|
|
175
|
-
|
|
176
|
-
function attrsElement(extra = {}) {
|
|
177
|
-
const attrs = new Map()
|
|
178
|
-
return {
|
|
179
|
-
attrs,
|
|
180
|
-
setAttribute(name, value) {
|
|
181
|
-
attrs.set(name, value)
|
|
182
|
-
},
|
|
183
|
-
removeAttribute(name) {
|
|
184
|
-
attrs.delete(name)
|
|
185
|
-
},
|
|
186
|
-
getAttribute(name) {
|
|
187
|
-
return attrs.get(name)
|
|
188
|
-
},
|
|
189
|
-
...extra,
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
test('client:diff 代码块不套插件 chrome,普通代码块才套', () => {
|
|
194
|
-
const dom = makeDom()
|
|
195
|
-
const module = loadClient(dom)
|
|
196
|
-
const diffCode = attrsElement({
|
|
197
|
-
querySelector: () => ({ textContent: 'diff' }),
|
|
198
|
-
closest: () => null,
|
|
199
|
-
})
|
|
200
|
-
const jsCode = attrsElement({
|
|
201
|
-
querySelector: () => ({ textContent: 'js' }),
|
|
202
|
-
closest: () => null,
|
|
203
|
-
})
|
|
204
|
-
const diffCopyCode = attrsElement({
|
|
205
|
-
querySelector: () => ({ textContent: 'diff复制' }),
|
|
206
|
-
closest: () => null,
|
|
207
|
-
})
|
|
208
|
-
assert.equal(module.__test.isDiffCode(diffCopyCode), true)
|
|
209
|
-
const root = {
|
|
210
|
-
querySelectorAll(selector) {
|
|
211
|
-
if (selector.includes('.md-code-block')) return [diffCode, jsCode, diffCopyCode]
|
|
212
|
-
return []
|
|
213
|
-
},
|
|
214
|
-
}
|
|
215
|
-
module.__test.decorate(root)
|
|
216
|
-
assert.equal(diffCode.attrs.get('data-dsh-structured-skip'), 'code')
|
|
217
|
-
assert.equal(diffCode.attrs.get('data-dsh-structured-code'), undefined)
|
|
218
|
-
assert.equal(diffCode.attrs.get('data-dsh-structured-lang'), undefined)
|
|
219
|
-
assert.equal(diffCopyCode.attrs.get('data-dsh-structured-skip'), 'code')
|
|
220
|
-
assert.equal(jsCode.attrs.get('data-dsh-structured-code'), '1')
|
|
221
|
-
assert.equal(jsCode.attrs.get('data-dsh-structured-lang'), 'js')
|
|
222
|
-
})
|
|
223
|
-
|
|
224
|
-
test('client:普通状态表仍套插件表格样式并标状态行', () => {
|
|
225
|
-
const dom = makeDom()
|
|
226
|
-
const module = loadClient(dom)
|
|
227
|
-
const row = attrsElement({
|
|
228
|
-
textContent: '✅ 完成',
|
|
229
|
-
cells: [{ textContent: '✅ 完成' }],
|
|
230
|
-
closest: () => null,
|
|
231
|
-
})
|
|
232
|
-
const table = attrsElement({
|
|
233
|
-
closest: () => null,
|
|
234
|
-
querySelectorAll: (selector) => (selector === 'tbody tr' ? [row] : []),
|
|
235
|
-
})
|
|
236
|
-
const root = {
|
|
237
|
-
querySelectorAll(selector) {
|
|
238
|
-
if (selector === '[class*="_markdown_"] table') return [table]
|
|
239
|
-
if (selector === '[class*="_markdown_"] tbody tr') return [row]
|
|
240
|
-
if (selector === '[class*="_markdown_"] table tr') return [row]
|
|
241
|
-
return []
|
|
242
|
-
},
|
|
243
|
-
}
|
|
244
|
-
module.__test.decorate(root)
|
|
245
|
-
assert.equal(table.attrs.get('data-dsh-structured-status-table'), '1')
|
|
246
|
-
assert.equal(table.attrs.get('data-dsh-structured-skip'), undefined)
|
|
247
|
-
assert.equal(row.attrs.get('data-dsh-structured-row'), 'success')
|
|
248
|
-
})
|
|
249
|
-
|
|
250
|
-
test('client:GenUI 区域不写入 skip,保持旧行为', () => {
|
|
251
|
-
const dom = makeDom()
|
|
252
|
-
const module = loadClient(dom)
|
|
253
|
-
const code = attrsElement({
|
|
254
|
-
querySelector: () => ({ textContent: 'js' }),
|
|
255
|
-
closest: (selector) => (selector.includes('genui') ? {} : null),
|
|
256
|
-
})
|
|
257
|
-
const table = attrsElement({
|
|
258
|
-
closest: (selector) => (selector.includes('genui') ? {} : null),
|
|
259
|
-
querySelectorAll: () => [],
|
|
260
|
-
})
|
|
261
|
-
const root = {
|
|
262
|
-
querySelectorAll(selector) {
|
|
263
|
-
if (selector.includes('.md-code-block')) return [code]
|
|
264
|
-
if (selector === '[class*="_markdown_"] table') return [table]
|
|
265
|
-
return []
|
|
266
|
-
},
|
|
267
|
-
}
|
|
268
|
-
module.__test.decorate(root)
|
|
269
|
-
assert.equal(code.attrs.get('data-dsh-structured-skip'), undefined)
|
|
270
|
-
assert.equal(code.attrs.get('data-dsh-structured-code'), undefined)
|
|
271
|
-
assert.equal(table.attrs.get('data-dsh-structured-skip'), undefined)
|
|
272
|
-
})
|
|
273
|
-
|
|
274
|
-
test('client:diff 表格不套插件表格样式', () => {
|
|
275
|
-
const dom = makeDom()
|
|
276
|
-
const module = loadClient(dom)
|
|
277
|
-
const table = attrsElement({
|
|
278
|
-
closest: (selector) => (selector.includes('[data-diff]') ? {} : null),
|
|
279
|
-
querySelectorAll: () => [],
|
|
280
|
-
})
|
|
281
|
-
const root = {
|
|
282
|
-
querySelectorAll(selector) {
|
|
283
|
-
if (selector === '[class*="_markdown_"] table') return [table]
|
|
284
|
-
return []
|
|
285
|
-
},
|
|
286
|
-
}
|
|
287
|
-
module.__test.decorate(root)
|
|
288
|
-
assert.equal(table.attrs.get('data-dsh-structured-skip'), 'table')
|
|
289
|
-
assert.equal(table.attrs.get('data-dsh-structured-status-table'), undefined)
|
|
290
|
-
})
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import { readFileSync } from 'node:fs'
|
|
3
|
+
import test from 'node:test'
|
|
4
|
+
import vm from 'node:vm'
|
|
5
|
+
|
|
6
|
+
function makeDom() {
|
|
7
|
+
const styles = new Map()
|
|
8
|
+
const attrs = new Map()
|
|
9
|
+
const element = () => ({
|
|
10
|
+
children: [],
|
|
11
|
+
appendChild(child) {
|
|
12
|
+
this.children.push(child)
|
|
13
|
+
},
|
|
14
|
+
remove() {},
|
|
15
|
+
setAttribute(name, value) {
|
|
16
|
+
this[name] = value
|
|
17
|
+
},
|
|
18
|
+
removeAttribute(name) {
|
|
19
|
+
delete this[name]
|
|
20
|
+
},
|
|
21
|
+
querySelectorAll() {
|
|
22
|
+
return []
|
|
23
|
+
},
|
|
24
|
+
closest() {
|
|
25
|
+
return null
|
|
26
|
+
},
|
|
27
|
+
tagName: 'P',
|
|
28
|
+
})
|
|
29
|
+
const document = {
|
|
30
|
+
body: element(),
|
|
31
|
+
documentElement: {
|
|
32
|
+
setAttribute(name, value) {
|
|
33
|
+
attrs.set(name, value)
|
|
34
|
+
},
|
|
35
|
+
removeAttribute(name) {
|
|
36
|
+
attrs.delete(name)
|
|
37
|
+
},
|
|
38
|
+
getAttribute(name) {
|
|
39
|
+
return attrs.get(name)
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
head: {
|
|
43
|
+
children: [],
|
|
44
|
+
appendChild(style) {
|
|
45
|
+
this.children.push(style)
|
|
46
|
+
styles.set(style.id, style)
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
createElement() {
|
|
50
|
+
const style = element()
|
|
51
|
+
style.textContent = ''
|
|
52
|
+
style.id = ''
|
|
53
|
+
style.remove = () => {
|
|
54
|
+
styles.delete(style.id)
|
|
55
|
+
}
|
|
56
|
+
return style
|
|
57
|
+
},
|
|
58
|
+
getElementById(id) {
|
|
59
|
+
return styles.get(id) ?? null
|
|
60
|
+
},
|
|
61
|
+
querySelectorAll() {
|
|
62
|
+
return []
|
|
63
|
+
},
|
|
64
|
+
}
|
|
65
|
+
return { document, styles, attrs }
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function loadClient(dom) {
|
|
69
|
+
const code = readFileSync(new URL('../src/client.js', import.meta.url), 'utf8')
|
|
70
|
+
let module = null
|
|
71
|
+
const window = {
|
|
72
|
+
__ModuleLoader__: {
|
|
73
|
+
load(definition) {
|
|
74
|
+
module = definition.factory()
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
}
|
|
78
|
+
const context = {
|
|
79
|
+
window,
|
|
80
|
+
document: dom.document,
|
|
81
|
+
MutationObserver: class {
|
|
82
|
+
observe() {}
|
|
83
|
+
disconnect() {}
|
|
84
|
+
},
|
|
85
|
+
requestAnimationFrame: (fn) => fn(),
|
|
86
|
+
cancelAnimationFrame: () => {},
|
|
87
|
+
setTimeout: (fn) => fn(),
|
|
88
|
+
clearTimeout: () => {},
|
|
89
|
+
console: { info() {}, warn() {}, error() {} },
|
|
90
|
+
}
|
|
91
|
+
vm.runInNewContext(code, context, { filename: 'src/client.js' })
|
|
92
|
+
assert.ok(module, 'client module should register')
|
|
93
|
+
return module
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function makeContext(form) {
|
|
97
|
+
return {
|
|
98
|
+
inject(services, callback) {
|
|
99
|
+
callback({
|
|
100
|
+
configForms: {
|
|
101
|
+
get(id) {
|
|
102
|
+
return id === 'dsh-structured' ? form : undefined
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
effect(fn) {
|
|
106
|
+
return fn()
|
|
107
|
+
},
|
|
108
|
+
})
|
|
109
|
+
},
|
|
110
|
+
effect(fn) {
|
|
111
|
+
return fn()
|
|
112
|
+
},
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
test('client:visual=false 停用视觉层,visual=true 恢复', () => {
|
|
117
|
+
const dom = makeDom()
|
|
118
|
+
const form = {
|
|
119
|
+
value: { visual: false, theme: 'codex' },
|
|
120
|
+
listeners: new Set(),
|
|
121
|
+
getSnapshot() {
|
|
122
|
+
return { value: this.value }
|
|
123
|
+
},
|
|
124
|
+
subscribe(listener) {
|
|
125
|
+
this.listeners.add(listener)
|
|
126
|
+
return () => this.listeners.delete(listener)
|
|
127
|
+
},
|
|
128
|
+
}
|
|
129
|
+
const module = loadClient(dom)
|
|
130
|
+
const dispose = module.apply(makeContext(form))
|
|
131
|
+
|
|
132
|
+
assert.equal(dom.styles.has('dsh-structured-skin-style'), false)
|
|
133
|
+
assert.equal(dom.attrs.has('data-dsh-structured'), false)
|
|
134
|
+
assert.equal(dom.document.body['data-dsh-structured-theme'], undefined)
|
|
135
|
+
|
|
136
|
+
form.value = { visual: true, theme: 'codex' }
|
|
137
|
+
for (const listener of form.listeners) listener()
|
|
138
|
+
assert.equal(dom.styles.has('dsh-structured-skin-style'), true)
|
|
139
|
+
assert.equal(dom.attrs.get('data-dsh-structured'), 'active')
|
|
140
|
+
assert.equal(dom.document.body['data-dsh-structured-theme'], 'codex')
|
|
141
|
+
|
|
142
|
+
form.value = { visual: true, theme: 'anthropic' }
|
|
143
|
+
for (const listener of form.listeners) listener()
|
|
144
|
+
assert.equal(dom.document.body['data-dsh-structured-theme'], 'anthropic')
|
|
145
|
+
|
|
146
|
+
form.value = { visual: true, theme: 'bogus' }
|
|
147
|
+
for (const listener of form.listeners) listener()
|
|
148
|
+
assert.equal(dom.document.body['data-dsh-structured-theme'], 'deepseek')
|
|
149
|
+
|
|
150
|
+
form.value = { visual: false, theme: 'anthropic' }
|
|
151
|
+
for (const listener of form.listeners) listener()
|
|
152
|
+
assert.equal(dom.styles.has('dsh-structured-skin-style'), false)
|
|
153
|
+
assert.equal(dom.attrs.has('data-dsh-structured'), false)
|
|
154
|
+
assert.equal(dom.document.body['data-dsh-structured-theme'], undefined)
|
|
155
|
+
|
|
156
|
+
dispose()
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
test('client:configForms 不可用时视觉层保持默认开启', () => {
|
|
160
|
+
const dom = makeDom()
|
|
161
|
+
const module = loadClient(dom)
|
|
162
|
+
const ctx = {
|
|
163
|
+
inject(services, callback) {
|
|
164
|
+
// 模拟服务缺失:不调用 callback
|
|
165
|
+
},
|
|
166
|
+
effect(fn) {
|
|
167
|
+
return fn()
|
|
168
|
+
},
|
|
169
|
+
}
|
|
170
|
+
const dispose = module.apply(ctx)
|
|
171
|
+
assert.equal(dom.styles.has('dsh-structured-skin-style'), true)
|
|
172
|
+
assert.equal(dom.attrs.get('data-dsh-structured'), 'active')
|
|
173
|
+
dispose()
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
function attrsElement(extra = {}) {
|
|
177
|
+
const attrs = new Map()
|
|
178
|
+
return {
|
|
179
|
+
attrs,
|
|
180
|
+
setAttribute(name, value) {
|
|
181
|
+
attrs.set(name, value)
|
|
182
|
+
},
|
|
183
|
+
removeAttribute(name) {
|
|
184
|
+
attrs.delete(name)
|
|
185
|
+
},
|
|
186
|
+
getAttribute(name) {
|
|
187
|
+
return attrs.get(name)
|
|
188
|
+
},
|
|
189
|
+
...extra,
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
test('client:diff 代码块不套插件 chrome,普通代码块才套', () => {
|
|
194
|
+
const dom = makeDom()
|
|
195
|
+
const module = loadClient(dom)
|
|
196
|
+
const diffCode = attrsElement({
|
|
197
|
+
querySelector: () => ({ textContent: 'diff' }),
|
|
198
|
+
closest: () => null,
|
|
199
|
+
})
|
|
200
|
+
const jsCode = attrsElement({
|
|
201
|
+
querySelector: () => ({ textContent: 'js' }),
|
|
202
|
+
closest: () => null,
|
|
203
|
+
})
|
|
204
|
+
const diffCopyCode = attrsElement({
|
|
205
|
+
querySelector: () => ({ textContent: 'diff复制' }),
|
|
206
|
+
closest: () => null,
|
|
207
|
+
})
|
|
208
|
+
assert.equal(module.__test.isDiffCode(diffCopyCode), true)
|
|
209
|
+
const root = {
|
|
210
|
+
querySelectorAll(selector) {
|
|
211
|
+
if (selector.includes('.md-code-block')) return [diffCode, jsCode, diffCopyCode]
|
|
212
|
+
return []
|
|
213
|
+
},
|
|
214
|
+
}
|
|
215
|
+
module.__test.decorate(root)
|
|
216
|
+
assert.equal(diffCode.attrs.get('data-dsh-structured-skip'), 'code')
|
|
217
|
+
assert.equal(diffCode.attrs.get('data-dsh-structured-code'), undefined)
|
|
218
|
+
assert.equal(diffCode.attrs.get('data-dsh-structured-lang'), undefined)
|
|
219
|
+
assert.equal(diffCopyCode.attrs.get('data-dsh-structured-skip'), 'code')
|
|
220
|
+
assert.equal(jsCode.attrs.get('data-dsh-structured-code'), '1')
|
|
221
|
+
assert.equal(jsCode.attrs.get('data-dsh-structured-lang'), 'js')
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
test('client:普通状态表仍套插件表格样式并标状态行', () => {
|
|
225
|
+
const dom = makeDom()
|
|
226
|
+
const module = loadClient(dom)
|
|
227
|
+
const row = attrsElement({
|
|
228
|
+
textContent: '✅ 完成',
|
|
229
|
+
cells: [{ textContent: '✅ 完成' }],
|
|
230
|
+
closest: () => null,
|
|
231
|
+
})
|
|
232
|
+
const table = attrsElement({
|
|
233
|
+
closest: () => null,
|
|
234
|
+
querySelectorAll: (selector) => (selector === 'tbody tr' ? [row] : []),
|
|
235
|
+
})
|
|
236
|
+
const root = {
|
|
237
|
+
querySelectorAll(selector) {
|
|
238
|
+
if (selector === '[class*="_markdown_"] table') return [table]
|
|
239
|
+
if (selector === '[class*="_markdown_"] tbody tr') return [row]
|
|
240
|
+
if (selector === '[class*="_markdown_"] table tr') return [row]
|
|
241
|
+
return []
|
|
242
|
+
},
|
|
243
|
+
}
|
|
244
|
+
module.__test.decorate(root)
|
|
245
|
+
assert.equal(table.attrs.get('data-dsh-structured-status-table'), '1')
|
|
246
|
+
assert.equal(table.attrs.get('data-dsh-structured-skip'), undefined)
|
|
247
|
+
assert.equal(row.attrs.get('data-dsh-structured-row'), 'success')
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
test('client:GenUI 区域不写入 skip,保持旧行为', () => {
|
|
251
|
+
const dom = makeDom()
|
|
252
|
+
const module = loadClient(dom)
|
|
253
|
+
const code = attrsElement({
|
|
254
|
+
querySelector: () => ({ textContent: 'js' }),
|
|
255
|
+
closest: (selector) => (selector.includes('genui') ? {} : null),
|
|
256
|
+
})
|
|
257
|
+
const table = attrsElement({
|
|
258
|
+
closest: (selector) => (selector.includes('genui') ? {} : null),
|
|
259
|
+
querySelectorAll: () => [],
|
|
260
|
+
})
|
|
261
|
+
const root = {
|
|
262
|
+
querySelectorAll(selector) {
|
|
263
|
+
if (selector.includes('.md-code-block')) return [code]
|
|
264
|
+
if (selector === '[class*="_markdown_"] table') return [table]
|
|
265
|
+
return []
|
|
266
|
+
},
|
|
267
|
+
}
|
|
268
|
+
module.__test.decorate(root)
|
|
269
|
+
assert.equal(code.attrs.get('data-dsh-structured-skip'), undefined)
|
|
270
|
+
assert.equal(code.attrs.get('data-dsh-structured-code'), undefined)
|
|
271
|
+
assert.equal(table.attrs.get('data-dsh-structured-skip'), undefined)
|
|
272
|
+
})
|
|
273
|
+
|
|
274
|
+
test('client:diff 表格不套插件表格样式', () => {
|
|
275
|
+
const dom = makeDom()
|
|
276
|
+
const module = loadClient(dom)
|
|
277
|
+
const table = attrsElement({
|
|
278
|
+
closest: (selector) => (selector.includes('[data-diff]') ? {} : null),
|
|
279
|
+
querySelectorAll: () => [],
|
|
280
|
+
})
|
|
281
|
+
const root = {
|
|
282
|
+
querySelectorAll(selector) {
|
|
283
|
+
if (selector === '[class*="_markdown_"] table') return [table]
|
|
284
|
+
return []
|
|
285
|
+
},
|
|
286
|
+
}
|
|
287
|
+
module.__test.decorate(root)
|
|
288
|
+
assert.equal(table.attrs.get('data-dsh-structured-skip'), 'table')
|
|
289
|
+
assert.equal(table.attrs.get('data-dsh-structured-status-table'), undefined)
|
|
290
|
+
})
|
|
291
|
+
|
|
292
|
+
test('client:标题图标使用悬挂缩进,换行行不顶回左边', () => {
|
|
293
|
+
const dom = makeDom()
|
|
294
|
+
loadClient(dom).apply(makeContext(null))
|
|
295
|
+
const css = dom.styles.get('dsh-structured-skin-style')?.textContent ?? ''
|
|
296
|
+
assert.match(css, /h2\[data-dsh-structured-icon\]\{padding-left:42px;text-indent:-30px\}/)
|
|
297
|
+
assert.match(css, /h3\[data-dsh-structured-icon\]\{padding-left:30px;text-indent:-30px\}/)
|
|
298
|
+
assert.match(css, /h2\[data-dsh-structured-icon\]::before,[^{]*\{[^}]*text-indent:0/)
|
|
299
|
+
})
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="zh-CN">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
+
<title>dsh-structured 0.9.6 fixture</title>
|
|
7
|
+
<style>
|
|
8
|
+
html,body{margin:0;padding:0;background:#f4f6f8;color:#111827;font-family:system-ui,-apple-system,"Segoe UI","Microsoft YaHei",sans-serif}
|
|
9
|
+
.stage{padding:40px 28px 64px;display:flex;justify-content:center}
|
|
10
|
+
.reply{width:980px;max-width:calc(100vw - 56px)}
|
|
11
|
+
.meta{font-size:12px;color:#6b7280;margin:0 0 16px 2px}
|
|
12
|
+
.surface{background:#fff;border:1px solid #e5e7eb;border-radius:16px;padding:28px}
|
|
13
|
+
.markdown{font-size:15px;line-height:1.72}
|
|
14
|
+
body[data-dsh-structured-theme="codex"]{background:#f7fbf9}
|
|
15
|
+
body[data-dsh-structured-theme="anthropic"]{background:#f5f1e8}
|
|
16
|
+
body[data-dsh-structured-theme="anthropic"] .surface{background:#faf9f5;border-color:#e5e0d5}
|
|
17
|
+
body[data-ds-dark-theme]{background:#0b1220;color:#e5e7eb}
|
|
18
|
+
body[data-ds-dark-theme] .surface{background:#111827;border-color:#374151}
|
|
19
|
+
body[data-ds-dark-theme][data-dsh-structured-theme="codex"]{background:#07100d}
|
|
20
|
+
body[data-ds-dark-theme][data-dsh-structured-theme="codex"] .surface{background:#0b1512;border-color:#233a31}
|
|
21
|
+
body[data-ds-dark-theme][data-dsh-structured-theme="anthropic"]{background:#141413}
|
|
22
|
+
body[data-ds-dark-theme][data-dsh-structured-theme="anthropic"] .surface{background:#1f1e1d;border-color:#3a3733}
|
|
23
|
+
html:has(body[data-ds-dark-theme]){background:#0b1220}
|
|
24
|
+
html:has(body[data-ds-dark-theme][data-dsh-structured-theme="codex"]){background:#07100d}
|
|
25
|
+
html:has(body[data-ds-dark-theme][data-dsh-structured-theme="anthropic"]){background:#141413}
|
|
26
|
+
</style>
|
|
27
|
+
</head>
|
|
28
|
+
<body>
|
|
29
|
+
<div class="stage">
|
|
30
|
+
<div class="reply">
|
|
31
|
+
<div class="meta">助手回复</div>
|
|
32
|
+
<div class="surface">
|
|
33
|
+
<div class="_markdown_fixture markdown">
|
|
34
|
+
<blockquote data-dsh-structured-card="1" data-dsh-structured-card-type="statement" data-dsh-structured-tone="info">
|
|
35
|
+
<p><strong>这个方案可以推进,但先补一次真实用户验证</strong><br>方向、成本、排期都成立;真实用户验证是唯一缺口。</p>
|
|
36
|
+
</blockquote>
|
|
37
|
+
<p data-dsh-structured-lead="table"><strong>表格结论:</strong> 三项里两项成立,一项待验证。</p>
|
|
38
|
+
<table data-dsh-structured-status-table="1">
|
|
39
|
+
<thead><tr><th>项</th><th>状态</th><th>说明</th></tr></thead>
|
|
40
|
+
<tbody>
|
|
41
|
+
<tr data-dsh-structured-row="success"><td>✅ 完成</td><td>方向</td><td>和现有目标一致</td></tr>
|
|
42
|
+
<tr data-dsh-structured-row="success"><td>✅ 完成</td><td>成本</td><td>不新增预算</td></tr>
|
|
43
|
+
<tr data-dsh-structured-row="warn"><td>⚠️ 待做</td><td>用户验证</td><td>需要 5 个真实用户</td></tr>
|
|
44
|
+
</tbody>
|
|
45
|
+
</table>
|
|
46
|
+
<blockquote data-dsh-structured-card="1" data-dsh-structured-card-type="risk" data-dsh-structured-tone="warn">
|
|
47
|
+
<p><strong>风险</strong><br>用户验证没做,直接推进会把假设当结论。</p>
|
|
48
|
+
</blockquote>
|
|
49
|
+
<blockquote data-dsh-structured-card="1" data-dsh-structured-card-type="next" data-dsh-structured-tone="info">
|
|
50
|
+
<p><strong>下一步</strong></p>
|
|
51
|
+
<ol>
|
|
52
|
+
<li>找 5 个真实用户,跑核心路径。</li>
|
|
53
|
+
<li>看他们卡在哪,不听他们说要什么。</li>
|
|
54
|
+
<li>验证过了再排期,没过就停。</li>
|
|
55
|
+
</ol>
|
|
56
|
+
</blockquote>
|
|
57
|
+
</div>
|
|
58
|
+
</div>
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
<script>
|
|
62
|
+
window.__ModuleLoader__ = {
|
|
63
|
+
load(definition) {
|
|
64
|
+
window.__client = definition
|
|
65
|
+
},
|
|
66
|
+
}
|
|
67
|
+
</script>
|
|
68
|
+
<script src="../../src/client.js"></script>
|
|
69
|
+
<script>
|
|
70
|
+
window.addEventListener('load', () => {
|
|
71
|
+
const module = window.__client.factory()
|
|
72
|
+
module.apply({
|
|
73
|
+
inject() {},
|
|
74
|
+
effect(fn) { fn() },
|
|
75
|
+
})
|
|
76
|
+
document.body.setAttribute('data-dsh-structured-theme', 'deepseek')
|
|
77
|
+
})
|
|
78
|
+
</script>
|
|
79
|
+
</body>
|
|
80
|
+
</html>
|