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.
@@ -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);