@wdprlib/parser 3.0.0 → 3.1.1

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/dist/index.cjs CHANGED
@@ -9129,6 +9129,237 @@ function mapElementChildrenWithState(element, state, transform) {
9129
9129
  return { element, state };
9130
9130
  }
9131
9131
 
9132
+ // packages/parser/src/parser/rules/block/module/iftags/condition.ts
9133
+ function parseTagCondition(condition) {
9134
+ const required = [];
9135
+ const forbidden = [];
9136
+ const optional = [];
9137
+ const parts = condition.trim().split(/\s+/);
9138
+ for (const part of parts) {
9139
+ if (!part)
9140
+ continue;
9141
+ if (part.startsWith("+")) {
9142
+ const tag = part.slice(1);
9143
+ if (tag)
9144
+ required.push(tag);
9145
+ } else if (part.startsWith("-")) {
9146
+ const tag = part.slice(1);
9147
+ if (tag)
9148
+ forbidden.push(tag);
9149
+ } else {
9150
+ optional.push(part);
9151
+ }
9152
+ }
9153
+ return { required, forbidden, optional };
9154
+ }
9155
+ function evaluateTagCondition(condition, pageTags) {
9156
+ if (condition.required.length === 0 && condition.forbidden.length === 0 && condition.optional.length === 0) {
9157
+ return false;
9158
+ }
9159
+ const tagSet = new Set(pageTags);
9160
+ for (const tag of condition.required) {
9161
+ if (!tagSet.has(tag)) {
9162
+ return false;
9163
+ }
9164
+ }
9165
+ for (const tag of condition.forbidden) {
9166
+ if (tagSet.has(tag)) {
9167
+ return false;
9168
+ }
9169
+ }
9170
+ if (condition.optional.length > 0) {
9171
+ if (!condition.optional.some((tag) => tagSet.has(tag))) {
9172
+ return false;
9173
+ }
9174
+ }
9175
+ return true;
9176
+ }
9177
+
9178
+ // packages/parser/src/parser/rules/block/module/iftags/preprocess.ts
9179
+ var BASE_PLACEHOLDER_OPEN = "";
9180
+ var BASE_PLACEHOLDER_CLOSE = "";
9181
+ var INNERMOST_IFTAGS_PATTERN = /\[\[\s*iftags\b([^\]]*)\]\]((?:(?!\[\[\s*iftags\b|\[\[\/\s*iftags\s*\]\]).)*)\[\[\/\s*iftags\s*\]\]/gis;
9182
+ var RAW_BLOCK_OPEN_PATTERN = /\[\[\s*(code|html)\b[^\]]*\]\]/iy;
9183
+ function preprocessIftags(source, pageTags) {
9184
+ if (!source.includes("[["))
9185
+ return source;
9186
+ const sentinels = makeUniqueSentinels(source);
9187
+ const { masked, placeholders } = maskRawRegions(source, sentinels);
9188
+ const reduced = reduceIftags(masked, pageTags);
9189
+ return restorePlaceholders(reduced, placeholders, sentinels);
9190
+ }
9191
+ function makeUniqueSentinels(source) {
9192
+ let open = BASE_PLACEHOLDER_OPEN;
9193
+ let close = BASE_PLACEHOLDER_CLOSE;
9194
+ while (source.includes(open) || source.includes(close)) {
9195
+ open += BASE_PLACEHOLDER_OPEN;
9196
+ close += BASE_PLACEHOLDER_CLOSE;
9197
+ }
9198
+ return { open, close };
9199
+ }
9200
+ function reduceIftags(source, pageTags) {
9201
+ let current = source;
9202
+ const maxIterations = source.length + 1;
9203
+ const tagSet = pageTags ?? [];
9204
+ for (let i = 0;i < maxIterations; i++) {
9205
+ const depths = pageTags === null ? computeBracketDepths(current) : null;
9206
+ let changed = false;
9207
+ const next = current.replace(INNERMOST_IFTAGS_PATTERN, (match, cond, body, offset) => {
9208
+ if (depths !== null && depths[offset] === 0) {
9209
+ return match;
9210
+ }
9211
+ changed = true;
9212
+ const condition = parseTagCondition(cond);
9213
+ return evaluateTagCondition(condition, tagSet) ? body : "";
9214
+ });
9215
+ if (!changed)
9216
+ return current;
9217
+ current = next;
9218
+ }
9219
+ return current;
9220
+ }
9221
+ function computeBracketDepths(masked) {
9222
+ const n = masked.length;
9223
+ const depths = new Int32Array(n + 1);
9224
+ let depth = 0;
9225
+ let i = 0;
9226
+ while (i < n) {
9227
+ depths[i] = depth;
9228
+ const c = masked.charCodeAt(i);
9229
+ const c1 = i + 1 < n ? masked.charCodeAt(i + 1) : -1;
9230
+ const c2 = i + 2 < n ? masked.charCodeAt(i + 2) : -1;
9231
+ if (depth > 0 && c === 34 && precededByEqualsAttr(masked, i)) {
9232
+ const end = findQuoteEnd(masked, i + 1);
9233
+ for (let k = i;k <= end; k++)
9234
+ depths[k] = depth;
9235
+ i = end + 1;
9236
+ continue;
9237
+ }
9238
+ if (c === 91 && c1 === 91 && c2 === 91) {
9239
+ const end = findTripleLinkEnd(masked, i + 3);
9240
+ for (let k = i;k <= end; k++)
9241
+ depths[k] = depth;
9242
+ i = end + 1;
9243
+ continue;
9244
+ }
9245
+ if (c === 91 && c1 === 91) {
9246
+ depth++;
9247
+ depths[i + 1] = depth;
9248
+ i += 2;
9249
+ continue;
9250
+ }
9251
+ if (c === 93 && c1 === 93) {
9252
+ depth = Math.max(0, depth - 1);
9253
+ depths[i + 1] = depth;
9254
+ i += 2;
9255
+ continue;
9256
+ }
9257
+ if (c === 10) {
9258
+ depth = 0;
9259
+ }
9260
+ i++;
9261
+ }
9262
+ depths[n] = depth;
9263
+ return depths;
9264
+ }
9265
+ function precededByEqualsAttr(s, i) {
9266
+ let j = i - 1;
9267
+ while (j >= 0) {
9268
+ const ch = s.charCodeAt(j);
9269
+ if (ch === 32 || ch === 9) {
9270
+ j--;
9271
+ continue;
9272
+ }
9273
+ return ch === 61;
9274
+ }
9275
+ return false;
9276
+ }
9277
+ function findQuoteEnd(s, from) {
9278
+ for (let i = from;i < s.length; i++) {
9279
+ const ch = s.charCodeAt(i);
9280
+ if (ch === 34 || ch === 10)
9281
+ return i;
9282
+ }
9283
+ return s.length - 1;
9284
+ }
9285
+ function findTripleLinkEnd(s, from) {
9286
+ for (let i = from;i < s.length; i++) {
9287
+ if (s.charCodeAt(i) === 93 && i + 2 < s.length && s.charCodeAt(i + 1) === 93 && s.charCodeAt(i + 2) === 93) {
9288
+ return i + 2;
9289
+ }
9290
+ if (s.charCodeAt(i) === 10 && i + 1 < s.length && s.charCodeAt(i + 1) === 10) {
9291
+ return i;
9292
+ }
9293
+ }
9294
+ return s.length - 1;
9295
+ }
9296
+ function maskRawRegions(source, sentinels) {
9297
+ const placeholders = [];
9298
+ let masked = "";
9299
+ let i = 0;
9300
+ while (i < source.length) {
9301
+ if (source[i] === "[" && source[i + 1] === "[") {
9302
+ RAW_BLOCK_OPEN_PATTERN.lastIndex = i;
9303
+ const openMatch = RAW_BLOCK_OPEN_PATTERN.exec(source);
9304
+ if (openMatch) {
9305
+ const name = openMatch[1].toLowerCase();
9306
+ const openLen = openMatch[0].length;
9307
+ const closePattern = new RegExp(`\\[\\[\\/\\s*${name}\\s*\\]\\]`, "ig");
9308
+ closePattern.lastIndex = i + openLen;
9309
+ const closeMatch = closePattern.exec(source);
9310
+ if (closeMatch) {
9311
+ const regionEnd = closeMatch.index + closeMatch[0].length;
9312
+ masked += pushPlaceholder(placeholders, source.slice(i, regionEnd), sentinels);
9313
+ i = regionEnd;
9314
+ continue;
9315
+ }
9316
+ if (name === "code") {
9317
+ masked += pushPlaceholder(placeholders, source.slice(i), sentinels);
9318
+ i = source.length;
9319
+ continue;
9320
+ }
9321
+ }
9322
+ }
9323
+ if (source[i] === "@" && source[i + 1] === "<") {
9324
+ const close = source.indexOf(">@", i + 2);
9325
+ const newline = source.indexOf(`
9326
+ `, i + 2);
9327
+ if (close !== -1 && (newline === -1 || close < newline)) {
9328
+ const regionEnd = close + 2;
9329
+ masked += pushPlaceholder(placeholders, source.slice(i, regionEnd), sentinels);
9330
+ i = regionEnd;
9331
+ continue;
9332
+ }
9333
+ }
9334
+ if (source[i] === "@" && source[i + 1] === "@") {
9335
+ const close = source.indexOf("@@", i + 2);
9336
+ const newline = source.indexOf(`
9337
+ `, i + 2);
9338
+ if (close !== -1 && (newline === -1 || close < newline)) {
9339
+ const regionEnd = close + 2;
9340
+ masked += pushPlaceholder(placeholders, source.slice(i, regionEnd), sentinels);
9341
+ i = regionEnd;
9342
+ continue;
9343
+ }
9344
+ }
9345
+ masked += source[i];
9346
+ i++;
9347
+ }
9348
+ return { masked, placeholders };
9349
+ }
9350
+ function pushPlaceholder(placeholders, text, sentinels) {
9351
+ const idx = placeholders.length;
9352
+ placeholders.push(text);
9353
+ return `${sentinels.open}${idx}${sentinels.close}`;
9354
+ }
9355
+ function escapeRegex(str) {
9356
+ return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
9357
+ }
9358
+ function restorePlaceholders(source, placeholders, sentinels) {
9359
+ const pattern = new RegExp(`${escapeRegex(sentinels.open)}(\\d+)${escapeRegex(sentinels.close)}`, "g");
9360
+ return source.replace(pattern, (_, idx) => placeholders[Number(idx)] ?? "");
9361
+ }
9362
+
9132
9363
  // packages/parser/src/parser/parse.ts
9133
9364
  class Parser {
9134
9365
  ctx;
@@ -9237,7 +9468,8 @@ class Parser {
9237
9468
  }
9238
9469
  }
9239
9470
  function parse(source, options) {
9240
- const preprocessed = preprocess(source);
9471
+ const iftagsProcessed = options?.pageTags !== undefined ? preprocessIftags(source, options.pageTags) : source;
9472
+ const preprocessed = preprocess(iftagsProcessed);
9241
9473
  const tokens = tokenize(preprocessed, { trackPositions: options?.trackPositions });
9242
9474
  return new Parser(tokens, options).parse();
9243
9475
  }
@@ -10150,51 +10382,6 @@ function resolveAndNormalizeQuery(requirement, urlParams) {
10150
10382
  const resolved = resolveQuery(requirement, urlParams);
10151
10383
  return normalizeQuery(resolved);
10152
10384
  }
10153
- // packages/parser/src/parser/rules/block/module/iftags/condition.ts
10154
- function parseTagCondition(condition) {
10155
- const required = [];
10156
- const forbidden = [];
10157
- const optional = [];
10158
- const parts = condition.trim().split(/\s+/);
10159
- for (const part of parts) {
10160
- if (!part)
10161
- continue;
10162
- if (part.startsWith("+")) {
10163
- const tag = part.slice(1);
10164
- if (tag)
10165
- required.push(tag);
10166
- } else if (part.startsWith("-")) {
10167
- const tag = part.slice(1);
10168
- if (tag)
10169
- forbidden.push(tag);
10170
- } else {
10171
- optional.push(part);
10172
- }
10173
- }
10174
- return { required, forbidden, optional };
10175
- }
10176
- function evaluateTagCondition(condition, pageTags) {
10177
- if (condition.required.length === 0 && condition.forbidden.length === 0 && condition.optional.length === 0) {
10178
- return false;
10179
- }
10180
- const tagSet = new Set(pageTags);
10181
- for (const tag of condition.required) {
10182
- if (!tagSet.has(tag)) {
10183
- return false;
10184
- }
10185
- }
10186
- for (const tag of condition.forbidden) {
10187
- if (tagSet.has(tag)) {
10188
- return false;
10189
- }
10190
- }
10191
- if (condition.optional.length > 0) {
10192
- if (!condition.optional.some((tag) => tagSet.has(tag))) {
10193
- return false;
10194
- }
10195
- }
10196
- return true;
10197
- }
10198
10385
  // packages/parser/src/parser/rules/block/module/iftags/resolve.ts
10199
10386
  function isIfTagsElement(element) {
10200
10387
  return element.element === "if-tags";
@@ -10207,110 +10394,6 @@ function resolveIfTags(data, pageTags) {
10207
10394
  const matched = evaluateTagCondition(condition, pageTags);
10208
10395
  return { evaluated: true, matched };
10209
10396
  }
10210
- // packages/parser/src/parser/rules/block/module/iftags/preprocess.ts
10211
- var BASE_PLACEHOLDER_OPEN = "";
10212
- var BASE_PLACEHOLDER_CLOSE = "";
10213
- var INNERMOST_IFTAGS_PATTERN = /\[\[\s*iftags\b([^\]]*)\]\]((?:(?!\[\[\s*iftags\b|\[\[\/\s*iftags\s*\]\]).)*)\[\[\/\s*iftags\s*\]\]/gis;
10214
- var RAW_BLOCK_OPEN_PATTERN = /\[\[\s*(code|html)\b[^\]]*\]\]/iy;
10215
- function preprocessIftags(source, pageTags) {
10216
- if (pageTags === null)
10217
- return source;
10218
- if (!source.includes("[["))
10219
- return source;
10220
- const sentinels = makeUniqueSentinels(source);
10221
- const { masked, placeholders } = maskRawRegions(source, sentinels);
10222
- const reduced = reduceIftags(masked, pageTags);
10223
- return restorePlaceholders(reduced, placeholders, sentinels);
10224
- }
10225
- function makeUniqueSentinels(source) {
10226
- let open = BASE_PLACEHOLDER_OPEN;
10227
- let close = BASE_PLACEHOLDER_CLOSE;
10228
- while (source.includes(open) || source.includes(close)) {
10229
- open += BASE_PLACEHOLDER_OPEN;
10230
- close += BASE_PLACEHOLDER_CLOSE;
10231
- }
10232
- return { open, close };
10233
- }
10234
- function reduceIftags(source, pageTags) {
10235
- let current = source;
10236
- const maxIterations = source.length + 1;
10237
- for (let i = 0;i < maxIterations; i++) {
10238
- const next = current.replace(INNERMOST_IFTAGS_PATTERN, (_, cond, body) => {
10239
- const condition = parseTagCondition(cond);
10240
- return evaluateTagCondition(condition, pageTags) ? body : "";
10241
- });
10242
- if (next === current)
10243
- return current;
10244
- current = next;
10245
- }
10246
- return current;
10247
- }
10248
- function maskRawRegions(source, sentinels) {
10249
- const placeholders = [];
10250
- let masked = "";
10251
- let i = 0;
10252
- while (i < source.length) {
10253
- if (source[i] === "[" && source[i + 1] === "[") {
10254
- RAW_BLOCK_OPEN_PATTERN.lastIndex = i;
10255
- const openMatch = RAW_BLOCK_OPEN_PATTERN.exec(source);
10256
- if (openMatch) {
10257
- const name = openMatch[1].toLowerCase();
10258
- const openLen = openMatch[0].length;
10259
- const closePattern = new RegExp(`\\[\\[\\/\\s*${name}\\s*\\]\\]`, "ig");
10260
- closePattern.lastIndex = i + openLen;
10261
- const closeMatch = closePattern.exec(source);
10262
- if (closeMatch) {
10263
- const regionEnd = closeMatch.index + closeMatch[0].length;
10264
- masked += pushPlaceholder(placeholders, source.slice(i, regionEnd), sentinels);
10265
- i = regionEnd;
10266
- continue;
10267
- }
10268
- if (name === "code") {
10269
- masked += pushPlaceholder(placeholders, source.slice(i), sentinels);
10270
- i = source.length;
10271
- continue;
10272
- }
10273
- }
10274
- }
10275
- if (source[i] === "@" && source[i + 1] === "<") {
10276
- const close = source.indexOf(">@", i + 2);
10277
- const newline = source.indexOf(`
10278
- `, i + 2);
10279
- if (close !== -1 && (newline === -1 || close < newline)) {
10280
- const regionEnd = close + 2;
10281
- masked += pushPlaceholder(placeholders, source.slice(i, regionEnd), sentinels);
10282
- i = regionEnd;
10283
- continue;
10284
- }
10285
- }
10286
- if (source[i] === "@" && source[i + 1] === "@") {
10287
- const close = source.indexOf("@@", i + 2);
10288
- const newline = source.indexOf(`
10289
- `, i + 2);
10290
- if (close !== -1 && (newline === -1 || close < newline)) {
10291
- const regionEnd = close + 2;
10292
- masked += pushPlaceholder(placeholders, source.slice(i, regionEnd), sentinels);
10293
- i = regionEnd;
10294
- continue;
10295
- }
10296
- }
10297
- masked += source[i];
10298
- i++;
10299
- }
10300
- return { masked, placeholders };
10301
- }
10302
- function pushPlaceholder(placeholders, text, sentinels) {
10303
- const idx = placeholders.length;
10304
- placeholders.push(text);
10305
- return `${sentinels.open}${idx}${sentinels.close}`;
10306
- }
10307
- function escapeRegex(str) {
10308
- return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
10309
- }
10310
- function restorePlaceholders(source, placeholders, sentinels) {
10311
- const pattern = new RegExp(`${escapeRegex(sentinels.open)}(\\d+)${escapeRegex(sentinels.close)}`, "g");
10312
- return source.replace(pattern, (_, idx) => placeholders[Number(idx)] ?? "");
10313
- }
10314
10397
  // packages/parser/src/parser/rules/block/module/include/resolve.ts
10315
10398
  function resolveIncludes(source, fetcher, options) {
10316
10399
  if (options?.settings && !options.settings.enablePageSyntax) {
@@ -10363,8 +10446,9 @@ function isRestOfLineBlank(source, pos) {
10363
10446
  if (ch === `
10364
10447
  `)
10365
10448
  return true;
10366
- if (ch !== " " && ch !== "\t" && ch !== "\r")
10367
- return false;
10449
+ if (ch === " " || ch === "\t" || ch === "\r" || ch === "]")
10450
+ continue;
10451
+ return false;
10368
10452
  }
10369
10453
  return true;
10370
10454
  }
package/dist/index.d.cts CHANGED
@@ -169,6 +169,25 @@ interface ParserOptions {
169
169
  * Defaults to {@link DEFAULT_SETTINGS} (full page mode).
170
170
  */
171
171
  settings?: WikitextSettings;
172
+ /**
173
+ * Page tags consulted when expanding `[[iftags]]` directives that are
174
+ * embedded inside another block's opener — e.g.
175
+ * `[[div_ class="x" [[iftags +foo]]style="..."[[/iftags]]]]`. Such
176
+ * iftags must be collapsed at text level *before* tokenization;
177
+ * otherwise the outer opener loses its well-formed structure and the
178
+ * whole `[[div_ ... ]]` line is emitted as raw text.
179
+ *
180
+ * Values:
181
+ * - omitted / `undefined`: no preprocess pass. Existing behaviour;
182
+ * opener-embedded iftags will still break the surrounding opener.
183
+ * - `null`: opener-embedded iftags only, evaluated as if the page has
184
+ * no tags (lossy fallback that keeps tokenization working when real
185
+ * tags are unknown). Block-level iftags remain in the AST for
186
+ * `resolveModules` to evaluate later via `getPageTags`.
187
+ * - `string[]`: every iftags block is evaluated against the given
188
+ * tags eagerly; no `if-tags` nodes survive in the AST.
189
+ */
190
+ pageTags?: string[] | null;
172
191
  }
173
192
  /**
174
193
  * Converts a token stream into a Wikidot {@link SyntaxTree}.
@@ -861,19 +880,19 @@ declare function normalizeQuery(query: ListPagesQuery): NormalizedListPagesQuery
861
880
  * Expand `[[iftags ...]]X[[/iftags]]` directives in `source` against the
862
881
  * current page's tags.
863
882
  *
864
- * Returns `source` unchanged when `pageTags` is `null`, which signals
865
- * that the caller could not resolve tag membership (e.g. rendering a
866
- * draft preview without a real page). In that case the AST-level
867
- * resolver remains responsible for evaluation later.
868
- *
869
883
  * Behaviour:
870
884
  * - Raw regions (`[[code]]`, `[[html]]`, `@@...@@`, `@<...>@`) are
871
885
  * protected: literal `[[iftags]]` tokens inside them are not expanded.
872
886
  * - Nested `[[iftags]]` are processed innermost-first, so an outer
873
887
  * block can re-process the now-flattened inner body uniformly.
888
+ * - `pageTags === null`: only `[[iftags]]` blocks embedded inside
889
+ * another block's opener are collapsed (using an empty-tag fallback
890
+ * so `+tag` conditions fail and `-tag` conditions pass). Block-level
891
+ * iftags are left intact for the AST-level resolver.
874
892
  *
875
893
  * @param source Raw wikitext (typically after include expansion).
876
- * @param pageTags Tags of the page being rendered, or `null` to skip.
894
+ * @param pageTags Tags of the page being rendered, or `null` for the
895
+ * opener-embedded-only fallback mode.
877
896
  * @returns Source with matching iftags replaced by their bodies and
878
897
  * unmatched iftags removed entirely.
879
898
  */
package/dist/index.d.ts CHANGED
@@ -169,6 +169,25 @@ interface ParserOptions {
169
169
  * Defaults to {@link DEFAULT_SETTINGS} (full page mode).
170
170
  */
171
171
  settings?: WikitextSettings;
172
+ /**
173
+ * Page tags consulted when expanding `[[iftags]]` directives that are
174
+ * embedded inside another block's opener — e.g.
175
+ * `[[div_ class="x" [[iftags +foo]]style="..."[[/iftags]]]]`. Such
176
+ * iftags must be collapsed at text level *before* tokenization;
177
+ * otherwise the outer opener loses its well-formed structure and the
178
+ * whole `[[div_ ... ]]` line is emitted as raw text.
179
+ *
180
+ * Values:
181
+ * - omitted / `undefined`: no preprocess pass. Existing behaviour;
182
+ * opener-embedded iftags will still break the surrounding opener.
183
+ * - `null`: opener-embedded iftags only, evaluated as if the page has
184
+ * no tags (lossy fallback that keeps tokenization working when real
185
+ * tags are unknown). Block-level iftags remain in the AST for
186
+ * `resolveModules` to evaluate later via `getPageTags`.
187
+ * - `string[]`: every iftags block is evaluated against the given
188
+ * tags eagerly; no `if-tags` nodes survive in the AST.
189
+ */
190
+ pageTags?: string[] | null;
172
191
  }
173
192
  /**
174
193
  * Converts a token stream into a Wikidot {@link SyntaxTree}.
@@ -861,19 +880,19 @@ declare function normalizeQuery(query: ListPagesQuery): NormalizedListPagesQuery
861
880
  * Expand `[[iftags ...]]X[[/iftags]]` directives in `source` against the
862
881
  * current page's tags.
863
882
  *
864
- * Returns `source` unchanged when `pageTags` is `null`, which signals
865
- * that the caller could not resolve tag membership (e.g. rendering a
866
- * draft preview without a real page). In that case the AST-level
867
- * resolver remains responsible for evaluation later.
868
- *
869
883
  * Behaviour:
870
884
  * - Raw regions (`[[code]]`, `[[html]]`, `@@...@@`, `@<...>@`) are
871
885
  * protected: literal `[[iftags]]` tokens inside them are not expanded.
872
886
  * - Nested `[[iftags]]` are processed innermost-first, so an outer
873
887
  * block can re-process the now-flattened inner body uniformly.
888
+ * - `pageTags === null`: only `[[iftags]]` blocks embedded inside
889
+ * another block's opener are collapsed (using an empty-tag fallback
890
+ * so `+tag` conditions fail and `-tag` conditions pass). Block-level
891
+ * iftags are left intact for the AST-level resolver.
874
892
  *
875
893
  * @param source Raw wikitext (typically after include expansion).
876
- * @param pageTags Tags of the page being rendered, or `null` to skip.
894
+ * @param pageTags Tags of the page being rendered, or `null` for the
895
+ * opener-embedded-only fallback mode.
877
896
  * @returns Source with matching iftags replaced by their bodies and
878
897
  * unmatched iftags removed entirely.
879
898
  */
package/dist/index.js CHANGED
@@ -9072,6 +9072,237 @@ function mapElementChildrenWithState(element, state, transform) {
9072
9072
  return { element, state };
9073
9073
  }
9074
9074
 
9075
+ // packages/parser/src/parser/rules/block/module/iftags/condition.ts
9076
+ function parseTagCondition(condition) {
9077
+ const required = [];
9078
+ const forbidden = [];
9079
+ const optional = [];
9080
+ const parts = condition.trim().split(/\s+/);
9081
+ for (const part of parts) {
9082
+ if (!part)
9083
+ continue;
9084
+ if (part.startsWith("+")) {
9085
+ const tag = part.slice(1);
9086
+ if (tag)
9087
+ required.push(tag);
9088
+ } else if (part.startsWith("-")) {
9089
+ const tag = part.slice(1);
9090
+ if (tag)
9091
+ forbidden.push(tag);
9092
+ } else {
9093
+ optional.push(part);
9094
+ }
9095
+ }
9096
+ return { required, forbidden, optional };
9097
+ }
9098
+ function evaluateTagCondition(condition, pageTags) {
9099
+ if (condition.required.length === 0 && condition.forbidden.length === 0 && condition.optional.length === 0) {
9100
+ return false;
9101
+ }
9102
+ const tagSet = new Set(pageTags);
9103
+ for (const tag of condition.required) {
9104
+ if (!tagSet.has(tag)) {
9105
+ return false;
9106
+ }
9107
+ }
9108
+ for (const tag of condition.forbidden) {
9109
+ if (tagSet.has(tag)) {
9110
+ return false;
9111
+ }
9112
+ }
9113
+ if (condition.optional.length > 0) {
9114
+ if (!condition.optional.some((tag) => tagSet.has(tag))) {
9115
+ return false;
9116
+ }
9117
+ }
9118
+ return true;
9119
+ }
9120
+
9121
+ // packages/parser/src/parser/rules/block/module/iftags/preprocess.ts
9122
+ var BASE_PLACEHOLDER_OPEN = "";
9123
+ var BASE_PLACEHOLDER_CLOSE = "";
9124
+ var INNERMOST_IFTAGS_PATTERN = /\[\[\s*iftags\b([^\]]*)\]\]((?:(?!\[\[\s*iftags\b|\[\[\/\s*iftags\s*\]\]).)*)\[\[\/\s*iftags\s*\]\]/gis;
9125
+ var RAW_BLOCK_OPEN_PATTERN = /\[\[\s*(code|html)\b[^\]]*\]\]/iy;
9126
+ function preprocessIftags(source, pageTags) {
9127
+ if (!source.includes("[["))
9128
+ return source;
9129
+ const sentinels = makeUniqueSentinels(source);
9130
+ const { masked, placeholders } = maskRawRegions(source, sentinels);
9131
+ const reduced = reduceIftags(masked, pageTags);
9132
+ return restorePlaceholders(reduced, placeholders, sentinels);
9133
+ }
9134
+ function makeUniqueSentinels(source) {
9135
+ let open = BASE_PLACEHOLDER_OPEN;
9136
+ let close = BASE_PLACEHOLDER_CLOSE;
9137
+ while (source.includes(open) || source.includes(close)) {
9138
+ open += BASE_PLACEHOLDER_OPEN;
9139
+ close += BASE_PLACEHOLDER_CLOSE;
9140
+ }
9141
+ return { open, close };
9142
+ }
9143
+ function reduceIftags(source, pageTags) {
9144
+ let current = source;
9145
+ const maxIterations = source.length + 1;
9146
+ const tagSet = pageTags ?? [];
9147
+ for (let i = 0;i < maxIterations; i++) {
9148
+ const depths = pageTags === null ? computeBracketDepths(current) : null;
9149
+ let changed = false;
9150
+ const next = current.replace(INNERMOST_IFTAGS_PATTERN, (match, cond, body, offset) => {
9151
+ if (depths !== null && depths[offset] === 0) {
9152
+ return match;
9153
+ }
9154
+ changed = true;
9155
+ const condition = parseTagCondition(cond);
9156
+ return evaluateTagCondition(condition, tagSet) ? body : "";
9157
+ });
9158
+ if (!changed)
9159
+ return current;
9160
+ current = next;
9161
+ }
9162
+ return current;
9163
+ }
9164
+ function computeBracketDepths(masked) {
9165
+ const n = masked.length;
9166
+ const depths = new Int32Array(n + 1);
9167
+ let depth = 0;
9168
+ let i = 0;
9169
+ while (i < n) {
9170
+ depths[i] = depth;
9171
+ const c = masked.charCodeAt(i);
9172
+ const c1 = i + 1 < n ? masked.charCodeAt(i + 1) : -1;
9173
+ const c2 = i + 2 < n ? masked.charCodeAt(i + 2) : -1;
9174
+ if (depth > 0 && c === 34 && precededByEqualsAttr(masked, i)) {
9175
+ const end = findQuoteEnd(masked, i + 1);
9176
+ for (let k = i;k <= end; k++)
9177
+ depths[k] = depth;
9178
+ i = end + 1;
9179
+ continue;
9180
+ }
9181
+ if (c === 91 && c1 === 91 && c2 === 91) {
9182
+ const end = findTripleLinkEnd(masked, i + 3);
9183
+ for (let k = i;k <= end; k++)
9184
+ depths[k] = depth;
9185
+ i = end + 1;
9186
+ continue;
9187
+ }
9188
+ if (c === 91 && c1 === 91) {
9189
+ depth++;
9190
+ depths[i + 1] = depth;
9191
+ i += 2;
9192
+ continue;
9193
+ }
9194
+ if (c === 93 && c1 === 93) {
9195
+ depth = Math.max(0, depth - 1);
9196
+ depths[i + 1] = depth;
9197
+ i += 2;
9198
+ continue;
9199
+ }
9200
+ if (c === 10) {
9201
+ depth = 0;
9202
+ }
9203
+ i++;
9204
+ }
9205
+ depths[n] = depth;
9206
+ return depths;
9207
+ }
9208
+ function precededByEqualsAttr(s, i) {
9209
+ let j = i - 1;
9210
+ while (j >= 0) {
9211
+ const ch = s.charCodeAt(j);
9212
+ if (ch === 32 || ch === 9) {
9213
+ j--;
9214
+ continue;
9215
+ }
9216
+ return ch === 61;
9217
+ }
9218
+ return false;
9219
+ }
9220
+ function findQuoteEnd(s, from) {
9221
+ for (let i = from;i < s.length; i++) {
9222
+ const ch = s.charCodeAt(i);
9223
+ if (ch === 34 || ch === 10)
9224
+ return i;
9225
+ }
9226
+ return s.length - 1;
9227
+ }
9228
+ function findTripleLinkEnd(s, from) {
9229
+ for (let i = from;i < s.length; i++) {
9230
+ if (s.charCodeAt(i) === 93 && i + 2 < s.length && s.charCodeAt(i + 1) === 93 && s.charCodeAt(i + 2) === 93) {
9231
+ return i + 2;
9232
+ }
9233
+ if (s.charCodeAt(i) === 10 && i + 1 < s.length && s.charCodeAt(i + 1) === 10) {
9234
+ return i;
9235
+ }
9236
+ }
9237
+ return s.length - 1;
9238
+ }
9239
+ function maskRawRegions(source, sentinels) {
9240
+ const placeholders = [];
9241
+ let masked = "";
9242
+ let i = 0;
9243
+ while (i < source.length) {
9244
+ if (source[i] === "[" && source[i + 1] === "[") {
9245
+ RAW_BLOCK_OPEN_PATTERN.lastIndex = i;
9246
+ const openMatch = RAW_BLOCK_OPEN_PATTERN.exec(source);
9247
+ if (openMatch) {
9248
+ const name = openMatch[1].toLowerCase();
9249
+ const openLen = openMatch[0].length;
9250
+ const closePattern = new RegExp(`\\[\\[\\/\\s*${name}\\s*\\]\\]`, "ig");
9251
+ closePattern.lastIndex = i + openLen;
9252
+ const closeMatch = closePattern.exec(source);
9253
+ if (closeMatch) {
9254
+ const regionEnd = closeMatch.index + closeMatch[0].length;
9255
+ masked += pushPlaceholder(placeholders, source.slice(i, regionEnd), sentinels);
9256
+ i = regionEnd;
9257
+ continue;
9258
+ }
9259
+ if (name === "code") {
9260
+ masked += pushPlaceholder(placeholders, source.slice(i), sentinels);
9261
+ i = source.length;
9262
+ continue;
9263
+ }
9264
+ }
9265
+ }
9266
+ if (source[i] === "@" && source[i + 1] === "<") {
9267
+ const close = source.indexOf(">@", i + 2);
9268
+ const newline = source.indexOf(`
9269
+ `, i + 2);
9270
+ if (close !== -1 && (newline === -1 || close < newline)) {
9271
+ const regionEnd = close + 2;
9272
+ masked += pushPlaceholder(placeholders, source.slice(i, regionEnd), sentinels);
9273
+ i = regionEnd;
9274
+ continue;
9275
+ }
9276
+ }
9277
+ if (source[i] === "@" && source[i + 1] === "@") {
9278
+ const close = source.indexOf("@@", i + 2);
9279
+ const newline = source.indexOf(`
9280
+ `, i + 2);
9281
+ if (close !== -1 && (newline === -1 || close < newline)) {
9282
+ const regionEnd = close + 2;
9283
+ masked += pushPlaceholder(placeholders, source.slice(i, regionEnd), sentinels);
9284
+ i = regionEnd;
9285
+ continue;
9286
+ }
9287
+ }
9288
+ masked += source[i];
9289
+ i++;
9290
+ }
9291
+ return { masked, placeholders };
9292
+ }
9293
+ function pushPlaceholder(placeholders, text, sentinels) {
9294
+ const idx = placeholders.length;
9295
+ placeholders.push(text);
9296
+ return `${sentinels.open}${idx}${sentinels.close}`;
9297
+ }
9298
+ function escapeRegex(str) {
9299
+ return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
9300
+ }
9301
+ function restorePlaceholders(source, placeholders, sentinels) {
9302
+ const pattern = new RegExp(`${escapeRegex(sentinels.open)}(\\d+)${escapeRegex(sentinels.close)}`, "g");
9303
+ return source.replace(pattern, (_, idx) => placeholders[Number(idx)] ?? "");
9304
+ }
9305
+
9075
9306
  // packages/parser/src/parser/parse.ts
9076
9307
  class Parser {
9077
9308
  ctx;
@@ -9180,7 +9411,8 @@ class Parser {
9180
9411
  }
9181
9412
  }
9182
9413
  function parse(source, options) {
9183
- const preprocessed = preprocess(source);
9414
+ const iftagsProcessed = options?.pageTags !== undefined ? preprocessIftags(source, options.pageTags) : source;
9415
+ const preprocessed = preprocess(iftagsProcessed);
9184
9416
  const tokens = tokenize(preprocessed, { trackPositions: options?.trackPositions });
9185
9417
  return new Parser(tokens, options).parse();
9186
9418
  }
@@ -10093,51 +10325,6 @@ function resolveAndNormalizeQuery(requirement, urlParams) {
10093
10325
  const resolved = resolveQuery(requirement, urlParams);
10094
10326
  return normalizeQuery(resolved);
10095
10327
  }
10096
- // packages/parser/src/parser/rules/block/module/iftags/condition.ts
10097
- function parseTagCondition(condition) {
10098
- const required = [];
10099
- const forbidden = [];
10100
- const optional = [];
10101
- const parts = condition.trim().split(/\s+/);
10102
- for (const part of parts) {
10103
- if (!part)
10104
- continue;
10105
- if (part.startsWith("+")) {
10106
- const tag = part.slice(1);
10107
- if (tag)
10108
- required.push(tag);
10109
- } else if (part.startsWith("-")) {
10110
- const tag = part.slice(1);
10111
- if (tag)
10112
- forbidden.push(tag);
10113
- } else {
10114
- optional.push(part);
10115
- }
10116
- }
10117
- return { required, forbidden, optional };
10118
- }
10119
- function evaluateTagCondition(condition, pageTags) {
10120
- if (condition.required.length === 0 && condition.forbidden.length === 0 && condition.optional.length === 0) {
10121
- return false;
10122
- }
10123
- const tagSet = new Set(pageTags);
10124
- for (const tag of condition.required) {
10125
- if (!tagSet.has(tag)) {
10126
- return false;
10127
- }
10128
- }
10129
- for (const tag of condition.forbidden) {
10130
- if (tagSet.has(tag)) {
10131
- return false;
10132
- }
10133
- }
10134
- if (condition.optional.length > 0) {
10135
- if (!condition.optional.some((tag) => tagSet.has(tag))) {
10136
- return false;
10137
- }
10138
- }
10139
- return true;
10140
- }
10141
10328
  // packages/parser/src/parser/rules/block/module/iftags/resolve.ts
10142
10329
  function isIfTagsElement(element) {
10143
10330
  return element.element === "if-tags";
@@ -10150,110 +10337,6 @@ function resolveIfTags(data, pageTags) {
10150
10337
  const matched = evaluateTagCondition(condition, pageTags);
10151
10338
  return { evaluated: true, matched };
10152
10339
  }
10153
- // packages/parser/src/parser/rules/block/module/iftags/preprocess.ts
10154
- var BASE_PLACEHOLDER_OPEN = "";
10155
- var BASE_PLACEHOLDER_CLOSE = "";
10156
- var INNERMOST_IFTAGS_PATTERN = /\[\[\s*iftags\b([^\]]*)\]\]((?:(?!\[\[\s*iftags\b|\[\[\/\s*iftags\s*\]\]).)*)\[\[\/\s*iftags\s*\]\]/gis;
10157
- var RAW_BLOCK_OPEN_PATTERN = /\[\[\s*(code|html)\b[^\]]*\]\]/iy;
10158
- function preprocessIftags(source, pageTags) {
10159
- if (pageTags === null)
10160
- return source;
10161
- if (!source.includes("[["))
10162
- return source;
10163
- const sentinels = makeUniqueSentinels(source);
10164
- const { masked, placeholders } = maskRawRegions(source, sentinels);
10165
- const reduced = reduceIftags(masked, pageTags);
10166
- return restorePlaceholders(reduced, placeholders, sentinels);
10167
- }
10168
- function makeUniqueSentinels(source) {
10169
- let open = BASE_PLACEHOLDER_OPEN;
10170
- let close = BASE_PLACEHOLDER_CLOSE;
10171
- while (source.includes(open) || source.includes(close)) {
10172
- open += BASE_PLACEHOLDER_OPEN;
10173
- close += BASE_PLACEHOLDER_CLOSE;
10174
- }
10175
- return { open, close };
10176
- }
10177
- function reduceIftags(source, pageTags) {
10178
- let current = source;
10179
- const maxIterations = source.length + 1;
10180
- for (let i = 0;i < maxIterations; i++) {
10181
- const next = current.replace(INNERMOST_IFTAGS_PATTERN, (_, cond, body) => {
10182
- const condition = parseTagCondition(cond);
10183
- return evaluateTagCondition(condition, pageTags) ? body : "";
10184
- });
10185
- if (next === current)
10186
- return current;
10187
- current = next;
10188
- }
10189
- return current;
10190
- }
10191
- function maskRawRegions(source, sentinels) {
10192
- const placeholders = [];
10193
- let masked = "";
10194
- let i = 0;
10195
- while (i < source.length) {
10196
- if (source[i] === "[" && source[i + 1] === "[") {
10197
- RAW_BLOCK_OPEN_PATTERN.lastIndex = i;
10198
- const openMatch = RAW_BLOCK_OPEN_PATTERN.exec(source);
10199
- if (openMatch) {
10200
- const name = openMatch[1].toLowerCase();
10201
- const openLen = openMatch[0].length;
10202
- const closePattern = new RegExp(`\\[\\[\\/\\s*${name}\\s*\\]\\]`, "ig");
10203
- closePattern.lastIndex = i + openLen;
10204
- const closeMatch = closePattern.exec(source);
10205
- if (closeMatch) {
10206
- const regionEnd = closeMatch.index + closeMatch[0].length;
10207
- masked += pushPlaceholder(placeholders, source.slice(i, regionEnd), sentinels);
10208
- i = regionEnd;
10209
- continue;
10210
- }
10211
- if (name === "code") {
10212
- masked += pushPlaceholder(placeholders, source.slice(i), sentinels);
10213
- i = source.length;
10214
- continue;
10215
- }
10216
- }
10217
- }
10218
- if (source[i] === "@" && source[i + 1] === "<") {
10219
- const close = source.indexOf(">@", i + 2);
10220
- const newline = source.indexOf(`
10221
- `, i + 2);
10222
- if (close !== -1 && (newline === -1 || close < newline)) {
10223
- const regionEnd = close + 2;
10224
- masked += pushPlaceholder(placeholders, source.slice(i, regionEnd), sentinels);
10225
- i = regionEnd;
10226
- continue;
10227
- }
10228
- }
10229
- if (source[i] === "@" && source[i + 1] === "@") {
10230
- const close = source.indexOf("@@", i + 2);
10231
- const newline = source.indexOf(`
10232
- `, i + 2);
10233
- if (close !== -1 && (newline === -1 || close < newline)) {
10234
- const regionEnd = close + 2;
10235
- masked += pushPlaceholder(placeholders, source.slice(i, regionEnd), sentinels);
10236
- i = regionEnd;
10237
- continue;
10238
- }
10239
- }
10240
- masked += source[i];
10241
- i++;
10242
- }
10243
- return { masked, placeholders };
10244
- }
10245
- function pushPlaceholder(placeholders, text, sentinels) {
10246
- const idx = placeholders.length;
10247
- placeholders.push(text);
10248
- return `${sentinels.open}${idx}${sentinels.close}`;
10249
- }
10250
- function escapeRegex(str) {
10251
- return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
10252
- }
10253
- function restorePlaceholders(source, placeholders, sentinels) {
10254
- const pattern = new RegExp(`${escapeRegex(sentinels.open)}(\\d+)${escapeRegex(sentinels.close)}`, "g");
10255
- return source.replace(pattern, (_, idx) => placeholders[Number(idx)] ?? "");
10256
- }
10257
10340
  // packages/parser/src/parser/rules/block/module/include/resolve.ts
10258
10341
  function resolveIncludes(source, fetcher, options) {
10259
10342
  if (options?.settings && !options.settings.enablePageSyntax) {
@@ -10306,8 +10389,9 @@ function isRestOfLineBlank(source, pos) {
10306
10389
  if (ch === `
10307
10390
  `)
10308
10391
  return true;
10309
- if (ch !== " " && ch !== "\t" && ch !== "\r")
10310
- return false;
10392
+ if (ch === " " || ch === "\t" || ch === "\r" || ch === "]")
10393
+ continue;
10394
+ return false;
10311
10395
  }
10312
10396
  return true;
10313
10397
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wdprlib/parser",
3
- "version": "3.0.0",
3
+ "version": "3.1.1",
4
4
  "description": "Parser for Wikidot markup",
5
5
  "keywords": [
6
6
  "ast",