fix(editor): render wikilinks inside tables
This commit is contained in:
@@ -34,21 +34,24 @@ describe('preProcessWikilinks', () => {
|
||||
expect(preProcessWikilinks(input)).toBe(input)
|
||||
})
|
||||
|
||||
it('preserves wikilinks and inline formatting inside markdown tables', () => {
|
||||
it('replaces wikilinks inside markdown tables without introducing extra cell delimiters', () => {
|
||||
const input = [
|
||||
'| Topic | Reference |',
|
||||
'| --- | --- |',
|
||||
'| [[Project Alpha]] | **bold** and [[Project Beta]] |',
|
||||
'| [[Project Alpha]] | **bold** and [[project/beta|Project Beta]] |',
|
||||
'',
|
||||
'Outside [[Project Gamma]]',
|
||||
].join('\n')
|
||||
|
||||
const result = preProcessWikilinks(input)
|
||||
const row = result.split('\n')[2]
|
||||
|
||||
expect(result).toContain('| [[Project Alpha]] | **bold** and [[Project Beta]] |')
|
||||
expect(row.match(/\|/g)).toHaveLength(3)
|
||||
expect(row).toContain('WIKILINK:ENC:Project%20Alpha')
|
||||
expect(row).toContain('WIKILINK:ENC:project%2Fbeta%7CProject%20Beta')
|
||||
expect(result).toContain('WIKILINK:Project Gamma')
|
||||
expect(result).not.toContain('WIKILINK:Project Alpha')
|
||||
expect(result).not.toContain('WIKILINK:Project Beta')
|
||||
expect(result).not.toContain('[[Project Alpha]]')
|
||||
expect(result).not.toContain('[[project/beta|Project Beta]]')
|
||||
})
|
||||
|
||||
it('handles empty string', () => {
|
||||
@@ -150,6 +153,32 @@ describe('injectWikilinks', () => {
|
||||
expect(result[0].content![0].text).toBe('text ')
|
||||
expect(result[0].content![1].type).toBe('wikilink')
|
||||
})
|
||||
|
||||
it('converts encoded placeholder text inside table cells into wikilink nodes', () => {
|
||||
const blocks = [{
|
||||
type: 'table',
|
||||
content: {
|
||||
type: 'tableContent',
|
||||
rows: [{
|
||||
cells: [{
|
||||
type: 'tableCell',
|
||||
content: [
|
||||
{ type: 'text', text: `${WL_START}ENC:project%2Fbeta%7CProject%20Beta${WL_END}` },
|
||||
],
|
||||
}],
|
||||
}],
|
||||
},
|
||||
children: [],
|
||||
}]
|
||||
|
||||
const result = injectWikilinks(blocks) as TestBlock[]
|
||||
const tableContent = result[0].content as unknown as { rows: Array<{ cells: Array<{ content: TestBlock[] }> }> }
|
||||
expect(tableContent.rows[0].cells[0].content).toEqual([{
|
||||
type: 'wikilink',
|
||||
props: { target: 'project/beta|Project Beta' },
|
||||
content: undefined,
|
||||
}])
|
||||
})
|
||||
})
|
||||
|
||||
describe('splitFrontmatter', () => {
|
||||
@@ -379,6 +408,28 @@ describe('restoreWikilinksInBlocks', () => {
|
||||
expect(result[0].content![1]).toEqual({ type: 'link', text: 'a link', href: 'http://example.com' })
|
||||
})
|
||||
|
||||
it('converts wikilink nodes inside table cells back to markdown text', () => {
|
||||
const blocks = [{
|
||||
type: 'table',
|
||||
content: {
|
||||
type: 'tableContent',
|
||||
rows: [{
|
||||
cells: [{
|
||||
type: 'tableCell',
|
||||
content: [
|
||||
{ type: 'wikilink', props: { target: 'Project Alpha' }, content: undefined },
|
||||
],
|
||||
}],
|
||||
}],
|
||||
},
|
||||
children: [],
|
||||
}]
|
||||
|
||||
const result = restoreWikilinksInBlocks(blocks) as TestBlock[]
|
||||
const tableContent = result[0].content as unknown as { rows: Array<{ cells: Array<{ content: TestBlock[] }> }> }
|
||||
expect(tableContent.rows[0].cells[0].content).toEqual([{ type: 'text', text: '[[Project Alpha]]' }])
|
||||
})
|
||||
|
||||
it('handles blocks without content', () => {
|
||||
const blocks = [{ type: 'heading', props: { level: 1 } }]
|
||||
const result = restoreWikilinksInBlocks(blocks as unknown[]) as TestBlock[]
|
||||
|
||||
@@ -3,24 +3,43 @@ const WL_START = '\u2039WIKILINK:'
|
||||
const WL_END = '\u203A'
|
||||
const WL_RE = /\u2039WIKILINK:([^\u203A]+)\u203A/g
|
||||
const WIKILINK_RE = /\[\[([^\]]+)\]\]/g
|
||||
const TABLE_PLACEHOLDER_PREFIX = 'ENC:'
|
||||
const FORMAT_MARKERS = new Set(['*', '_', '`', '~'])
|
||||
|
||||
type MarkdownSource = string
|
||||
type MarkdownLine = string
|
||||
type MarkdownLines = MarkdownLine[]
|
||||
type TableLineMap = boolean[]
|
||||
type PlaceholderPayload = string
|
||||
type WikilinkTarget = string
|
||||
type FrontmatterSplit = [MarkdownSource, MarkdownSource]
|
||||
type CharacterCount = number
|
||||
type LineIndex = number
|
||||
type TextOffset = number
|
||||
type TokenSequence = string
|
||||
type ParsedTextRange = { text: MarkdownSource, nextIndex: TextOffset }
|
||||
type MatchTargets = Set<WikilinkTarget>
|
||||
type WordCount = number
|
||||
|
||||
/** Pre-process markdown: replace [[target]] with placeholder tokens */
|
||||
export function preProcessWikilinks(md: string): string {
|
||||
export function preProcessWikilinks(md: MarkdownSource): MarkdownSource {
|
||||
const lines = md.split('\n')
|
||||
const tableLines = findMarkdownTableLines(lines)
|
||||
return lines.map((line, index) => (
|
||||
tableLines[index] ? line : replaceWikilinksWithPlaceholders(line)
|
||||
replaceWikilinksWithPlaceholders(line, { encodePayload: tableLines[index] })
|
||||
)).join('\n')
|
||||
}
|
||||
|
||||
// Minimal shape of a BlockNote block for wikilink processing
|
||||
interface BlockLike {
|
||||
content?: InlineItem[]
|
||||
content?: BlockContent
|
||||
children?: BlockLike[]
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
type ScalarBlockContent = string | number | boolean | null | undefined
|
||||
type BlockContent = InlineItem[] | TableContentLike | Record<string, unknown> | ScalarBlockContent
|
||||
|
||||
interface InlineItem {
|
||||
type: string
|
||||
text?: string
|
||||
@@ -29,13 +48,59 @@ interface InlineItem {
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
type ContentTransform = (content: InlineItem[]) => InlineItem[]
|
||||
|
||||
function replaceWikilinksWithPlaceholders(line: string): string {
|
||||
return line.replace(WIKILINK_RE, (_match, target) => `${WL_START}${target}${WL_END}`)
|
||||
interface TableContentLike {
|
||||
type: 'tableContent'
|
||||
rows?: TableRowLike[]
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
function findMarkdownTableLines(lines: string[]): boolean[] {
|
||||
interface TableRowLike {
|
||||
cells?: TableCellLike[]
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
type TableCellLike = string | TableCellObjectLike
|
||||
|
||||
interface TableCellObjectLike {
|
||||
content?: InlineItem[]
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
type ContentTransform = (content: InlineItem[]) => InlineItem[]
|
||||
|
||||
interface WikilinkReplacementOptions {
|
||||
encodePayload: boolean
|
||||
}
|
||||
|
||||
function replaceWikilinksWithPlaceholders(
|
||||
line: MarkdownLine,
|
||||
options: WikilinkReplacementOptions,
|
||||
): MarkdownLine {
|
||||
return line.replace(WIKILINK_RE, (_match, target) => wikilinkPlaceholder(target, options))
|
||||
}
|
||||
|
||||
function wikilinkPlaceholder(
|
||||
target: WikilinkTarget,
|
||||
options: WikilinkReplacementOptions,
|
||||
): string {
|
||||
const payload = options.encodePayload
|
||||
? `${TABLE_PLACEHOLDER_PREFIX}${encodeURIComponent(target)}`
|
||||
: target
|
||||
return `${WL_START}${payload}${WL_END}`
|
||||
}
|
||||
|
||||
function decodePlaceholderPayload(payload: PlaceholderPayload): WikilinkTarget {
|
||||
if (!payload.startsWith(TABLE_PLACEHOLDER_PREFIX)) return payload
|
||||
|
||||
const encoded = payload.slice(TABLE_PLACEHOLDER_PREFIX.length)
|
||||
try {
|
||||
return decodeURIComponent(encoded)
|
||||
} catch {
|
||||
return encoded
|
||||
}
|
||||
}
|
||||
|
||||
function findMarkdownTableLines(lines: MarkdownLines): TableLineMap {
|
||||
const tableLines = lines.map(() => false)
|
||||
for (let index = 0; index < lines.length - 1; index++) {
|
||||
if (!isPotentialTableRow(lines[index]) || !isMarkdownTableSeparator(lines[index + 1])) {
|
||||
@@ -49,7 +114,11 @@ function findMarkdownTableLines(lines: string[]): boolean[] {
|
||||
return tableLines
|
||||
}
|
||||
|
||||
function markTableBodyLines(lines: string[], tableLines: boolean[], start: number): number {
|
||||
function markTableBodyLines(
|
||||
lines: MarkdownLines,
|
||||
tableLines: TableLineMap,
|
||||
start: LineIndex,
|
||||
): LineIndex {
|
||||
let index = start
|
||||
while (index < lines.length && isPotentialTableRow(lines[index])) {
|
||||
tableLines[index] = true
|
||||
@@ -58,24 +127,24 @@ function markTableBodyLines(lines: string[], tableLines: boolean[], start: numbe
|
||||
return index
|
||||
}
|
||||
|
||||
function isPotentialTableRow(line: string): boolean {
|
||||
function isPotentialTableRow(line: MarkdownLine): boolean {
|
||||
const trimmed = line.trim()
|
||||
return trimmed.includes('|') && trimmed !== '|'
|
||||
}
|
||||
|
||||
function isMarkdownTableSeparator(line: string): boolean {
|
||||
function isMarkdownTableSeparator(line: MarkdownLine): boolean {
|
||||
const cells = splitTableCells(line)
|
||||
return cells.length > 1 && cells.every(isMarkdownTableSeparatorCell)
|
||||
}
|
||||
|
||||
function splitTableCells(line: string): string[] {
|
||||
function splitTableCells(line: MarkdownLine): MarkdownLines {
|
||||
let trimmed = line.trim()
|
||||
if (trimmed.startsWith('|')) trimmed = trimmed.slice(1)
|
||||
if (trimmed.endsWith('|')) trimmed = trimmed.slice(0, -1)
|
||||
return trimmed.split('|').map((cell) => cell.trim()).filter(Boolean)
|
||||
}
|
||||
|
||||
function isMarkdownTableSeparatorCell(cell: string): boolean {
|
||||
function isMarkdownTableSeparatorCell(cell: MarkdownLine): boolean {
|
||||
return /^:?-{3,}:?$/.test(cell)
|
||||
}
|
||||
|
||||
@@ -83,9 +152,7 @@ function isMarkdownTableSeparatorCell(cell: string): boolean {
|
||||
function walkBlocks(blocks: unknown[], transform: ContentTransform, clone = false): unknown[] {
|
||||
return (blocks as BlockLike[]).map(block => {
|
||||
const b = clone ? { ...block } : block
|
||||
if (b.content && Array.isArray(b.content)) {
|
||||
b.content = transform(b.content)
|
||||
}
|
||||
b.content = transformBlockContent(b.content, transform)
|
||||
if (b.children && Array.isArray(b.children)) {
|
||||
b.children = walkBlocks(b.children, transform, clone) as BlockLike[]
|
||||
}
|
||||
@@ -93,11 +160,45 @@ function walkBlocks(blocks: unknown[], transform: ContentTransform, clone = fals
|
||||
})
|
||||
}
|
||||
|
||||
function textSegment(item: InlineItem, text: string): InlineItem {
|
||||
function transformBlockContent(content: BlockContent, transform: ContentTransform): BlockContent {
|
||||
if (Array.isArray(content)) return transform(content)
|
||||
if (isTableContent(content)) return transformTableContent(content, transform)
|
||||
return content
|
||||
}
|
||||
|
||||
function isTableContent(content: BlockContent): content is TableContentLike {
|
||||
return Boolean(
|
||||
content
|
||||
&& typeof content === 'object'
|
||||
&& !Array.isArray(content)
|
||||
&& (content as TableContentLike).type === 'tableContent'
|
||||
&& Array.isArray((content as TableContentLike).rows),
|
||||
)
|
||||
}
|
||||
|
||||
function transformTableContent(
|
||||
content: TableContentLike,
|
||||
transform: ContentTransform,
|
||||
): TableContentLike {
|
||||
return {
|
||||
...content,
|
||||
rows: content.rows?.map((row) => ({
|
||||
...row,
|
||||
cells: row.cells?.map((cell) => transformTableCell(cell, transform)),
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
function transformTableCell(cell: TableCellLike, transform: ContentTransform): TableCellLike {
|
||||
if (typeof cell === 'string' || !Array.isArray(cell.content)) return cell
|
||||
return { ...cell, content: transform(cell.content) }
|
||||
}
|
||||
|
||||
function textSegment(item: InlineItem, text: MarkdownSource): InlineItem {
|
||||
return { ...item, text }
|
||||
}
|
||||
|
||||
function wikilinkItem(target: string): InlineItem {
|
||||
function wikilinkItem(target: WikilinkTarget): InlineItem {
|
||||
return {
|
||||
type: 'wikilink',
|
||||
props: { target },
|
||||
@@ -136,7 +237,7 @@ function expandWikilinksInItem(item: InlineItem): InlineItem[] {
|
||||
let match
|
||||
while ((match = WL_RE.exec(item.text)) !== null) {
|
||||
if (match.index > lastIndex) result.push(textSegment(item, item.text.slice(lastIndex, match.index)))
|
||||
result.push(wikilinkItem(match[1]))
|
||||
result.push(wikilinkItem(decodePlaceholderPayload(match[1])))
|
||||
lastIndex = match.index + match[0].length
|
||||
}
|
||||
if (lastIndex < item.text.length) result.push(textSegment(item, item.text.slice(lastIndex)))
|
||||
@@ -155,17 +256,17 @@ function collapseWikilinksInContent(content: InlineItem[]): InlineItem[] {
|
||||
return result
|
||||
}
|
||||
|
||||
function frontmatterOpeningLength(content: string): number | null {
|
||||
function frontmatterOpeningLength(content: MarkdownSource): CharacterCount | null {
|
||||
if (content.startsWith('---\r\n')) return 5
|
||||
if (content.startsWith('---\n')) return 4
|
||||
return null
|
||||
}
|
||||
|
||||
function precedingLineEndingLength(value: string): number {
|
||||
function precedingLineEndingLength(value: MarkdownSource): CharacterCount {
|
||||
return value.startsWith('\r\n') ? 2 : value.startsWith('\n') ? 1 : 0
|
||||
}
|
||||
|
||||
function frontmatterCloseLength(value: string): number {
|
||||
function frontmatterCloseLength(value: MarkdownSource): CharacterCount {
|
||||
const lineEndingLength = precedingLineEndingLength(value)
|
||||
if (value.endsWith('\r\n')) return lineEndingLength + 5
|
||||
if (value.endsWith('\n')) return lineEndingLength + 4
|
||||
@@ -173,7 +274,7 @@ function frontmatterCloseLength(value: string): number {
|
||||
}
|
||||
|
||||
/** Strip YAML frontmatter from markdown, returning [frontmatter, body] */
|
||||
export function splitFrontmatter(content: string): [string, string] {
|
||||
export function splitFrontmatter(content: MarkdownSource): FrontmatterSplit {
|
||||
const openLength = frontmatterOpeningLength(content)
|
||||
if (openLength === null) return ['', content]
|
||||
|
||||
@@ -188,8 +289,8 @@ export function splitFrontmatter(content: string): [string, string] {
|
||||
/** Extract all outgoing wikilink targets from content.
|
||||
* Finds [[target]] and [[target|display]] patterns, returning just the target part.
|
||||
* Returns a sorted, deduplicated array. */
|
||||
export function extractOutgoingLinks(content: string): string[] {
|
||||
const links: string[] = []
|
||||
export function extractOutgoingLinks(content: MarkdownSource): WikilinkTarget[] {
|
||||
const links: WikilinkTarget[] = []
|
||||
const re = /\[\[([^\]]+)\]\]/g
|
||||
let match
|
||||
while ((match = re.exec(content)) !== null) {
|
||||
@@ -205,10 +306,10 @@ export function extractOutgoingLinks(content: string): string[] {
|
||||
* Searches for any target in the set, returns the first matching paragraph trimmed
|
||||
* to a max length. Returns null if no match found. */
|
||||
export function extractBacklinkContext(
|
||||
content: string,
|
||||
matchTargets: Set<string>,
|
||||
maxLength = 120,
|
||||
): string | null {
|
||||
content: MarkdownSource,
|
||||
matchTargets: MatchTargets,
|
||||
maxLength: CharacterCount = 120,
|
||||
): MarkdownSource | null {
|
||||
const [, body] = splitFrontmatter(content)
|
||||
// Remove the H1 title line
|
||||
const withoutTitle = body.replace(/^\s*# [^\n]+\n?/, '')
|
||||
@@ -236,13 +337,13 @@ export function extractBacklinkContext(
|
||||
}
|
||||
|
||||
/** Check if a line is useful for snippet extraction (not blank, heading, code fence, or rule). */
|
||||
function isSnippetLine(line: string): boolean {
|
||||
function isSnippetLine(line: MarkdownLine): boolean {
|
||||
const t = line.trim()
|
||||
return t !== '' && !t.startsWith('#') && !t.startsWith('```') && !t.startsWith('---')
|
||||
}
|
||||
|
||||
/** Strip leading list markers (*, -, +, 1.) from a line. */
|
||||
function stripListMarker(line: string): string {
|
||||
function stripListMarker(line: MarkdownLine): MarkdownLine {
|
||||
const t = line.trimStart()
|
||||
for (const prefix of ['* ', '- ', '+ ']) {
|
||||
if (t.startsWith(prefix)) return t.slice(prefix.length)
|
||||
@@ -254,13 +355,13 @@ function stripListMarker(line: string): string {
|
||||
return t
|
||||
}
|
||||
|
||||
function isOrderedListMarker(line: string, dotPos: number): boolean {
|
||||
function isOrderedListMarker(line: MarkdownLine, dotPos: TextOffset): boolean {
|
||||
if (dotPos < 1 || dotPos > 3) return false
|
||||
return /^\d+$/.test(line.slice(0, dotPos))
|
||||
}
|
||||
|
||||
/** Remove the first H1 heading line, allowing leading blank lines. */
|
||||
function removeH1Line(body: string): string {
|
||||
function removeH1Line(body: MarkdownSource): MarkdownSource {
|
||||
const lines = body.split('\n')
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
if (lines[i].trim().startsWith('# ')) return lines.slice(i + 1).join('\n')
|
||||
@@ -270,7 +371,7 @@ function removeH1Line(body: string): string {
|
||||
}
|
||||
|
||||
/** Strip markdown formatting chars: bold, italic, code, strikethrough, and resolve links. */
|
||||
function stripMarkdownChars(s: string): string {
|
||||
function stripMarkdownChars(s: MarkdownSource): MarkdownSource {
|
||||
let result = ''
|
||||
let i = 0
|
||||
while (i < s.length) {
|
||||
@@ -292,32 +393,40 @@ function stripMarkdownChars(s: string): string {
|
||||
return result
|
||||
}
|
||||
|
||||
function readUntilSequence(value: string, start: number, sequence: string): { text: string, nextIndex: number } {
|
||||
function readUntilSequence(
|
||||
value: MarkdownSource,
|
||||
start: TextOffset,
|
||||
sequence: TokenSequence,
|
||||
): ParsedTextRange {
|
||||
const end = value.indexOf(sequence, start)
|
||||
if (end === -1) return { text: value.slice(start), nextIndex: value.length }
|
||||
return { text: value.slice(start, end), nextIndex: end + sequence.length }
|
||||
}
|
||||
|
||||
function readUntilChar(value: string, start: number, char: string): { text: string, nextIndex: number } {
|
||||
function readUntilChar(
|
||||
value: MarkdownSource,
|
||||
start: TextOffset,
|
||||
char: TokenSequence,
|
||||
): ParsedTextRange {
|
||||
const end = value.indexOf(char, start)
|
||||
if (end === -1) return { text: value.slice(start), nextIndex: value.length }
|
||||
return { text: value.slice(start, end), nextIndex: end + 1 }
|
||||
}
|
||||
|
||||
function skipMarkdownLinkDestination(value: string, start: number): number {
|
||||
function skipMarkdownLinkDestination(value: MarkdownSource, start: TextOffset): TextOffset {
|
||||
if (value[start] !== '(') return start
|
||||
|
||||
const end = value.indexOf(')', start + 1)
|
||||
return end === -1 ? value.length : end + 1
|
||||
}
|
||||
|
||||
function wikilinkDisplayText(inner: string): string {
|
||||
function wikilinkDisplayText(inner: WikilinkTarget): MarkdownSource {
|
||||
const pipe = inner.indexOf('|')
|
||||
return pipe === -1 ? inner : inner.slice(pipe + 1)
|
||||
}
|
||||
|
||||
/** Extract sub-heading text (## , ### , etc.) stripped of the # prefix. */
|
||||
function extractSubheadingText(line: string): string | null {
|
||||
function extractSubheadingText(line: MarkdownLine): MarkdownSource | null {
|
||||
const t = line.trim()
|
||||
const stripped = t.replace(/^#+/, '')
|
||||
if (stripped.length < t.length && stripped.startsWith(' ')) {
|
||||
@@ -329,7 +438,7 @@ function extractSubheadingText(line: string): string | null {
|
||||
|
||||
/** Extract a snippet: first ~160 chars of body content, stripped of markdown.
|
||||
* Mirrors the Rust extract_snippet() logic for frontend use. */
|
||||
export function extractSnippet(content: string): string {
|
||||
export function extractSnippet(content: MarkdownSource): MarkdownSource {
|
||||
const [, body] = splitFrontmatter(content)
|
||||
const withoutH1 = removeH1Line(body)
|
||||
const clean = withoutH1.split('\n').filter(isSnippetLine).map(stripListMarker).join(' ')
|
||||
@@ -341,7 +450,7 @@ export function extractSnippet(content: string): string {
|
||||
// Fallback: collect sub-heading text when no paragraph content exists
|
||||
const headingText = withoutH1.split('\n')
|
||||
.map(extractSubheadingText)
|
||||
.filter((t): t is string => t !== null)
|
||||
.filter((t): t is MarkdownSource => t !== null)
|
||||
.join(' ')
|
||||
const headingStripped = stripMarkdownChars(headingText).trim()
|
||||
if (!headingStripped) return ''
|
||||
@@ -349,7 +458,7 @@ export function extractSnippet(content: string): string {
|
||||
return headingStripped.slice(0, 160) + '...'
|
||||
}
|
||||
|
||||
export function countWords(content: string): number {
|
||||
export function countWords(content: MarkdownSource): WordCount {
|
||||
const [, body] = splitFrontmatter(content)
|
||||
const withoutTitle = body.replace(/^\s*# [^\n]+\n?/, '')
|
||||
const withoutWikilinks = withoutTitle.replace(/\[\[[^\]]*\]\]/g, '')
|
||||
|
||||
@@ -7,6 +7,10 @@ import { seedBlockNoteTable, triggerMenuCommand } from './testBridge'
|
||||
let tempVaultDir: string
|
||||
const TABLE_RELOAD_NOTE_PATH = '/project/table-reload-regression.md'
|
||||
const TABLE_RELOAD_NOTE_TITLE = 'Table Reload Regression'
|
||||
const TABLE_WIKILINK_NOTE_PATH = '/table-wikilink-regression.md'
|
||||
const TABLE_WIKILINK_NOTE_TITLE = 'Table Wikilink Regression'
|
||||
const TABLE_WIKILINK_TARGET = 'application-design-and-build'
|
||||
const TABLE_WIKILINK_TARGET_TITLE = 'Application Design and Build'
|
||||
|
||||
function writeTableReloadNote(vaultDir: string): void {
|
||||
fs.writeFileSync(
|
||||
@@ -26,6 +30,37 @@ status: draft
|
||||
)
|
||||
}
|
||||
|
||||
function writeTableWikilinkNotes(vaultDir: string): void {
|
||||
fs.writeFileSync(
|
||||
path.join(vaultDir, `${TABLE_WIKILINK_TARGET}.md`),
|
||||
`---
|
||||
title: ${TABLE_WIKILINK_TARGET_TITLE}
|
||||
type: Note
|
||||
---
|
||||
# ${TABLE_WIKILINK_TARGET_TITLE}
|
||||
|
||||
Target note for table wikilink navigation.
|
||||
`,
|
||||
)
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(vaultDir, TABLE_WIKILINK_NOTE_PATH.slice(1)),
|
||||
`---
|
||||
title: ${TABLE_WIKILINK_NOTE_TITLE}
|
||||
type: Note
|
||||
---
|
||||
# ${TABLE_WIKILINK_NOTE_TITLE}
|
||||
|
||||
| Domain | Weight |
|
||||
| --- | --- |
|
||||
| [[${TABLE_WIKILINK_TARGET}]] | 99% |
|
||||
|
||||
## Domain Links
|
||||
- [[${TABLE_WIKILINK_TARGET}]]
|
||||
`,
|
||||
)
|
||||
}
|
||||
|
||||
function trackUnexpectedErrors(page: Page): string[] {
|
||||
const errors: string[] = []
|
||||
|
||||
@@ -292,4 +327,29 @@ test.describe('table hover crash regression', () => {
|
||||
await expect(page.locator('table')).toHaveCount(1)
|
||||
expect(errors).toEqual([])
|
||||
})
|
||||
|
||||
test('@smoke wikilinks inside table cells render and navigate', async ({ page }) => {
|
||||
const errors = trackUnexpectedErrors(page)
|
||||
|
||||
writeTableWikilinkNotes(tempVaultDir)
|
||||
await openFixtureVaultTauri(page, tempVaultDir)
|
||||
const tableNote = page
|
||||
.getByTestId('note-list-container')
|
||||
.locator('[data-note-path]')
|
||||
.filter({ hasText: TABLE_WIKILINK_NOTE_TITLE })
|
||||
.first()
|
||||
await expect(tableNote).toBeVisible({ timeout: 5_000 })
|
||||
await tableNote.click()
|
||||
|
||||
const tableWikilink = page
|
||||
.locator('table .wikilink')
|
||||
.filter({ hasText: TABLE_WIKILINK_TARGET_TITLE })
|
||||
.first()
|
||||
await expect(tableWikilink).toBeVisible({ timeout: 5_000 })
|
||||
await expect(tableWikilink).toHaveAttribute('data-target', TABLE_WIKILINK_TARGET)
|
||||
|
||||
await tableWikilink.click({ modifiers: ['Meta'] })
|
||||
await expect(page.locator('.bn-editor h1').first()).toHaveText(TABLE_WIKILINK_TARGET_TITLE, { timeout: 5_000 })
|
||||
expect(errors).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user