# All files / src/utils wikilinks.ts

100% Statements 61/61
97.22% Branches 35/36
100% Functions 11/11
100% Lines 55/55

Press n or j to go to the next uncovered block, b, p or k for the previous block.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    6x
    6x
    6x

    27x

    39x
    17x
    17x
    15x

    17x
    3x

    17x

    30x

    6x

    9x
    9x
    8x
    2x
    2x

    6x
    6x
    6x

    6x
    7x
    5x

    7x

    7x

    6x
    3x

    9x

    6x
    6x
    12x
    5x

    7x

    6x

    154x
    92x
    92x
    88x
    88x
    88x

    8x
    8x

    8x
    10x
    10x
    10x
    10x

    8x

    127x
    127x
    127x
    127x
    127x
    57x

     // Wikilink placeholder tokens for markdown round-trip
    const WL_START = '\u2039WIKILINK:'
    const WL_END = '\u203A'
    const WL_RE = new RegExp(`${WL_START.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}([^${WL_END}]+)${WL_END.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`, 'g')

    /** Pre-process markdown: replace [[target]] with placeholder tokens */
    export function preProcessWikilinks(md: string): string {
      return md.replace(/\[\[([^\]]+)\]\]/g, (_m, target) => `${WL_START}${target}${WL_END}`)
    }

    // Minimal shape of a BlockNote block for wikilink processing
    interface BlockLike {
      content?: InlineItem[]
      children?: BlockLike[]
      [key: string]: unknown
    }

    interface InlineItem {
      type: string
      text?: string
      props?: Record<string, string>
      content?: unknown
      [key: string]: unknown
    }

    type ContentTransform = (content: InlineItem[]) => InlineItem[]

    /** Walk blocks recursively, applying a transform to each block's inline content */
    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)
        }
        if (b.children && Array.isArray(b.children)) {
          b.children = walkBlocks(b.children, transform, clone) as BlockLike[]
        }
        return b
      })
    }

    /** Walk blocks and replace placeholder text with wikilink inline content */
    export function injectWikilinks(blocks: unknown[]): unknown[] {
      return walkBlocks(blocks, expandWikilinksInContent)
    }

    /**
     * Deep-clone blocks and convert wikilink inline content back to [[target]] text.
     * This is the reverse of injectWikilinks — used before blocksToMarkdownLossy
     * so that wikilinks survive the markdown round-trip.
     */
    export function restoreWikilinksInBlocks(blocks: unknown[]): unknown[] {
      return walkBlocks(blocks, collapseWikilinksInContent, true)
    }

    function expandWikilinksInContent(content: InlineItem[]): InlineItem[] {
      const result: InlineItem[] = []
      for (const item of content) {
        if (item.type !== 'text' || typeof item.text !== 'string' || !item.text.includes(WL_START)) {
          result.push(item)
          continue
        }
        const text = item.text as string
        let lastIndex = 0
        WL_RE.lastIndex = 0
        let match
        while ((match = WL_RE.exec(text)) !== null) {
          if (match.index > lastIndex) {
            result.push({ ...item, text: text.slice(lastIndex, match.index) })
          }
          result.push({
            type: 'wikilink',
            props: { target: match[1] },
            content: undefined,
          })
          lastIndex = match.index + match[0].length
        }
        if (lastIndex < text.length) {
          result.push({ ...item, text: text.slice(lastIndex) })
        }
      }
      return result
    }

    function collapseWikilinksInContent(content: InlineItem[]): InlineItem[] {
      const result: InlineItem[] = []
      for (const item of content) {
        if (item.type === 'wikilink' && item.props?.target) {
          result.push({ type: 'text', text: `[[${item.props.target}]]` })
        } else {
          result.push(item)
        }
      }
      return result
    }

    /** Strip YAML frontmatter from markdown, returning [frontmatter, body] */
    export function splitFrontmatter(content: string): [string, string] {
      if (!content.startsWith('---')) return ['', content]
      const end = content.indexOf('\n---', 3)
      if (end === -1) return ['', content]
      let to = end + 4
      if (content[to] === '\n') to++
      return [content.slice(0, to), content.slice(to)]
    }

    /** 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[] = []
      const re = /\[\[([^\]]+)\]\]/g
      let match
      while ((match = re.exec(content)) !== null) {
        const inner = match[1]
        const pipeIdx = inner.indexOf('|')
        const target = pipeIdx !== -1 ? inner.slice(0, pipeIdx) : inner
        Eif (target) links.push(target)
      }
      return [...new Set(links)].sort()
    }

    export function countWords(content: string): number {
      const [, body] = splitFrontmatter(content)
      const withoutTitle = body.replace(/^\s*# [^\n]+\n?/, '')
      const withoutWikilinks = withoutTitle.replace(/\[\[[^\]]*\]\]/g, '')
      const text = withoutWikilinks.replace(/[#*_[\]`>~\-|]/g, '').trim()
      if (!text) return 0
      return text.split(/\s+/).filter(Boolean).length
    }