@wdprlib/parser 1.1.0 → 1.1.2

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
@@ -5113,6 +5113,10 @@ function parseBibliographyEntry(ctx, startPos) {
5113
5113
  consumed++;
5114
5114
  break;
5115
5115
  }
5116
+ contentNodes.push({ element: "line-break" });
5117
+ pos++;
5118
+ consumed++;
5119
+ continue;
5116
5120
  }
5117
5121
  const inlineCtx = { ...ctx, pos };
5118
5122
  const result = parseInlineUntil(inlineCtx, "NEWLINE");
@@ -5610,6 +5614,11 @@ var linkTripleRule = {
5610
5614
  break;
5611
5615
  }
5612
5616
  if (token.type === "NEWLINE") {
5617
+ if (foundPipe) {
5618
+ labelText += " ";
5619
+ } else {
5620
+ target += " ";
5621
+ }
5613
5622
  consumed++;
5614
5623
  pos++;
5615
5624
  continue;
@@ -6003,10 +6012,8 @@ var newlineLineBreakRule = {
6003
6012
  }
6004
6013
  const nextMeaningfulToken = ctx.tokens[ctx.pos + lookAhead];
6005
6014
  let isValidBlock = isBlockStartToken(nextMeaningfulToken?.type);
6006
- if (isValidBlock && (nextMeaningfulToken?.type === "LIST_BULLET" || nextMeaningfulToken?.type === "LIST_NUMBER")) {
6007
- if (!nextMeaningfulToken.lineStart) {
6008
- isValidBlock = false;
6009
- }
6015
+ if (isValidBlock && !nextMeaningfulToken?.lineStart) {
6016
+ isValidBlock = false;
6010
6017
  }
6011
6018
  if (isValidBlock && nextMeaningfulToken?.type === "HEADING_MARKER") {
6012
6019
  const markerLen = nextMeaningfulToken.value.length;
@@ -6718,7 +6725,15 @@ var footnoteRule = {
6718
6725
  if (token.type === "NEWLINE") {
6719
6726
  pos++;
6720
6727
  consumed++;
6721
- if (ctx.tokens[pos]?.type === "NEWLINE") {
6728
+ let peekPos = pos;
6729
+ let peekConsumed = 0;
6730
+ while (ctx.tokens[peekPos]?.type === "WHITESPACE") {
6731
+ peekPos++;
6732
+ peekConsumed++;
6733
+ }
6734
+ if (ctx.tokens[peekPos]?.type === "NEWLINE") {
6735
+ pos = peekPos;
6736
+ consumed += peekConsumed;
6722
6737
  while (ctx.tokens[pos]?.type === "NEWLINE") {
6723
6738
  pos++;
6724
6739
  consumed++;
@@ -7215,7 +7230,7 @@ var anchorRule = {
7215
7230
  consumed++;
7216
7231
  }
7217
7232
  foundClose = true;
7218
- while (paragraphStrip && ctx.tokens[pos]?.type === "NEWLINE") {
7233
+ if (paragraphStrip && ctx.tokens[pos]?.type === "NEWLINE" && ctx.tokens[pos + 1]?.type !== "NEWLINE") {
7219
7234
  pos++;
7220
7235
  consumed++;
7221
7236
  }
@@ -7753,6 +7768,7 @@ var bibciteRule = {
7753
7768
  return { success: false };
7754
7769
  }
7755
7770
  let label = "";
7771
+ let foundClose = false;
7756
7772
  while (pos < ctx.tokens.length) {
7757
7773
  const t = ctx.tokens[pos];
7758
7774
  if (!t)
@@ -7761,6 +7777,7 @@ var bibciteRule = {
7761
7777
  const nextT = ctx.tokens[pos + 1];
7762
7778
  if (nextT?.type === "TEXT" && nextT.value === ")") {
7763
7779
  consumed += 2;
7780
+ foundClose = true;
7764
7781
  break;
7765
7782
  }
7766
7783
  }
@@ -7771,6 +7788,9 @@ var bibciteRule = {
7771
7788
  pos++;
7772
7789
  consumed++;
7773
7790
  }
7791
+ if (!foundClose) {
7792
+ return { success: false };
7793
+ }
7774
7794
  label = label.trim();
7775
7795
  if (!label) {
7776
7796
  return { success: false };
@@ -9627,16 +9647,31 @@ function resolveIncludes(source, fetcher, options) {
9627
9647
  };
9628
9648
  return expandText(source, cachedFetcher, 0, maxDepth, []);
9629
9649
  }
9630
- var INCLUDE_PATTERN = /\[\[include\s+([\s\S]*?)\]\]/gi;
9650
+ var INCLUDE_PATTERN = /\[\[include\s([^\]]*(?:\](?!\])[^\]]*)*)\]\]/gi;
9631
9651
  function parseIncludeDirective(inner) {
9632
9652
  const normalized = inner.replace(/\n/g, " ");
9633
9653
  const parts = normalized.split("|");
9634
- const target = parts[0].trim();
9635
- const variables = {};
9654
+ const firstSegment = parts[0].trim();
9655
+ const spaceIndex = firstSegment.indexOf(" ");
9656
+ let target;
9657
+ const varSegments = [];
9658
+ if (spaceIndex !== -1) {
9659
+ target = firstSegment.slice(0, spaceIndex);
9660
+ const rest = firstSegment.slice(spaceIndex + 1).trim();
9661
+ if (rest) {
9662
+ varSegments.push(rest);
9663
+ }
9664
+ } else {
9665
+ target = firstSegment;
9666
+ }
9636
9667
  for (let i = 1;i < parts.length; i++) {
9637
9668
  const segment = parts[i].trim();
9638
- if (!segment)
9639
- continue;
9669
+ if (segment) {
9670
+ varSegments.push(segment);
9671
+ }
9672
+ }
9673
+ const variables = {};
9674
+ for (const segment of varSegments) {
9640
9675
  const eqIndex = segment.indexOf("=");
9641
9676
  if (eqIndex !== -1) {
9642
9677
  const key = segment.slice(0, eqIndex).trim();
package/dist/index.d.cts CHANGED
@@ -4,32 +4,77 @@ import { WikitextMode, WikitextSettings as WikitextSettings4 } from "@wdprlib/as
4
4
  import { createSettings, DEFAULT_SETTINGS } from "@wdprlib/ast";
5
5
  import { Position } from "@wdprlib/ast";
6
6
  /**
7
- * Token types for Wikidot markup
7
+ * Every distinct lexeme the Wikidot lexer can produce.
8
+ *
9
+ * Each value corresponds to a fixed character sequence (or class of
10
+ * sequences) in Wikidot markup. The inline comments show the literal
11
+ * text that produces each token type.
12
+ *
13
+ * @group Lexer
8
14
  */
9
15
  type TokenType = "EOF" | "TEXT" | "IDENTIFIER" | "NEWLINE" | "WHITESPACE" | "BLOCK_OPEN" | "BLOCK_CLOSE" | "BLOCK_END_OPEN" | "BOLD_MARKER" | "ITALIC_MARKER" | "UNDERLINE_MARKER" | "STRIKE_MARKER" | "SUPER_MARKER" | "SUB_MARKER" | "MONO_MARKER" | "MONO_CLOSE" | "HEADING_MARKER" | "HR_MARKER" | "LIST_BULLET" | "LIST_NUMBER" | "BLOCKQUOTE_MARKER" | "TABLE_MARKER" | "TABLE_HEADER" | "TABLE_LEFT" | "TABLE_CENTER" | "TABLE_RIGHT" | "CODE_OPEN" | "CODE_CLOSE" | "LINK_OPEN" | "LINK_CLOSE" | "BRACKET_OPEN" | "BRACKET_CLOSE" | "BRACKET_ANCHOR" | "BRACKET_STAR" | "PIPE" | "EQUALS" | "COLON" | "SLASH" | "STAR" | "HASH" | "AT" | "AMPERSAND" | "BACKSLASH" | "QUOTED_STRING" | "RAW_OPEN" | "RAW_CLOSE" | "RAW_BLOCK_OPEN" | "RAW_BLOCK_CLOSE" | "COLOR_MARKER" | "UNDERSCORE" | "BACKSLASH_BREAK" | "COMMENT_OPEN" | "COMMENT_CLOSE" | "CLEAR_FLOAT" | "CLEAR_FLOAT_LEFT" | "CLEAR_FLOAT_RIGHT" | "LEFT_DOUBLE_ANGLE" | "RIGHT_DOUBLE_ANGLE";
10
16
  /**
11
- * Token
17
+ * A single lexical token produced by the `Lexer`.
18
+ *
19
+ * Tokens are the input to the parser stage. Each token carries its
20
+ * literal text (`value`), source location (`position`), and a flag
21
+ * indicating whether it appeared at the beginning of a line — which
22
+ * matters because several Wikidot constructs (headings, lists,
23
+ * blockquotes, horizontal rules) are only valid at line start.
24
+ *
25
+ * @group Lexer
12
26
  */
13
27
  interface Token {
28
+ /** The lexeme category */
14
29
  type: TokenType;
30
+ /** The literal source text that produced this token */
15
31
  value: string;
32
+ /** Start/end location in the original source string */
16
33
  position: Position;
17
- /** Whether this token appears at the start of a line */
34
+ /**
35
+ * `true` when this token is the first non-whitespace token on its
36
+ * line. Block-level rules (headings, lists, blockquotes) check this
37
+ * flag before attempting to match.
38
+ */
18
39
  lineStart: boolean;
19
40
  }
20
41
  /**
21
- * Create a token
42
+ * Construct a {@link Token} value.
43
+ *
44
+ * @param type - The lexeme category
45
+ * @param value - Literal source text
46
+ * @param position - Source location range
47
+ * @param lineStart - Whether the token starts a new line
48
+ * @returns A new token object
49
+ *
50
+ * @group Lexer
22
51
  */
23
52
  declare function createToken(type: TokenType, value: string, position: Position, lineStart?: boolean): Token;
24
53
  /**
25
- * Lexer options
54
+ * Configuration for the {@link Lexer}.
55
+ *
56
+ * @group Lexer
26
57
  */
27
58
  interface LexerOptions {
28
- /** Track position information */
59
+ /**
60
+ * When `true` (default), every token carries accurate line/column/offset
61
+ * data. Set to `false` to skip position tracking for faster tokenisation
62
+ * when source-map information is not needed.
63
+ */
29
64
  trackPositions?: boolean;
30
65
  }
31
66
  /**
32
- * Wikidot markup lexer
67
+ * Converts a Wikidot markup source string into a flat array of {@link Token}s.
68
+ *
69
+ * The lexer is single-pass and greedy: it tries the longest-matching
70
+ * multi-character pattern first (e.g. `[[[` before `[[`, `**` before `*`).
71
+ * Context-sensitive constructs (line-start headings, blockquote markers)
72
+ * are disambiguated via the `lineStart` state flag.
73
+ *
74
+ * For convenience, use the standalone {@link tokenize} function instead
75
+ * of constructing a `Lexer` directly.
76
+ *
77
+ * @group Lexer
33
78
  */
34
79
  declare class Lexer {
35
80
  private state;
@@ -78,23 +123,59 @@ declare class Lexer {
78
123
  private isAlphanumeric;
79
124
  }
80
125
  /**
81
- * Tokenize source string
126
+ * Tokenise a Wikidot markup source string in one call.
127
+ *
128
+ * Shorthand for `new Lexer(source, options).tokenize()`.
129
+ *
130
+ * @param source - Raw Wikidot markup
131
+ * @param options - Optional lexer configuration
132
+ * @returns A flat array of tokens, ending with an `EOF` token
133
+ *
134
+ * @group Lexer
82
135
  */
83
136
  declare function tokenize(source: string, options?: LexerOptions): Token[];
84
137
  import { SyntaxTree, WikitextSettings } from "@wdprlib/ast";
85
138
  /**
86
- * Parser options
139
+ * Configuration for the {@link Parser} and the {@link parse} function.
140
+ *
141
+ * All fields are optional; sensible defaults are applied when omitted.
142
+ *
143
+ * @group Parser
87
144
  */
88
145
  interface ParserOptions {
89
- /** Wikidot version */
146
+ /** Markup dialect. Currently only `"wikidot"` is supported. */
90
147
  version?: "wikidot";
91
- /** Track position information */
148
+ /**
149
+ * Propagate source-position data into every AST node.
150
+ * Defaults to `true`. Set to `false` for smaller output when positions
151
+ * are not needed.
152
+ */
92
153
  trackPositions?: boolean;
93
- /** Wikitext settings controlling syntax availability */
154
+ /**
155
+ * Context-dependent feature flags (page vs. forum-post, etc.).
156
+ * Defaults to {@link DEFAULT_SETTINGS} (full page mode).
157
+ */
94
158
  settings?: WikitextSettings;
95
159
  }
96
160
  /**
97
- * Wikidot markup parser
161
+ * Converts a token stream into a Wikidot {@link SyntaxTree}.
162
+ *
163
+ * The parser consumes tokens produced by the `Lexer` and emits a
164
+ * tree of {@link Element} nodes. Block-level rules are tried in priority
165
+ * order; when none match, the fallback paragraph rule collects inline
166
+ * tokens until the next blank line.
167
+ *
168
+ * After the main parse pass, two post-processing steps run:
169
+ *
170
+ * 1. **Span-strip merging** — `[[span_]]` elements that set
171
+ * `_paragraphStrip` are merged with adjacent paragraphs.
172
+ * 2. **Internal-flag cleanup** — all `_`-prefixed bookkeeping fields
173
+ * are removed from the final AST.
174
+ *
175
+ * For most use-cases the standalone {@link parse} function is simpler
176
+ * than constructing a `Parser` directly.
177
+ *
178
+ * @group Parser
98
179
  */
99
180
  declare class Parser {
100
181
  private ctx;
@@ -131,58 +212,119 @@ declare class Parser {
131
212
  declare function parse(source: string, options?: ParserOptions): SyntaxTree;
132
213
  import { Element as Element2 } from "@wdprlib/ast";
133
214
  /**
134
- * Parser function type for re-parsing substituted templates
135
- * Used by ListPages and ListUsers modules
215
+ * Parser function type for re-parsing substituted template output as wikitext.
216
+ *
217
+ * Used by ListPages and ListUsers modules during the resolution phase. After
218
+ * template variables are substituted with actual data, the resulting string
219
+ * needs to be parsed as wikitext to produce AST elements.
220
+ *
221
+ * @param input - Wikitext string to parse
222
+ * @returns Object containing the parsed elements
136
223
  */
137
224
  type ParseFunction = (input: string) => {
138
225
  elements: Element2[];
139
226
  };
140
227
  /**
141
- * ListUsers module types
228
+ *
229
+ * Type definitions for the ListUsers module.
230
+ *
231
+ * The `[[module ListUsers users="."]]` block displays information about site
232
+ * members. Currently, only `users="."` (the logged-in user) is supported.
233
+ * The template body can reference three variables: `%%number%%`, `%%title%%`,
234
+ * and `%%name%%`.
235
+ *
236
+ * @module
142
237
  */
143
238
  /**
144
- * Supported template variables
239
+ * Supported template variables for ListUsers.
240
+ *
241
+ * - `number` - The user's numeric ID
242
+ * - `title` - The user's display title/nickname
243
+ * - `name` - The user's account name
145
244
  */
146
245
  type ListUsersVariable = "number" | "title" | "name";
147
246
  /**
148
- * User data provided by external source
247
+ * User data that must be provided by the external data source.
248
+ *
249
+ * Each field corresponds to a template variable of the same name.
149
250
  */
150
251
  interface ListUsersUserData {
252
+ /** The user's numeric ID, rendered by `%%number%%` */
151
253
  number: number;
254
+ /** The user's display title, rendered by `%%title%%` */
152
255
  title: string;
256
+ /** The user's account name, rendered by `%%name%%` */
153
257
  name: string;
154
258
  }
155
259
  /**
156
- * Data requirement for a single ListUsers module
260
+ * Data requirement for a single ListUsers module instance.
261
+ *
262
+ * Produced by the extraction phase and consumed by the application to
263
+ * determine what data to fetch.
157
264
  */
158
265
  interface ListUsersDataRequirement {
266
+ /** Unique identifier for this module instance (sequential, 0-based) */
159
267
  id: number;
268
+ /** The `users` attribute value (currently only `"."` is supported) */
160
269
  users: string;
270
+ /** Template variables that need data from the external source */
161
271
  neededVariables: ListUsersVariable[];
162
272
  }
163
273
  /**
164
- * External data for a single ListUsers module
165
- * Note: ListUsers returns only the logged-in user
274
+ * External data provided by the application for a single ListUsers module.
275
+ *
276
+ * Currently ListUsers only returns information about the logged-in user.
277
+ * Return null/undefined from the fetcher to indicate no user is logged in.
166
278
  */
167
279
  interface ListUsersExternalData {
280
+ /** Data for the logged-in user */
168
281
  user: ListUsersUserData;
169
282
  }
170
283
  /**
171
- * Callback to fetch data for a ListUsers module
284
+ * Callback to fetch user data for a ListUsers module.
285
+ *
286
+ * Called during the resolution phase for each ListUsers module in the AST.
287
+ * Return null/undefined to skip the module (outputs nothing, e.g., when
288
+ * no user is logged in).
289
+ *
290
+ * @param requirement - The data requirement describing what data is needed
291
+ * @returns User data, null/undefined to skip, or a Promise of the same
172
292
  */
173
293
  type ListUsersDataFetcher = (requirement: ListUsersDataRequirement) => ListUsersExternalData | null | undefined | Promise<ListUsersExternalData | null | undefined>;
174
294
  /**
175
- * Context passed to compiled template
295
+ * Context passed to a compiled ListUsers template function during rendering.
176
296
  */
177
297
  interface ListUsersVariableContext {
298
+ /** The user whose data is being rendered */
178
299
  user: ListUsersUserData;
179
300
  }
180
301
  /**
181
- * Compiled template function
302
+ * A compiled ListUsers template function.
303
+ *
304
+ * Accepts a variable context and returns the rendered wikitext string
305
+ * with all `%%variable%%` placeholders substituted.
306
+ *
307
+ * @param ctx - The variable context containing user data
308
+ * @returns Rendered wikitext string
182
309
  */
183
310
  type ListUsersCompiledTemplate = (ctx: ListUsersVariableContext) => string;
184
311
  /**
185
- * ListPages module types
312
+ *
313
+ * Type definitions for the ListPages module system.
314
+ *
315
+ * This file defines the complete type vocabulary for the ListPages lifecycle:
316
+ *
317
+ * - **Query types**: Raw and normalized representations of ListPages filter/sort parameters
318
+ * - **Variable types**: The `%%variable%%` names supported in ListPages templates
319
+ * - **Data requirement types**: What the parser tells the application it needs to fetch
320
+ * - **External data types**: What the application provides back (page data, user info, site context)
321
+ * - **Template types**: Compiled template function signatures and their execution context
322
+ * - **Normalized query types**: Structured representations of parsed query parameters
323
+ *
324
+ * Security note: Several fields contain untrusted user input from wikitext.
325
+ * See `ListPagesQuery` documentation for safe usage guidelines.
326
+ *
327
+ * @module
186
328
  */
187
329
  /**
188
330
  * ListPages query parameters for page selection
@@ -385,7 +527,7 @@ interface ListPagesExternalData {
385
527
  * Callback to fetch data for a ListPages module
386
528
  *
387
529
  * Called by resolveModules for each ListPages module in the AST.
388
- * Receives a normalized query with all @URL parameters resolved.
530
+ * Receives a normalized query with all `@URL` parameters resolved.
389
531
  * Return null/undefined to skip the module (outputs nothing).
390
532
  *
391
533
  * @param query - Normalized query with structured types (tags, category, order, etc.)
@@ -527,47 +669,60 @@ interface NormalizedListPagesQuery {
527
669
  reverse?: boolean;
528
670
  }
529
671
  /**
530
- * Callback to get current page's tags
672
+ * Callback to retrieve the current page's tags during the resolve phase.
531
673
  *
532
- * Called during resolve phase to evaluate [[iftags]] conditions.
674
+ * Called when evaluating `[[iftags]]` conditions. The application must provide
675
+ * this callback with access to the current page's tag list.
533
676
  *
534
677
  * @returns Array of tag names for the current page
535
678
  */
536
679
  type IfTagsResolver = () => string[];
537
680
  /**
538
- * Data provider interface for resolving modules that require external data
681
+ * Callback bag for supplying external data during module resolution.
539
682
  *
540
- * All callbacks are optional - if not provided, the corresponding
541
- * module/syntax will be output as-is in the AST without resolution.
683
+ * Pass an instance to `resolveModules()`. Every callback is optional:
684
+ * when a callback is missing the corresponding module node is kept as-is
685
+ * in the output AST — useful when you only need to resolve a subset of
686
+ * modules (e.g. only `[[iftags]]` on the client side).
542
687
  *
543
- * Note: Include resolution is handled separately via resolveIncludes().
688
+ * @group Module Resolution
544
689
  */
545
690
  interface DataProvider {
546
691
  /**
547
- * Fetch data for ListPages module
548
- * Called during resolve phase with query parameters
692
+ * Fetch page data for `[[module ListPages]]` expansion.
549
693
  *
550
- * @security `req.query` / `req.rawAttributes` are **untrusted user input** from wikitext.
551
- * When building database queries:
552
- * - **NEVER** interpolate them into SQL strings
553
- * - **ALWAYS** use parameterized queries / prepared statements
554
- * - For `order` (ORDER BY), use a whitelist of allowed column names
694
+ * Called once per ListPages instance in the AST with the normalised
695
+ * query parameters extracted from the module's wikitext attributes.
696
+ *
697
+ * @security The query fields originate from **untrusted user input**.
698
+ * When building database queries from the returned requirement:
699
+ * - **Never** interpolate `req.query` / `req.rawAttributes` into SQL
700
+ * - **Always** use parameterised queries or prepared statements
701
+ * - For `order` (ORDER BY), validate against a whitelist of column names
555
702
  */
556
703
  fetchListPages?: ListPagesDataFetcher;
557
704
  /**
558
- * Fetch data for ListUsers module
559
- * Called during resolve phase with user query parameters
705
+ * Fetch user data for `[[module ListUsers]]` expansion.
706
+ *
707
+ * Called once per ListUsers instance with the parsed query parameters.
560
708
  */
561
709
  fetchListUsers?: ListUsersDataFetcher;
562
710
  /**
563
- * Get current page's tags for iftags evaluation
564
- * Called during resolve phase to evaluate [[iftags]] conditions
711
+ * Return the current page's tags for `[[iftags]]` evaluation.
712
+ *
713
+ * If provided, `[[iftags]]` blocks are evaluated and either kept or
714
+ * discarded based on whether the page's tags satisfy the condition.
715
+ * If omitted, `[[iftags]]` blocks pass through unresolved.
565
716
  */
566
717
  getPageTags?: IfTagsResolver;
567
718
  }
568
719
  import { SyntaxTree as SyntaxTree2 } from "@wdprlib/ast";
569
720
  /**
570
- * Result of extraction including compiled templates
721
+ * Complete result of extracting data requirements from an AST.
722
+ *
723
+ * Contains everything needed to fetch external data and then resolve modules:
724
+ * the data requirements tell the application what to fetch, and the pre-compiled
725
+ * templates are used during the resolution phase to efficiently render results.
571
726
  */
572
727
  interface ExtractionResult {
573
728
  /** Data requirements for external fetching */
@@ -578,11 +733,29 @@ interface ExtractionResult {
578
733
  compiledListUsersTemplates: Map<number, ListUsersCompiledTemplate>;
579
734
  }
580
735
  /**
581
- * Extract data requirements from a parsed AST
736
+ * Extract all data requirements from a parsed AST.
737
+ *
738
+ * Walks the entire AST to find ListPages and ListUsers module elements,
739
+ * analyzes their templates to determine which variables are used, builds
740
+ * query objects from their attributes, and pre-compiles their templates.
741
+ *
742
+ * Each module is assigned a sequential ID (separate counters for ListPages
743
+ * and ListUsers) that is used to correlate requirements with fetched data
744
+ * and compiled templates during the resolution phase.
745
+ *
746
+ * @param ast - The parsed syntax tree to analyze
747
+ * @returns Extraction result containing requirements and compiled templates
582
748
  */
583
749
  declare function extractDataRequirements(ast: SyntaxTree2): ExtractionResult;
584
750
  /**
585
- * Compile a template string into an executable function
751
+ * Compile a ListPages template string into an executable function.
752
+ *
753
+ * The template is split into alternating static strings and dynamic getter
754
+ * functions. The returned function concatenates these parts with the getter
755
+ * functions evaluated against the provided variable context.
756
+ *
757
+ * @param template - The template string containing `%%variable%%` placeholders
758
+ * @returns A compiled function that accepts a `VariableContext` and returns the rendered string
586
759
  */
587
760
  declare function compileTemplate(template: string): CompiledTemplate;
588
761
  /**
@@ -694,49 +867,85 @@ interface ResolveIncludesOptions {
694
867
  */
695
868
  declare function resolveIncludes(source: string, fetcher: IncludeFetcher, options?: ResolveIncludesOptions): string;
696
869
  /**
697
- * Compile a template string into an executable function
870
+ * Compile a ListUsers template string into an executable function.
871
+ *
872
+ * The template is split into alternating static strings and dynamic getter
873
+ * functions. The returned function concatenates these parts for each call.
874
+ *
875
+ * @param template - The template string containing `%%variable%%` placeholders
876
+ * @returns A compiled function that accepts a `ListUsersVariableContext` and returns rendered text
698
877
  */
699
878
  declare function compileListUsersTemplate(template: string): ListUsersCompiledTemplate;
700
879
  /**
701
- * Extract needed variables from a template string
880
+ * Extract the set of template variables referenced in a ListUsers template string.
881
+ *
882
+ * Scans for `%%variable%%` patterns and returns only those that match known
883
+ * ListUsers variables. Unknown variables are silently ignored.
884
+ *
885
+ * @param template - The template string from the module body
886
+ * @returns Deduplicated array of referenced variable names
702
887
  */
703
888
  declare function extractListUsersVariables(template: string): ListUsersVariable[];
704
889
  import { Element as Element5, Module as Module3 } from "@wdprlib/ast";
705
890
  /**
706
- * ListUsers module data type
891
+ * Narrowed type for the list-users variant of the Module discriminated union.
707
892
  */
708
893
  type ListUsersModuleData = Extract<Module3, {
709
894
  module: "list-users";
710
895
  }>;
711
896
  /**
712
- * Type guard for list-users module
897
+ * Type guard to check if a Module is a list-users module.
898
+ *
899
+ * @param module - A Module discriminated union value
900
+ * @returns true if the module is a list-users module
713
901
  */
714
902
  declare function isListUsersModule(module: Module3): module is ListUsersModuleData;
715
903
  /**
716
- * Resolve a single ListUsers module
717
- * Note: ListUsers returns only the logged-in user
904
+ * Resolve a single ListUsers module by substituting fetched user data into
905
+ * the pre-compiled template and re-parsing the result as wikitext.
906
+ *
907
+ * Currently ListUsers only renders the logged-in user (no iteration over
908
+ * multiple users), so the template is executed exactly once.
909
+ *
910
+ * @param _module - The list-users module data from the AST (unused, reserved for future use)
911
+ * @param data - External user data fetched by the application
912
+ * @param compiledTemplate - Pre-compiled template function from the extraction phase
913
+ * @param parse - Parser function for re-parsing the substituted template as wikitext
914
+ * @returns Array of AST elements produced by parsing the rendered template
718
915
  */
719
916
  declare function resolveListUsers(_module: ListUsersModuleData, data: ListUsersExternalData, compiledTemplate: ListUsersCompiledTemplate, parse: ParseFunction): Element5[];
720
917
  import { SyntaxTree as SyntaxTree3 } from "@wdprlib/ast";
721
918
  /**
722
- * Options for resolving modules
919
+ * Configuration for {@link resolveModules}.
920
+ *
921
+ * Callers must supply pre-extracted requirements and pre-compiled
922
+ * templates (obtained from `extractDataRequirements()` and
923
+ * `compileTemplate()` / `compileListUsersTemplate()`).
924
+ *
925
+ * @group Module Resolution
723
926
  */
724
927
  interface ResolveOptions {
725
- /** Parser function for re-parsing templates */
928
+ /** Parser function used to re-parse expanded template markup into AST nodes */
726
929
  parse: ParseFunction;
727
- /** Pre-compiled templates for ListPages */
930
+ /** Pre-compiled ListPages body templates, keyed by requirement ID */
728
931
  compiledListPagesTemplates: Map<number, CompiledTemplate>;
729
- /** Pre-compiled templates for ListUsers */
932
+ /** Pre-compiled ListUsers body templates, keyed by requirement ID */
730
933
  compiledListUsersTemplates?: Map<number, ListUsersCompiledTemplate>;
731
- /** Data requirements grouped by module type */
934
+ /**
935
+ * Data requirements grouped by module type.
936
+ * Obtained from `extractDataRequirements()`.
937
+ */
732
938
  requirements: {
733
939
  listPages?: ListPagesDataRequirement[];
734
940
  listUsers?: ListUsersDataRequirement[];
735
941
  };
736
942
  /**
737
- * URL path for @URL parameter resolution (HPC support)
738
- * Format: "/page-name/param/value/param/value"
739
- * Example: "/scp-001/offset/10/page2_limit/5"
943
+ * URL path for `@URL` parameter resolution (HPC / pagination support).
944
+ *
945
+ * Wikidot encodes pagination state in the URL path as key/value pairs
946
+ * after the page name, e.g. `"/scp-001/offset/10/page2_limit/5"`.
947
+ * When provided, `@URL` references in ListPages queries are replaced
948
+ * with the corresponding values from this path.
740
949
  */
741
950
  urlPath?: string;
742
951
  }
package/dist/index.d.ts CHANGED
@@ -4,32 +4,77 @@ import { WikitextMode, WikitextSettings as WikitextSettings4 } from "@wdprlib/as
4
4
  import { createSettings, DEFAULT_SETTINGS } from "@wdprlib/ast";
5
5
  import { Position } from "@wdprlib/ast";
6
6
  /**
7
- * Token types for Wikidot markup
7
+ * Every distinct lexeme the Wikidot lexer can produce.
8
+ *
9
+ * Each value corresponds to a fixed character sequence (or class of
10
+ * sequences) in Wikidot markup. The inline comments show the literal
11
+ * text that produces each token type.
12
+ *
13
+ * @group Lexer
8
14
  */
9
15
  type TokenType = "EOF" | "TEXT" | "IDENTIFIER" | "NEWLINE" | "WHITESPACE" | "BLOCK_OPEN" | "BLOCK_CLOSE" | "BLOCK_END_OPEN" | "BOLD_MARKER" | "ITALIC_MARKER" | "UNDERLINE_MARKER" | "STRIKE_MARKER" | "SUPER_MARKER" | "SUB_MARKER" | "MONO_MARKER" | "MONO_CLOSE" | "HEADING_MARKER" | "HR_MARKER" | "LIST_BULLET" | "LIST_NUMBER" | "BLOCKQUOTE_MARKER" | "TABLE_MARKER" | "TABLE_HEADER" | "TABLE_LEFT" | "TABLE_CENTER" | "TABLE_RIGHT" | "CODE_OPEN" | "CODE_CLOSE" | "LINK_OPEN" | "LINK_CLOSE" | "BRACKET_OPEN" | "BRACKET_CLOSE" | "BRACKET_ANCHOR" | "BRACKET_STAR" | "PIPE" | "EQUALS" | "COLON" | "SLASH" | "STAR" | "HASH" | "AT" | "AMPERSAND" | "BACKSLASH" | "QUOTED_STRING" | "RAW_OPEN" | "RAW_CLOSE" | "RAW_BLOCK_OPEN" | "RAW_BLOCK_CLOSE" | "COLOR_MARKER" | "UNDERSCORE" | "BACKSLASH_BREAK" | "COMMENT_OPEN" | "COMMENT_CLOSE" | "CLEAR_FLOAT" | "CLEAR_FLOAT_LEFT" | "CLEAR_FLOAT_RIGHT" | "LEFT_DOUBLE_ANGLE" | "RIGHT_DOUBLE_ANGLE";
10
16
  /**
11
- * Token
17
+ * A single lexical token produced by the `Lexer`.
18
+ *
19
+ * Tokens are the input to the parser stage. Each token carries its
20
+ * literal text (`value`), source location (`position`), and a flag
21
+ * indicating whether it appeared at the beginning of a line — which
22
+ * matters because several Wikidot constructs (headings, lists,
23
+ * blockquotes, horizontal rules) are only valid at line start.
24
+ *
25
+ * @group Lexer
12
26
  */
13
27
  interface Token {
28
+ /** The lexeme category */
14
29
  type: TokenType;
30
+ /** The literal source text that produced this token */
15
31
  value: string;
32
+ /** Start/end location in the original source string */
16
33
  position: Position;
17
- /** Whether this token appears at the start of a line */
34
+ /**
35
+ * `true` when this token is the first non-whitespace token on its
36
+ * line. Block-level rules (headings, lists, blockquotes) check this
37
+ * flag before attempting to match.
38
+ */
18
39
  lineStart: boolean;
19
40
  }
20
41
  /**
21
- * Create a token
42
+ * Construct a {@link Token} value.
43
+ *
44
+ * @param type - The lexeme category
45
+ * @param value - Literal source text
46
+ * @param position - Source location range
47
+ * @param lineStart - Whether the token starts a new line
48
+ * @returns A new token object
49
+ *
50
+ * @group Lexer
22
51
  */
23
52
  declare function createToken(type: TokenType, value: string, position: Position, lineStart?: boolean): Token;
24
53
  /**
25
- * Lexer options
54
+ * Configuration for the {@link Lexer}.
55
+ *
56
+ * @group Lexer
26
57
  */
27
58
  interface LexerOptions {
28
- /** Track position information */
59
+ /**
60
+ * When `true` (default), every token carries accurate line/column/offset
61
+ * data. Set to `false` to skip position tracking for faster tokenisation
62
+ * when source-map information is not needed.
63
+ */
29
64
  trackPositions?: boolean;
30
65
  }
31
66
  /**
32
- * Wikidot markup lexer
67
+ * Converts a Wikidot markup source string into a flat array of {@link Token}s.
68
+ *
69
+ * The lexer is single-pass and greedy: it tries the longest-matching
70
+ * multi-character pattern first (e.g. `[[[` before `[[`, `**` before `*`).
71
+ * Context-sensitive constructs (line-start headings, blockquote markers)
72
+ * are disambiguated via the `lineStart` state flag.
73
+ *
74
+ * For convenience, use the standalone {@link tokenize} function instead
75
+ * of constructing a `Lexer` directly.
76
+ *
77
+ * @group Lexer
33
78
  */
34
79
  declare class Lexer {
35
80
  private state;
@@ -78,23 +123,59 @@ declare class Lexer {
78
123
  private isAlphanumeric;
79
124
  }
80
125
  /**
81
- * Tokenize source string
126
+ * Tokenise a Wikidot markup source string in one call.
127
+ *
128
+ * Shorthand for `new Lexer(source, options).tokenize()`.
129
+ *
130
+ * @param source - Raw Wikidot markup
131
+ * @param options - Optional lexer configuration
132
+ * @returns A flat array of tokens, ending with an `EOF` token
133
+ *
134
+ * @group Lexer
82
135
  */
83
136
  declare function tokenize(source: string, options?: LexerOptions): Token[];
84
137
  import { SyntaxTree, WikitextSettings } from "@wdprlib/ast";
85
138
  /**
86
- * Parser options
139
+ * Configuration for the {@link Parser} and the {@link parse} function.
140
+ *
141
+ * All fields are optional; sensible defaults are applied when omitted.
142
+ *
143
+ * @group Parser
87
144
  */
88
145
  interface ParserOptions {
89
- /** Wikidot version */
146
+ /** Markup dialect. Currently only `"wikidot"` is supported. */
90
147
  version?: "wikidot";
91
- /** Track position information */
148
+ /**
149
+ * Propagate source-position data into every AST node.
150
+ * Defaults to `true`. Set to `false` for smaller output when positions
151
+ * are not needed.
152
+ */
92
153
  trackPositions?: boolean;
93
- /** Wikitext settings controlling syntax availability */
154
+ /**
155
+ * Context-dependent feature flags (page vs. forum-post, etc.).
156
+ * Defaults to {@link DEFAULT_SETTINGS} (full page mode).
157
+ */
94
158
  settings?: WikitextSettings;
95
159
  }
96
160
  /**
97
- * Wikidot markup parser
161
+ * Converts a token stream into a Wikidot {@link SyntaxTree}.
162
+ *
163
+ * The parser consumes tokens produced by the `Lexer` and emits a
164
+ * tree of {@link Element} nodes. Block-level rules are tried in priority
165
+ * order; when none match, the fallback paragraph rule collects inline
166
+ * tokens until the next blank line.
167
+ *
168
+ * After the main parse pass, two post-processing steps run:
169
+ *
170
+ * 1. **Span-strip merging** — `[[span_]]` elements that set
171
+ * `_paragraphStrip` are merged with adjacent paragraphs.
172
+ * 2. **Internal-flag cleanup** — all `_`-prefixed bookkeeping fields
173
+ * are removed from the final AST.
174
+ *
175
+ * For most use-cases the standalone {@link parse} function is simpler
176
+ * than constructing a `Parser` directly.
177
+ *
178
+ * @group Parser
98
179
  */
99
180
  declare class Parser {
100
181
  private ctx;
@@ -131,58 +212,119 @@ declare class Parser {
131
212
  declare function parse(source: string, options?: ParserOptions): SyntaxTree;
132
213
  import { Element as Element2 } from "@wdprlib/ast";
133
214
  /**
134
- * Parser function type for re-parsing substituted templates
135
- * Used by ListPages and ListUsers modules
215
+ * Parser function type for re-parsing substituted template output as wikitext.
216
+ *
217
+ * Used by ListPages and ListUsers modules during the resolution phase. After
218
+ * template variables are substituted with actual data, the resulting string
219
+ * needs to be parsed as wikitext to produce AST elements.
220
+ *
221
+ * @param input - Wikitext string to parse
222
+ * @returns Object containing the parsed elements
136
223
  */
137
224
  type ParseFunction = (input: string) => {
138
225
  elements: Element2[];
139
226
  };
140
227
  /**
141
- * ListUsers module types
228
+ *
229
+ * Type definitions for the ListUsers module.
230
+ *
231
+ * The `[[module ListUsers users="."]]` block displays information about site
232
+ * members. Currently, only `users="."` (the logged-in user) is supported.
233
+ * The template body can reference three variables: `%%number%%`, `%%title%%`,
234
+ * and `%%name%%`.
235
+ *
236
+ * @module
142
237
  */
143
238
  /**
144
- * Supported template variables
239
+ * Supported template variables for ListUsers.
240
+ *
241
+ * - `number` - The user's numeric ID
242
+ * - `title` - The user's display title/nickname
243
+ * - `name` - The user's account name
145
244
  */
146
245
  type ListUsersVariable = "number" | "title" | "name";
147
246
  /**
148
- * User data provided by external source
247
+ * User data that must be provided by the external data source.
248
+ *
249
+ * Each field corresponds to a template variable of the same name.
149
250
  */
150
251
  interface ListUsersUserData {
252
+ /** The user's numeric ID, rendered by `%%number%%` */
151
253
  number: number;
254
+ /** The user's display title, rendered by `%%title%%` */
152
255
  title: string;
256
+ /** The user's account name, rendered by `%%name%%` */
153
257
  name: string;
154
258
  }
155
259
  /**
156
- * Data requirement for a single ListUsers module
260
+ * Data requirement for a single ListUsers module instance.
261
+ *
262
+ * Produced by the extraction phase and consumed by the application to
263
+ * determine what data to fetch.
157
264
  */
158
265
  interface ListUsersDataRequirement {
266
+ /** Unique identifier for this module instance (sequential, 0-based) */
159
267
  id: number;
268
+ /** The `users` attribute value (currently only `"."` is supported) */
160
269
  users: string;
270
+ /** Template variables that need data from the external source */
161
271
  neededVariables: ListUsersVariable[];
162
272
  }
163
273
  /**
164
- * External data for a single ListUsers module
165
- * Note: ListUsers returns only the logged-in user
274
+ * External data provided by the application for a single ListUsers module.
275
+ *
276
+ * Currently ListUsers only returns information about the logged-in user.
277
+ * Return null/undefined from the fetcher to indicate no user is logged in.
166
278
  */
167
279
  interface ListUsersExternalData {
280
+ /** Data for the logged-in user */
168
281
  user: ListUsersUserData;
169
282
  }
170
283
  /**
171
- * Callback to fetch data for a ListUsers module
284
+ * Callback to fetch user data for a ListUsers module.
285
+ *
286
+ * Called during the resolution phase for each ListUsers module in the AST.
287
+ * Return null/undefined to skip the module (outputs nothing, e.g., when
288
+ * no user is logged in).
289
+ *
290
+ * @param requirement - The data requirement describing what data is needed
291
+ * @returns User data, null/undefined to skip, or a Promise of the same
172
292
  */
173
293
  type ListUsersDataFetcher = (requirement: ListUsersDataRequirement) => ListUsersExternalData | null | undefined | Promise<ListUsersExternalData | null | undefined>;
174
294
  /**
175
- * Context passed to compiled template
295
+ * Context passed to a compiled ListUsers template function during rendering.
176
296
  */
177
297
  interface ListUsersVariableContext {
298
+ /** The user whose data is being rendered */
178
299
  user: ListUsersUserData;
179
300
  }
180
301
  /**
181
- * Compiled template function
302
+ * A compiled ListUsers template function.
303
+ *
304
+ * Accepts a variable context and returns the rendered wikitext string
305
+ * with all `%%variable%%` placeholders substituted.
306
+ *
307
+ * @param ctx - The variable context containing user data
308
+ * @returns Rendered wikitext string
182
309
  */
183
310
  type ListUsersCompiledTemplate = (ctx: ListUsersVariableContext) => string;
184
311
  /**
185
- * ListPages module types
312
+ *
313
+ * Type definitions for the ListPages module system.
314
+ *
315
+ * This file defines the complete type vocabulary for the ListPages lifecycle:
316
+ *
317
+ * - **Query types**: Raw and normalized representations of ListPages filter/sort parameters
318
+ * - **Variable types**: The `%%variable%%` names supported in ListPages templates
319
+ * - **Data requirement types**: What the parser tells the application it needs to fetch
320
+ * - **External data types**: What the application provides back (page data, user info, site context)
321
+ * - **Template types**: Compiled template function signatures and their execution context
322
+ * - **Normalized query types**: Structured representations of parsed query parameters
323
+ *
324
+ * Security note: Several fields contain untrusted user input from wikitext.
325
+ * See `ListPagesQuery` documentation for safe usage guidelines.
326
+ *
327
+ * @module
186
328
  */
187
329
  /**
188
330
  * ListPages query parameters for page selection
@@ -385,7 +527,7 @@ interface ListPagesExternalData {
385
527
  * Callback to fetch data for a ListPages module
386
528
  *
387
529
  * Called by resolveModules for each ListPages module in the AST.
388
- * Receives a normalized query with all @URL parameters resolved.
530
+ * Receives a normalized query with all `@URL` parameters resolved.
389
531
  * Return null/undefined to skip the module (outputs nothing).
390
532
  *
391
533
  * @param query - Normalized query with structured types (tags, category, order, etc.)
@@ -527,47 +669,60 @@ interface NormalizedListPagesQuery {
527
669
  reverse?: boolean;
528
670
  }
529
671
  /**
530
- * Callback to get current page's tags
672
+ * Callback to retrieve the current page's tags during the resolve phase.
531
673
  *
532
- * Called during resolve phase to evaluate [[iftags]] conditions.
674
+ * Called when evaluating `[[iftags]]` conditions. The application must provide
675
+ * this callback with access to the current page's tag list.
533
676
  *
534
677
  * @returns Array of tag names for the current page
535
678
  */
536
679
  type IfTagsResolver = () => string[];
537
680
  /**
538
- * Data provider interface for resolving modules that require external data
681
+ * Callback bag for supplying external data during module resolution.
539
682
  *
540
- * All callbacks are optional - if not provided, the corresponding
541
- * module/syntax will be output as-is in the AST without resolution.
683
+ * Pass an instance to `resolveModules()`. Every callback is optional:
684
+ * when a callback is missing the corresponding module node is kept as-is
685
+ * in the output AST — useful when you only need to resolve a subset of
686
+ * modules (e.g. only `[[iftags]]` on the client side).
542
687
  *
543
- * Note: Include resolution is handled separately via resolveIncludes().
688
+ * @group Module Resolution
544
689
  */
545
690
  interface DataProvider {
546
691
  /**
547
- * Fetch data for ListPages module
548
- * Called during resolve phase with query parameters
692
+ * Fetch page data for `[[module ListPages]]` expansion.
549
693
  *
550
- * @security `req.query` / `req.rawAttributes` are **untrusted user input** from wikitext.
551
- * When building database queries:
552
- * - **NEVER** interpolate them into SQL strings
553
- * - **ALWAYS** use parameterized queries / prepared statements
554
- * - For `order` (ORDER BY), use a whitelist of allowed column names
694
+ * Called once per ListPages instance in the AST with the normalised
695
+ * query parameters extracted from the module's wikitext attributes.
696
+ *
697
+ * @security The query fields originate from **untrusted user input**.
698
+ * When building database queries from the returned requirement:
699
+ * - **Never** interpolate `req.query` / `req.rawAttributes` into SQL
700
+ * - **Always** use parameterised queries or prepared statements
701
+ * - For `order` (ORDER BY), validate against a whitelist of column names
555
702
  */
556
703
  fetchListPages?: ListPagesDataFetcher;
557
704
  /**
558
- * Fetch data for ListUsers module
559
- * Called during resolve phase with user query parameters
705
+ * Fetch user data for `[[module ListUsers]]` expansion.
706
+ *
707
+ * Called once per ListUsers instance with the parsed query parameters.
560
708
  */
561
709
  fetchListUsers?: ListUsersDataFetcher;
562
710
  /**
563
- * Get current page's tags for iftags evaluation
564
- * Called during resolve phase to evaluate [[iftags]] conditions
711
+ * Return the current page's tags for `[[iftags]]` evaluation.
712
+ *
713
+ * If provided, `[[iftags]]` blocks are evaluated and either kept or
714
+ * discarded based on whether the page's tags satisfy the condition.
715
+ * If omitted, `[[iftags]]` blocks pass through unresolved.
565
716
  */
566
717
  getPageTags?: IfTagsResolver;
567
718
  }
568
719
  import { SyntaxTree as SyntaxTree2 } from "@wdprlib/ast";
569
720
  /**
570
- * Result of extraction including compiled templates
721
+ * Complete result of extracting data requirements from an AST.
722
+ *
723
+ * Contains everything needed to fetch external data and then resolve modules:
724
+ * the data requirements tell the application what to fetch, and the pre-compiled
725
+ * templates are used during the resolution phase to efficiently render results.
571
726
  */
572
727
  interface ExtractionResult {
573
728
  /** Data requirements for external fetching */
@@ -578,11 +733,29 @@ interface ExtractionResult {
578
733
  compiledListUsersTemplates: Map<number, ListUsersCompiledTemplate>;
579
734
  }
580
735
  /**
581
- * Extract data requirements from a parsed AST
736
+ * Extract all data requirements from a parsed AST.
737
+ *
738
+ * Walks the entire AST to find ListPages and ListUsers module elements,
739
+ * analyzes their templates to determine which variables are used, builds
740
+ * query objects from their attributes, and pre-compiles their templates.
741
+ *
742
+ * Each module is assigned a sequential ID (separate counters for ListPages
743
+ * and ListUsers) that is used to correlate requirements with fetched data
744
+ * and compiled templates during the resolution phase.
745
+ *
746
+ * @param ast - The parsed syntax tree to analyze
747
+ * @returns Extraction result containing requirements and compiled templates
582
748
  */
583
749
  declare function extractDataRequirements(ast: SyntaxTree2): ExtractionResult;
584
750
  /**
585
- * Compile a template string into an executable function
751
+ * Compile a ListPages template string into an executable function.
752
+ *
753
+ * The template is split into alternating static strings and dynamic getter
754
+ * functions. The returned function concatenates these parts with the getter
755
+ * functions evaluated against the provided variable context.
756
+ *
757
+ * @param template - The template string containing `%%variable%%` placeholders
758
+ * @returns A compiled function that accepts a `VariableContext` and returns the rendered string
586
759
  */
587
760
  declare function compileTemplate(template: string): CompiledTemplate;
588
761
  /**
@@ -694,49 +867,85 @@ interface ResolveIncludesOptions {
694
867
  */
695
868
  declare function resolveIncludes(source: string, fetcher: IncludeFetcher, options?: ResolveIncludesOptions): string;
696
869
  /**
697
- * Compile a template string into an executable function
870
+ * Compile a ListUsers template string into an executable function.
871
+ *
872
+ * The template is split into alternating static strings and dynamic getter
873
+ * functions. The returned function concatenates these parts for each call.
874
+ *
875
+ * @param template - The template string containing `%%variable%%` placeholders
876
+ * @returns A compiled function that accepts a `ListUsersVariableContext` and returns rendered text
698
877
  */
699
878
  declare function compileListUsersTemplate(template: string): ListUsersCompiledTemplate;
700
879
  /**
701
- * Extract needed variables from a template string
880
+ * Extract the set of template variables referenced in a ListUsers template string.
881
+ *
882
+ * Scans for `%%variable%%` patterns and returns only those that match known
883
+ * ListUsers variables. Unknown variables are silently ignored.
884
+ *
885
+ * @param template - The template string from the module body
886
+ * @returns Deduplicated array of referenced variable names
702
887
  */
703
888
  declare function extractListUsersVariables(template: string): ListUsersVariable[];
704
889
  import { Element as Element5, Module as Module3 } from "@wdprlib/ast";
705
890
  /**
706
- * ListUsers module data type
891
+ * Narrowed type for the list-users variant of the Module discriminated union.
707
892
  */
708
893
  type ListUsersModuleData = Extract<Module3, {
709
894
  module: "list-users";
710
895
  }>;
711
896
  /**
712
- * Type guard for list-users module
897
+ * Type guard to check if a Module is a list-users module.
898
+ *
899
+ * @param module - A Module discriminated union value
900
+ * @returns true if the module is a list-users module
713
901
  */
714
902
  declare function isListUsersModule(module: Module3): module is ListUsersModuleData;
715
903
  /**
716
- * Resolve a single ListUsers module
717
- * Note: ListUsers returns only the logged-in user
904
+ * Resolve a single ListUsers module by substituting fetched user data into
905
+ * the pre-compiled template and re-parsing the result as wikitext.
906
+ *
907
+ * Currently ListUsers only renders the logged-in user (no iteration over
908
+ * multiple users), so the template is executed exactly once.
909
+ *
910
+ * @param _module - The list-users module data from the AST (unused, reserved for future use)
911
+ * @param data - External user data fetched by the application
912
+ * @param compiledTemplate - Pre-compiled template function from the extraction phase
913
+ * @param parse - Parser function for re-parsing the substituted template as wikitext
914
+ * @returns Array of AST elements produced by parsing the rendered template
718
915
  */
719
916
  declare function resolveListUsers(_module: ListUsersModuleData, data: ListUsersExternalData, compiledTemplate: ListUsersCompiledTemplate, parse: ParseFunction): Element5[];
720
917
  import { SyntaxTree as SyntaxTree3 } from "@wdprlib/ast";
721
918
  /**
722
- * Options for resolving modules
919
+ * Configuration for {@link resolveModules}.
920
+ *
921
+ * Callers must supply pre-extracted requirements and pre-compiled
922
+ * templates (obtained from `extractDataRequirements()` and
923
+ * `compileTemplate()` / `compileListUsersTemplate()`).
924
+ *
925
+ * @group Module Resolution
723
926
  */
724
927
  interface ResolveOptions {
725
- /** Parser function for re-parsing templates */
928
+ /** Parser function used to re-parse expanded template markup into AST nodes */
726
929
  parse: ParseFunction;
727
- /** Pre-compiled templates for ListPages */
930
+ /** Pre-compiled ListPages body templates, keyed by requirement ID */
728
931
  compiledListPagesTemplates: Map<number, CompiledTemplate>;
729
- /** Pre-compiled templates for ListUsers */
932
+ /** Pre-compiled ListUsers body templates, keyed by requirement ID */
730
933
  compiledListUsersTemplates?: Map<number, ListUsersCompiledTemplate>;
731
- /** Data requirements grouped by module type */
934
+ /**
935
+ * Data requirements grouped by module type.
936
+ * Obtained from `extractDataRequirements()`.
937
+ */
732
938
  requirements: {
733
939
  listPages?: ListPagesDataRequirement[];
734
940
  listUsers?: ListUsersDataRequirement[];
735
941
  };
736
942
  /**
737
- * URL path for @URL parameter resolution (HPC support)
738
- * Format: "/page-name/param/value/param/value"
739
- * Example: "/scp-001/offset/10/page2_limit/5"
943
+ * URL path for `@URL` parameter resolution (HPC / pagination support).
944
+ *
945
+ * Wikidot encodes pagination state in the URL path as key/value pairs
946
+ * after the page name, e.g. `"/scp-001/offset/10/page2_limit/5"`.
947
+ * When provided, `@URL` references in ListPages queries are replaced
948
+ * with the corresponding values from this path.
740
949
  */
741
950
  urlPath?: string;
742
951
  }
package/dist/index.js CHANGED
@@ -5059,6 +5059,10 @@ function parseBibliographyEntry(ctx, startPos) {
5059
5059
  consumed++;
5060
5060
  break;
5061
5061
  }
5062
+ contentNodes.push({ element: "line-break" });
5063
+ pos++;
5064
+ consumed++;
5065
+ continue;
5062
5066
  }
5063
5067
  const inlineCtx = { ...ctx, pos };
5064
5068
  const result = parseInlineUntil(inlineCtx, "NEWLINE");
@@ -5556,6 +5560,11 @@ var linkTripleRule = {
5556
5560
  break;
5557
5561
  }
5558
5562
  if (token.type === "NEWLINE") {
5563
+ if (foundPipe) {
5564
+ labelText += " ";
5565
+ } else {
5566
+ target += " ";
5567
+ }
5559
5568
  consumed++;
5560
5569
  pos++;
5561
5570
  continue;
@@ -5949,10 +5958,8 @@ var newlineLineBreakRule = {
5949
5958
  }
5950
5959
  const nextMeaningfulToken = ctx.tokens[ctx.pos + lookAhead];
5951
5960
  let isValidBlock = isBlockStartToken(nextMeaningfulToken?.type);
5952
- if (isValidBlock && (nextMeaningfulToken?.type === "LIST_BULLET" || nextMeaningfulToken?.type === "LIST_NUMBER")) {
5953
- if (!nextMeaningfulToken.lineStart) {
5954
- isValidBlock = false;
5955
- }
5961
+ if (isValidBlock && !nextMeaningfulToken?.lineStart) {
5962
+ isValidBlock = false;
5956
5963
  }
5957
5964
  if (isValidBlock && nextMeaningfulToken?.type === "HEADING_MARKER") {
5958
5965
  const markerLen = nextMeaningfulToken.value.length;
@@ -6664,7 +6671,15 @@ var footnoteRule = {
6664
6671
  if (token.type === "NEWLINE") {
6665
6672
  pos++;
6666
6673
  consumed++;
6667
- if (ctx.tokens[pos]?.type === "NEWLINE") {
6674
+ let peekPos = pos;
6675
+ let peekConsumed = 0;
6676
+ while (ctx.tokens[peekPos]?.type === "WHITESPACE") {
6677
+ peekPos++;
6678
+ peekConsumed++;
6679
+ }
6680
+ if (ctx.tokens[peekPos]?.type === "NEWLINE") {
6681
+ pos = peekPos;
6682
+ consumed += peekConsumed;
6668
6683
  while (ctx.tokens[pos]?.type === "NEWLINE") {
6669
6684
  pos++;
6670
6685
  consumed++;
@@ -7161,7 +7176,7 @@ var anchorRule = {
7161
7176
  consumed++;
7162
7177
  }
7163
7178
  foundClose = true;
7164
- while (paragraphStrip && ctx.tokens[pos]?.type === "NEWLINE") {
7179
+ if (paragraphStrip && ctx.tokens[pos]?.type === "NEWLINE" && ctx.tokens[pos + 1]?.type !== "NEWLINE") {
7165
7180
  pos++;
7166
7181
  consumed++;
7167
7182
  }
@@ -7699,6 +7714,7 @@ var bibciteRule = {
7699
7714
  return { success: false };
7700
7715
  }
7701
7716
  let label = "";
7717
+ let foundClose = false;
7702
7718
  while (pos < ctx.tokens.length) {
7703
7719
  const t = ctx.tokens[pos];
7704
7720
  if (!t)
@@ -7707,6 +7723,7 @@ var bibciteRule = {
7707
7723
  const nextT = ctx.tokens[pos + 1];
7708
7724
  if (nextT?.type === "TEXT" && nextT.value === ")") {
7709
7725
  consumed += 2;
7726
+ foundClose = true;
7710
7727
  break;
7711
7728
  }
7712
7729
  }
@@ -7717,6 +7734,9 @@ var bibciteRule = {
7717
7734
  pos++;
7718
7735
  consumed++;
7719
7736
  }
7737
+ if (!foundClose) {
7738
+ return { success: false };
7739
+ }
7720
7740
  label = label.trim();
7721
7741
  if (!label) {
7722
7742
  return { success: false };
@@ -9573,16 +9593,31 @@ function resolveIncludes(source, fetcher, options) {
9573
9593
  };
9574
9594
  return expandText(source, cachedFetcher, 0, maxDepth, []);
9575
9595
  }
9576
- var INCLUDE_PATTERN = /\[\[include\s+([\s\S]*?)\]\]/gi;
9596
+ var INCLUDE_PATTERN = /\[\[include\s([^\]]*(?:\](?!\])[^\]]*)*)\]\]/gi;
9577
9597
  function parseIncludeDirective(inner) {
9578
9598
  const normalized = inner.replace(/\n/g, " ");
9579
9599
  const parts = normalized.split("|");
9580
- const target = parts[0].trim();
9581
- const variables = {};
9600
+ const firstSegment = parts[0].trim();
9601
+ const spaceIndex = firstSegment.indexOf(" ");
9602
+ let target;
9603
+ const varSegments = [];
9604
+ if (spaceIndex !== -1) {
9605
+ target = firstSegment.slice(0, spaceIndex);
9606
+ const rest = firstSegment.slice(spaceIndex + 1).trim();
9607
+ if (rest) {
9608
+ varSegments.push(rest);
9609
+ }
9610
+ } else {
9611
+ target = firstSegment;
9612
+ }
9582
9613
  for (let i = 1;i < parts.length; i++) {
9583
9614
  const segment = parts[i].trim();
9584
- if (!segment)
9585
- continue;
9615
+ if (segment) {
9616
+ varSegments.push(segment);
9617
+ }
9618
+ }
9619
+ const variables = {};
9620
+ for (const segment of varSegments) {
9586
9621
  const eqIndex = segment.indexOf("=");
9587
9622
  if (eqIndex !== -1) {
9588
9623
  const key = segment.slice(0, eqIndex).trim();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wdprlib/parser",
3
- "version": "1.1.0",
3
+ "version": "1.1.2",
4
4
  "description": "Parser for Wikidot markup",
5
5
  "keywords": [
6
6
  "ast",