/** Syntax highlighting for /bin/edit (line-oriented, best-effort). */ const EDIT_KW_JS = /^(?:const|let|var|function|return|async|await|if|else|for|while|do|switch|case|break|continue|default|try|catch|finally|throw|new|typeof|instanceof|in|of|class|extends|super|this|static|import|export|from|as|default|void|delete|yield|enum|interface|type|public|private|protected|readonly)$/ /** * @param {string} path */ function bareEditDetectLang(path) { const p = String(path || '').toLowerCase() const dot = p.lastIndexOf('.') const ext = dot >= 0 ? p.slice(dot) : '' if (ext === '.json') return 'json' if (ext === '.md' || ext === '.markdown') return 'md' if (ext === '.sh' || ext === '.bash' || ext === '.zsh') return 'shell' if ( ext === '.js' || ext === '.mjs' || ext === '.cjs' || ext === '.ts' || ext === '.tsx' || ext === '.jsx' ) return 'js' return 'plain' } /** * Merge overlapping spans (later wins) — not used if we build non-overlapping. * @param {Array<{ start: number, end: number, cls: string }>} spans */ function bareEditMergeSpans(spans) { const s = spans.filter((x) => x.end > x.start).sort((a, b) => a.start - b.start || b.end - a.end) /** @type {typeof spans} */ const out = [] for (const cur of s) { const last = out[out.length - 1] if (!last || cur.start >= last.end) { out.push({ ...cur }) } else if (cur.end > last.end) { if (cur.start > last.start) { out[out.length - 1] = { start: last.start, end: cur.start, cls: last.cls } out.push({ ...cur }) } else { out[out.length - 1] = { ...cur } } } } return out } /** * @param {string} line * @returns {Array<{ start: number, end: number, cls: string }>} */ function bareEditSpansStringsCommentsJs(line) { /** @type {Array<{ start: number, end: number, cls: string }>} */ const spans = [] let i = 0 while (i < line.length) { const c = line[i] const next = line[i + 1] if (c === '/' && next === '/') { spans.push({ start: i, end: line.length, cls: 'comment' }) break } if (c === '/' && next === '*') { let j = i + 2 while (j < line.length - 1) { if (line[j] === '*' && line[j + 1] === '/') { j += 2 break } j++ } if (j > line.length) j = line.length spans.push({ start: i, end: j, cls: 'comment' }) i = j continue } if (c === '"' || c === "'" || c === '`') { const q = c const start = i i++ while (i < line.length) { if (line[i] === '\\') { i += 2 continue } if (line[i] === q) { i++ break } i++ } spans.push({ start, end: i, cls: 'string' }) continue } i++ } return bareEditMergeSpans(spans) } /** * @param {string} segment code only (no strings/comments inside) */ function bareEditSpansKeywordsNumbers(segment, offset) { /** @type {Array<{ start: number, end: number, cls: string }>} */ const out = [] const re = /\b([A-Za-z_$][\w$]*)\b|\b(\d+(?:\.\d+)?(?:[eE][+-]?\d+)?)\b/g let m while ((m = re.exec(segment)) !== null) { if (m[1]) { if (EDIT_KW_JS.test(m[1])) { out.push({ start: offset + m.index, end: offset + m.index + m[1].length, cls: 'keyword' }) } } else if (m[2]) { out.push({ start: offset + m.index, end: offset + m.index + m[2].length, cls: 'number' }) } } return out } /** * @param {string} line * @param {number} gapStart * @param {number} gapEnd * @param {Array<{ start: number, end: number, cls: string }>} base */ function bareEditFillGapKeywords(line, gapStart, gapEnd, base) { if (gapEnd <= gapStart) return const seg = line.slice(gapStart, gapEnd) const extra = bareEditSpansKeywordsNumbers(seg, gapStart) for (const e of extra) base.push(e) } /** * @param {string} line */ function bareEditHighlightJsLine(line) { const sc = bareEditSpansStringsCommentsJs(line) if (sc.length === 0) { const all = /** @type {typeof sc} */ ([]) bareEditFillGapKeywords(line, 0, line.length, all) return all.sort((a, b) => a.start - b.start) } /** @type {typeof sc} */ const out = [...sc] let cursor = 0 for (const sp of sc) { bareEditFillGapKeywords(line, cursor, sp.start, out) cursor = sp.end } bareEditFillGapKeywords(line, cursor, line.length, out) return out.sort((a, b) => a.start - b.start) } /** * @param {string} line */ function bareEditHighlightJsonLine(line) { const t = line.trimStart() if (t.startsWith('//')) { return [{ start: line.indexOf('//'), end: line.length, cls: 'comment' }] } /** @type {Array<{ start: number, end: number, cls: string }>} */ const spans = [] const re = /("(?:\\.|[^"\\])*")\s*:|("(?:\\.|[^"\\])*")|(\btrue|false|null\b)|(-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?)/g let m while ((m = re.exec(line)) !== null) { if (m[1]) { const keyEnd = m.index + m[1].length spans.push({ start: m.index, end: keyEnd, cls: 'keyword' }) } else if (m[2]) { spans.push({ start: m.index, end: m.index + m[2].length, cls: 'string' }) } else if (m[3]) { spans.push({ start: m.index, end: m.index + m[3].length, cls: 'keyword' }) } else if (m[4]) { spans.push({ start: m.index, end: m.index + m[4].length, cls: 'number' }) } } return spans.sort((a, b) => a.start - b.start) } /** * @param {string} line */ function bareEditHighlightShellLine(line) { const idx = line.indexOf('#') if (idx >= 0) { return [{ start: idx, end: line.length, cls: 'comment' }] } /** @type {Array<{ start: number, end: number, cls: string }>} */ const spans = [] const kw = /^\s*(if|then|else|elif|fi|for|while|do|done|case|esac|function|return|export|local|readonly|source|\.)[\s#;]|\b(if|then|else|elif|fi|for|in|do|done|case|esac|function|return|export|local|readonly)\b/g let m while ((m = kw.exec(line)) !== null) { const word = m[1] || m[2] if (!word) continue const start = m.index + m[0].indexOf(word) spans.push({ start, end: start + word.length, cls: 'keyword' }) } const dq = /"(?:\\.|[^"\\])*"/g while ((m = dq.exec(line)) !== null) { spans.push({ start: m.index, end: m.index + m[0].length, cls: 'string' }) } const sq = /'[^']*'/g while ((m = sq.exec(line)) !== null) { spans.push({ start: m.index, end: m.index + m[0].length, cls: 'string' }) } return spans.sort((a, b) => a.start - b.start) } /** * @param {string} line */ function bareEditHighlightMdLine(line) { /** @type {Array<{ start: number, end: number, cls: string }>} */ const spans = [] if (/^\s*#{1,6}\s/.test(line)) { const m = line.match(/^\s*(#{1,6}\s.*)$/) if (m) { const i = line.indexOf(m[1]) spans.push({ start: i, end: line.length, cls: 'keyword' }) } return spans } if (/^\s*(?:[-*+]|\d+\.)\s/.test(line)) { const m = line.match(/^\s*((?:[-*+]|\d+\.)\s.*)$/) if (m) { const i = line.indexOf(m[1]) spans.push({ start: i, end: line.length, cls: 'comment' }) } return spans } const bold = /\*\*[^*]+\*\*|__[^_]+__/g let m while ((m = bold.exec(line)) !== null) { spans.push({ start: m.index, end: m.index + m[0].length, cls: 'string' }) } const code = /`[^`]+`/g while ((m = code.exec(line)) !== null) { spans.push({ start: m.index, end: m.index + m[0].length, cls: 'number' }) } return spans.sort((a, b) => a.start - b.start) } /** * @param {string} line * @param {string} lang */ function bareEditHighlightLine(line, lang) { switch (lang) { case 'js': return bareEditHighlightJsLine(line) case 'json': return bareEditHighlightJsonLine(line) case 'shell': return bareEditHighlightShellLine(line) case 'md': return bareEditHighlightMdLine(line) default: return [] } }