papagaio 0.1.8 → 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/papagaio.js DELETED
@@ -1,659 +0,0 @@
1
- // ============================================
2
- // papagaio - a easy to use preprocessor
3
- // ============================================
4
-
5
- export class Papagaio {
6
- maxRecursion = 512;
7
-
8
- // Private state
9
- #counterState = { value: 0, unique: 0 };
10
-
11
- // Public configuration
12
- delimiters = [["{", "}"]];
13
- sigil = "$";
14
- keywords = {
15
- pattern: "pattern",
16
- macro: "macro",
17
- eval: "eval",
18
- scope: "scope"
19
- };
20
-
21
- // Public state - processing state
22
- content = "";
23
- #matchContent = "";
24
- #scopeContent = "";
25
- #evalContent = "";
26
-
27
- constructor() {
28
- this.#resetCounterState();
29
- }
30
-
31
- // ============================================
32
- // PUBLIC API
33
- // ============================================
34
-
35
- process(input) {
36
- this.content = input;
37
-
38
- let src = input;
39
- let last = null;
40
- let iter = 0;
41
-
42
- const open = this.#getDefaultOpen(); // delimitador atual
43
- const close = this.#getDefaultClose();
44
-
45
- // regex para detectar blocos papagaio remanescentes
46
- const pending = () => {
47
- const rEval = new RegExp(`\\b${this.keywords.eval}\\s*\\${open}`, "g");
48
- const rScope = new RegExp(`\\b${this.keywords.scope}\\s*\\${open}`, "g");
49
- const rPattern = new RegExp(`\\b${this.keywords.pattern}\\s*\\${open}`, "g");
50
- const rMacro = new RegExp(`\\b${this.keywords.macro}\\s+[A-Za-z_][A-Za-z0-9_]*\\s*\\${open}`, "g");
51
- return rEval.test(src)
52
- || rScope.test(src)
53
- || rPattern.test(src)
54
- || rMacro.test(src);
55
- };
56
-
57
- // fixpoint loop
58
- while (src !== last && iter < this.maxRecursion) {
59
- iter++;
60
- last = src;
61
-
62
- // --- pipeline padrão ---
63
- src = this.#processScopeBlocks(src);
64
- src = this.#processEvalBlocks(src);
65
-
66
- const [macros, s1] = this.#collectMacros(src);
67
- src = s1;
68
-
69
- const [patterns, s2] = this.#collectPatterns(src);
70
- src = s2;
71
-
72
- src = this.#applyPatterns(src, patterns);
73
- src = this.#expandMacros(src, macros);
74
-
75
- // --- se sobrou bloco papagaio → roda de novo ---
76
- if (!pending()) break;
77
- }
78
-
79
- this.content = src;
80
- return src;
81
- }
82
-
83
- // ============================================
84
- // PRIVATE METHODS
85
- // ============================================
86
-
87
- #resetCounterState() {
88
- this.#counterState.value = 0;
89
- this.#counterState.unique = 0;
90
- }
91
-
92
- #genUnique() {
93
- return "u" + (this.#counterState.unique++).toString(36);
94
- }
95
-
96
- #findClosingDelim(open) {
97
- for (const [o, c] of this.delimiters) {
98
- if (o === open) return c;
99
- }
100
- return null;
101
- }
102
-
103
- #isRegisteredOpen(ch) {
104
- return this.delimiters.some(([o, _]) => o === ch);
105
- }
106
-
107
- #getDefaultOpen() {
108
- return this.delimiters[0][0];
109
- }
110
-
111
- #getDefaultClose() {
112
- return this.delimiters[0][1];
113
- }
114
-
115
- #extractBlock(src, openpos, open = null, close = null) {
116
- if (!open) open = this.#getDefaultOpen();
117
- if (!close) close = this.#getDefaultClose();
118
-
119
- let i = openpos;
120
- let depth = 0;
121
- let startInner = null;
122
- let inString = false;
123
- let strChar = '';
124
-
125
- while (i < src.length) {
126
- let ch = src[i];
127
-
128
- if (inString) {
129
- if (ch === '\\') { i += 2; continue; }
130
- if (ch === strChar) { inString = false; strChar = ''; }
131
- i++;
132
- continue;
133
- } else {
134
- if (ch === '"' || ch === "'" || ch === "`") {
135
- inString = true;
136
- strChar = ch;
137
- i++;
138
- continue;
139
- }
140
- }
141
-
142
- if (ch === open) {
143
- depth++;
144
- if (startInner === null) startInner = i + 1;
145
- } else if (ch === close) {
146
- depth--;
147
- if (depth === 0) {
148
- const inner = startInner !== null ? src.substring(startInner, i) : '';
149
- return [inner, i + 1];
150
- }
151
- }
152
-
153
- i++;
154
- }
155
-
156
- const inner = startInner !== null ? src.substring(startInner) : '';
157
- return [inner, src.length];
158
- }
159
-
160
- #patternToRegex(pattern) {
161
- let regex = '';
162
- let i = 0;
163
-
164
- const S = this.sigil;
165
- const S2 = this.sigil + this.sigil;
166
- const open = this.#getDefaultOpen();
167
- const close = this.#getDefaultClose();
168
-
169
- while (i < pattern.length) {
170
- if (pattern.startsWith(S2, i)) {
171
- regex += '\\s*';
172
- i += S2.length;
173
- continue;
174
- }
175
-
176
- if (this.#isRegisteredOpen(pattern[i]) && pattern.startsWith(S, i + 1)) {
177
- const openDelim = pattern[i];
178
- const closeDelim = this.#findClosingDelim(openDelim);
179
-
180
- let j = i + 1 + S.length;
181
- while (j < pattern.length && /[A-Za-z0-9_]/.test(pattern[j])) j++;
182
-
183
- if (j < pattern.length && pattern[j] === closeDelim) {
184
- const escapedOpen = this.#escapeRegex(openDelim);
185
- const escapedClose = this.#escapeRegex(closeDelim);
186
- const innerRegex = this.#buildBalancedBlockRegex(openDelim, closeDelim);
187
-
188
- regex += `${escapedOpen}(${innerRegex})${escapedClose}`;
189
- i = j + 1;
190
- continue;
191
- }
192
- }
193
-
194
- if (pattern[i] === S) {
195
- let j = i + S.length;
196
- let varName = '';
197
- while (j < pattern.length && /[A-Za-z0-9_]/.test(pattern[j])) {
198
- varName += pattern[j];
199
- j++;
200
- }
201
-
202
- if (varName && pattern.slice(j, j + 3) === '...') {
203
- j += 3;
204
- let token = '';
205
- while (j < pattern.length && /\S/.test(pattern[j])) {
206
- token += pattern[j];
207
- j++;
208
- }
209
-
210
- // Aqui convertemos o token em uma parte de regex:
211
- // - qualquer ocorrência de $$ (S2) vira \s*
212
- // - o restante é escapado apropriadamente
213
- let tokenRegex = '';
214
- if (token.length === 0) {
215
- tokenRegex = ''; // sem token → apenas captura sem terminador
216
- } else {
217
- // dividir pelo S2 e escapar cada pedaço literal
218
- const parts = token.split(S2);
219
- tokenRegex = parts.map(p => this.#escapeRegex(p)).join('\\s*');
220
- }
221
-
222
- // captura não-gulosa para o ... seguida do token interpretado
223
- regex += `((?:.|\\r|\\n)*?)${tokenRegex}`;
224
- i = j;
225
- continue;
226
- }
227
-
228
- if (varName) {
229
- regex += '(\\S+)';
230
- i = j;
231
- continue;
232
- }
233
-
234
- regex += this.#escapeRegex(S);
235
- i += S.length;
236
- continue;
237
- }
238
-
239
- if (/\s/.test(pattern[i])) {
240
- regex += '\\s+';
241
- while (i < pattern.length && /\s/.test(pattern[i])) i++;
242
- continue;
243
- }
244
-
245
- const char = pattern[i];
246
- regex += /[.*+?^${}()|[\]\\]/.test(char) ? '\\' + char : char;
247
- i++;
248
- }
249
-
250
- return new RegExp(regex, 'g');
251
- }
252
-
253
-
254
- #buildBalancedBlockRegex(open, close) {
255
- const escapedOpen = open === '(' ? '\\(' : (open === '[' ? '\\[' : open === '{' ? '\\{' : open === '<' ? '\\<' : open);
256
- const escapedClose = close === ')' ? '\\)' : (close === ']' ? '\\]' : close === '}' ? '\\}' : close === '>' ? '\\>' : close);
257
-
258
- return `(?:[^${escapedOpen}${escapedClose}\\\\]|\\\\.|${escapedOpen}(?:[^${escapedOpen}${escapedClose}\\\\]|\\\\.)*${escapedClose})*`;
259
- }
260
-
261
- #extractVarNames(pattern) {
262
- const vars = [];
263
- const seen = new Set();
264
- const S = this.sigil;
265
- let i = 0;
266
-
267
- while (i < pattern.length) {
268
- if (this.#isRegisteredOpen(pattern[i]) && pattern.startsWith(S, i + 1)) {
269
- const openDelim = pattern[i];
270
- const closeDelim = this.#findClosingDelim(openDelim);
271
-
272
- let j = i + 1 + S.length;
273
- while (j < pattern.length && /[A-Za-z0-9_]/.test(pattern[j])) j++;
274
-
275
- if (j < pattern.length && pattern[j] === closeDelim) {
276
- const varName = pattern.slice(i + 1 + S.length, j);
277
-
278
- if (!seen.has(varName)) {
279
- vars.push(S + varName);
280
- seen.add(varName);
281
- }
282
-
283
- i = j + 1;
284
- continue;
285
- }
286
- }
287
-
288
- if (pattern.startsWith(S, i)) {
289
- let j = i + S.length;
290
- let varName = '';
291
-
292
- while (j < pattern.length && /[A-Za-z0-9_]/.test(pattern[j])) {
293
- varName += pattern[j];
294
- j++;
295
- }
296
-
297
- if (varName && pattern.slice(j, j + 3) === '...') {
298
- j += 3;
299
- let token = '';
300
- while (j < pattern.length && /\S/.test(pattern[j])) {
301
- token += pattern[j];
302
- j++;
303
- }
304
- if (!seen.has(varName)) {
305
- vars.push(S + varName);
306
- seen.add(varName);
307
- }
308
- i = j;
309
- continue;
310
- }
311
-
312
- if (varName && !seen.has(varName)) {
313
- vars.push(S + varName);
314
- seen.add(varName);
315
- }
316
- i = j;
317
- continue;
318
- }
319
-
320
- i++;
321
- }
322
-
323
- return vars;
324
- }
325
-
326
- #collectMacros(src) {
327
- const macros = {};
328
- const open = this.#getDefaultOpen();
329
- const macroRegex = new RegExp(`\\b${this.keywords.macro}\\s+([A-Za-z_][A-Za-z0-9_]*)\\s*\\${open}`, "g");
330
-
331
- let match;
332
- const matches = [];
333
-
334
- while ((match = macroRegex.exec(src)) !== null) {
335
- matches.push({
336
- name: match[1],
337
- matchStart: match.index,
338
- openPos: match.index + match[0].length - 1
339
- });
340
- }
341
-
342
- for (let j = matches.length - 1; j >= 0; j--) {
343
- const m = matches[j];
344
- const [body, posAfter] = this.#extractBlock(src, m.openPos);
345
- macros[m.name] = body;
346
-
347
- let left = src.substring(0, m.matchStart);
348
- let right = src.substring(posAfter);
349
- src = this.#collapseLocalNewlines(left, right);
350
- }
351
-
352
- return [macros, src];
353
- }
354
-
355
- #patternDepthAt(src, pos) {
356
- const open = this.keywords.pattern;
357
- let depth = 0;
358
-
359
- // scaneia até 'pos' contando quantos pattern{ e } ocorreram
360
- let i = 0;
361
- while (i < pos) {
362
- // identificação de pattern{
363
- if (src.startsWith(this.keywords.pattern, i)) {
364
- let j = i + this.keywords.pattern.length;
365
- while (j < src.length && /\s/.test(src[j])) j++;
366
-
367
- if (src[j] === "{") {
368
- depth++;
369
- }
370
- }
371
-
372
- if (src[i] === "}") {
373
- if (depth > 0) depth--;
374
- }
375
-
376
- i++;
377
- }
378
-
379
- return depth;
380
- }
381
-
382
-
383
- #collectPatterns(src) {
384
- const patterns = [];
385
- const open = this.#getDefaultOpen();
386
- const close = this.#getDefaultClose();
387
- const patternRegex = new RegExp(`\\b${this.keywords.pattern}\\s*\\{`, "g");
388
-
389
- let resultSrc = src;
390
- let out = "";
391
- let i = 0;
392
-
393
- while (i < resultSrc.length) {
394
- patternRegex.lastIndex = i;
395
- const m = patternRegex.exec(resultSrc);
396
- if (!m) break;
397
-
398
- const start = m.index;
399
-
400
- // Antes de aceitar, precisamos saber se estamos dentro de outro pattern
401
- const depth = this.#patternDepthAt(resultSrc, start);
402
-
403
- if (depth > 0) {
404
- // pattern interno: pula, não coleta, não remove
405
- i = start + 1;
406
- continue;
407
- }
408
-
409
- // Coletar pattern de nível global
410
- const openPos = m.index + m[0].length - 1;
411
- const [matchPattern, posAfterMatch] = this.#extractBlock(resultSrc, openPos);
412
-
413
- let k = posAfterMatch;
414
- while (k < resultSrc.length && /\s/.test(resultSrc[k])) k++;
415
-
416
- if (k < resultSrc.length && resultSrc[k] === open) {
417
- const [replacePattern, posAfterReplace] = this.#extractBlock(resultSrc, k);
418
-
419
- patterns.push({
420
- match: matchPattern.trim(),
421
- replace: replacePattern.trim()
422
- });
423
-
424
- // Remove o bloco completo do src
425
- resultSrc =
426
- resultSrc.slice(0, start) +
427
- resultSrc.slice(posAfterReplace);
428
-
429
- // Continua logo após o ponto removido
430
- i = start;
431
- continue;
432
- }
433
-
434
- i = start + 1;
435
- }
436
-
437
- return [patterns, resultSrc];
438
- }
439
-
440
- #applyPatterns(src, patterns) {
441
- let globalClearFlag = false;
442
- let lastResult = "";
443
- const S = this.sigil;
444
-
445
- for (const pattern of patterns) {
446
- let changed = true;
447
- let iterations = 0;
448
-
449
- while (changed && iterations < this.maxRecursion) {
450
- changed = false;
451
- iterations++;
452
-
453
- const regex = this.#patternToRegex(pattern.match);
454
- const varNames = this.#extractVarNames(pattern.match);
455
-
456
- src = src.replace(regex, (...args) => {
457
- changed = true;
458
- const fullMatch = args[0];
459
- const captures = args.slice(1, -2);
460
- const matchStart = args[args.length - 2];
461
- const matchEnd = matchStart + fullMatch.length;
462
-
463
- const varMap = {};
464
- for (let i = 0; i < varNames.length; i++) {
465
- varMap[varNames[i]] = captures[i] || '';
466
- }
467
-
468
- this.#matchContent = fullMatch;
469
-
470
- const _pre = src.slice(0, matchStart);
471
- const _post = src.slice(matchEnd);
472
-
473
- let result = pattern.replace;
474
-
475
- for (const [key, val] of Object.entries(varMap)) {
476
- const escaped = this.#escapeRegex(key);
477
- result = result.replace(new RegExp(escaped + '(?![A-Za-z0-9_])', 'g'), val);
478
- }
479
-
480
- result = result.replace(new RegExp(`${this.#escapeRegex(S)}unique\\b`, 'g'),
481
- () => this.#genUnique()
482
- );
483
-
484
- const S2 = S + S;
485
- result = result.replace(new RegExp(this.#escapeRegex(S2), 'g'), '');
486
-
487
- const clearRe = new RegExp(`${this.#escapeRegex(S)}clear\\b`, 'g');
488
- if (clearRe.test(result)) {
489
- result = result.replace(clearRe, '');
490
- globalClearFlag = true;
491
- }
492
-
493
- result = result
494
- .replace(new RegExp(`${this.#escapeRegex(S)}pre\\b`, 'g'), _pre)
495
- .replace(new RegExp(`${this.#escapeRegex(S)}post\\b`, 'g'), _post)
496
- .replace(new RegExp(`${this.#escapeRegex(S)}match\\b`, 'g'), fullMatch);
497
-
498
- lastResult = result;
499
- return result;
500
- });
501
-
502
- if (globalClearFlag) {
503
- src = lastResult;
504
- globalClearFlag = false;
505
- changed = true;
506
- }
507
- }
508
- }
509
-
510
- return src;
511
- }
512
-
513
- #expandMacros(src, macros) {
514
- const S = this.sigil;
515
-
516
- for (const name of Object.keys(macros)) {
517
- const body = macros[name];
518
- let changed = true;
519
- let iterations = 0;
520
-
521
- while (changed && iterations < this.maxRecursion) {
522
- changed = false;
523
- iterations++;
524
-
525
- const callRegex = new RegExp(`\\b${this.#escapeRegex(name)}\\s*\\(`, 'g');
526
-
527
- let match;
528
- const matches = [];
529
-
530
- while ((match = callRegex.exec(src)) !== null) {
531
- matches.push({
532
- matchStart: match.index,
533
- openPos: match.index + match[0].length - 1
534
- });
535
- }
536
-
537
- for (let j = matches.length - 1; j >= 0; j--) {
538
- const m = matches[j];
539
- const [argsStr, posAfter] = this.#extractBlock(src, m.openPos, '(', ')');
540
- const vals = argsStr.split(',').map(v => v.trim());
541
-
542
- let exp = body;
543
-
544
- // Substituição flexível de $0, $1, $2 etc, mesmo dentro de palavras
545
- for (let k = vals.length; k >= 0; k--) {
546
- const sigil = k === 0 ? S + '0' : S + k;
547
- const pattern = new RegExp(this.#escapeRegex(sigil) + '(?![0-9])', 'g');
548
- const replacement = k === 0 ? name : (vals[k - 1] !== undefined ? vals[k - 1] : '');
549
- exp = exp.replace(pattern, replacement);
550
- }
551
-
552
- let left = src.substring(0, m.matchStart);
553
- let right = src.substring(posAfter);
554
- src = left + exp + right;
555
- changed = true;
556
- }
557
- }
558
- }
559
-
560
- return src;
561
- }
562
-
563
- #escapeRegex(str) {
564
- return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
565
- }
566
-
567
- #collapseLocalNewlines(left, right) {
568
- left = left.replace(/\n+$/, '\n');
569
- right = right.replace(/^\n+/, '\n');
570
-
571
- if (left.endsWith('\n') && right.startsWith('\n')) {
572
- right = right.replace(/^\n+/, '\n');
573
- }
574
-
575
- if (left === '' && right.startsWith('\n')) {
576
- right = right.replace(/^\n+/, '');
577
- }
578
-
579
- return left + right;
580
- }
581
-
582
- #processEvalBlocks(src) {
583
- const open = this.#getDefaultOpen();
584
- const evalRegex = new RegExp(`\\b${this.keywords.eval}\\s*\\${open}`, "g");
585
-
586
- let match;
587
- const matches = [];
588
-
589
- while ((match = evalRegex.exec(src)) !== null) {
590
- matches.push({
591
- matchStart: match.index,
592
- openPos: match.index + match[0].length - 1
593
- });
594
- }
595
-
596
- for (let j = matches.length - 1; j >= 0; j--) {
597
- const m = matches[j];
598
-
599
- const [content, posAfter] = this.#extractBlock(src, m.openPos);
600
- this.#evalContent = content;
601
-
602
- let out = "";
603
- try {
604
- // O conteúdo é o corpo de uma função autoinvocada
605
- const wrappedCode = `"use strict"; return (function() { ${content} })();`;
606
-
607
- out = String(
608
- Function("papagaio", "ctx", wrappedCode)(this, {})
609
- );
610
- } catch (e) {
611
- out = "";
612
- }
613
-
614
- let left = src.substring(0, m.matchStart);
615
- let right = src.substring(posAfter);
616
- src = left + out + right;
617
- }
618
-
619
- return src;
620
- }
621
-
622
- #processScopeBlocks(src) {
623
- const open = this.#getDefaultOpen();
624
- const scopeRegex = new RegExp(`\\b${this.keywords.scope}\\s*\\${open}`, "g");
625
-
626
- let match;
627
- const matches = [];
628
-
629
- while ((match = scopeRegex.exec(src)) !== null) {
630
- matches.push({
631
- matchStart: match.index,
632
- openPos: match.index + match[0].length - 1
633
- });
634
- }
635
-
636
- for (let j = matches.length - 1; j >= 0; j--) {
637
- const m = matches[j];
638
- const [content, posAfter] = this.#extractBlock(src, m.openPos);
639
-
640
- this.#scopeContent = content;
641
- const processedContent = this.process(content);
642
-
643
- let left = src.substring(0, m.matchStart);
644
- let right = src.substring(posAfter);
645
-
646
- let prefix = "";
647
- if (left.endsWith("\n")) {
648
- prefix = "\n";
649
- left = left.slice(0, -1);
650
- }
651
-
652
- src = left + prefix + processedContent + right;
653
- }
654
-
655
- return src;
656
- }
657
-
658
-
659
- }