287 lines
7.9 KiB
JavaScript
287 lines
7.9 KiB
JavaScript
/** 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 []
|
|
}
|
|
}
|