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/README.md +227 -206
- package/{cli.js → bin/cli.js} +2 -2
- package/index.html +1 -1
- package/package.json +8 -7
- package/src/papagaio.js +319 -0
- package/tests/test.js +98 -0
- package/tests/tests.json +406 -0
- package/papagaio.js +0 -659
package/src/papagaio.js
ADDED
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
export class Papagaio {
|
|
2
|
+
maxRecursion = 512;
|
|
3
|
+
#counter = { value: 0, unique: 0 };
|
|
4
|
+
open = "{";
|
|
5
|
+
close = "}";
|
|
6
|
+
sigil = "$";
|
|
7
|
+
keywords = { pattern: "pattern", context: "context" };
|
|
8
|
+
content = "";
|
|
9
|
+
|
|
10
|
+
constructor() {
|
|
11
|
+
this.#counter.value = 0;
|
|
12
|
+
this.#counter.unique = 0;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
process(input) {
|
|
16
|
+
this.content = input;
|
|
17
|
+
let src = input, last = null, iter = 0;
|
|
18
|
+
const pending = () => {
|
|
19
|
+
const rCtx = new RegExp(`\\b${this.keywords.context}\\s*\\${this.open}`, "g");
|
|
20
|
+
const rPat = new RegExp(`\\b${this.keywords.pattern}\\s*\\${this.open}`, "g");
|
|
21
|
+
return rCtx.test(src) || rPat.test(src);
|
|
22
|
+
};
|
|
23
|
+
while (src !== last && iter < this.maxRecursion) {
|
|
24
|
+
iter++;
|
|
25
|
+
last = src;
|
|
26
|
+
src = this.#procContext(src);
|
|
27
|
+
const [patterns, s2] = this.#collectPatterns(src);
|
|
28
|
+
src = s2;
|
|
29
|
+
src = this.#applyPatterns(src, patterns);
|
|
30
|
+
if (!pending()) break;
|
|
31
|
+
}
|
|
32
|
+
return this.content = src, src;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
#procContext(src) {
|
|
36
|
+
const ctxRe = new RegExp(`\\b${this.keywords.context}\\s*\\${this.open}`, "g");
|
|
37
|
+
let m, matches = [];
|
|
38
|
+
while ((m = ctxRe.exec(src)) !== null)
|
|
39
|
+
matches.push({ idx: m.index, pos: m.index + m[0].length - 1 });
|
|
40
|
+
for (let j = matches.length - 1; j >= 0; j--) {
|
|
41
|
+
const x = matches[j], [content, posAfter] = this.#extractBlock(src, x.pos);
|
|
42
|
+
if (!content.trim()) {
|
|
43
|
+
src = src.slice(0, x.idx) + src.slice(posAfter);
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
const proc = this.process(content);
|
|
47
|
+
let left = src.substring(0, x.idx), right = src.substring(posAfter);
|
|
48
|
+
let prefix = left.endsWith("\n") ? "\n" : "";
|
|
49
|
+
if (prefix) left = left.slice(0, -1);
|
|
50
|
+
src = left + prefix + proc + right;
|
|
51
|
+
}
|
|
52
|
+
return src;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
#extractBlock(src, openPos, openDelim = this.open, closeDelim = this.close) {
|
|
56
|
+
let i = openPos, depth = 0, innerStart = null, inStr = false, strChar = '';
|
|
57
|
+
|
|
58
|
+
// Se openDelim tem múltiplos caracteres, processa diferente
|
|
59
|
+
if (openDelim.length > 1) {
|
|
60
|
+
// Pula o delimitador de abertura
|
|
61
|
+
if (src.substring(i, i + openDelim.length) === openDelim) {
|
|
62
|
+
i += openDelim.length;
|
|
63
|
+
innerStart = i;
|
|
64
|
+
// Procura pelo delimitador de fechamento com balanceamento
|
|
65
|
+
let d = 0;
|
|
66
|
+
while (i < src.length) {
|
|
67
|
+
if (src.substring(i, i + openDelim.length) === openDelim) {
|
|
68
|
+
d++;
|
|
69
|
+
i += openDelim.length;
|
|
70
|
+
} else if (src.substring(i, i + closeDelim.length) === closeDelim) {
|
|
71
|
+
if (d === 0) {
|
|
72
|
+
return [src.substring(innerStart, i), i + closeDelim.length];
|
|
73
|
+
}
|
|
74
|
+
d--;
|
|
75
|
+
i += closeDelim.length;
|
|
76
|
+
} else {
|
|
77
|
+
i++;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return [src.substring(innerStart), src.length];
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Para delimitadores single-char, balanceia
|
|
85
|
+
while (i < src.length) {
|
|
86
|
+
const ch = src[i];
|
|
87
|
+
if (inStr) {
|
|
88
|
+
if (ch === '\\') i += 2;
|
|
89
|
+
else if (ch === strChar) { inStr = false; strChar = ''; i++; }
|
|
90
|
+
else i++;
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
if (ch === "'" || ch === "`") {
|
|
94
|
+
inStr = true;
|
|
95
|
+
strChar = ch;
|
|
96
|
+
i++;
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
if (ch === openDelim) {
|
|
100
|
+
depth++;
|
|
101
|
+
if (innerStart === null) innerStart = i + 1;
|
|
102
|
+
} else if (ch === closeDelim) {
|
|
103
|
+
depth--;
|
|
104
|
+
if (depth === 0) return [innerStart !== null ? src.substring(innerStart, i) : '', i + 1];
|
|
105
|
+
}
|
|
106
|
+
i++;
|
|
107
|
+
}
|
|
108
|
+
return [innerStart !== null ? src.substring(innerStart) : '', src.length];
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
#patternToRegex(pattern) {
|
|
112
|
+
let regex = '', i = 0;
|
|
113
|
+
const S = this.sigil, S2 = S + S;
|
|
114
|
+
while (i < pattern.length) {
|
|
115
|
+
if (pattern.startsWith(S2, i)) {
|
|
116
|
+
regex += '\\s*';
|
|
117
|
+
i += S2.length;
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
if (pattern.startsWith(S + 'block', i)) {
|
|
121
|
+
let j = i + S.length + 'block'.length;
|
|
122
|
+
while (j < pattern.length && /\s/.test(pattern[j])) j++;
|
|
123
|
+
|
|
124
|
+
let varName = '';
|
|
125
|
+
while (j < pattern.length && /[A-Za-z0-9_]/.test(pattern[j])) {
|
|
126
|
+
varName += pattern[j++];
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (varName) {
|
|
130
|
+
while (j < pattern.length && /\s/.test(pattern[j])) j++;
|
|
131
|
+
|
|
132
|
+
// Extrair delimitador de abertura - com balanceamento
|
|
133
|
+
let openDelim = this.open;
|
|
134
|
+
if (j < pattern.length && pattern[j] === this.open) {
|
|
135
|
+
const [c, e] = this.#extractBlock(pattern, j);
|
|
136
|
+
openDelim = c.trim() || this.open;
|
|
137
|
+
j = e;
|
|
138
|
+
while (j < pattern.length && /\s/.test(pattern[j])) j++;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Extrair delimitador de fechamento - com balanceamento
|
|
142
|
+
let closeDelim = this.close;
|
|
143
|
+
if (j < pattern.length && pattern[j] === this.open) {
|
|
144
|
+
const [c, e] = this.#extractBlock(pattern, j, this.open, this.close);
|
|
145
|
+
closeDelim = c.trim() || this.close;
|
|
146
|
+
j = e;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const eoMask = this.#escapeRegex(openDelim);
|
|
150
|
+
const ecMask = this.#escapeRegex(closeDelim);
|
|
151
|
+
// Use greedy match to allow nested delimiters to be captured (will match up to the last closing delimiter)
|
|
152
|
+
regex += `${eoMask}([\\s\\S]*)${ecMask}`;
|
|
153
|
+
i = j;
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
if (pattern[i] === S) {
|
|
158
|
+
let j = i + S.length, varName = '';
|
|
159
|
+
while (j < pattern.length && /[A-Za-z0-9_]/.test(pattern[j])) varName += pattern[j++];
|
|
160
|
+
if (varName) {
|
|
161
|
+
regex += '(\\S+)';
|
|
162
|
+
i = j;
|
|
163
|
+
} else {
|
|
164
|
+
regex += this.#escapeRegex(S);
|
|
165
|
+
i += S.length;
|
|
166
|
+
}
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
if (/\s/.test(pattern[i])) {
|
|
170
|
+
regex += '\\s+';
|
|
171
|
+
while (i < pattern.length && /\s/.test(pattern[i])) i++;
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
const ch = pattern[i];
|
|
175
|
+
regex += /[.*+?^${}()|[\]\\]/.test(ch) ? '\\' + ch : ch;
|
|
176
|
+
i++;
|
|
177
|
+
}
|
|
178
|
+
return new RegExp(regex, 'g');
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
#extractVarNames(pattern) {
|
|
182
|
+
const vars = [], seen = new Set(), S = this.sigil, S2 = S + S;
|
|
183
|
+
let i = 0;
|
|
184
|
+
while (i < pattern.length) {
|
|
185
|
+
if (pattern.startsWith(S + 'block', i)) {
|
|
186
|
+
let j = i + S.length + 'block'.length;
|
|
187
|
+
while (j < pattern.length && /\s/.test(pattern[j])) j++;
|
|
188
|
+
|
|
189
|
+
// Extrair nome como identificador simples
|
|
190
|
+
let varName = '';
|
|
191
|
+
while (j < pattern.length && /[A-Za-z0-9_]/.test(pattern[j])) {
|
|
192
|
+
varName += pattern[j++];
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (varName && !seen.has(varName)) {
|
|
196
|
+
vars.push(S + varName);
|
|
197
|
+
seen.add(varName);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (varName) {
|
|
201
|
+
while (j < pattern.length && /\s/.test(pattern[j])) j++;
|
|
202
|
+
|
|
203
|
+
// Pular delimitador de abertura
|
|
204
|
+
if (j < pattern.length && pattern[j] === this.open) {
|
|
205
|
+
const [, ne] = this.#extractBlock(pattern, j);
|
|
206
|
+
j = ne;
|
|
207
|
+
while (j < pattern.length && /\s/.test(pattern[j])) j++;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Pular delimitador de fechamento
|
|
211
|
+
if (j < pattern.length && pattern[j] === this.open) {
|
|
212
|
+
const [, ne] = this.#extractBlock(pattern, j);
|
|
213
|
+
j = ne;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
i = j;
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
if (pattern.startsWith(S2, i)) {
|
|
221
|
+
i += S2.length;
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
if (pattern.startsWith(S, i)) {
|
|
225
|
+
let j = i + S.length, varName = '';
|
|
226
|
+
while (j < pattern.length && /[A-Za-z0-9_]/.test(pattern[j])) varName += pattern[j++];
|
|
227
|
+
if (varName && !seen.has(varName)) {
|
|
228
|
+
vars.push(S + varName);
|
|
229
|
+
seen.add(varName);
|
|
230
|
+
}
|
|
231
|
+
i = j;
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
i++;
|
|
235
|
+
}
|
|
236
|
+
return vars;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
#collectPatterns(src) {
|
|
240
|
+
const patterns = [], patRe = new RegExp(`\\b${this.keywords.pattern}\\s*\\${this.open}`, "g");
|
|
241
|
+
let result = src, i = 0;
|
|
242
|
+
while (i < result.length) {
|
|
243
|
+
patRe.lastIndex = i;
|
|
244
|
+
const m = patRe.exec(result);
|
|
245
|
+
if (!m) break;
|
|
246
|
+
const start = m.index, openPos = m.index + m[0].length - 1;
|
|
247
|
+
const [matchPat, posAfterMatch] = this.#extractBlock(result, openPos);
|
|
248
|
+
let k = posAfterMatch;
|
|
249
|
+
while (k < result.length && /\s/.test(result[k])) k++;
|
|
250
|
+
if (k < result.length && result[k] === this.open) {
|
|
251
|
+
const [replacePat, posAfterReplace] = this.#extractBlock(result, k);
|
|
252
|
+
patterns.push({ match: matchPat.trim(), replace: replacePat.trim() });
|
|
253
|
+
result = result.slice(0, start) + result.slice(posAfterReplace);
|
|
254
|
+
i = start;
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
i = start + 1;
|
|
258
|
+
}
|
|
259
|
+
return [patterns, result];
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
#applyPatterns(src, patterns) {
|
|
263
|
+
let clearFlag = false, lastResult = "", S = this.sigil;
|
|
264
|
+
for (const pat of patterns) {
|
|
265
|
+
// Aplicar uma única vez
|
|
266
|
+
const regex = this.#patternToRegex(pat.match);
|
|
267
|
+
const varNames = this.#extractVarNames(pat.match);
|
|
268
|
+
src = src.replace(regex, (...args) => {
|
|
269
|
+
const fullMatch = args[0];
|
|
270
|
+
const captures = args.slice(1, -2);
|
|
271
|
+
const matchStart = args[args.length - 2];
|
|
272
|
+
const matchEnd = matchStart + fullMatch.length;
|
|
273
|
+
let result = pat.replace;
|
|
274
|
+
const varMap = {};
|
|
275
|
+
for (let i = 0; i < varNames.length; i++)
|
|
276
|
+
varMap[varNames[i]] = captures[i] || '';
|
|
277
|
+
for (const [k, v] of Object.entries(varMap)) {
|
|
278
|
+
const keyEsc = this.#escapeRegex(k);
|
|
279
|
+
result = result.replace(new RegExp(keyEsc + '(?![A-Za-z0-9_])', 'g'), v);
|
|
280
|
+
}
|
|
281
|
+
result = result.replace(new RegExp(`${this.#escapeRegex(S)}unique\\b`, 'g'),
|
|
282
|
+
() => this.#genUnique());
|
|
283
|
+
result = result.replace(/\$eval\{([^}]*)\}/g, (_, code) => {
|
|
284
|
+
try {
|
|
285
|
+
const wrapped = `"use strict"; return (function() { ${code} })();`;
|
|
286
|
+
return String(Function("papagaio", "ctx", wrapped)(this, {}));
|
|
287
|
+
} catch {
|
|
288
|
+
return "";
|
|
289
|
+
}
|
|
290
|
+
});
|
|
291
|
+
const S2 = S + S;
|
|
292
|
+
result = result.replace(new RegExp(this.#escapeRegex(S2), 'g'), '');
|
|
293
|
+
if (new RegExp(`${this.#escapeRegex(S)}clear\\b`, 'g').test(result)) {
|
|
294
|
+
result = result.replace(new RegExp(`${this.#escapeRegex(S)}clear\\b`, 'g'), '');
|
|
295
|
+
clearFlag = true;
|
|
296
|
+
}
|
|
297
|
+
result = result
|
|
298
|
+
.replace(new RegExp(`${this.#escapeRegex(S)}prefix\\b`, 'g'), src.slice(0, matchStart))
|
|
299
|
+
.replace(new RegExp(`${this.#escapeRegex(S)}suffix\\b`, 'g'), src.slice(matchEnd))
|
|
300
|
+
.replace(new RegExp(`${this.#escapeRegex(S)}match\\b`, 'g'), fullMatch);
|
|
301
|
+
lastResult = result;
|
|
302
|
+
return result;
|
|
303
|
+
});
|
|
304
|
+
if (clearFlag) {
|
|
305
|
+
src = lastResult;
|
|
306
|
+
clearFlag = false;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
return src;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
#genUnique() {
|
|
313
|
+
return "u" + (this.#counter.unique++).toString(36);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
#escapeRegex(str) {
|
|
317
|
+
return str.replace(/[.*+?^${}()|[\]\\\"']/g, '\\$&');
|
|
318
|
+
}
|
|
319
|
+
}
|
package/tests/test.js
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { Papagaio } from '../src/papagaio.js';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
|
|
6
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
|
|
8
|
+
// ANSI color codes
|
|
9
|
+
const colors = {
|
|
10
|
+
reset: '\x1b[0m',
|
|
11
|
+
green: '\x1b[32m',
|
|
12
|
+
red: '\x1b[31m',
|
|
13
|
+
yellow: '\x1b[33m',
|
|
14
|
+
cyan: '\x1b[36m',
|
|
15
|
+
white: '\x1b[37m'
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
// Read tests JSON file
|
|
19
|
+
const testsPath = path.join(__dirname, 'tests.json');
|
|
20
|
+
const testsData = JSON.parse(fs.readFileSync(testsPath, 'utf-8'));
|
|
21
|
+
const tests = testsData.tests;
|
|
22
|
+
|
|
23
|
+
const p = new Papagaio();
|
|
24
|
+
|
|
25
|
+
console.log(`${colors.cyan}[TEST] PAPAGAIO - TEST RUNNER${colors.reset}\n`);
|
|
26
|
+
console.log('='.repeat(80));
|
|
27
|
+
|
|
28
|
+
let passed = 0;
|
|
29
|
+
let failed = 0;
|
|
30
|
+
const failedTests = [];
|
|
31
|
+
|
|
32
|
+
for (const test of tests) {
|
|
33
|
+
try {
|
|
34
|
+
const result = p.process(test.code).trim();
|
|
35
|
+
const success = result.includes(test.shouldContain);
|
|
36
|
+
|
|
37
|
+
if (success) {
|
|
38
|
+
console.log(`${colors.green}[PASS]${colors.reset} [${test.id}] ${test.name}`);
|
|
39
|
+
passed++;
|
|
40
|
+
} else {
|
|
41
|
+
console.log(`${colors.red}[FAIL]${colors.reset} [${test.id}] ${test.name}`);
|
|
42
|
+
console.log(` ${colors.yellow}Expected:${colors.reset} "${test.shouldContain}"`);
|
|
43
|
+
console.log(` ${colors.yellow}Got:${colors.reset} "${result.substring(0, 80)}${result.length > 80 ? '...' : ''}"`);
|
|
44
|
+
failed++;
|
|
45
|
+
failedTests.push({
|
|
46
|
+
id: test.id,
|
|
47
|
+
name: test.name,
|
|
48
|
+
expected: test.shouldContain,
|
|
49
|
+
got: result.substring(0, 150)
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
} catch (e) {
|
|
53
|
+
console.log(`${colors.red}[ERR!]${colors.reset} [${test.id}] ${test.name}`);
|
|
54
|
+
console.log(` ${colors.yellow}ERROR:${colors.reset} ${e.message}`);
|
|
55
|
+
failed++;
|
|
56
|
+
failedTests.push({
|
|
57
|
+
id: test.id,
|
|
58
|
+
name: test.name,
|
|
59
|
+
error: e.message
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
console.log('\n' + '='.repeat(80));
|
|
65
|
+
console.log(`\n${colors.cyan}[INFO]${colors.reset} FINAL RESULT: ${passed}/${tests.length} tests passed`);
|
|
66
|
+
console.log(` ${colors.green}[PASS]${colors.reset} Passed: ${passed}`);
|
|
67
|
+
console.log(` ${colors.red}[FAIL]${colors.reset} Failed: ${failed}`);
|
|
68
|
+
console.log(` Success rate: ${Math.round((passed / tests.length) * 100)}%\n`);
|
|
69
|
+
|
|
70
|
+
if (failedTests.length > 0) {
|
|
71
|
+
console.log(`${colors.red}[FAIL]${colors.reset} FAILED TESTS:`);
|
|
72
|
+
console.log('-'.repeat(80));
|
|
73
|
+
for (const test of failedTests) {
|
|
74
|
+
console.log(`\n[${test.id}] ${test.name}`);
|
|
75
|
+
if (test.error) {
|
|
76
|
+
console.log(` ${colors.red}Error:${colors.reset} ${test.error}`);
|
|
77
|
+
} else {
|
|
78
|
+
console.log(` ${colors.yellow}Expected:${colors.reset} ${test.expected}`);
|
|
79
|
+
console.log(` ${colors.yellow}Got:${colors.reset} ${test.got}`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Generate test report JSON
|
|
85
|
+
const report = {
|
|
86
|
+
timestamp: new Date().toISOString(),
|
|
87
|
+
totalTests: tests.length,
|
|
88
|
+
passed,
|
|
89
|
+
failed,
|
|
90
|
+
successRate: Math.round((passed / tests.length) * 100),
|
|
91
|
+
failedTests
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const reportPath = path.join(__dirname, 'test-report.json');
|
|
95
|
+
fs.writeFileSync(reportPath, JSON.stringify(report, null, 2));
|
|
96
|
+
console.log(`\n${colors.cyan}[INFO]${colors.reset} Report saved at: ${reportPath}`);
|
|
97
|
+
|
|
98
|
+
process.exit(failed > 0 ? 1 : 0);
|