Skip to content

Commit 11ce18b

Browse files
committed
feat: update math parsing and rendering capabilities, enhance bug report template, and improve share link functionality
1 parent d19fe9b commit 11ce18b

File tree

15 files changed

+354
-72
lines changed

15 files changed

+354
-72
lines changed

.github/ISSUE_TEMPLATE/bug_report.yml

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -22,15 +22,6 @@ body:
2222
placeholder: Reproduction URL and steps
2323
validations:
2424
required: true
25-
- type: textarea
26-
id: system-info
27-
attributes:
28-
label: System Info
29-
description: Output of `npx envinfo --system --npmPackages '{vite,@vitejs/*}' --binaries --browsers`
30-
render: shell
31-
placeholder: System, Binaries, Browsers
32-
validations:
33-
required: true
3425
- type: dropdown
3526
id: package-manager
3627
attributes:

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,14 @@ These features make the library especially suited for real-time, AI-driven, and
6161
- [Streaming playground](https://vue-markdown-renderer.simonhe.me/) — try large Markdown files and progressive diagrams to feel the difference.
6262
- [Markdown vs v-html comparison](https://vue-markdown-renderer.simonhe.me/markdown) — contrast the library's reactive rendering with a traditional static pipeline.
6363

64+
### Interactive Test Page
65+
66+
- Try the interactive test page for quick verification and debugging: https://vue-markdown-renderer.simonhe.me/test
67+
68+
This page provides a left-side editor and right-side live preview powered by the library. It also includes a "生成并复制分享链接" action that encodes your input into the URL for easy sharing, and a fallback flow to open or prefill a GitHub issue when the input is too long for URL embedding.
69+
70+
Use this page to reproduce rendering issues, verify math/mermaid/code block behaviour, and quickly produce a shareable link or an issue with prefilled reproduction steps.
71+
6472
### Intro Video
6573

6674
This short video introduces the vue-renderer-markdown component library and highlights key features and usage patterns.

README.zh-CN.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,14 @@
6161
- [Streaming playground](https://vue-markdown-renderer.simonhe.me/) — 在浏览器中试用大文件、渐进式图表等特性。
6262
- [Markdown vs v-html comparison](https://vue-markdown-renderer.simonhe.me/markdown) — 对比本库的响应式渲染与传统静态管线。
6363

64+
### 交互测试页面
65+
66+
- 试用交互式测试页面以便快速验证与调试: https://vue-markdown-renderer.simonhe.me/test
67+
68+
此页面提供左侧编辑器与右侧实时预览(由本库驱动)。页面包含“生成并复制分享链接”功能,会将你的输入编码到 URL 中以便分享;当输入过长无法嵌入 URL 时,会提供直接打开或预填 GitHub Issue 的回退流程。
69+
70+
你可以使用该页面复现渲染问题,验证数学公式 / Mermaid / 代码块的渲染行为,并快速生成可共享链接或带复现信息的 issue。
71+
6472
### 介绍视频
6573

6674
一段短视频介绍了 vue-renderer-markdown 的关键特性与使用方式。

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,9 +103,10 @@
103103
},
104104
"dependencies": {
105105
"@floating-ui/dom": "^1.7.4",
106-
"stream-markdown-parser": "^0.0.10"
106+
"stream-markdown-parser": "^0.0.11"
107107
},
108108
"devDependencies": {
109+
"lz-string": "^1.5.0",
109110
"@antfu/eslint-config": "^5.4.1",
110111
"@types/node": "^18.19.130",
111112
"@vitejs/plugin-vue": "^5.2.4",

packages/markdown-parser/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "stream-markdown-parser",
33
"type": "module",
4-
"version": "0.0.10",
4+
"version": "0.0.11",
55
"packageManager": "[email protected]",
66
"description": "Pure markdown parser and renderer utilities with streaming support - framework agnostic",
77
"author": "Simon He",

packages/markdown-parser/src/plugins/isMathLike.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,8 @@ export function isMathLike(s: string) {
5959
// If the content looks like a timestamp or date, it's not math.
6060
if (DATE_TIME_RE.test(stripped))
6161
return false
62+
if (stripped.includes('**'))
63+
return false
6264
if (stripped.length > 2000)
6365
return true // very long blocks likely math
6466

packages/markdown-parser/src/plugins/math.ts

Lines changed: 57 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ export const KATEX_COMMANDS = [
2020
'cdots',
2121
'quad',
2222
'in',
23+
'end',
2324
'infty',
2425
'perp',
2526
'mid',
@@ -69,7 +70,6 @@ export const KATEX_COMMANDS = [
6970
'text',
7071
'left',
7172
'right',
72-
'times',
7373
]
7474

7575
// Precompute escaped KATEX commands and default regex used by
@@ -145,17 +145,27 @@ export function normalizeStandaloneBackslashT(s: string, opts?: MathOptions) {
145145
: [commands.map(c => c.replace(/[.*+?^${}()|[\\]\\\]/g, '\\$&')).join('|'), ESCAPED_TEX_BRACE_COMMANDS].filter(Boolean).join('|')
146146
let result = out
147147
if (braceEscaped) {
148-
const braceCmdRe = new RegExp(`(^|[^\\\\])(${braceEscaped})\\s*\\{`, 'g')
148+
const braceCmdRe = new RegExp(`(^|[^\\\\\\w])(${braceEscaped})\\s*\\{`, 'g')
149149
result = result.replace(braceCmdRe, (_m: string, p1: string, p2: string) => `${p1}\\${p2}{`)
150150
}
151151
result = result.replace(/span\{([^}]+)\}/, 'span\\{$1\\}')
152152
.replace(/\\operatorname\{span\}\{((?:[^{}]|\{[^}]*\})+)\}/, '\\operatorname{span}\\{$1\\}')
153+
154+
// If a single backslash appears immediately before a newline (e.g. "... 8 \n5..."),
155+
// it's likely intended as a LaTeX linebreak (`\\`). Double it, but avoid
156+
// changing already escaped `\\` sequences.
157+
// Match a single backslash not preceded by another backslash, followed by an optional CR and a LF.
158+
result = result.replace(/(^|[^\\])\\\r?\n/g, '$1\\\\\n')
159+
160+
// If the string ends with a single backslash (no trailing newline), double it.
161+
result = result.replace(/(^|[^\\])\\$/g, '$1\\\\')
153162
return result
154163
}
155164
export function applyMath(md: MarkdownIt, mathOpts?: MathOptions) {
156165
// Inline rule for \(...\) and $$...$$ and $...$
157166
const mathInline = (state: unknown, silent: boolean) => {
158167
const s = state as any
168+
159169
if (/^\*[^*]+/.test(s.src)) {
160170
return false
161171
}
@@ -228,43 +238,54 @@ export function applyMath(md: MarkdownIt, mathOpts?: MathOptions) {
228238
if (endIdx === -1) {
229239
// no matching close for this opener; skip forward
230240
const content = src.slice(index + open.length)
231-
if (isMathLike(content)) {
232-
searchPos = index + open.length
233-
foundAny = true
234-
if (!silent) {
235-
s.pending = ''
236-
const toPushBefore = preMathPos ? src.slice(preMathPos, searchPos) : src.slice(0, searchPos)
237-
const isStrongPrefix = countUnescapedStrong(toPushBefore) % 2 === 1
238-
239-
if (preMathPos)
240-
pushText(src.slice(preMathPos, searchPos))
241-
else
242-
pushText(src.slice(0, searchPos))
243-
if (isStrongPrefix) {
244-
const strongToken = s.push('strong_open', '', 0)
245-
strongToken.markup = src.slice(0, index + 2)
246-
const token = s.push('math_inline', 'math', 0)
247-
token.content = normalizeStandaloneBackslashT(content, mathOpts)
248-
token.markup = open === '$$' ? '$$' : open === '\\(' ? '\\(\\)' : open === '$' ? '$' : '()'
249-
token.raw = `${open}${content}${close}`
250-
token.loading = true
251-
strongToken.content = content
252-
s.push('strong_close', '', 0)
253-
}
254-
else {
255-
const token = s.push('math_inline', 'math', 0)
256-
token.content = normalizeStandaloneBackslashT(content, mathOpts)
257-
token.markup = open === '$$' ? '$$' : open === '\\(' ? '\\(\\)' : open === '$' ? '$' : '()'
258-
token.raw = `${open}${content}${close}`
259-
token.loading = true
241+
if (content.includes(open)) {
242+
searchPos = src.indexOf(open, index + open.length)
243+
continue
244+
}
245+
if (endIdx === -1) {
246+
if (isMathLike(content)) {
247+
searchPos = index + open.length
248+
foundAny = true
249+
if (!silent) {
250+
s.pending = ''
251+
const toPushBefore = preMathPos ? src.slice(preMathPos, searchPos) : src.slice(0, searchPos)
252+
const isStrongPrefix = countUnescapedStrong(toPushBefore) % 2 === 1
253+
254+
if (preMathPos) {
255+
pushText(src.slice(preMathPos, searchPos))
256+
}
257+
else {
258+
let text = src.slice(0, searchPos)
259+
if (text.endsWith(open))
260+
text = text.slice(0, text.length - open.length)
261+
pushText(text)
262+
}
263+
if (isStrongPrefix) {
264+
const strongToken = s.push('strong_open', '', 0)
265+
strongToken.markup = src.slice(0, index + 2)
266+
const token = s.push('math_inline', 'math', 0)
267+
token.content = normalizeStandaloneBackslashT(content, mathOpts)
268+
token.markup = open === '$$' ? '$$' : open === '\\(' ? '\\(\\)' : open === '$' ? '$' : '()'
269+
token.raw = `${open}${content}${close}`
270+
token.loading = true
271+
strongToken.content = content
272+
s.push('strong_close', '', 0)
273+
}
274+
else {
275+
const token = s.push('math_inline', 'math', 0)
276+
token.content = normalizeStandaloneBackslashT(content, mathOpts)
277+
token.markup = open === '$$' ? '$$' : open === '\\(' ? '\\(\\)' : open === '$' ? '$' : '()'
278+
token.raw = `${open}${content}${close}`
279+
token.loading = true
280+
}
281+
// consume the full inline source
282+
s.pos = src.length
260283
}
261-
// consume the full inline source
262-
s.pos = src.length
284+
searchPos = src.length
285+
preMathPos = searchPos
263286
}
264-
searchPos = src.length
265-
preMathPos = searchPos
287+
break
266288
}
267-
break
268289
}
269290
const content = src.slice(index + open.length, endIdx)
270291
if (!isMathLike(content)) {
@@ -388,12 +409,6 @@ export function applyMath(md: MarkdownIt, mathOpts?: MathOptions) {
388409
if (open.includes('[')) {
389410
if (lineText.replace('\\', '') === '[') {
390411
if (startLine + 1 < endLine) {
391-
// const nextLineStart
392-
// = state.bMarks[startLine + 1] + state.tShift[startLine + 1]
393-
// const nextLineText = state.src.slice(
394-
// nextLineStart,
395-
// state.eMarks[startLine + 1],
396-
// )
397412
matched = true
398413
openDelim = open
399414
closeDelim = close
@@ -429,7 +444,6 @@ export function applyMath(md: MarkdownIt, mathOpts?: MathOptions) {
429444
startDelimIndex + openDelim.length,
430445
endDelimIndex,
431446
)
432-
433447
const token: any = s.push('math_block', 'math', 0)
434448
token.content = normalizeStandaloneBackslashT(content)
435449
token.markup

playground/src/const/markdown.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,14 @@ e^x = 1 + x + \frac{x^2}{2!} + \frac{x^3}{3!} + \cdots + \frac{x^n}{n!} + \cdots
8585
(1+x)^m = 1 + mx + \frac{m(m-1)}{2!}x^2 + \frac{m(m-1)(m-2)}{3!}x^3 + \cdots, \quad |x| < 1
8686
\]
8787
88+
- **矩阵**:
89+
\[
90+
\begin{bmatrix}
91+
2x_2 - 8x_3 = 8 \\
92+
5x_1 - 5x_3 = 10
93+
\end{bmatrix}
94+
\]
95+
8896
- **公式**
8997
9098

0 commit comments

Comments
 (0)