@podlite/schema 0.0.41 → 0.0.43

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.
Files changed (50) hide show
  1. package/CHANGELOG.podlite +18 -2
  2. package/esm/blocks-helpers.d.ts +5 -0
  3. package/esm/blocks-helpers.js +3 -0
  4. package/esm/blocks-helpers.js.map +1 -1
  5. package/esm/exportAny.d.ts +1 -0
  6. package/esm/exportAny.js +1 -1
  7. package/esm/exportAny.js.map +1 -1
  8. package/esm/exportHtml.js +47 -5
  9. package/esm/exportHtml.js.map +1 -1
  10. package/esm/exportMarkdown.js +37 -10
  11. package/esm/exportMarkdown.js.map +1 -1
  12. package/esm/grammar.js +232 -160
  13. package/esm/grammar.js.map +1 -1
  14. package/esm/grammarfc.js +418 -406
  15. package/esm/grammarfc.js.map +1 -1
  16. package/esm/helpers/handlers.d.ts +11 -0
  17. package/esm/helpers/handlers.js +25 -0
  18. package/esm/helpers/handlers.js.map +1 -1
  19. package/esm/helpers/makeInterator.js +2 -4
  20. package/esm/helpers/makeInterator.js.map +1 -1
  21. package/esm/index.js +2 -0
  22. package/esm/index.js.map +1 -1
  23. package/esm/plugin-data-table.d.ts +2 -0
  24. package/esm/plugin-data-table.js +152 -0
  25. package/esm/plugin-data-table.js.map +1 -0
  26. package/esm/plugin-tables.d.ts +10 -0
  27. package/esm/plugin-tables.js +7 -7
  28. package/esm/plugin-tables.js.map +1 -1
  29. package/esm/selectors.d.ts +17 -1
  30. package/esm/selectors.js +309 -19
  31. package/esm/selectors.js.map +1 -1
  32. package/lib/blocks-helpers.d.ts +5 -0
  33. package/lib/blocks-helpers.js +5 -1
  34. package/lib/exportAny.d.ts +1 -0
  35. package/lib/exportAny.js +1 -1
  36. package/lib/exportHtml.js +46 -4
  37. package/lib/exportMarkdown.js +36 -9
  38. package/lib/grammar.js +232 -160
  39. package/lib/grammarfc.js +418 -406
  40. package/lib/helpers/handlers.d.ts +11 -0
  41. package/lib/helpers/handlers.js +28 -1
  42. package/lib/helpers/makeInterator.js +2 -4
  43. package/lib/index.js +2 -0
  44. package/lib/plugin-data-table.d.ts +2 -0
  45. package/lib/plugin-data-table.js +157 -0
  46. package/lib/plugin-tables.d.ts +10 -0
  47. package/lib/plugin-tables.js +8 -0
  48. package/lib/selectors.d.ts +17 -1
  49. package/lib/selectors.js +309 -19
  50. package/package.json +1 -1
package/lib/selectors.js CHANGED
@@ -2,36 +2,327 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.runSelector = exports.parseSelector = void 0;
4
4
  const index_1 = require("./index");
5
+ // --- predicate parser ---------------------------------------------------
6
+ const isIdentStart = (c) => /[a-zA-Z_]/.test(c);
7
+ const isIdentCont = (c) => /[a-zA-Z0-9_-]/.test(c);
8
+ // Read an angle-bracketed value starting at `<`. Returns the inner content
9
+ // (without delimiters) and the index right after the closing `>`.
10
+ // Nested `<>` pairs are matched recursively.
11
+ const readAngleValue = (s, start) => {
12
+ if (s[start] !== '<')
13
+ return undefined;
14
+ let depth = 1;
15
+ let i = start + 1;
16
+ while (i < s.length && depth > 0) {
17
+ if (s[i] === '<')
18
+ depth++;
19
+ else if (s[i] === '>') {
20
+ depth--;
21
+ if (depth === 0)
22
+ return { value: s.slice(start + 1, i), end: i + 1 };
23
+ }
24
+ i++;
25
+ }
26
+ return undefined;
27
+ };
28
+ const parseCondition = (raw) => {
29
+ const s = raw.trim();
30
+ if (!s.startsWith(':'))
31
+ return undefined;
32
+ let i = 1;
33
+ let modifier;
34
+ if (s.startsWith('!?', i)) {
35
+ modifier = '!?';
36
+ i += 2;
37
+ }
38
+ else if (s[i] === '!') {
39
+ modifier = '!';
40
+ i += 1;
41
+ }
42
+ else if (s[i] === '?') {
43
+ modifier = '?';
44
+ i += 1;
45
+ }
46
+ if (i >= s.length || !isIdentStart(s[i]))
47
+ return undefined;
48
+ const nameStart = i;
49
+ while (i < s.length && isIdentCont(s[i]))
50
+ i++;
51
+ const attrName = s.slice(nameStart, i);
52
+ if (i >= s.length)
53
+ return { modifier, attrName };
54
+ if (s[i] === '~' && s[i + 1] === '<') {
55
+ const read = readAngleValue(s, i + 1);
56
+ if (!read || read.end !== s.length)
57
+ return undefined;
58
+ return { modifier, attrName, valueSpec: { kind: 'contains', value: read.value } };
59
+ }
60
+ if (s[i] === '<') {
61
+ const read = readAngleValue(s, i);
62
+ if (!read || read.end !== s.length)
63
+ return undefined;
64
+ return { modifier, attrName, valueSpec: { kind: 'angle', value: read.value } };
65
+ }
66
+ return undefined;
67
+ };
68
+ // Split predicate body by whitespace at top level, respecting <...> nesting.
69
+ const splitConditions = (body) => {
70
+ const chunks = [];
71
+ let depth = 0;
72
+ let buf = '';
73
+ for (let i = 0; i < body.length; i++) {
74
+ const c = body[i];
75
+ if (c === '<')
76
+ depth++;
77
+ else if (c === '>') {
78
+ if (depth === 0)
79
+ return undefined;
80
+ depth--;
81
+ }
82
+ if (depth === 0 && /\s/.test(c)) {
83
+ if (buf) {
84
+ chunks.push(buf);
85
+ buf = '';
86
+ }
87
+ continue;
88
+ }
89
+ buf += c;
90
+ }
91
+ if (depth !== 0)
92
+ return undefined;
93
+ if (buf)
94
+ chunks.push(buf);
95
+ return chunks;
96
+ };
97
+ const parsePredicate = (body) => {
98
+ const chunks = splitConditions(body);
99
+ if (!chunks)
100
+ return undefined;
101
+ const conditions = [];
102
+ for (const chunk of chunks) {
103
+ const cond = parseCondition(chunk);
104
+ if (!cond)
105
+ return undefined;
106
+ conditions.push(cond);
107
+ }
108
+ return conditions;
109
+ };
110
+ const parsePattern = (raw) => {
111
+ const s = raw.trim();
112
+ if (!s)
113
+ return undefined;
114
+ let blockType;
115
+ let i = 0;
116
+ if (s[0] === '*') {
117
+ blockType = '*';
118
+ i = 1;
119
+ }
120
+ else if (isIdentStart(s[0])) {
121
+ let j = 1;
122
+ while (j < s.length && isIdentCont(s[j]))
123
+ j++;
124
+ blockType = s.slice(0, j);
125
+ i = j;
126
+ }
127
+ else {
128
+ return undefined;
129
+ }
130
+ // skip optional whitespace between block-type and predicate
131
+ while (i < s.length && /\s/.test(s[i]))
132
+ i++;
133
+ if (i >= s.length)
134
+ return { blockType };
135
+ if (s[i] !== '[' || s[s.length - 1] !== ']')
136
+ return undefined;
137
+ const body = s.slice(i + 1, s.length - 1).trim();
138
+ if (!body)
139
+ return undefined;
140
+ const predicate = parsePredicate(body);
141
+ if (!predicate)
142
+ return undefined;
143
+ return { blockType, predicate };
144
+ };
145
+ // Split pattern-list by comma at top level, respecting [...] and <...> nesting.
146
+ const splitPatterns = (s) => {
147
+ const chunks = [];
148
+ let bracketDepth = 0;
149
+ let angleDepth = 0;
150
+ let buf = '';
151
+ for (let i = 0; i < s.length; i++) {
152
+ const c = s[i];
153
+ if (c === '[')
154
+ bracketDepth++;
155
+ else if (c === ']') {
156
+ if (bracketDepth === 0)
157
+ return undefined;
158
+ bracketDepth--;
159
+ }
160
+ else if (c === '<')
161
+ angleDepth++;
162
+ else if (c === '>') {
163
+ if (angleDepth === 0)
164
+ return undefined;
165
+ angleDepth--;
166
+ }
167
+ if (c === ',' && bracketDepth === 0 && angleDepth === 0) {
168
+ chunks.push(buf);
169
+ buf = '';
170
+ continue;
171
+ }
172
+ buf += c;
173
+ }
174
+ if (bracketDepth !== 0 || angleDepth !== 0)
175
+ return undefined;
176
+ chunks.push(buf);
177
+ return chunks;
178
+ };
179
+ const parsePatternList = (filterPart) => {
180
+ if (!filterPart)
181
+ return [];
182
+ const chunks = splitPatterns(filterPart);
183
+ if (!chunks)
184
+ return undefined;
185
+ const patterns = [];
186
+ for (const chunk of chunks) {
187
+ const trimmed = chunk.trim();
188
+ if (!trimmed)
189
+ continue;
190
+ const p = parsePattern(trimmed);
191
+ if (!p)
192
+ return undefined;
193
+ patterns.push(p);
194
+ }
195
+ return patterns;
196
+ };
5
197
  const parseSelector = (selector) => {
6
198
  const trimmed = selector.trim();
7
199
  if (!trimmed)
8
200
  return undefined;
9
- // Split on the first '|' — left is source, right is blocks selector
201
+ // Split on the first '|' — left is source, right is pattern-list
10
202
  const pipeIdx = trimmed.indexOf('|');
11
203
  const sourcePart = (pipeIdx === -1 ? trimmed : trimmed.slice(0, pipeIdx)).trim();
12
204
  const filterPart = pipeIdx === -1 ? '' : trimmed.slice(pipeIdx + 1).trim();
13
- const blockFilters = filterPart
14
- ? filterPart
15
- .split(',')
16
- .map(s => s.trim())
17
- .filter(Boolean)
18
- : [];
19
- // Source: scheme:path or scheme:path#anchor
20
- const sourceMatch = sourcePart.match(/^([^:]+):([^#]+)(?:#(.+))?$/);
205
+ const patterns = parsePatternList(filterPart);
206
+ if (patterns === undefined)
207
+ return undefined;
208
+ // Source: scheme:path or scheme:path#anchor. Scheme is identifier-shaped
209
+ // so that bare predicates like `*[:attr<v>]` don't get consumed as scheme.
210
+ const sourceMatch = sourcePart.match(/^([a-zA-Z][a-zA-Z0-9-]*):([^#]+)(?:#(.+))?$/);
21
211
  if (sourceMatch) {
22
212
  return {
23
213
  scheme: sourceMatch[1],
24
214
  document: sourceMatch[2].trim(),
25
215
  anchor: sourceMatch[3],
26
- blockFilters,
216
+ patterns,
27
217
  };
28
218
  }
29
- if (blockFilters.length > 0) {
30
- return { blockFilters };
219
+ if (patterns.length > 0) {
220
+ return { patterns };
221
+ }
222
+ // No pipe — treat the whole input as a pattern-list (CLI-friendly shorthand
223
+ // for filter-only selectors, e.g. `head1, code[:lang<python>]`).
224
+ if (pipeIdx === -1) {
225
+ const fallback = parsePatternList(sourcePart);
226
+ if (fallback && fallback.length > 0)
227
+ return { patterns: fallback };
31
228
  }
32
229
  return undefined;
33
230
  };
34
231
  exports.parseSelector = parseSelector;
232
+ const matchCondition = (node, cond, ctx) => {
233
+ const attrs = (0, index_1.makeAttrs)(node, ctx);
234
+ const exists = attrs.exists(cond.attrName);
235
+ if (!cond.valueSpec) {
236
+ switch (cond.modifier) {
237
+ case undefined:
238
+ return exists && Boolean(attrs.getFirstValue(cond.attrName));
239
+ case '!':
240
+ return exists && attrs.getFirstValue(cond.attrName) === false;
241
+ case '?':
242
+ return exists;
243
+ case '!?':
244
+ return !exists;
245
+ }
246
+ }
247
+ if (cond.valueSpec.kind === 'angle') {
248
+ if (!exists)
249
+ return false;
250
+ const equal = String(attrs.getFirstValue(cond.attrName)) === cond.valueSpec.value;
251
+ return cond.modifier === '!' ? !equal : equal;
252
+ }
253
+ if (cond.valueSpec.kind === 'contains') {
254
+ if (!exists)
255
+ return false;
256
+ // Author-facing semantics: `:tags<a b c>` is a list of three elements.
257
+ // Grammar parses unquoted identifier sequences as a single string, so
258
+ // split string values on whitespace/comma to recover list shape.
259
+ const tokens = [];
260
+ for (const v of attrs.getAllValues(cond.attrName)) {
261
+ if (typeof v === 'string') {
262
+ for (const t of v.split(/[\s,]+/))
263
+ if (t)
264
+ tokens.push(t);
265
+ }
266
+ else {
267
+ tokens.push(String(v));
268
+ }
269
+ }
270
+ const present = tokens.includes(cond.valueSpec.value);
271
+ return cond.modifier === '!' ? !present : present;
272
+ }
273
+ return false;
274
+ };
275
+ // Replicate name/level handling from getFromTree for backward compat with
276
+ // 'head1' / 'item' style block-types.
277
+ const blockTypeMatches = (node, blockType) => {
278
+ if (blockType === '*')
279
+ return true;
280
+ const anyNode = node;
281
+ if (anyNode.name === blockType)
282
+ return true;
283
+ const m = blockType.match(/^(head|item)(\d+)?$/);
284
+ if (m) {
285
+ const [, baseName, levelStr] = m;
286
+ if (anyNode.name !== baseName)
287
+ return false;
288
+ const expectedLevel = levelStr ? parseInt(levelStr, 10) : baseName === 'item' ? 1 : undefined;
289
+ if (expectedLevel === undefined)
290
+ return true;
291
+ // Heading plugin stores level as the regex capture string; coerce.
292
+ return Number(anyNode.level) === expectedLevel;
293
+ }
294
+ return false;
295
+ };
296
+ const matchesPattern = (node, pattern, ctx) => {
297
+ if (!blockTypeMatches(node, pattern.blockType))
298
+ return false;
299
+ if (!pattern.predicate)
300
+ return true;
301
+ return pattern.predicate.every(c => matchCondition(node, c, ctx));
302
+ };
303
+ // Walk one document in source order, accumulating =config defaults forward
304
+ // (last-wins, shared across the subtree) so a block matches against the same
305
+ // effective attributes the renderer would apply.
306
+ const collectMatches = (node, patterns, config, seen, out) => {
307
+ if (Array.isArray(node)) {
308
+ for (const child of node)
309
+ collectMatches(child, patterns, config, seen, out);
310
+ return;
311
+ }
312
+ if (!node || typeof node !== 'object')
313
+ return;
314
+ const anyNode = node;
315
+ if (anyNode.type === 'config' && typeof anyNode.name === 'string' && anyNode.config) {
316
+ config[anyNode.name] = anyNode.config;
317
+ }
318
+ else if (anyNode.type === 'block' && !seen.has(node) && patterns.some(p => matchesPattern(node, p, { config }))) {
319
+ out.push(node);
320
+ seen.add(node);
321
+ }
322
+ if (anyNode.content !== undefined) {
323
+ collectMatches(anyNode.content, patterns, config, seen, out);
324
+ }
325
+ };
35
326
  // Normalize a path for loose suffix comparison:
36
327
  // 'src/foo.podlite' ~= 'foo.podlite'
37
328
  // './includes/x.podlite' ~= 'includes/x.podlite'
@@ -124,7 +415,7 @@ const runSelector = (selector, docs) => {
124
415
  const parsed = (0, exports.parseSelector)(selector);
125
416
  if (!parsed)
126
417
  return [];
127
- const { scheme, document, anchor, blockFilters } = parsed;
418
+ const { scheme, document, anchor, patterns } = parsed;
128
419
  let matchedDocs = docs;
129
420
  if (scheme === 'doc' && document) {
130
421
  matchedDocs = docs.filter(doc => getDocIDs(doc).includes(document));
@@ -146,17 +437,16 @@ const runSelector = (selector, docs) => {
146
437
  }
147
438
  return collectedBlocks;
148
439
  }
149
- // Block filters extract blocks by name across matched docs
150
- if (blockFilters.length > 0) {
440
+ // Patternssource-order traversal, apply each pattern, dedupe across patterns
441
+ if (patterns.length > 0) {
151
442
  const collectedBlocks = [];
443
+ const seen = new Set();
152
444
  for (const d of matchedDocs) {
153
- for (const name of blockFilters) {
154
- collectedBlocks.push(...(0, index_1.getFromTree)(d.node, name));
155
- }
445
+ collectMatches(d.node, patterns, {}, seen, collectedBlocks);
156
446
  }
157
447
  return collectedBlocks;
158
448
  }
159
- // No anchor, no filter — return whole docs
449
+ // No anchor, no patterns — return whole docs
160
450
  return matchedDocs.map(d => d.node);
161
451
  };
162
452
  exports.runSelector = runSelector;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@podlite/schema",
3
- "version": "0.0.41",
3
+ "version": "0.0.43",
4
4
  "description": "AST tools and schema for Podlite markup language — validate, traverse, and transform document trees",
5
5
  "main": "lib/index.js",
6
6
  "homepage": "https://podlite.org",