tricu

A lazy functional language based on Tree Calculus
Log | Files | Refs | README | LICENSE

syntax.js (6905B)


      1 const keywords = new Set(['let', 'in', 'where', 'do', 'case', 'of', 'rec']);
      2 export function tokenize(source) {
      3     // Decoration must remain cheap even for large read-only library sources.
      4     if (source.length > 32_768)
      5         return [{ start: 0, end: source.length, kind: '', text: source }];
      6     const tokens = [];
      7     const add = (start, end, kind) => { tokens.push({ start, end, kind, text: source.slice(start, end) }); };
      8     let i = 0;
      9     while (i < source.length) {
     10         const start = i, tail = source.slice(i);
     11         if (tail.startsWith('--')) {
     12             const end = source.indexOf('\n', i);
     13             i = end < 0 ? source.length : end;
     14             add(start, i, 'comment');
     15         }
     16         else if (tail.startsWith('|-')) {
     17             const end = source.indexOf('-|', i + 2);
     18             i = end < 0 ? source.length : end + 2;
     19             add(start, i, 'comment');
     20         }
     21         else if (source[i] === '"') {
     22             i++;
     23             let part = start;
     24             while (i < source.length) {
     25                 if (source[i] === '\\') {
     26                     add(part, i, 'string');
     27                     const escape = i;
     28                     i = Math.min(i + 1 + ((source.codePointAt(i + 1) ?? 0) > 0xffff ? 2 : 1), source.length);
     29                     add(escape, i, /^[\\][ntrfb\\"']$/.test(source.slice(escape, i)) ? 'escape' : 'invalid');
     30                     part = i;
     31                 }
     32                 else if (source[i++] === '"')
     33                     break;
     34             }
     35             if (part < i)
     36                 add(part, i, 'string');
     37         }
     38         else {
     39             const match = /^(\s+|![A-Za-z]+|[A-Za-z_][A-Za-z0-9_?'$%.\-]*|[0-9]+|=@|<-|<\||\|>|.)/su.exec(tail);
     40             const text = match[0];
     41             i += text.length;
     42             const kind = /^\s/.test(text) ? '' : text === 't' ? 'tree' : keywords.has(text) ? 'keyword'
     43                 : ['!import', '!export'].includes(text) ? 'directive' : /^[0-9]/.test(text) ? 'number'
     44                     : /^[A-Za-z_]/.test(text) ? 'name' : /^[()[\]{}]$/.test(text) ? 'bracket' : 'operator';
     45             add(start, i, kind);
     46         }
     47     }
     48     // Conservative declaration-site recognition, not semantic highlighting.
     49     // Layout contexts distinguish local equation heads from case-arm captures.
     50     const significant = tokens.filter(t => t.kind && t.kind !== 'comment');
     51     const contexts = [];
     52     let depth = 0, previous, directiveLine = -1, line = 0;
     53     let lineEnd = source.indexOf('\n');
     54     if (lineEnd < 0)
     55         lineEnd = source.length;
     56     let indent = /^ */.exec(source)[0].length;
     57     for (let n = 0; n < significant.length; n++) {
     58         const token = significant[n];
     59         while (lineEnd < token.start) {
     60             line = lineEnd + 1;
     61             const next = source.indexOf('\n', line);
     62             lineEnd = next < 0 ? source.length : next;
     63             indent = /^ */.exec(source.slice(line))[0].length;
     64         }
     65         const first = token.start === line + indent;
     66         if (first)
     67             while (contexts.length && indent <= contexts.at(-1).indent)
     68                 contexts.pop();
     69         if (/^[)\]}]$/.test(token.text))
     70             depth--;
     71         while (contexts.length && depth < contexts.at(-1).depth)
     72             contexts.pop();
     73         if (token.kind === 'directive')
     74             directiveLine = line;
     75         if (directiveLine === line && ['as', 'from'].includes(token.text))
     76             token.kind = 'keyword';
     77         if (token.kind === 'name' && ['@', '=@'].includes(previous?.text ?? ''))
     78             token.kind = 'contract';
     79         const localHead = previous && ['let', 'where', 'rec'].includes(previous.text);
     80         if (token.kind === 'name' && directiveLine !== line && (localHead || first && (indent === 0 || ['let', 'where'].includes(contexts.at(-1)?.kind ?? '')))) {
     81             let nesting = 0;
     82             for (let j = n + 1; j < significant.length; j++) {
     83                 const next = significant[j];
     84                 if (next.start > lineEnd || keywords.has(next.text))
     85                     break;
     86                 if (nesting === 0 && (next.text === '=' || next.text === '=@')) {
     87                     token.kind = 'definition';
     88                     break;
     89                 }
     90                 if (/^[([{]$/.test(next.text))
     91                     nesting++;
     92                 if (/^[)\]}]$/.test(next.text) && --nesting < 0)
     93                     break;
     94                 if (!nesting && [':', '<-'].includes(next.text))
     95                     break;
     96             }
     97         }
     98         if (['let', 'where', 'of'].includes(token.text))
     99             contexts.push({ kind: token.text, indent, depth });
    100         if (token.text === 'in') {
    101             const index = contexts.map(c => c.kind).lastIndexOf('let');
    102             if (index >= 0)
    103                 contexts.splice(index);
    104         }
    105         if (/^[([{]$/.test(token.text))
    106             depth++;
    107         previous = token;
    108     }
    109     return tokens;
    110 }
    111 export function bracketPair(tokens, caret) {
    112     const brackets = tokens.filter(t => t.kind === 'bracket');
    113     const selected = brackets.find(t => t.start === caret) ?? brackets.find(t => t.end === caret);
    114     if (!selected)
    115         return [];
    116     const stack = [], pairs = new Map();
    117     for (const token of brackets) {
    118         if ('([{'.includes(token.text))
    119             stack.push(token);
    120         else {
    121             const open = stack.pop();
    122             if (open && '([{'.indexOf(open.text) === ')]}'.indexOf(token.text)) {
    123                 pairs.set(open.start, token.start);
    124                 pairs.set(token.start, open.start);
    125             }
    126             else
    127                 stack.length = 0; // Never match across malformed nesting.
    128         }
    129     }
    130     const partner = pairs.get(selected.start);
    131     return partner === undefined ? [selected.start] : [selected.start, partner];
    132 }
    133 export function paintSyntax(target, tokens, pair = []) {
    134     const fragment = document.createDocumentFragment();
    135     for (const token of tokens) {
    136         if (!token.kind) {
    137             fragment.append(document.createTextNode(token.text));
    138             continue;
    139         }
    140         const span = document.createElement('span');
    141         span.className = `syntax-${token.kind}`;
    142         span.textContent = token.text;
    143         if (pair.includes(token.start))
    144             span.classList.add(pair.length === 2 ? 'bracket-match' : 'bracket-unmatched');
    145         fragment.append(span);
    146     }
    147     target.replaceChildren(fragment);
    148 }
    149 // Unique top-level declaration heads recognized by lexical layout.
    150 export function definitionSites(source, tokens = tokenize(source)) {
    151     const seen = new Set();
    152     return tokens.filter(token => {
    153         if (token.kind !== 'definition' || token.start > 0 && source[token.start - 1] !== '\n' || seen.has(token.text))
    154             return false;
    155         seen.add(token.text);
    156         return true;
    157     }).map(token => ({ name: token.text, start: token.start }));
    158 }