({ ed, defaultCode, onRun, highlight })
| 10 | |
| 11 | // `highlight(text) -> html` is injected (Shiki in the docs); CodeJar calls it on every render. |
| 12 | export function createEditor({ ed, defaultCode, onRun, highlight }) { |
| 13 | |
| 14 | // constants |
| 15 | |
| 16 | const PAIRS = { '(': ')', '[': ']', '{': '}', '"': '"', "'": "'" }; |
| 17 | const OPENERS = new Set(Object.keys(PAIRS)); |
| 18 | const CLOSERS = new Set(Object.values(PAIRS)); |
| 19 | const STRING_START = /^([fFrRbBuU]{0,2})("""|'''|"|')/; |
| 20 | const WS = /[ \t]/; |
| 21 | // `decreaseIndentPattern`, dedent on `:`. |
| 22 | const DEDENT_RE = /^\s*(?:elif|else|except|finally|case)\b[^:]*:$/; |
| 23 | |
| 24 | // pure helpers |
| 25 | |
| 26 | // Walk from 0 to caret toggling code/string. `quote === ''` means code. |
| 27 | const stringCtx = (src, caret) => { |
| 28 | let i = 0, quote = '', isF = false; |
| 29 | while (i < caret) { |
| 30 | if (!quote) { |
| 31 | if (src[i] === '#') { |
| 32 | const nl = src.indexOf('\n', i); |
| 33 | if (nl === -1 || nl >= caret) return { inStr: false, isF: false }; |
| 34 | i = nl + 1; continue; |
| 35 | } |
| 36 | const m = src.slice(i).match(STRING_START); |
| 37 | if (m && i + m[0].length <= caret) { |
| 38 | quote = m[2]; isF = /[fF]/.test(m[1]); |
| 39 | i += m[0].length; continue; |
| 40 | } |
| 41 | i++; |
| 42 | } else { |
| 43 | if (quote.length === 1 && src[i] === '\\') { i += 2; continue; } |
| 44 | if (src.slice(i, i + quote.length) === quote) { |
| 45 | i += quote.length; quote = ''; isF = false; continue; |
| 46 | } |
| 47 | i++; |
| 48 | } |
| 49 | } |
| 50 | return { inStr: !!quote, isF }; |
| 51 | }; |
| 52 | |
| 53 | const lineAt = (text, caret) => { |
| 54 | const start = text.lastIndexOf('\n', caret - 1) + 1; |
| 55 | const nl = text.indexOf('\n', caret); |
| 56 | const end = nl === -1 ? text.length : nl; |
| 57 | return { start, end, body: text.slice(start, end), col: caret - start }; |
| 58 | }; |
| 59 | const firstNonWS = (s) => { for (let i = 0; i < s.length; i++) if (!WS.test(s[i])) return i; return s.length; }; |
| 60 | |
| 61 | // `prevIndentTabStop`, distance to the previous tab stop from `col`. |
| 62 | const prevTabDist = (col) => { const r = col % TAB_SIZE; return r === 0 ? TAB_SIZE : r; }; |
| 63 | |
| 64 | // Full lines covered by `sel`; excludes the trailing line if `sel.end` sits exactly past a `\n`. |
| 65 | const lineRange = (text, sel) => { |
| 66 | const start = text.lastIndexOf('\n', sel.start - 1) + 1; |
| 67 | const anchor = (sel.end > sel.start && text[sel.end - 1] === '\n') ? sel.end - 1 : sel.end; |
| 68 | let end = text.indexOf('\n', anchor); |
| 69 | if (end === -1) end = text.length; |
no test coverage detected