Files
bare-operating-system/packages/bare-os-coreutils/lib/agent/agent-markdown.js
T
2026-08-18 18:11:28 -04:00

331 lines
7.6 KiB
JavaScript

/**
* Markdown → ANSI for /bin/agent replies (TTY-friendly, no deps).
*/
/**
* @param {boolean} useColor
* @param {'bold'|'dim'|'italic'|'code'|'heading'|'quote'|'hr'|'bullet'|'link'} kind
*/
function bareAgentMdSgr(useColor, kind) {
if (!useColor) return ''
switch (kind) {
case 'bold':
return '\x1b[1m'
case 'dim':
return '\x1b[2m'
case 'italic':
return '\x1b[3m'
case 'code':
return '\x1b[36m'
case 'heading':
return '\x1b[1;36m'
case 'quote':
return '\x1b[2;37m'
case 'hr':
return '\x1b[2m'
case 'bullet':
return '\x1b[33m'
case 'link':
return '\x1b[4;36m'
default:
return ''
}
}
/**
* Inline markdown on a single line (no nested fences).
* @param {string} line
* @param {boolean} useColor
*/
function bareAgentMdInline(line, useColor) {
const reset = useColor ? '\x1b[0m' : ''
let s = String(line || '')
// escape-ish: leave raw backslashes mostly alone
/** @type {string[]} */
const out = []
let i = 0
while (i < s.length) {
// inline code
if (s[i] === '`') {
const end = s.indexOf('`', i + 1)
if (end > i) {
out.push(
bareAgentMdSgr(useColor, 'code') + s.slice(i + 1, end) + reset
)
i = end + 1
continue
}
}
// bold ** or __
if (
(s[i] === '*' && s[i + 1] === '*') ||
(s[i] === '_' && s[i + 1] === '_')
) {
const mark = s[i]
const end = s.indexOf(mark + mark, i + 2)
if (end > i) {
out.push(
bareAgentMdSgr(useColor, 'bold') +
bareAgentMdInline(s.slice(i + 2, end), useColor) +
reset
)
i = end + 2
continue
}
}
// italic * or _
if (s[i] === '*' || s[i] === '_') {
const mark = s[i]
// avoid matching list markers at start handled elsewhere
const end = s.indexOf(mark, i + 1)
if (end > i + 1) {
out.push(
bareAgentMdSgr(useColor, 'italic') +
s.slice(i + 1, end) +
reset
)
i = end + 1
continue
}
}
// strike ~~
if (s[i] === '~' && s[i + 1] === '~') {
const end = s.indexOf('~~', i + 2)
if (end > i) {
out.push('\x1b[9m' + s.slice(i + 2, end) + reset)
i = end + 2
continue
}
}
// links [text](url)
if (s[i] === '[') {
const mid = s.indexOf('](', i + 1)
const end = mid >= 0 ? s.indexOf(')', mid + 2) : -1
if (mid > i && end > mid) {
const label = s.slice(i + 1, mid)
const url = s.slice(mid + 2, end)
out.push(
bareAgentMdSgr(useColor, 'link') +
label +
reset +
bareAgentMdSgr(useColor, 'dim') +
' (' +
url +
')' +
reset
)
i = end + 1
continue
}
}
out.push(s[i])
i++
}
return out.join('')
}
/**
* Visible width ignoring ANSI CSI sequences.
* @param {string} s
*/
function bareAgentMdVisibleWidth(s) {
return String(s || '')
.replace(/\x1b\[[0-9;]*m/g, '')
.length
}
/**
* Wrap a string to width by visible columns (ANSI-aware, crude).
* @param {string} s
* @param {number} width
* @returns {string[]}
*/
function bareAgentMdWrapAnsi(s, width) {
const w = Math.max(8, width | 0)
const raw = String(s || '')
if (!raw) return ['']
/** @type {string[]} */
const lines = []
let cur = ''
let vis = 0
let i = 0
while (i < raw.length) {
if (raw[i] === '\x1b' && raw[i + 1] === '[') {
let j = i + 2
while (j < raw.length && raw[j] !== 'm') j++
cur += raw.slice(i, Math.min(j + 1, raw.length))
i = Math.min(j + 1, raw.length)
continue
}
if (raw[i] === '\n') {
lines.push(cur)
cur = ''
vis = 0
i++
continue
}
if (vis >= w) {
lines.push(cur)
cur = ''
vis = 0
}
cur += raw[i]
vis++
i++
}
if (cur || !lines.length) lines.push(cur)
return lines
}
/**
* Render markdown source to ANSI lines (no trailing newline join).
* @param {string} md
* @param {{ useColor?: boolean, width?: number }} [opts]
* @returns {string}
*/
function bareAgentRenderMarkdown(md, opts) {
const useColor = !opts || opts.useColor !== false
const width = Math.max(
40,
Math.min(500, (opts && opts.width) || 80)
)
const reset = useColor ? '\x1b[0m' : ''
const src = String(md || '').replace(/\r\n/g, '\n').replace(/\r/g, '\n')
const rows = src.split('\n')
/** @type {string[]} */
const out = []
let i = 0
let inFence = false
let fenceLang = ''
while (i < rows.length) {
const line = rows[i]
// fenced code
const fenceOpen = /^```\s*(\S*)\s*$/.exec(line)
if (fenceOpen && !inFence) {
inFence = true
fenceLang = fenceOpen[1] || ''
const label = fenceLang
? bareAgentMdSgr(useColor, 'dim') + '┌─ ' + fenceLang + ' ' + reset
: bareAgentMdSgr(useColor, 'dim') + '┌──' + reset
out.push(label)
i++
continue
}
if (inFence) {
if (/^```\s*$/.test(line)) {
inFence = false
out.push(bareAgentMdSgr(useColor, 'dim') + '└──' + reset)
i++
continue
}
out.push(
bareAgentMdSgr(useColor, 'dim') +
'│ ' +
reset +
bareAgentMdSgr(useColor, 'code') +
line +
reset
)
i++
continue
}
// hr
if (/^\s*(?:---+|\*\*\*+|___+)\s*$/.test(line)) {
out.push(
bareAgentMdSgr(useColor, 'hr') + '─'.repeat(Math.min(width, 40)) + reset
)
i++
continue
}
// headings
const hm = /^(#{1,6})\s+(.*)$/.exec(line)
if (hm) {
const level = hm[1].length
const text = hm[2]
const prefix = level <= 2 ? '' : ' '.repeat(level - 2)
out.push(
prefix +
bareAgentMdSgr(useColor, 'heading') +
bareAgentMdInline(text, useColor) +
reset
)
if (level === 1) {
out.push(
bareAgentMdSgr(useColor, 'dim') +
'━'.repeat(Math.min(width, Math.max(8, text.length))) +
reset
)
}
i++
continue
}
// blockquote
const qm = /^>\s?(.*)$/.exec(line)
if (qm) {
out.push(
bareAgentMdSgr(useColor, 'quote') +
'┃ ' +
reset +
bareAgentMdInline(qm[1], useColor)
)
i++
continue
}
// unordered list
const ul = /^(\s*)([-*+])\s+(.*)$/.exec(line)
if (ul) {
const indent = Math.min(6, Math.floor(ul[1].length / 2))
const pad = ' '.repeat(indent)
out.push(
pad +
bareAgentMdSgr(useColor, 'bullet') +
'• ' +
reset +
bareAgentMdInline(ul[3], useColor)
)
i++
continue
}
// ordered list
const ol = /^(\s*)(\d+)\.\s+(.*)$/.exec(line)
if (ol) {
const indent = Math.min(6, Math.floor(ol[1].length / 2))
const pad = ' '.repeat(indent)
out.push(
pad +
bareAgentMdSgr(useColor, 'bullet') +
ol[2] +
'. ' +
reset +
bareAgentMdInline(ol[3], useColor)
)
i++
continue
}
// empty
if (!line.trim()) {
out.push('')
i++
continue
}
// paragraph — wrap
const rendered = bareAgentMdInline(line, useColor)
for (const wline of bareAgentMdWrapAnsi(rendered, width)) out.push(wline)
i++
}
// Ensure fence close if stream was truncated
if (inFence) out.push(bareAgentMdSgr(useColor, 'dim') + '└──' + reset)
return out.join('\n') + (out.length ? '\n' : '')
}