231 lines
8.5 KiB
JavaScript
231 lines
8.5 KiB
JavaScript
// Lexer minimal pe limbaje: comentariile devin spatii (liniile si coloanele raman pe loc),
|
|
// sirurile raman in text, dar li se tin minte intervalele, ca un detector sa poata cere
|
|
// "numele API-ului e COD, nu text dintr-un sir".
|
|
//
|
|
// Nu e un parser: e cat trebuie ca un tipar sa nu se potriveasca in comentarii, in
|
|
// docstring-uri Python sau in textul unui sir.
|
|
|
|
const KW_REGEX = new Set(['return', 'typeof', 'instanceof', 'in', 'of', 'new', 'delete', 'void',
|
|
'throw', 'case', 'do', 'else', 'yield', 'await']);
|
|
const PUNCT_REGEX = '(,=:[!&|?{};+-*%<>~^';
|
|
|
|
export function curata(text, limbaj) {
|
|
switch (limbaj) {
|
|
case 'javascript': return curataC(text, { template: true, regex: true });
|
|
case 'java': return curataC(text, { textBlock: true });
|
|
case 'go': return curataC(text, { raw: true });
|
|
case 'python': return curataPython(text);
|
|
default: return { code: text, siruri: [] };
|
|
}
|
|
}
|
|
|
|
function golitor(out) {
|
|
return (a, b) => {
|
|
for (let k = a; k < b; k++) if (out[k] !== '\n' && out[k] !== '\r') out[k] = ' ';
|
|
};
|
|
}
|
|
|
|
function curataC(text, o) {
|
|
const n = text.length;
|
|
const out = text.split('');
|
|
const goleste = golitor(out);
|
|
const siruri = [];
|
|
const stiva = [];
|
|
let acolade = 0;
|
|
let prevSig = '';
|
|
let prevWord = '';
|
|
let i = 0;
|
|
let esecRegex = null; // bit 1: din (pozitie, in afara clasei) nu se ajunge la sfarsitul literalului; bit 2: la fel, in clasa
|
|
|
|
// scaneaza un segment de template literal pornind de la j; intoarce pozitia si daca s-a deschis ${
|
|
const scanTpl = (inceput, j) => {
|
|
while (j < n) {
|
|
const ch = text[j];
|
|
if (ch === '\\') { j += 2; continue; }
|
|
if (ch === '`') { siruri.push([inceput, j + 1]); return { pos: j + 1, deschis: false }; }
|
|
if (ch === '$' && text[j + 1] === '{') { siruri.push([inceput, j + 2]); return { pos: j + 2, deschis: true }; }
|
|
j++;
|
|
}
|
|
siruri.push([inceput, n]);
|
|
return { pos: n, deschis: false };
|
|
};
|
|
|
|
while (i < n) {
|
|
const c = text[i];
|
|
const d = text[i + 1];
|
|
if (c === '/' && d === '/') {
|
|
let j = i;
|
|
while (j < n && text[j] !== '\n') j++;
|
|
goleste(i, j); i = j; continue;
|
|
}
|
|
if (c === '/' && d === '*') {
|
|
let j = text.indexOf('*/', i + 2);
|
|
j = j < 0 ? n : j + 2;
|
|
goleste(i, j); i = j; continue;
|
|
}
|
|
if (o.textBlock && c === '"' && text.startsWith('"""', i)) {
|
|
let j = text.indexOf('"""', i + 3);
|
|
j = j < 0 ? n : j + 3;
|
|
siruri.push([i, j]); i = j; prevSig = '"'; continue;
|
|
}
|
|
if (c === '"' || c === "'") {
|
|
let j = i + 1;
|
|
while (j < n && text[j] !== c && text[j] !== '\n') { if (text[j] === '\\') j++; j++; }
|
|
j = Math.min(n, j + 1);
|
|
siruri.push([i, j]); i = j; prevSig = c; continue;
|
|
}
|
|
if (c === '`' && o.raw) {
|
|
let j = text.indexOf('`', i + 1);
|
|
j = j < 0 ? n : j + 1;
|
|
siruri.push([i, j]); i = j; prevSig = '`'; continue;
|
|
}
|
|
if (c === '`' && o.template) {
|
|
const r = scanTpl(i, i + 1);
|
|
i = r.pos;
|
|
if (r.deschis) { stiva.push(acolade); acolade++; }
|
|
prevSig = '`'; continue;
|
|
}
|
|
if (c === '/' && o.regex) {
|
|
const permis = prevSig === '' || PUNCT_REGEX.includes(prevSig) || (prevSig === 'a' && KW_REGEX.has(prevWord));
|
|
if (permis) {
|
|
// Cautarea sfarsitului unui literal regex e determinista din (pozitie, "in clasa"), deci o stare din care s-a ajuns o data
|
|
// la capatul randului fara sfarsit duce acolo si a doua oara. Starile acestea se tin minte (2026-09-29, revizuirea
|
|
// adversariala): fara ele un rand lung cu multe `/` nedeschise (de ex. `=/[` repetat) costa patratic, minute pe 1 MB.
|
|
let j = i + 1;
|
|
let inClasa = false;
|
|
let bun = false;
|
|
const vizitate = [];
|
|
while (j < n && text[j] !== '\n') {
|
|
if (esecRegex && esecRegex[j] & (inClasa ? 2 : 1)) break;
|
|
vizitate.push(inClasa ? -j - 1 : j);
|
|
const ch = text[j];
|
|
if (ch === '\\') { j += 2; continue; }
|
|
if (ch === '[') inClasa = true;
|
|
else if (ch === ']') inClasa = false;
|
|
else if (ch === '/' && !inClasa) { bun = true; break; }
|
|
j++;
|
|
}
|
|
if (!bun) {
|
|
if (!esecRegex) esecRegex = new Uint8Array(n);
|
|
for (const v of vizitate) { if (v < 0) esecRegex[-v - 1] |= 2; else esecRegex[v] |= 1; }
|
|
}
|
|
if (bun) {
|
|
j++;
|
|
while (j < n && /[a-z]/i.test(text[j])) j++;
|
|
siruri.push([i, j]); i = j; prevSig = '/'; continue;
|
|
}
|
|
}
|
|
prevSig = '/'; i++; continue;
|
|
}
|
|
if (c === '{') { acolade++; prevSig = '{'; i++; continue; }
|
|
if (c === '}') {
|
|
acolade--;
|
|
if (stiva.length && acolade === stiva[stiva.length - 1]) {
|
|
stiva.pop();
|
|
const r = scanTpl(i, i + 1);
|
|
i = r.pos;
|
|
if (r.deschis) { stiva.push(acolade); acolade++; }
|
|
prevSig = '`'; continue;
|
|
}
|
|
prevSig = '}'; i++; continue;
|
|
}
|
|
if (/[A-Za-z_$]/.test(c)) {
|
|
let j = i + 1;
|
|
while (j < n && /[\w$]/.test(text[j])) j++;
|
|
prevWord = text.slice(i, j); prevSig = 'a'; i = j; continue;
|
|
}
|
|
if (/[0-9]/.test(c)) {
|
|
let j = i + 1;
|
|
while (j < n && /[\w.]/.test(text[j])) j++;
|
|
prevWord = ''; prevSig = '0'; i = j; continue;
|
|
}
|
|
if (!/\s/.test(c)) prevSig = c;
|
|
i++;
|
|
}
|
|
return { code: out.join(''), siruri };
|
|
}
|
|
|
|
function curataPython(text) {
|
|
const n = text.length;
|
|
const out = text.split('');
|
|
const goleste = golitor(out);
|
|
const siruri = [];
|
|
let i = 0;
|
|
// primul caracter care nu e spatiu pe randul curent (-1 = niciunul inca). Tinut din mers (2026-09-29, revizuirea adversariala):
|
|
// forma veche cauta inapoi inceputul randului la FIECARE sir triplu, deci un rand lung cu multe siruri triple costa patratic.
|
|
let primulPeRand = -1;
|
|
while (i < n) {
|
|
const c = text[i];
|
|
if (c === '\n') { primulPeRand = -1; i++; continue; }
|
|
if (c === '#') {
|
|
let j = i;
|
|
while (j < n && text[j] !== '\n') j++;
|
|
goleste(i, j); i = j; continue;
|
|
}
|
|
if (c === '"' || c === "'") {
|
|
// prefixul (r, b, f, u, rb, ...) face parte din sir
|
|
let start = i;
|
|
while (start > 0 && /[rRbBuUfF]/.test(text[start - 1]) && i - start < 2) start--;
|
|
if (start < i && start > 0 && /[\w]/.test(text[start - 1])) start = i;
|
|
const triplu = text.startsWith(c.repeat(3), i);
|
|
let j;
|
|
// inainte de sir, pe randul lui, sunt numai spatii (prefixul r/b/f/u face parte din sir)?
|
|
const singurPeRand = primulPeRand < 0 || primulPeRand >= start;
|
|
let ultimaLinieNoua = -1;
|
|
if (triplu) {
|
|
j = i + 3;
|
|
while (j < n && !text.startsWith(c.repeat(3), j)) {
|
|
if (text[j] === '\\') { if (text[j + 1] === '\n') ultimaLinieNoua = j + 1; j++; } else if (text[j] === '\n') ultimaLinieNoua = j;
|
|
j++;
|
|
}
|
|
j = Math.min(n, j + 3);
|
|
} else {
|
|
j = i + 1;
|
|
while (j < n && text[j] !== c && text[j] !== '\n') { if (text[j] === '\\') { if (text[j + 1] === '\n') ultimaLinieNoua = j + 1; j++; } j++; }
|
|
if (j < n && text[j] === '\n') ultimaLinieNoua = j; // sir neterminat: intervalul lui se opreste DUPA acest rand nou
|
|
j = Math.min(n, j + 1);
|
|
}
|
|
// dupa sir: daca sirul a trecut peste un rand nou, randul curent incepe in el (coada lui nu e spatiu), afara de cazul in
|
|
// care sirul se termina chiar cu randul nou; altfel randul curent are acum cel putin sirul
|
|
if (ultimaLinieNoua >= 0) primulPeRand = ultimaLinieNoua === j - 1 ? -1 : ultimaLinieNoua + 1;
|
|
else if (primulPeRand < 0) primulPeRand = start;
|
|
// docstring: sirul triplu e singur pe randul lui, ca instructiune; se trateaza ca un comentariu
|
|
if (triplu && singurPeRand) { goleste(start, j); i = j; continue; }
|
|
siruri.push([start, j]); i = j; continue;
|
|
}
|
|
if (primulPeRand < 0 && c !== ' ' && c !== '\t') primulPeRand = i;
|
|
i++;
|
|
}
|
|
return { code: out.join(''), siruri };
|
|
}
|
|
|
|
// cautare binara: pozitia e inauntrul unui sir?
|
|
export function inSir(siruri, pos) {
|
|
let lo = 0;
|
|
let hi = siruri.length - 1;
|
|
while (lo <= hi) {
|
|
const mid = (lo + hi) >> 1;
|
|
const [s, e] = siruri[mid];
|
|
if (pos < s) hi = mid - 1;
|
|
else if (pos >= e) lo = mid + 1;
|
|
else return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
export function inceputuriDeRand(text) {
|
|
const r = [0];
|
|
for (let k = 0; k < text.length; k++) if (text.charCodeAt(k) === 10) r.push(k + 1);
|
|
return r;
|
|
}
|
|
|
|
export function randul(inceputuri, pos) {
|
|
let lo = 0;
|
|
let hi = inceputuri.length - 1;
|
|
while (lo < hi) {
|
|
const mid = (lo + hi + 1) >> 1;
|
|
if (inceputuri[mid] <= pos) lo = mid; else hi = mid - 1;
|
|
}
|
|
return { line: lo + 1, column: pos - inceputuri[lo] + 1 };
|
|
}
|