mirror of
https://github.com/stablyai/orca.git
synced 2026-10-30 08:37:49 +00:00
fix(editor): match Unicode heading anchors across rich and preview modes (#35077)
* fix(editor): match Unicode heading anchors across rich and preview modes * test(editor): drop slugger tests that only re-test github-slugger The preview-parity test in markdown-anchor-scroll.test.ts covers the behavior that matters. --------- Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
This commit is contained in:
@@ -316,6 +316,7 @@
|
||||
"emoji-picker-react": "^4.22.3",
|
||||
"emojibase-data": "17.0.0",
|
||||
"esbuild": "^0.28.2",
|
||||
"github-slugger": "2.0.0",
|
||||
"happy-dom": "^21.0.4",
|
||||
"hast-util-from-html-isomorphic": "2.0.0",
|
||||
"hast-util-to-jsx-runtime": "2.3.6",
|
||||
|
||||
Generated
+3
@@ -474,6 +474,9 @@ importers:
|
||||
esbuild:
|
||||
specifier: ^0.28.2
|
||||
version: 0.28.2
|
||||
github-slugger:
|
||||
specifier: 2.0.0
|
||||
version: 2.0.0
|
||||
happy-dom:
|
||||
specifier: ^21.0.4
|
||||
version: 21.0.4
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
// @vitest-environment happy-dom
|
||||
import { expect, it, vi } from 'vitest'
|
||||
import { scrollToAnchorInEditor } from './markdown-anchor-scroll'
|
||||
import { parseMarkdownPreviewDocument } from './markdown-preview-document-tree'
|
||||
|
||||
it.each([['Release ½'], ['Use ①', 'Use ²'], ['New \u{1e4d0}'], ['Café 中文', 'Café 中文']])(
|
||||
'resolves preview anchors for rich headings: %j',
|
||||
(...titles) => {
|
||||
const { document: preview } = parseMarkdownPreviewDocument(
|
||||
titles.map((title) => `## ${title}`).join('\n\n')
|
||||
)
|
||||
const root = document.createElement('div')
|
||||
const headings = titles.map((title) => {
|
||||
const heading = document.createElement('h2')
|
||||
heading.textContent = title
|
||||
root.append(heading)
|
||||
return vi.spyOn(heading, 'scrollIntoView').mockImplementation(() => {})
|
||||
})
|
||||
|
||||
for (const [index, anchor] of preview.blocks.flatMap((block) => block.anchors).entries()) {
|
||||
headings.forEach((scroll) => scroll.mockClear())
|
||||
scrollToAnchorInEditor(root, encodeURIComponent(anchor))
|
||||
expect(headings.map((scroll) => scroll.mock.calls.length)).toEqual(
|
||||
headings.map((_, headingIndex) => Number(index === headingIndex))
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -1,29 +0,0 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { MarkdownHeadingSlugger, slugMarkdownHeading } from './markdown-heading-slug'
|
||||
|
||||
describe('markdown heading slugger', () => {
|
||||
it('matches GitHub-style punctuation and space handling used by markdown anchors', () => {
|
||||
expect(slugMarkdownHeading('A & B')).toBe('a--b')
|
||||
expect(slugMarkdownHeading('https://example.com')).toBe('httpsexamplecom')
|
||||
expect(slugMarkdownHeading('Keep_under-score')).toBe('keep_under-score')
|
||||
})
|
||||
|
||||
it('adds stable duplicate suffixes', () => {
|
||||
const slugger = new MarkdownHeadingSlugger()
|
||||
|
||||
expect([slugger.slug('Repeat'), slugger.slug('Repeat'), slugger.slug('Repeat')]).toEqual([
|
||||
'repeat',
|
||||
'repeat-1',
|
||||
'repeat-2'
|
||||
])
|
||||
})
|
||||
|
||||
it('can reset duplicate state between render passes', () => {
|
||||
const slugger = new MarkdownHeadingSlugger()
|
||||
|
||||
expect(slugger.slug('Repeat')).toBe('repeat')
|
||||
expect(slugger.slug('Repeat')).toBe('repeat-1')
|
||||
slugger.reset()
|
||||
expect(slugger.slug('Repeat')).toBe('repeat')
|
||||
})
|
||||
})
|
||||
@@ -1,28 +1 @@
|
||||
// Match github-slugger@2.0.0 / rehype-slug@6.0.0 anchors without adding a runtime dependency.
|
||||
const markdownSlugPunctuationPattern = /[^\p{L}\p{M}\p{N} _-]/gu
|
||||
|
||||
export class MarkdownHeadingSlugger {
|
||||
private readonly occurrences = new Map<string, number>()
|
||||
|
||||
reset(): void {
|
||||
this.occurrences.clear()
|
||||
}
|
||||
|
||||
slug(value: string): string {
|
||||
const baseSlug = slugMarkdownHeading(value)
|
||||
let nextSlug = baseSlug
|
||||
|
||||
while (this.occurrences.has(nextSlug)) {
|
||||
const nextCount = (this.occurrences.get(baseSlug) ?? 0) + 1
|
||||
this.occurrences.set(baseSlug, nextCount)
|
||||
nextSlug = `${baseSlug}-${nextCount}`
|
||||
}
|
||||
|
||||
this.occurrences.set(nextSlug, 0)
|
||||
return nextSlug
|
||||
}
|
||||
}
|
||||
|
||||
export function slugMarkdownHeading(value: string): string {
|
||||
return value.toLowerCase().replace(markdownSlugPunctuationPattern, '').replace(/ /g, '-')
|
||||
}
|
||||
export { default as MarkdownHeadingSlugger, slug as slugMarkdownHeading } from 'github-slugger'
|
||||
|
||||
Reference in New Issue
Block a user