papagaio 0.2.3 → 0.2.4

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 CHANGED
@@ -52,17 +52,6 @@ Output: `hello & world` (both)
52
52
 
53
53
  `$$` = zero or more spaces/tabs/newlines.
54
54
 
55
- ### 4. Literal Output (`$$`)
56
-
57
- ```
58
- Price: $$50
59
- ```
60
- Output: `Price: $50`
61
-
62
- Use `$$` for literal `$` output.
63
-
64
- ---
65
-
66
55
  ## Blocks
67
56
 
68
57
  Capture content between delimiters.
@@ -281,6 +270,9 @@ Output:
281
270
  - Reuse: `$x` appears multiple times in replace
282
271
  - Undefined: becomes empty string
283
272
 
273
+ ### Sigil
274
+ - You cannot match words containing the sigil character.
275
+
284
276
  ---
285
277
 
286
278
  ## Troubleshooting
@@ -293,25 +285,19 @@ Output:
293
285
  | Infinite recursion | Use `$clear` or reduce `maxRecursion` |
294
286
  | $eval not working | Errors return empty string, use try-catch |
295
287
 
296
- ---
297
-
298
- ## Performance
288
+ ## Known Bugs
299
289
 
300
- - Simple patterns > complex patterns
301
- - Blocks have minor overhead
302
- - Avoid unlimited recursion
303
- - For large inputs, use multiple contexts
290
+ - Multi-character block delimiters that contains double quotes doesnt match properly.
304
291
 
305
292
  ---
306
293
 
307
294
  ## Syntax Reference
308
295
 
309
296
  ```
310
- pattern {$x $y} {$y, $x} # basic pattern
311
- pattern {$x$$y} {$x-$y} # flexible whitespace
312
- pattern {$block n {o}{c}} {$n} # block
297
+ pattern {$x $y} {$y, $x} # basic pattern
298
+ pattern {$x$$y} {$x-$y} # flexible whitespace
299
+ pattern {$block n {o}{c}} {$n} # block
313
300
  context { ... } # recursive scope
314
- $$literal # output literal $
315
301
  $unique # unique ID
316
302
  $match # full match
317
303
  $prefix / $suffix # before/after
@@ -327,7 +313,6 @@ $eval{code} # execute JS
327
313
  context {
328
314
  # Markdown headers
329
315
  pattern {# $title} {<h1>$title</h1>}
330
- pattern {## $title} {<h2>$title</h2>}
331
316
 
332
317
  # Lists
333
318
  pattern {- $item} {<li>$item</li>}
@@ -338,7 +323,7 @@ context {
338
323
 
339
324
  # Process content
340
325
  # Welcome
341
- ## Getting Started
326
+ # Getting Started
342
327
  This is **important** and *italic*
343
328
  - First item
344
329
  - Second item
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "papagaio",
3
- "version": "0.2.3",
3
+ "version": "0.2.4",
4
4
  "description": "easy yet powerful preprocessor",
5
5
  "main": "src/papagaio.js",
6
6
  "type": "module",
package/src/papagaio.js CHANGED
@@ -8,8 +8,7 @@ export class Papagaio {
8
8
  content = "";
9
9
 
10
10
  constructor() {
11
- this.#counter.value = 0;
12
- this.#counter.unique = 0;
11
+ this.#counter = { value: 0, unique: 0 };
13
12
  }
14
13
 
15
14
  process(input) {
@@ -23,16 +22,15 @@ export class Papagaio {
23
22
  while (src !== last && iter < this.maxRecursion) {
24
23
  iter++;
25
24
  last = src;
26
- src = this.#procContext(src);
25
+ src = this.#processContext(src);
27
26
  const [patterns, s2] = this.#collectPatterns(src);
28
- src = s2;
29
- src = this.#applyPatterns(src, patterns);
27
+ src = this.#applyPatterns(s2, patterns);
30
28
  if (!pending()) break;
31
29
  }
32
30
  return this.content = src, src;
33
31
  }
34
32
 
35
- #procContext(src) {
33
+ #processContext(src) {
36
34
  const ctxRe = new RegExp(`\\b${this.keywords.context}\\s*\\${this.open}`, "g");
37
35
  let m, matches = [];
38
36
  while ((m = ctxRe.exec(src)) !== null)
@@ -53,103 +51,75 @@ export class Papagaio {
53
51
  }
54
52
 
55
53
  #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
54
+ let i = openPos;
55
+ if (openDelim.length > 1 || closeDelim.length > 1) {
61
56
  if (src.substring(i, i + openDelim.length) === openDelim) {
62
57
  i += openDelim.length;
63
- innerStart = i;
64
- // Procura pelo delimitador de fechamento com balanceamento
58
+ const innerStart = i;
65
59
  let d = 0;
66
60
  while (i < src.length) {
67
61
  if (src.substring(i, i + openDelim.length) === openDelim) {
68
62
  d++;
69
63
  i += openDelim.length;
70
64
  } else if (src.substring(i, i + closeDelim.length) === closeDelim) {
71
- if (d === 0) {
72
- return [src.substring(innerStart, i), i + closeDelim.length];
73
- }
65
+ if (d === 0) return [src.substring(innerStart, i), i + closeDelim.length];
74
66
  d--;
75
67
  i += closeDelim.length;
76
- } else {
77
- i++;
78
- }
68
+ } else i++;
79
69
  }
80
70
  return [src.substring(innerStart), src.length];
81
71
  }
82
72
  }
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
- }
73
+ if (src[i] === openDelim) {
106
74
  i++;
75
+ const innerStart = i;
76
+ if (openDelim === closeDelim) {
77
+ while (i < src.length && src[i] !== closeDelim) i++;
78
+ return [src.substring(innerStart, i), i + 1];
79
+ } else {
80
+ let depth = 1;
81
+ while (i < src.length && depth > 0) {
82
+ if (src[i] === openDelim) depth++;
83
+ else if (src[i] === closeDelim) depth--;
84
+ if (depth > 0) i++;
85
+ }
86
+ return [src.substring(innerStart, i), i + 1];
87
+ }
107
88
  }
108
- return [innerStart !== null ? src.substring(innerStart) : '', src.length];
89
+ return ['', i];
109
90
  }
110
91
 
111
- #patternToRegex(pattern) {
112
- let regex = '', i = 0;
92
+ #parsePattern(pattern) {
93
+ const tokens = [];
94
+ let i = 0;
113
95
  const S = this.sigil, S2 = S + S;
114
96
  while (i < pattern.length) {
115
97
  if (pattern.startsWith(S2, i)) {
116
- regex += '\\s*';
98
+ tokens.push({ type: 'whitespace-optional' });
117
99
  i += S2.length;
118
100
  continue;
119
101
  }
120
102
  if (pattern.startsWith(S + 'block', i)) {
121
103
  let j = i + S.length + 'block'.length;
122
104
  while (j < pattern.length && /\s/.test(pattern[j])) j++;
123
-
124
105
  let varName = '';
125
- while (j < pattern.length && /[A-Za-z0-9_]/.test(pattern[j])) {
126
- varName += pattern[j++];
127
- }
128
-
106
+ while (j < pattern.length && /[A-Za-z0-9_]/.test(pattern[j])) varName += pattern[j++];
129
107
  if (varName) {
130
108
  while (j < pattern.length && /\s/.test(pattern[j])) j++;
131
-
132
- // Extrair delimitador de abertura - com balanceamento
133
109
  let openDelim = this.open;
134
110
  if (j < pattern.length && pattern[j] === this.open) {
135
111
  const [c, e] = this.#extractBlock(pattern, j);
136
- openDelim = c.trim() || this.open;
112
+ openDelim = this.#unescapeDelimiter(c.trim()) || this.open;
137
113
  j = e;
138
114
  while (j < pattern.length && /\s/.test(pattern[j])) j++;
139
115
  }
140
-
141
- // Extrair delimitador de fechamento - com balanceamento
142
116
  let closeDelim = this.close;
143
117
  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;
118
+ const [c, e] = this.#extractBlock(pattern, j);
119
+ closeDelim = this.#unescapeDelimiter(c.trim()) || this.close;
146
120
  j = e;
147
121
  }
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}`;
122
+ tokens.push({ type: 'block', varName, openDelim, closeDelim });
153
123
  i = j;
154
124
  continue;
155
125
  }
@@ -158,92 +128,89 @@ export class Papagaio {
158
128
  let j = i + S.length, varName = '';
159
129
  while (j < pattern.length && /[A-Za-z0-9_]/.test(pattern[j])) varName += pattern[j++];
160
130
  if (varName) {
161
- regex += '(\\S+)';
131
+ tokens.push({ type: 'var', varName });
162
132
  i = j;
163
- } else {
164
- regex += this.#escapeRegex(S);
165
- i += S.length;
133
+ continue;
166
134
  }
135
+ tokens.push({ type: 'literal', value: S });
136
+ i += S.length;
167
137
  continue;
168
138
  }
169
139
  if (/\s/.test(pattern[i])) {
170
- regex += '\\s+';
171
- while (i < pattern.length && /\s/.test(pattern[i])) i++;
140
+ let ws = '';
141
+ while (i < pattern.length && /\s/.test(pattern[i])) ws += pattern[i++];
142
+ tokens.push({ type: 'whitespace', value: ws });
172
143
  continue;
173
144
  }
174
- const ch = pattern[i];
175
- regex += /[.*+?^${}()|[\]\\]/.test(ch) ? '\\' + ch : ch;
176
- i++;
145
+ let literal = '';
146
+ while (i < pattern.length && !pattern.startsWith(S, i) && !/\s/.test(pattern[i])) literal += pattern[i++];
147
+ if (literal) tokens.push({ type: 'literal', value: literal });
177
148
  }
178
- return new RegExp(regex, 'g');
149
+ return tokens;
179
150
  }
180
151
 
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;
152
+ #matchPattern(src, tokens, startPos = 0) {
153
+ let pos = startPos;
154
+ const captures = {};
155
+ for (let ti = 0; ti < tokens.length; ti++) {
156
+ const token = tokens[ti];
157
+ if (token.type === 'whitespace-optional') {
158
+ while (pos < src.length && /\s/.test(src[pos])) pos++;
218
159
  continue;
219
160
  }
220
- if (pattern.startsWith(S2, i)) {
221
- i += S2.length;
161
+ if (token.type === 'whitespace') {
162
+ if (pos >= src.length || !/\s/.test(src[pos])) return null;
163
+ while (pos < src.length && /\s/.test(src[pos])) pos++;
222
164
  continue;
223
165
  }
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);
166
+ if (token.type === 'literal') {
167
+ if (!src.startsWith(token.value, pos)) return null;
168
+ pos += token.value.length;
169
+ continue;
170
+ }
171
+ if (token.type === 'var') {
172
+ const nextToken = ti + 1 < tokens.length ? tokens[ti + 1] : null;
173
+ let varValue = '';
174
+ if (nextToken) {
175
+ if (nextToken.type === 'whitespace' || nextToken.type === 'whitespace-optional') {
176
+ while (pos < src.length && !/\s/.test(src[pos])) varValue += src[pos++];
177
+ } else if (nextToken.type === 'literal') {
178
+ const stopChar = nextToken.value[0];
179
+ while (pos < src.length && src[pos] !== stopChar && !/\s/.test(src[pos])) varValue += src[pos++];
180
+ } else if (nextToken.type === 'block') {
181
+ while (pos < src.length && !src.startsWith(nextToken.openDelim, pos) && !/\s/.test(src[pos])) varValue += src[pos++];
182
+ } else {
183
+ while (pos < src.length && !/\s/.test(src[pos])) varValue += src[pos++];
184
+ }
185
+ } else {
186
+ while (pos < src.length && !/\s/.test(src[pos])) varValue += src[pos++];
230
187
  }
231
- i = j;
188
+ if (!varValue) return null;
189
+ captures[this.sigil + token.varName] = varValue;
190
+ continue;
191
+ }
192
+ if (token.type === 'block') {
193
+ const { varName, openDelim, closeDelim } = token;
194
+ if (!src.startsWith(openDelim, pos)) return null;
195
+ const [blockContent, endPos] = this.#extractBlock(src, pos, openDelim, closeDelim);
196
+ captures[this.sigil + varName] = blockContent;
197
+ pos = endPos;
232
198
  continue;
233
199
  }
234
- i++;
235
200
  }
236
- return vars;
201
+ return { captures, endPos: pos };
237
202
  }
238
203
 
239
204
  #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;
205
+ const patterns = [];
206
+ const patRe = new RegExp(`\\b${this.keywords.pattern}\\s*\\${this.open}`, "g");
207
+ let result = src;
208
+ while (true) {
209
+ patRe.lastIndex = 0;
244
210
  const m = patRe.exec(result);
245
211
  if (!m) break;
246
- const start = m.index, openPos = m.index + m[0].length - 1;
212
+ const start = m.index;
213
+ const openPos = m.index + m[0].length - 1;
247
214
  const [matchPat, posAfterMatch] = this.#extractBlock(result, openPos);
248
215
  let k = posAfterMatch;
249
216
  while (k < result.length && /\s/.test(result[k])) k++;
@@ -251,10 +218,9 @@ export class Papagaio {
251
218
  const [replacePat, posAfterReplace] = this.#extractBlock(result, k);
252
219
  patterns.push({ match: matchPat.trim(), replace: replacePat.trim() });
253
220
  result = result.slice(0, start) + result.slice(posAfterReplace);
254
- i = start;
255
221
  continue;
256
222
  }
257
- i = start + 1;
223
+ result = result.slice(0, start) + result.slice(posAfterMatch);
258
224
  }
259
225
  return [patterns, result];
260
226
  }
@@ -262,47 +228,49 @@ export class Papagaio {
262
228
  #applyPatterns(src, patterns) {
263
229
  let clearFlag = false, lastResult = "", S = this.sigil;
264
230
  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 "";
231
+ const tokens = this.#parsePattern(pat.match);
232
+ let newSrc = '';
233
+ let pos = 0, matched = false;
234
+ while (pos < src.length) {
235
+ const matchResult = this.#matchPattern(src, tokens, pos);
236
+ if (matchResult) {
237
+ matched = true;
238
+ const { captures, endPos } = matchResult;
239
+ let result = pat.replace;
240
+ for (const [k, v] of Object.entries(captures)) {
241
+ const keyEsc = this.#escapeRegex(k);
242
+ result = result.replace(new RegExp(keyEsc + '(?![A-Za-z0-9_])', 'g'), v);
289
243
  }
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;
244
+ result = result.replace(new RegExp(`${this.#escapeRegex(S)}unique\\b`, 'g'), () => this.#genUnique());
245
+ result = result.replace(/\$eval\{([^}]*)\}/g, (_, code) => {
246
+ try {
247
+ const wrapped = `"use strict"; return (function() { ${code} })();`;
248
+ return String(Function("papagaio", "ctx", wrapped)(this, {}));
249
+ } catch {
250
+ return "";
251
+ }
252
+ });
253
+ const S2 = S + S;
254
+ result = result.replace(new RegExp(this.#escapeRegex(S2), 'g'), '');
255
+ if (new RegExp(`${this.#escapeRegex(S)}clear\\b`, 'g').test(result)) {
256
+ result = result.replace(new RegExp(`${this.#escapeRegex(S)}clear\\b`, 'g'), '');
257
+ clearFlag = true;
258
+ }
259
+ const matchStart = pos, matchEnd = endPos;
260
+ result = result
261
+ .replace(new RegExp(`${this.#escapeRegex(S)}prefix\\b`, 'g'), src.slice(0, matchStart))
262
+ .replace(new RegExp(`${this.#escapeRegex(S)}suffix\\b`, 'g'), src.slice(matchEnd))
263
+ .replace(new RegExp(`${this.#escapeRegex(S)}match\\b`, 'g'), src.slice(matchStart, matchEnd));
264
+ newSrc += result;
265
+ lastResult = result;
266
+ pos = endPos;
267
+ } else {
268
+ newSrc += src[pos];
269
+ pos++;
296
270
  }
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;
271
+ }
272
+ if (matched) {
273
+ src = clearFlag ? lastResult : newSrc;
306
274
  clearFlag = false;
307
275
  }
308
276
  }
@@ -314,6 +282,24 @@ export class Papagaio {
314
282
  }
315
283
 
316
284
  #escapeRegex(str) {
317
- return str.replace(/[.*+?^${}()|[\]\\\"']/g, '\\$&');
285
+ return str.replace(/[.*+?^${}()|[\]\\""']/g, '\\$&');
286
+ }
287
+
288
+ #unescapeDelimiter(str) {
289
+ let result = '';
290
+ for (let i = 0; i < str.length; i++) {
291
+ if (str[i] === '\\' && i + 1 < str.length) {
292
+ const next = str[i + 1];
293
+ if (next === '"' || next === "'" || next === '\\') {
294
+ result += next;
295
+ i++;
296
+ } else {
297
+ result += str[i];
298
+ }
299
+ } else {
300
+ result += str[i];
301
+ }
302
+ }
303
+ return result;
318
304
  }
319
305
  }
package/tests/tests.json CHANGED
@@ -261,7 +261,7 @@
261
261
  {
262
262
  "id": 44,
263
263
  "name": "Block with double quotes",
264
- "code": "pattern {$block str {\\\"}{\\\"}} {STR[$str]}\n\\\"hello\\\"",
264
+ "code": "pattern {$block str {\"}{\"}} {STR[$str]}\n\"hello\"",
265
265
  "shouldContain": "STR[hello]"
266
266
  },
267
267
  {
@@ -326,7 +326,7 @@
326
326
  },
327
327
  {
328
328
  "id": 55,
329
- "name": "Block captures first simple angle block(known issue)",
329
+ "name": "Block captures first simple angle block",
330
330
  "code": "pattern {$block txt {<}{>}} {CAP[$txt]}\nstart <one> middle <two> end",
331
331
  "shouldContain": "CAP[one]"
332
332
  },
@@ -344,8 +344,8 @@
344
344
  },
345
345
  {
346
346
  "id": 58,
347
- "name": "Dot in variable suffix(known issue)",
348
- "code": "pattern {$file.ext} {file: $file}\nphoto.png",
347
+ "name": "Simple Test",
348
+ "code": "pattern {$file.$ext} {file: $file}\nphoto.png",
349
349
  "shouldContain": "file: photo"
350
350
  },
351
351
  {
@@ -362,7 +362,7 @@
362
362
  },
363
363
  {
364
364
  "id": 61,
365
- "name": "Block with pipe delimiters(known issue)",
365
+ "name": "Block with pipe delimiters",
366
366
  "code": "pattern {$block mid {|}{|}} {PIPE[$mid]}\n|a| and |b|",
367
367
  "shouldContain": "PIPE[a]"
368
368
  },
@@ -380,7 +380,7 @@
380
380
  },
381
381
  {
382
382
  "id": 64,
383
- "name": "Apostrophe in pattern literal(known issue)",
383
+ "name": "Apostrophe in pattern literal",
384
384
  "code": "pattern {It's $name} {Hello $name}\nIt's John",
385
385
  "shouldContain": "Hello John"
386
386
  },
@@ -401,6 +401,18 @@
401
401
  "name": "Literal special regex characters in pattern",
402
402
  "code": "pattern {a.*b} {PAT}\na.*b",
403
403
  "shouldContain": "PAT"
404
+ },
405
+ {
406
+ "id": 68,
407
+ "name": "Block with multi-character delimiters containing double quotes(known issue)",
408
+ "code": "pattern {$block str {\"k}{\"k}} {STR[$str]}\n\"khello\"k",
409
+ "shouldContain": "STR[hello]"
410
+ },
411
+ {
412
+ "id": 69,
413
+ "name": "Block with multi-character delimiters",
414
+ "code": "pattern {$block str {open}{close}} {$str World!}\nopenHelloclose",
415
+ "shouldContain": "Hello World!"
404
416
  }
405
417
  ]
406
418
  }