daily-question 1.0.6 → 1.1.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.md +16 -14
- package/client.js +128 -400
- package/lib/routes.js +14 -14
- package/package.json +2 -2
- package/vendor/mathjax/LICENSE +202 -0
- package/vendor/mathjax/tex-svg.js +34 -0
- package/assets/LICENSE-latin-modern-math.txt +0 -93
- package/assets/latinmodern-math.otf +0 -0
package/client.js
CHANGED
|
@@ -109,351 +109,119 @@ window.__ModuleLoader__.load({
|
|
|
109
109
|
return { text: '未作答', tone: 'idle' }
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
-
/* ==============================
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
to: '→', rightarrow: '→', leftarrow: '←', leftrightarrow: '↔', Rightarrow: '⇒', Leftarrow: '⇐',
|
|
123
|
-
Leftrightarrow: '⇔', mapsto: '↦', implies: '⟹', iff: '⟺',
|
|
124
|
-
partial: '∂', nabla: '∇', forall: '∀', exists: '∃', nexists: '∄', neg: '¬', in: '∈', notin: '∉',
|
|
125
|
-
subset: '⊂', subseteq: '⊆', supset: '⊃', supseteq: '⊇', cup: '∪', cap: '∩', emptyset: '∅',
|
|
126
|
-
varnothing: '∅', setminus: '∖', land: '∧', lor: '∨',
|
|
127
|
-
mid: '∣', nmid: '∤', vert: '|', lvert: '|', rvert: '|',
|
|
128
|
-
Vert: '‖', lVert: '‖', rVert: '‖', colon: ':', '|': '‖',
|
|
129
|
-
ldots: '…', cdots: '⋯', vdots: '⋮', ddots: '⋱', dots: '…',
|
|
130
|
-
angle: '∠', perp: '⊥', parallel: '∥', circ: '∘', degree: '°', prime: '′',
|
|
131
|
-
lceil: '⌈', rceil: '⌉', lfloor: '⌊', rfloor: '⌋', langle: '⟨', rangle: '⟩',
|
|
132
|
-
ell: 'ℓ', Re: 'ℜ', Im: 'ℑ', hbar: 'ℏ',
|
|
133
|
-
therefore: '∴', because: '∵', triangle: '△', square: '□',
|
|
134
|
-
bullet: '∙', ni: '∋', backslash: '\\', Box: '□', bigcirc: '○', dagger: '†', ddagger: '‡',
|
|
135
|
-
quad: '\u2003', qquad: '\u2003\u2003',
|
|
136
|
-
'%': '%', '&': '&', '#': '#', '_': '_', '{': '{', '}': '}',
|
|
137
|
-
' ': ' ', ',': '\u2009', ';': '\u2005', ':': '\u2005', '!': '',
|
|
138
|
-
}
|
|
139
|
-
const LATEX_FUNCS = new Set(['sin', 'cos', 'tan', 'cot', 'sec', 'csc', 'arcsin', 'arccos', 'arctan',
|
|
140
|
-
'sinh', 'cosh', 'tanh', 'ln', 'log', 'lg', 'exp', 'max', 'min', 'sup', 'inf', 'det', 'gcd', 'lcm',
|
|
141
|
-
'deg', 'dim', 'ker', 'arg', 'Pr'])
|
|
142
|
-
const LATEX_BIGOPS = {
|
|
143
|
-
sum: '∑', prod: '∏', int: '∫', iint: '∬', iiint: '∭', oint: '∮',
|
|
144
|
-
lim: 'lim', limsup: 'lim sup', liminf: 'lim inf', bigcup: '⋃', bigcap: '⋂',
|
|
145
|
-
}
|
|
146
|
-
const DOUBLE_STRUCK = { R: 'ℝ', N: 'ℕ', Z: 'ℤ', Q: 'ℚ', C: 'ℂ', P: 'ℙ' }
|
|
147
|
-
|
|
148
|
-
/** 连写撇号对应的 prime 字符(1~4 阶,更多阶用 4 阶 + 单撇补)。 */
|
|
149
|
-
const PRIME_CHARS = { 1: '\u2032', 2: '\u2033', 3: '\u2034', 4: '\u2057' }
|
|
150
|
-
|
|
151
|
-
/**
|
|
152
|
-
* 数学模式里的字符修正。只处理公式内部的裸文本(\text / \mathrm 走原样,不受影响):
|
|
153
|
-
*
|
|
154
|
-
* 1. 连字符 `-` 在 LaTeX 数学模式里是**减号**,换成 U+2212 MINUS SIGN。
|
|
155
|
-
* 留成 U+002D 的话,Latin Modern Math 会画成一个短横,看着像分隔符。
|
|
156
|
-
* 2. 撇号 `'`(含中文输入法常见的 ’)是**导数记号**,换成 U+2032 起的 prime
|
|
157
|
-
* 连写:' → ′、'' → ″、''' → ‴、'''' → ⁗。
|
|
158
|
-
* 3. ASCII 字母换成 Unicode 数学斜体码位(U+1D434… 大写、U+1D44E… 小写,
|
|
159
|
-
* 例外 h → U+210E),Latin Modern Math 与 Cambria Math 都完整覆盖,
|
|
160
|
-
* 因此变量是真斜体,而数字、运算符、函数名(\sin 等)保持直立。
|
|
161
|
-
*/
|
|
162
|
-
function toMathText(text) {
|
|
163
|
-
return String(text)
|
|
164
|
-
.replace(/['\u2018\u2019]+/g, (run) => {
|
|
165
|
-
const count = [...run].length
|
|
166
|
-
if (count <= 4) return PRIME_CHARS[count]
|
|
167
|
-
return '\u2057' + '\u2032'.repeat(count - 4)
|
|
168
|
-
})
|
|
169
|
-
.replace(/-/g, '\u2212')
|
|
170
|
-
.replace(/\*/g, '\u2217')
|
|
171
|
-
.replace(/\.{3,}/g, '\u2026')
|
|
172
|
-
.replace(/[A-Za-z]/g, (ch) => {
|
|
173
|
-
if (ch === 'h') return '\u210E'
|
|
174
|
-
const code = ch.charCodeAt(0)
|
|
175
|
-
if (code >= 97 && code <= 122) return String.fromCodePoint(0x1d44e + (code - 97))
|
|
176
|
-
return String.fromCodePoint(0x1d434 + (code - 65))
|
|
177
|
-
})
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
function readGroup(cur) {
|
|
181
|
-
// cur.src[cur.i] === '{'
|
|
182
|
-
let depth = 0
|
|
183
|
-
let j = cur.i
|
|
184
|
-
const start = cur.i + 1
|
|
185
|
-
while (j < cur.src.length) {
|
|
186
|
-
const c = cur.src[j]
|
|
187
|
-
if (c === '\\') { j += 2; continue }
|
|
188
|
-
if (c === '{') depth += 1
|
|
189
|
-
else if (c === '}') {
|
|
190
|
-
depth -= 1
|
|
191
|
-
if (depth === 0) {
|
|
192
|
-
cur.i = j + 1
|
|
193
|
-
return cur.src.slice(start, j)
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
j += 1
|
|
112
|
+
/* ============================== 公式(MathJax 3) ============================== */
|
|
113
|
+
// MathJax 的 SVG 输出自带字形路径,不依赖任何字体文件;单文件 bundle 随包分发,
|
|
114
|
+
// 由宿主在 /api/daily-question/vendor/tex-svg.js 提供。
|
|
115
|
+
const MATHJAX_SRC = '/api/daily-question/vendor/tex-svg.js'
|
|
116
|
+
const mathState = { status: 'idle' }
|
|
117
|
+
const mathWatchers = new Set()
|
|
118
|
+
|
|
119
|
+
function notifyMath() {
|
|
120
|
+
for (const watcher of [...mathWatchers]) {
|
|
121
|
+
try { watcher() } catch (error) { /* 单个订阅出错不影响其它 */ }
|
|
197
122
|
}
|
|
198
|
-
cur.i = cur.src.length
|
|
199
|
-
return cur.src.slice(start)
|
|
200
123
|
}
|
|
201
124
|
|
|
202
|
-
function
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
const end = cur.src.indexOf(']', cur.i)
|
|
206
|
-
if (end < 0) return null
|
|
207
|
-
const body = cur.src.slice(cur.i + 1, end)
|
|
208
|
-
cur.i = end + 1
|
|
209
|
-
return body
|
|
125
|
+
function subscribeMath(fn) {
|
|
126
|
+
mathWatchers.add(fn)
|
|
127
|
+
return () => { mathWatchers.delete(fn) }
|
|
210
128
|
}
|
|
211
129
|
|
|
212
|
-
function
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
return latexNodes(command[0])
|
|
130
|
+
function ensureMathJax() {
|
|
131
|
+
if (mathState.status !== 'idle') return
|
|
132
|
+
mathState.status = 'loading'
|
|
133
|
+
window.MathJax = {
|
|
134
|
+
startup: { typeset: false },
|
|
135
|
+
// 每条公式自带 <defs>,注入 innerHTML 后不依赖全局字体缓存
|
|
136
|
+
svg: { fontCache: 'local' },
|
|
220
137
|
}
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
138
|
+
const script = document.createElement('script')
|
|
139
|
+
script.src = MATHJAX_SRC
|
|
140
|
+
script.async = true
|
|
141
|
+
script.onload = () => {
|
|
142
|
+
const startup = window.MathJax && window.MathJax.startup
|
|
143
|
+
const done = () => {
|
|
144
|
+
mathState.status = 'ready'
|
|
145
|
+
ensureMathStylesheet()
|
|
146
|
+
notifyMath()
|
|
147
|
+
}
|
|
148
|
+
const failed = () => { mathState.status = 'failed'; notifyMath() }
|
|
149
|
+
if (startup && startup.promise) startup.promise.then(done, failed)
|
|
150
|
+
else done()
|
|
224
151
|
}
|
|
225
|
-
|
|
226
|
-
|
|
152
|
+
script.onerror = () => { mathState.status = 'failed'; notifyMath() }
|
|
153
|
+
document.head.appendChild(script)
|
|
227
154
|
}
|
|
228
155
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
const
|
|
236
|
-
if (
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
break
|
|
156
|
+
/** SVG 输出依赖 MathJax 自己生成的样式表;手动调用 tex2svg 时不会自动插,补一次。 */
|
|
157
|
+
function ensureMathStylesheet() {
|
|
158
|
+
try {
|
|
159
|
+
const mathJax = window.MathJax
|
|
160
|
+
if (!mathJax || typeof mathJax.svgStylesheet !== 'function') return
|
|
161
|
+
if (document.getElementById('MJX-SVG-styles')) return
|
|
162
|
+
const sheet = mathJax.svgStylesheet()
|
|
163
|
+
if (sheet) document.head.appendChild(sheet)
|
|
164
|
+
} catch (error) {
|
|
165
|
+
// 没有样式表也能显示,只是对齐略差,不阻断渲染
|
|
240
166
|
}
|
|
241
|
-
return { sub, sup }
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
function readMatrix(cur, env) {
|
|
245
|
-
const marker = `\\end{${env}}`
|
|
246
|
-
const end = cur.src.indexOf(marker, cur.i)
|
|
247
|
-
const body = end < 0 ? cur.src.slice(cur.i) : cur.src.slice(cur.i, end)
|
|
248
|
-
cur.i = end < 0 ? cur.src.length : end + marker.length
|
|
249
|
-
const rows = body.split(/\\\\/).map((row) => row.trim()).filter((row) => row.length)
|
|
250
|
-
const cells = rows.map((row) => row.split('&').map((cell) => latexNodes(cell.trim())))
|
|
251
|
-
const table = h('table', { className: 'kd-matrix', key: nextKey('mx') },
|
|
252
|
-
h('tbody', null, cells.map((row, rowIndex) => h('tr', { key: rowIndex },
|
|
253
|
-
row.map((cell, cellIndex) => h('td', { key: cellIndex }, cell))))))
|
|
254
|
-
const wrapCls = env === 'cases' ? 'kd-cases'
|
|
255
|
-
: env === 'bmatrix' ? 'kd-bmatrix'
|
|
256
|
-
: env === 'vmatrix' ? 'kd-vmatrix'
|
|
257
|
-
: env === 'pmatrix' ? 'kd-pmatrix' : 'kd-matrixwrap'
|
|
258
|
-
const open = env === 'cases' ? '{' : env === 'bmatrix' ? '[' : env === 'pmatrix' ? '(' : ''
|
|
259
|
-
const close = env === 'bmatrix' ? ']' : env === 'pmatrix' ? ')' : ''
|
|
260
|
-
const children = []
|
|
261
|
-
if (open) children.push(h('span', { className: 'kd-delim', key: 'open' }, open))
|
|
262
|
-
children.push(table)
|
|
263
|
-
if (close) children.push(h('span', { className: 'kd-delim', key: 'close' }, close))
|
|
264
|
-
return h('span', { className: `kd-matrixbox ${wrapCls}`, key: nextKey('mb') }, children)
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
function latexNodes(src) {
|
|
268
|
-
const cur = { src: String(src || ''), i: 0 }
|
|
269
|
-
return latexSeq(cur, '')
|
|
270
167
|
}
|
|
271
168
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
const c = cur.src[cur.i]
|
|
283
|
-
if (stopChars && stopChars.includes(c)) break
|
|
284
|
-
if (c === '}') { buf += c; cur.i += 1; continue }
|
|
285
|
-
if (c === '{') {
|
|
286
|
-
flush()
|
|
287
|
-
out.push(...latexNodes(readGroup(cur)))
|
|
288
|
-
continue
|
|
289
|
-
}
|
|
290
|
-
if (c === '^' || c === '_') {
|
|
291
|
-
flush()
|
|
292
|
-
cur.i += 1
|
|
293
|
-
const arg = readArg(cur)
|
|
294
|
-
out.push(h(c === '^' ? 'sup' : 'sub', { className: c === '^' ? 'kd-sup' : 'kd-sub', key: nextKey('sc') }, arg))
|
|
295
|
-
continue
|
|
296
|
-
}
|
|
297
|
-
if (c === '&') {
|
|
298
|
-
flush()
|
|
299
|
-
cur.i += 1
|
|
300
|
-
out.push(h('span', { className: 'kd-colsep', key: nextKey('cs') }, '\u2003'))
|
|
301
|
-
continue
|
|
302
|
-
}
|
|
303
|
-
if (c === '~') {
|
|
304
|
-
buf += '\u00a0'
|
|
305
|
-
cur.i += 1
|
|
306
|
-
continue
|
|
307
|
-
}
|
|
308
|
-
if (c === '\\') {
|
|
309
|
-
const rest = cur.src.slice(cur.i)
|
|
310
|
-
const command = /^\\([A-Za-z]+)/.exec(rest)
|
|
311
|
-
if (command) {
|
|
312
|
-
flush()
|
|
313
|
-
cur.i += command[0].length
|
|
314
|
-
const node = latexCommand(cur, command[1])
|
|
315
|
-
if (node !== null && node !== undefined) out.push(node)
|
|
316
|
-
continue
|
|
317
|
-
}
|
|
318
|
-
const symbol = rest[1]
|
|
319
|
-
cur.i += symbol === undefined ? 1 : 2
|
|
320
|
-
if (symbol === '\\') {
|
|
321
|
-
flush()
|
|
322
|
-
out.push(h('br', { key: nextKey('br') }))
|
|
323
|
-
continue
|
|
324
|
-
}
|
|
325
|
-
if (LATEX_SYMBOLS[symbol] !== undefined) buf += LATEX_SYMBOLS[symbol]
|
|
326
|
-
else if (symbol !== undefined) buf += symbol
|
|
327
|
-
continue
|
|
328
|
-
}
|
|
329
|
-
if (c === ' ') { buf += ' '; cur.i += 1; continue }
|
|
330
|
-
buf += c
|
|
331
|
-
cur.i += 1
|
|
332
|
-
}
|
|
333
|
-
flush()
|
|
334
|
-
return out
|
|
169
|
+
/** 让组件跟着公式引擎的加载状态重渲染。 */
|
|
170
|
+
function useMathJaxStatus() {
|
|
171
|
+
const [status, setStatus] = useState(mathState.status)
|
|
172
|
+
useEffect(() => {
|
|
173
|
+
ensureMathJax()
|
|
174
|
+
const update = () => setStatus(mathState.status)
|
|
175
|
+
update()
|
|
176
|
+
return subscribeMath(update)
|
|
177
|
+
}, [])
|
|
178
|
+
return status
|
|
335
179
|
}
|
|
336
180
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
if (name === 'binom' || name === 'dbinom' || name === 'tbinom') {
|
|
359
|
-
const top = readArg(cur)
|
|
360
|
-
const bottom = readArg(cur)
|
|
361
|
-
return h('span', { className: 'kd-binom', key: nextKey('bn') },
|
|
362
|
-
h('span', { className: 'kd-delim', key: 'l' }, '('),
|
|
363
|
-
h('span', { className: 'kd-frac kd-binom-body', key: 'b' },
|
|
364
|
-
h('span', { className: 'kd-frac-n', key: 'n' }, top),
|
|
365
|
-
h('span', { className: 'kd-frac-d', key: 'd' }, bottom)),
|
|
366
|
-
h('span', { className: 'kd-delim', key: 'r' }, ')'))
|
|
367
|
-
}
|
|
368
|
-
if (name === 'sqrt') {
|
|
369
|
-
const index = readOptionalGroup(cur)
|
|
370
|
-
const body = readArg(cur)
|
|
371
|
-
return h('span', { className: 'kd-sqrt', key: nextKey('sq') },
|
|
372
|
-
h('span', { className: 'kd-sqrt-sign', key: 's' }, '√'),
|
|
373
|
-
index ? h('sup', { className: 'kd-sqrt-index', key: 'i' }, latexNodes(index)) : null,
|
|
374
|
-
h('span', { className: 'kd-sqrt-body', key: 'b' }, body))
|
|
375
|
-
}
|
|
376
|
-
if (name === 'text' || name === 'textrm' || name === 'mbox' || name === 'mathrm' || name === 'operatorname') {
|
|
377
|
-
const raw = readGroup(cur)
|
|
378
|
-
return h('span', { className: 'kd-roman', key: nextKey('rm') }, raw.replace(/\\([,;: !])/g, ' '))
|
|
379
|
-
}
|
|
380
|
-
if (name === 'mathbf' || name === 'boldsymbol' || name === 'bm' || name === 'textbf') {
|
|
381
|
-
return h('strong', { className: 'kd-bold', key: nextKey('bd') }, latexNodes(readGroup(cur)))
|
|
382
|
-
}
|
|
383
|
-
if (name === 'mathbb' || name === 'mathcal' || name === 'mathscr') {
|
|
384
|
-
const raw = readGroup(cur).trim()
|
|
385
|
-
return h('span', { className: 'kd-blackboard', key: nextKey('bb') }, DOUBLE_STRUCK[raw] || raw)
|
|
386
|
-
}
|
|
387
|
-
if (name === 'overline' || name === 'bar' || name === 'underline' || name === 'vec'
|
|
388
|
-
|| name === 'hat' || name === 'widehat' || name === 'tilde' || name === 'dot' || name === 'ddot') {
|
|
389
|
-
const body = readArg(cur)
|
|
390
|
-
return h('span', { className: `kd-accent kd-accent-${name}`, key: nextKey('ac') }, body)
|
|
391
|
-
}
|
|
392
|
-
if (name === 'overrightarrow' || name === 'overleftarrow') {
|
|
393
|
-
return h('span', { className: `kd-accent kd-accent-${name}`, key: nextKey('ac') }, readArg(cur))
|
|
394
|
-
}
|
|
395
|
-
// \underbrace{x}_{说明} / \overbrace{x}^{说明}:花括号画不出来,保留内容与标记
|
|
396
|
-
if (name === 'underbrace' || name === 'overbrace') {
|
|
397
|
-
return h('span', { className: 'kd-group', key: nextKey('grp') }, readArg(cur))
|
|
398
|
-
}
|
|
399
|
-
// \overset{上}{基} / \underset{下}{基}:上下堆叠
|
|
400
|
-
if (name === 'overset' || name === 'underset') {
|
|
401
|
-
const mark = readArg(cur)
|
|
402
|
-
const base = readArg(cur)
|
|
403
|
-
return h('span', {
|
|
404
|
-
className: `kd-stack kd-stack-${name === 'overset' ? 'over' : 'under'}`,
|
|
405
|
-
key: nextKey('st'),
|
|
406
|
-
},
|
|
407
|
-
name === 'overset' ? h('span', { className: 'kd-stack-mark', key: 'm' }, mark) : null,
|
|
408
|
-
h('span', { className: 'kd-stack-base', key: 'b' }, base),
|
|
409
|
-
name === 'underset' ? h('span', { className: 'kd-stack-mark', key: 'm' }, mark) : null)
|
|
410
|
-
}
|
|
411
|
-
if (name === 'left' || name === 'right' || name === 'middle') {
|
|
412
|
-
while (cur.i < cur.src.length && cur.src[cur.i] === ' ') cur.i += 1
|
|
413
|
-
const rest = cur.src.slice(cur.i)
|
|
414
|
-
const command = /^\\[A-Za-z]+/.exec(rest)
|
|
415
|
-
let token
|
|
416
|
-
if (command) { token = command[0]; cur.i += command[0].length } else { token = rest[0] || ''; cur.i += 1 }
|
|
417
|
-
const display = token === '.' ? '' : (LATEX_SYMBOLS[token.replace(/^\\/, '')] || token.replace(/^\\/, ''))
|
|
418
|
-
return h('span', { className: 'kd-delim', key: nextKey('dl') }, display)
|
|
419
|
-
}
|
|
420
|
-
if (name === 'begin') {
|
|
421
|
-
const env = readGroup(cur).trim()
|
|
422
|
-
return readMatrix(cur, env)
|
|
423
|
-
}
|
|
424
|
-
if (name === 'displaystyle' || name === 'textstyle' || name === 'limits' || name === 'nolimits' || name === 'notag'
|
|
425
|
-
|| name === 'nonumber' || name === 'label' || name === 'tag') {
|
|
426
|
-
// \tag{...} 之类的排版修饰我们不实现,但也不能把内容原样吐到公式里
|
|
427
|
-
if (name === 'tag' || name === 'label') readGroup(cur)
|
|
428
|
-
return null
|
|
429
|
-
}
|
|
430
|
-
// \big( \Big[ \bigl\{ … 只是放大定界符,实际字符由后面的记号自己渲染
|
|
431
|
-
if (/^(?:big|Big|bigg|Bigg)[lrm]?$/.test(name)) return null
|
|
432
|
-
if (name === 'bmod') return h('span', { className: 'kd-roman', key: nextKey('bm') }, 'mod')
|
|
433
|
-
if (name === 'pmod') {
|
|
434
|
-
return h('span', { className: 'kd-roman', key: nextKey('pm') }, '(', 'mod ',
|
|
435
|
-
...readArg(cur), ')')
|
|
181
|
+
/**
|
|
182
|
+
* 规范化模型输出的 TeX:
|
|
183
|
+
* - 去掉自带的 $ / $$ 包裹;
|
|
184
|
+
* - 过滤能改页面结构或注入链接的宏(公式来自模型输出,不当作可信内容);
|
|
185
|
+
* - 公式块里的裸 \\ 换行在 MathJax 3 需要放进 \displaylines(Typora 也是这么处理的);
|
|
186
|
+
* - 其余交给 MathJax,不再自己做子集解析。
|
|
187
|
+
*/
|
|
188
|
+
const RISKY_TEX = [
|
|
189
|
+
/\\href\s*\{[^{}]*\}/g,
|
|
190
|
+
/\\url\s*\{[^{}]*\}/g,
|
|
191
|
+
/\\(?:style|class|cssId|htmlId|htmlClass|htmlStyle)\s*\{[^{}]*\}/g,
|
|
192
|
+
/\\require\s*\{[^{}]*\}/g,
|
|
193
|
+
/\\includegraphics\s*(?:\[[^\]]*\])?\s*\{[^{}]*\}/g,
|
|
194
|
+
]
|
|
195
|
+
|
|
196
|
+
function normalizeTeX(source, display) {
|
|
197
|
+
let tex = String(source || '').trim()
|
|
198
|
+
tex = tex.replace(/^\${1,2}/, '').replace(/\${1,2}$/, '')
|
|
199
|
+
for (const pattern of RISKY_TEX) tex = tex.replace(pattern, '')
|
|
200
|
+
if (display && /\\\\/.test(tex) && !/\\displaylines/.test(tex) && !/\\begin\{/.test(tex)) {
|
|
201
|
+
tex = `\\displaylines{${tex}}`
|
|
436
202
|
}
|
|
437
|
-
|
|
438
|
-
if (name === 'hline') return null
|
|
439
|
-
if (name === 'space' || name === 'newline') return h('br', { key: nextKey('br') })
|
|
440
|
-
if (LATEX_FUNCS.has(name)) return h('span', { className: 'kd-roman', key: nextKey('fn') }, name)
|
|
441
|
-
if (LATEX_SYMBOLS[name] !== undefined) return LATEX_SYMBOLS[name]
|
|
442
|
-
// 未知命令:保留原始内容并提示,绝不丢弃。
|
|
443
|
-
return h('span', { className: 'kd-latex-unknown', title: `未知的 LaTeX 命令:\\${name}`, key: nextKey('un') }, `\\${name}`)
|
|
203
|
+
return tex
|
|
444
204
|
}
|
|
445
205
|
|
|
446
206
|
function renderMath(source, display) {
|
|
447
|
-
|
|
207
|
+
const tex = normalizeTeX(source, display)
|
|
208
|
+
const key = nextKey('math')
|
|
209
|
+
if (mathState.status !== 'ready') {
|
|
210
|
+
// 引擎还没就绪(或加载失败):先显示原文,就绪后组件会重渲染
|
|
211
|
+
return h('span', { className: 'kd-math-pending', key, title: '公式引擎正在加载' }, tex)
|
|
212
|
+
}
|
|
448
213
|
try {
|
|
449
|
-
|
|
214
|
+
const node = window.MathJax.tex2svg(tex, { display: Boolean(display) })
|
|
215
|
+
const html = node && node.outerHTML
|
|
216
|
+
if (!html) throw new Error('empty svg')
|
|
217
|
+
return h(display ? 'div' : 'span', {
|
|
218
|
+
className: display ? 'kd-mathblock' : 'kd-math',
|
|
219
|
+
key,
|
|
220
|
+
dangerouslySetInnerHTML: { __html: html },
|
|
221
|
+
})
|
|
450
222
|
} catch (error) {
|
|
451
|
-
|
|
223
|
+
return h('span', { className: 'kd-math-error', key, title: '公式无法渲染' }, tex)
|
|
452
224
|
}
|
|
453
|
-
return h(display ? 'div' : 'span', {
|
|
454
|
-
className: display ? 'kd-mathblock' : 'kd-math',
|
|
455
|
-
key: nextKey('math'),
|
|
456
|
-
}, nodes)
|
|
457
225
|
}
|
|
458
226
|
|
|
459
227
|
/* ============================== Markdown ============================== */
|
|
@@ -484,6 +252,10 @@ window.__ModuleLoader__.load({
|
|
|
484
252
|
})
|
|
485
253
|
}
|
|
486
254
|
|
|
255
|
+
/** Markdown 里有转义意义的字符:只有这些才允许把反斜杠吃掉。 */
|
|
256
|
+
const MD_ESCAPABLE = new Set(['\\', '`', '*', '_', '{', '}', '[', ']', '(', ')',
|
|
257
|
+
'#', '+', '-', '.', '!', '>', '|', '~', '<', '$', '"', "'"])
|
|
258
|
+
|
|
487
259
|
function inlineNodes(text) {
|
|
488
260
|
const out = []
|
|
489
261
|
let buffer = ''
|
|
@@ -504,8 +276,16 @@ window.__ModuleLoader__.load({
|
|
|
504
276
|
}
|
|
505
277
|
}
|
|
506
278
|
if (c === '\\' && index + 1 < text.length) {
|
|
507
|
-
|
|
508
|
-
|
|
279
|
+
const next = text[index + 1]
|
|
280
|
+
// 只有 Markdown 里有意义的字符才去掉反斜杠;其余原样保留,
|
|
281
|
+
// 免得把 \nabla、\neq 这类命令悄悄变成 "nabla"、"neq"。
|
|
282
|
+
if (MD_ESCAPABLE.has(next)) {
|
|
283
|
+
buffer += next
|
|
284
|
+
index += 2
|
|
285
|
+
continue
|
|
286
|
+
}
|
|
287
|
+
buffer += c
|
|
288
|
+
index += 1
|
|
509
289
|
continue
|
|
510
290
|
}
|
|
511
291
|
if (c === '`') {
|
|
@@ -634,7 +414,16 @@ window.__ModuleLoader__.load({
|
|
|
634
414
|
}
|
|
635
415
|
|
|
636
416
|
function renderBlocks(source) {
|
|
637
|
-
|
|
417
|
+
// 模型偶尔把换行写成字面量 `\n`(JSON 里多转义了一层),这里先还原成真换行,
|
|
418
|
+
// 否则会渲染成一串多余的字母 n,段落结构也会塌成一行。
|
|
419
|
+
// 用 (?!\w) 保证不会误伤 \nabla / \neq / \theta / \times 这类命令。
|
|
420
|
+
const normalized = String(source || '')
|
|
421
|
+
.replace(/\\r\\n/g, '\n')
|
|
422
|
+
.replace(/\\n(?!\w)/g, '\n')
|
|
423
|
+
.replace(/\\r(?!\w)/g, '\n')
|
|
424
|
+
.replace(/\\t(?!\w)/g, ' ')
|
|
425
|
+
.replace(/\r\n?/g, '\n')
|
|
426
|
+
const lines = normalized.split('\n')
|
|
638
427
|
const out = []
|
|
639
428
|
let index = 0
|
|
640
429
|
while (index < lines.length) {
|
|
@@ -738,13 +527,15 @@ window.__ModuleLoader__.load({
|
|
|
738
527
|
}
|
|
739
528
|
|
|
740
529
|
function Markdown({ text, className }) {
|
|
530
|
+
// 公式引擎加载完成后要重渲染一次,否则首次绘制出来的还是 TeX 原文
|
|
531
|
+
const mathStatus = useMathJaxStatus()
|
|
741
532
|
const nodes = useMemo(() => {
|
|
742
533
|
try {
|
|
743
534
|
return renderBlocks(text)
|
|
744
535
|
} catch (error) {
|
|
745
536
|
return [h('pre', { className: 'kd-code', key: 'raw' }, String(text || ''))]
|
|
746
537
|
}
|
|
747
|
-
}, [text])
|
|
538
|
+
}, [text, mathStatus])
|
|
748
539
|
return h('div', { className: `kd-md ${className || ''}` }, nodes)
|
|
749
540
|
}
|
|
750
541
|
|
|
@@ -752,12 +543,6 @@ window.__ModuleLoader__.load({
|
|
|
752
543
|
/** 由 apply 用 styles.insert 注入时置位;否则由组件渲染 <style> 兜底。 */
|
|
753
544
|
let styleInjected = false
|
|
754
545
|
const CSS = `
|
|
755
|
-
@font-face {
|
|
756
|
-
font-family: 'KD Latin Modern Math';
|
|
757
|
-
src: local('Latin Modern Math'), local('LatinModernMath'), local('LatinModernMath-Regular'),
|
|
758
|
-
url('/api/daily-question/font/latinmodern-math.otf') format('opentype');
|
|
759
|
-
font-display: swap;
|
|
760
|
-
}
|
|
761
546
|
.kd-page { height:100%; min-height:0; display:flex; flex-direction:column; position:relative;
|
|
762
547
|
background: var(--dsw-alias-bg-base); color: var(--dsw-alias-label-primary);
|
|
763
548
|
font-size:14px; line-height:1.65; -webkit-font-smoothing:antialiased; box-sizing:border-box; }
|
|
@@ -1022,72 +807,15 @@ window.__ModuleLoader__.load({
|
|
|
1022
807
|
.kd-image-fallback { display:inline-block; font-family:ui-monospace,monospace; font-size:12px;
|
|
1023
808
|
color:var(--dsw-alias-state-warn-primary); border:1px dashed var(--dsw-alias-state-warn-primary);
|
|
1024
809
|
border-radius:6px; padding:2px 6px; }
|
|
1025
|
-
.kd-math { display:inline-block; vertical-align:middle; }
|
|
1026
|
-
.kd-
|
|
1027
|
-
.kd-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
而不是下压半个字高;分子分母固定行高,保证分数线正好落在中线上。 */
|
|
1035
|
-
.kd-frac { display:inline-flex; flex-direction:column; align-items:stretch; vertical-align:middle;
|
|
1036
|
-
text-align:center; margin:0 .18em; line-height:1.15; }
|
|
1037
|
-
.kd-frac-n { padding:0 .25em .03em; }
|
|
1038
|
-
.kd-frac-d { border-top:1px solid currentColor; padding:.03em .25em 0; }
|
|
1039
|
-
/* boxed:把最终答案框起来 */
|
|
1040
|
-
.kd-boxed { display:inline-flex; align-items:center; padding:.06em .38em; margin:0 .12em;
|
|
1041
|
-
border:1px solid currentColor; border-radius:3px; }
|
|
1042
|
-
/* overset / underset:在基座上方或下方叠一行小字 */
|
|
1043
|
-
.kd-stack { display:inline-flex; flex-direction:column; align-items:center; vertical-align:middle;
|
|
1044
|
-
line-height:1.1; }
|
|
1045
|
-
.kd-stack-mark { font-size:.72em; }
|
|
1046
|
-
.kd-stack-base { line-height:1.2; }
|
|
1047
|
-
/* underbrace / overbrace 的花括号暂不绘制,只保留内容分组 */
|
|
1048
|
-
.kd-group { display:inline; }
|
|
1049
|
-
/* binom:没有分数线的上下堆叠 */
|
|
1050
|
-
.kd-binom { display:inline-flex; align-items:center; vertical-align:middle; margin:0 .1em; }
|
|
1051
|
-
.kd-binom .kd-frac-d { border-top:0; padding-top:0; }
|
|
1052
|
-
.kd-sup, .kd-sub { font-size:.72em; }
|
|
1053
|
-
.kd-sup { vertical-align:super; }
|
|
1054
|
-
.kd-sub { vertical-align:sub; }
|
|
1055
|
-
.kd-sqrt { display:inline-flex; align-items:flex-start; }
|
|
1056
|
-
.kd-sqrt-sign { font-size:1.05em; }
|
|
1057
|
-
.kd-sqrt-index { font-size:.6em; margin-left:-.35em; }
|
|
1058
|
-
.kd-sqrt-body { border-top:1px solid currentColor; padding:0 .12em; }
|
|
1059
|
-
.kd-op { display:inline-flex; flex-direction:column; align-items:center; vertical-align:middle;
|
|
1060
|
-
margin:0 .2em; line-height:1.15; }
|
|
1061
|
-
.kd-op-sym { font-size:1.35em; line-height:1; }
|
|
1062
|
-
.kd-op-lim { font-size:.62em; }
|
|
1063
|
-
.kd-roman { font-style:normal; }
|
|
1064
|
-
.kd-bold { font-weight:700; font-style:normal; }
|
|
1065
|
-
.kd-blackboard { font-style:normal; }
|
|
1066
|
-
.kd-colsep { display:inline-block; width:.5em; }
|
|
1067
|
-
.kd-matrixbox { display:inline-flex; align-items:center; vertical-align:middle; }
|
|
1068
|
-
.kd-matrix { border-collapse:collapse; }
|
|
1069
|
-
.kd-matrix td { padding:2px 7px; text-align:center; border:0; }
|
|
1070
|
-
.kd-delim { font-size:1.5em; line-height:1; }
|
|
1071
|
-
.kd-cases .kd-matrix td { text-align:left; }
|
|
1072
|
-
.kd-accent-overline, .kd-accent-bar { border-top:1px solid currentColor; }
|
|
1073
|
-
.kd-accent-underline { border-bottom:1px solid currentColor; }
|
|
1074
|
-
.kd-accent-vec { position:relative; }
|
|
1075
|
-
.kd-accent-vec::before { content:'→'; position:absolute; top:-.75em; left:50%; transform:translateX(-50%); font-size:.7em; }
|
|
1076
|
-
.kd-accent-overrightarrow { position:relative; }
|
|
1077
|
-
.kd-accent-overrightarrow::before { content:'→'; position:absolute; top:-.85em; left:0; right:0;
|
|
1078
|
-
text-align:center; font-size:.72em; line-height:1; }
|
|
1079
|
-
.kd-accent-overleftarrow { position:relative; }
|
|
1080
|
-
.kd-accent-overleftarrow::before { content:'←'; position:absolute; top:-.85em; left:0; right:0;
|
|
1081
|
-
text-align:center; font-size:.72em; line-height:1; }
|
|
1082
|
-
.kd-accent-hat::before, .kd-accent-widehat::before { content:'^'; position:absolute; top:-.5em; left:50%; transform:translateX(-50%); font-size:.8em; }
|
|
1083
|
-
.kd-accent-hat, .kd-accent-widehat { position:relative; }
|
|
1084
|
-
.kd-accent-tilde { position:relative; }
|
|
1085
|
-
.kd-accent-tilde::before { content:'~'; position:absolute; top:-.55em; left:50%; transform:translateX(-50%); }
|
|
1086
|
-
.kd-accent-dot { position:relative; }
|
|
1087
|
-
.kd-accent-dot::before { content:'·'; position:absolute; top:-.6em; left:50%; transform:translateX(-50%); }
|
|
1088
|
-
.kd-accent-ddot { position:relative; }
|
|
1089
|
-
.kd-accent-ddot::before { content:'··'; position:absolute; top:-.6em; left:50%; transform:translateX(-50%); }
|
|
1090
|
-
.kd-latex-unknown { color:var(--dsw-alias-state-warn-primary); font-size:.92em; }
|
|
810
|
+
.kd-math { display:inline-block; vertical-align:middle; max-width:100%; }
|
|
811
|
+
.kd-math mjx-container { color:currentColor; }
|
|
812
|
+
.kd-mathblock { display:block; margin:10px 0; padding:.35em .2em; overflow-x:auto; overflow-y:hidden;
|
|
813
|
+
text-align:center; }
|
|
814
|
+
.kd-mathblock mjx-container { display:inline-block; max-width:100%; }
|
|
815
|
+
.kd-math-pending { font-family:ui-monospace,SFMono-Regular,Consolas,monospace; font-size:.9em;
|
|
816
|
+
opacity:.55; white-space:pre-wrap; }
|
|
817
|
+
.kd-math-error { color:var(--dsw-alias-state-error-primary);
|
|
818
|
+
font-family:ui-monospace,SFMono-Regular,Consolas,monospace; font-size:.9em; }
|
|
1091
819
|
.kd-checkline { display:flex; gap:8px; align-items:flex-start; padding:5px 0; font-size:13px; }
|
|
1092
820
|
.kd-chip { display:inline-flex; align-items:center; gap:2px; padding:0 2px 0 8px; border-radius:999px;
|
|
1093
821
|
border:1px solid var(--dsw-alias-border-l2); background:var(--dsw-alias-bg-layer-1); font-size:12px; }
|