Newer
Older
navi-1 / webclient / src / composables / useMarkdown.js
@Eugene Sukhodolskiy Eugene Sukhodolskiy on 17 Apr 2 KB Webclient UI improvements + backend fixes
import { marked } from 'marked'
import hljs from 'highlight.js'

// Configure marked once
marked.setOptions({ breaks: true, gfm: true })

// Custom renderer to add code block header + copy button
const renderer = new marked.Renderer()
renderer.code = function({ text, lang }) {
  const language = lang && hljs.getLanguage(lang) ? lang : 'plaintext'
  const highlighted = hljs.highlight(text, { language }).value

  return `<div class="code-block"><div class="code-header"><span>${language}</span><button class="btn-icon copy-btn" data-code="${encodeURIComponent(text)}" title="Copy"><i class="ph ph-copy"></i></button></div><pre><code class="hljs language-${language}">${highlighted}</code></pre></div>`
}

marked.use({ renderer })

// Fix malformed GFM tables where the model merges the separator row with data.
// Pattern: line after a pipe-row whose cells aren't all valid separators (---/:---/---:).
function fixTables(text) {
  const lines = text.split('\n')
  const out = []
  const isPipeRow = l => l.trim().startsWith('|') && l.includes('|', 1)
  const isSepCell = c => /^:?-+:?$/.test(c.trim())
  const colCount = l => l.split('|').slice(1, -1).length

  for (let i = 0; i < lines.length; i++) {
    const line = lines[i]
    const prev = out[out.length - 1] ?? ''

    if (isPipeRow(line) && isPipeRow(prev)) {
      const cells = line.split('|').slice(1, -1)
      const allSep = cells.every(isSepCell)
      const noneSep = cells.every(c => !isSepCell(c))

      if (!allSep && !noneSep) {
        // Mixed: first cell looks like a mangled separator — inject a proper separator and re-emit line as data
        const n = colCount(prev)
        out.push('| ' + Array(n).fill('---').join(' | ') + ' |')
        out.push(line)
        continue
      }

      if (noneSep && i + 1 < lines.length && isPipeRow(lines[i + 1])) {
        // Separator row is completely missing — inject it before this data row
        const n = colCount(prev)
        out.push('| ' + Array(n).fill('---').join(' | ') + ' |')
      }
    }
    out.push(line)
  }
  return out.join('\n')
}

export function renderMarkdown(text) {
  const html = marked.parse(fixTables(text ?? ''))
  // Wrap tables in a scrollable container for mobile
  return html.replace(/<table>/g, '<div class="table-wrap"><table class="table">').replace(/<\/table>/g, '</table></div>')
}

// Attach copy button listeners after v-html renders
export function attachCopyButtons(el) {
  el?.querySelectorAll('.copy-btn').forEach(btn => {
    btn.onclick = () => {
      const code = decodeURIComponent(btn.dataset.code ?? '')
      navigator.clipboard.writeText(code).then(() => {
        btn.innerHTML = '<i class="ph ph-check"></i>'
        setTimeout(() => { btn.innerHTML = '<i class="ph ph-copy"></i>' }, 1500)
      })
    }
  })
}