Orginize
This commit is contained in:
@@ -0,0 +1,330 @@
|
||||
/**
|
||||
* 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' : '')
|
||||
}
|
||||
Reference in New Issue
Block a user