@podlite/schema 0.0.65 → 0.0.67

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 (60) hide show
  1. package/CHANGELOG.podlite +28 -0
  2. package/esm/block-names.d.ts +1 -0
  3. package/esm/block-names.js +6 -1
  4. package/esm/block-names.js.map +1 -1
  5. package/esm/blocks-helpers.d.ts +1 -1
  6. package/esm/blocks-helpers.js +2 -1
  7. package/esm/blocks-helpers.js.map +1 -1
  8. package/esm/exportHtml.js +10 -5
  9. package/esm/exportHtml.js.map +1 -1
  10. package/esm/exportMarkdown.js +58 -26
  11. package/esm/exportMarkdown.js.map +1 -1
  12. package/esm/grammarfc.js +7794 -6355
  13. package/esm/grammarfc.js.map +1 -1
  14. package/esm/guard.d.ts +5 -0
  15. package/esm/guard.js +39 -0
  16. package/esm/guard.js.map +1 -0
  17. package/esm/index.d.ts +2 -1
  18. package/esm/index.js +2 -1
  19. package/esm/index.js.map +1 -1
  20. package/esm/pluggableParser.js +3 -0
  21. package/esm/pluggableParser.js.map +1 -1
  22. package/esm/plugin-data-table.d.ts +1 -1
  23. package/esm/plugin-data-table.js +13 -11
  24. package/esm/plugin-data-table.js.map +1 -1
  25. package/esm/plugin-formatting-codes.js +49 -13
  26. package/esm/plugin-formatting-codes.js.map +1 -1
  27. package/esm/plugin-tables.d.ts +4 -2
  28. package/esm/plugin-tables.js +64 -17
  29. package/esm/plugin-tables.js.map +1 -1
  30. package/esm/selectors.d.ts +1 -0
  31. package/esm/selectors.js +42 -19
  32. package/esm/selectors.js.map +1 -1
  33. package/esm/types.d.ts +6 -2
  34. package/esm/version.d.ts +1 -1
  35. package/esm/version.js +1 -1
  36. package/lib/block-names.d.ts +1 -0
  37. package/lib/block-names.js +8 -2
  38. package/lib/blocks-helpers.d.ts +1 -1
  39. package/lib/blocks-helpers.js +2 -1
  40. package/lib/exportHtml.js +10 -5
  41. package/lib/exportMarkdown.js +58 -26
  42. package/lib/grammarfc.js +7794 -6355
  43. package/lib/guard.d.ts +5 -0
  44. package/lib/guard.js +47 -0
  45. package/lib/index.d.ts +2 -1
  46. package/lib/index.js +5 -1
  47. package/lib/pluggableParser.js +3 -0
  48. package/lib/plugin-data-table.d.ts +1 -1
  49. package/lib/plugin-data-table.js +12 -10
  50. package/lib/plugin-formatting-codes.js +49 -13
  51. package/lib/plugin-tables.d.ts +4 -2
  52. package/lib/plugin-tables.js +65 -17
  53. package/lib/selectors.d.ts +1 -0
  54. package/lib/selectors.js +44 -20
  55. package/lib/types.d.ts +6 -2
  56. package/lib/version.d.ts +1 -1
  57. package/lib/version.js +1 -1
  58. package/package.json +1 -1
  59. package/schema/AstTree.json +77 -1
  60. package/schema/PodliteDocument.json +77 -1
package/lib/guard.d.ts ADDED
@@ -0,0 +1,5 @@
1
+ export declare const isCovered: (node: unknown, ctx?: {
2
+ maskMode?: boolean;
3
+ renderMode?: string;
4
+ }) => boolean;
5
+ export declare const markGuarded: <T>(node: T, inherited?: boolean) => T;
package/lib/guard.js ADDED
@@ -0,0 +1,47 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.markGuarded = exports.isCovered = void 0;
7
+ const config_1 = __importDefault(require("./helpers/config"));
8
+ // Covered content is marked on the node, not decided at render time. A block
9
+ // carrying :masked or a G<> code can be lifted out of its document by =include,
10
+ // by a data: reference, by an alias expansion or by a selector; the mark travels
11
+ // with the node, so whatever pulls the node out cannot strip the cover off it.
12
+ const GUARD_CODE = 'G';
13
+ const isGuardCode = (node) => node.type === 'fcode' && node.name === GUARD_CODE;
14
+ const isMaskedBlock = (node) => {
15
+ if (node.type !== 'block')
16
+ return false;
17
+ const conf = (0, config_1.default)(node, {});
18
+ return conf.exists('masked') && Boolean(conf.getFirstValue('masked'));
19
+ };
20
+ // The mark says the content is covered; the render mode says whether the cover
21
+ // is applied. Draft shows what production hides, and the mark alone must not
22
+ // override that.
23
+ const isCovered = (node, ctx) => {
24
+ if (ctx?.maskMode)
25
+ return true;
26
+ if (ctx?.renderMode === 'draft')
27
+ return false;
28
+ return Boolean(node?.guarded);
29
+ };
30
+ exports.isCovered = isCovered;
31
+ const markGuarded = (node, inherited = false) => {
32
+ if (!node || typeof node !== 'object')
33
+ return node;
34
+ if (Array.isArray(node)) {
35
+ node.forEach(child => (0, exports.markGuarded)(child, inherited));
36
+ return node;
37
+ }
38
+ const target = node;
39
+ const covered = inherited || target.guarded === true || isGuardCode(target) || isMaskedBlock(target);
40
+ if (covered)
41
+ target.guarded = true;
42
+ if (Array.isArray(target.content))
43
+ target.content.forEach(child => (0, exports.markGuarded)(child, covered));
44
+ return node;
45
+ };
46
+ exports.markGuarded = markGuarded;
47
+ //# sourceMappingURL=guard.js.map
package/lib/index.d.ts CHANGED
@@ -87,7 +87,8 @@ export declare const parseFormattingCodes: (text: string, options?: {
87
87
  export { default as toHtml } from './exportHtml';
88
88
  export { default as toMarkdown } from './exportMarkdown';
89
89
  export { default as Writer } from './writer';
90
- export { parseSelector, runSelector } from './selectors';
90
+ export { parseSelector, runSelector, filePathMatches } from './selectors';
91
+ export { markGuarded, isCovered } from './guard';
91
92
  export type { SelectorDoc, ParsedSelector } from './selectors';
92
93
  export { applyImageBase } from './image-base';
93
94
  export { version } from './version';
package/lib/index.js CHANGED
@@ -39,7 +39,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
39
39
  return (mod && mod.__esModule) ? mod : { "default": mod };
40
40
  };
41
41
  Object.defineProperty(exports, "__esModule", { value: true });
42
- exports.version = exports.applyImageBase = exports.runSelector = exports.parseSelector = exports.Writer = exports.toMarkdown = exports.toHtml = exports.parseFormattingCodes = exports.parse = exports.frozenIds = exports.cleanIds = exports.podlitePluggable = exports.toAnyRules = exports.pluginCleanLocation = exports.readLinkConfig = exports.parseAttributes = exports.codeConfigWithDefaults = exports.makeAttrs = exports.toAny = exports.isSemanticBlock = exports.isNamedBlock = exports.makeTransformer = exports.makeInterator = void 0;
42
+ exports.version = exports.applyImageBase = exports.isCovered = exports.markGuarded = exports.filePathMatches = exports.runSelector = exports.parseSelector = exports.Writer = exports.toMarkdown = exports.toHtml = exports.parseFormattingCodes = exports.parse = exports.frozenIds = exports.cleanIds = exports.podlitePluggable = exports.toAnyRules = exports.pluginCleanLocation = exports.readLinkConfig = exports.parseAttributes = exports.codeConfigWithDefaults = exports.makeAttrs = exports.toAny = exports.isSemanticBlock = exports.isNamedBlock = exports.makeTransformer = exports.makeInterator = void 0;
43
43
  exports.toAst = toAst;
44
44
  exports.getTextContentFromNode = getTextContentFromNode;
45
45
  exports.getPodContentFromNode = getPodContentFromNode;
@@ -226,6 +226,10 @@ Object.defineProperty(exports, "Writer", { enumerable: true, get: function () {
226
226
  var selectors_1 = require("./selectors");
227
227
  Object.defineProperty(exports, "parseSelector", { enumerable: true, get: function () { return selectors_1.parseSelector; } });
228
228
  Object.defineProperty(exports, "runSelector", { enumerable: true, get: function () { return selectors_1.runSelector; } });
229
+ Object.defineProperty(exports, "filePathMatches", { enumerable: true, get: function () { return selectors_1.filePathMatches; } });
230
+ var guard_1 = require("./guard");
231
+ Object.defineProperty(exports, "markGuarded", { enumerable: true, get: function () { return guard_1.markGuarded; } });
232
+ Object.defineProperty(exports, "isCovered", { enumerable: true, get: function () { return guard_1.isCovered; } });
229
233
  var image_base_1 = require("./image-base");
230
234
  Object.defineProperty(exports, "applyImageBase", { enumerable: true, get: function () { return image_base_1.applyImageBase; } });
231
235
  var version_1 = require("./version");
@@ -11,6 +11,7 @@ const corePlugins_1 = __importDefault(require("./helpers/corePlugins"));
11
11
  const configPropagation_1 = require("./helpers/configPropagation");
12
12
  const headingNumbering_1 = require("./helpers/headingNumbering");
13
13
  const itemNumbering_1 = require("./helpers/itemNumbering");
14
+ const guard_1 = require("./guard");
14
15
  var ids_2 = require("./helpers/ids");
15
16
  Object.defineProperty(exports, "cleanIds", { enumerable: true, get: function () { return ids_2.cleanIds; } });
16
17
  Object.defineProperty(exports, "frozenIds", { enumerable: true, get: function () { return ids_2.frozenIds; } });
@@ -78,6 +79,7 @@ const podlitePluggable = ({ plugins = {} } = {}) => {
78
79
  (0, configPropagation_1.propagateConfigDefaults)(resultAfter.interator);
79
80
  (0, itemNumbering_1.promoteOrderedLists)(resultAfter.interator);
80
81
  (0, headingNumbering_1.attachHeadingNumberPrefix)(resultAfter.interator);
82
+ (0, guard_1.markGuarded)(resultAfter.interator);
81
83
  }
82
84
  return resultAfter;
83
85
  }
@@ -85,6 +87,7 @@ const podlitePluggable = ({ plugins = {} } = {}) => {
85
87
  (0, configPropagation_1.propagateConfigDefaults)(result.interator);
86
88
  (0, itemNumbering_1.promoteOrderedLists)(result.interator);
87
89
  (0, headingNumbering_1.attachHeadingNumberPrefix)(result.interator);
90
+ (0, guard_1.markGuarded)(result.interator);
88
91
  }
89
92
  return result;
90
93
  };
@@ -1,2 +1,2 @@
1
- declare const _default: () => (tree: any) => any;
1
+ declare const _default: (opt?: {}) => (tree: any) => any;
2
2
  export default _default;
@@ -43,7 +43,8 @@ function resolveColumnIndex(col, headerRow, maxCols) {
43
43
  }
44
44
  const idx = headerRow.indexOf(col);
45
45
  if (idx === -1) {
46
- throw new Error(`column name "${col}" not found in header`);
46
+ const names = headerRow.map(h => `"${h}"`).join(', ');
47
+ throw new Error(`column name "${col}" not found in header; header has ${names}`);
47
48
  }
48
49
  return idx;
49
50
  }
@@ -65,7 +66,7 @@ function applyRename(rows, rename, hasHeader) {
65
66
  }
66
67
  return [headerRow, ...rows.slice(1)];
67
68
  }
68
- function processDataTable(node, tree) {
69
+ function processDataTable(node, tree, report) {
69
70
  const attrs = (0, config_1.default)(node, {});
70
71
  const srcRaw = attrs.getFirstValue('src');
71
72
  const mimeAttr = attrs.getFirstValue('mime-type');
@@ -91,7 +92,7 @@ function processDataTable(node, tree) {
91
92
  if (scheme === 'data') {
92
93
  const dataBlock = (0, plugin_tables_1.findDataBlockByKey)(tree, target);
93
94
  if (!dataBlock) {
94
- console.warn(`[data-table] no =data block found for data:${target} rendered as empty`);
95
+ report('table-source-unreadable', `no =data block found for data:${target}, table rendered as empty`, node);
95
96
  return { ...node, name: 'table', content: [] };
96
97
  }
97
98
  if (mimeAttr) {
@@ -99,7 +100,7 @@ function processDataTable(node, tree) {
99
100
  }
100
101
  const dataMime = (0, config_1.default)(dataBlock, {}).getFirstValue('mime-type');
101
102
  if (!dataMime) {
102
- console.warn(`[data-table] =data block ${target} has no :mime-type rendered as empty`);
103
+ report('table-source-unreadable', `=data block ${target} has no :mime-type, table rendered as empty`, node);
103
104
  return { ...node, name: 'table', content: [] };
104
105
  }
105
106
  const parsed = (0, plugin_tables_1.parseMimeType)(dataMime);
@@ -123,12 +124,12 @@ function processDataTable(node, tree) {
123
124
  const isCsv = mimeType === 'text/csv';
124
125
  const isTsv = mimeType === 'text/tab-separated-values';
125
126
  if (!isCsv && !isTsv) {
126
- console.warn(`[data-table] unsupported mime-type ${mimeType} rendered as empty`);
127
+ report('table-source-unreadable', `unsupported mime-type ${mimeType}, table rendered as empty`, node);
127
128
  return { ...node, name: 'table', content: [] };
128
129
  }
129
130
  let rows = isCsv ? (0, plugin_tables_1.parseCsv)(csvText) : (0, plugin_tables_1.parseTsv)(csvText);
130
131
  if (rows.length === 0) {
131
- console.warn(`[data-table] ${isCsv ? 'CSV' : 'TSV'} parse produced no rows rendered as empty`);
132
+ report('table-source-unreadable', `${isCsv ? 'CSV' : 'TSV'} source has no rows, table rendered as empty`, node);
132
133
  return { ...node, name: 'table', content: [] };
133
134
  }
134
135
  if (columns) {
@@ -142,16 +143,17 @@ function processDataTable(node, tree) {
142
143
  name: 'table',
143
144
  content: (0, plugin_tables_1.csvToTableContent)(rows, hasHeader, attrs.getAllValues('allow')),
144
145
  };
145
- return (0, plugin_tables_1.normalizeCellCounts)(filledNode, 'data-table');
146
+ return (0, plugin_tables_1.normalizeCellCounts)(filledNode, 'data-table', report);
146
147
  }
147
- exports.default = () => tree => {
148
+ exports.default = (opt = {}) => tree => {
149
+ const report = (0, plugin_tables_1.makeTableReport)(opt);
148
150
  const transformer = (0, makeTransformer_1.default)({
149
151
  'data-table': node => {
150
152
  try {
151
- return processDataTable(node, tree);
153
+ return processDataTable(node, tree, report);
152
154
  }
153
155
  catch (err) {
154
- console.warn(`[data-table] ${err.message}`);
156
+ report('table-source-unreadable', err.message, node);
155
157
  return { ...node, name: 'table', content: [] };
156
158
  }
157
159
  },
@@ -41,49 +41,85 @@ const makeTransformer_1 = __importDefault(require("./helpers/makeTransformer"));
41
41
  const makeTransformer_2 = require("./helpers/makeTransformer");
42
42
  const config_1 = __importDefault(require("./helpers/config"));
43
43
  const parseAttributes_1 = require("./helpers/parseAttributes");
44
+ // `=config C<> :allow<I>` names a markup code, not a block: the trailing angles
45
+ // are the marker, and what it declares belongs to the code they name
46
+ const codeConfigOwner = (name) => typeof name === 'string' && /^[A-Z]<>$/.test(name) ? name[0] : null;
47
+ const collectAllowedIn = (content, inherited) => {
48
+ if (!Array.isArray(content))
49
+ return inherited;
50
+ let map = inherited;
51
+ for (const child of content) {
52
+ if (!child || typeof child !== 'object')
53
+ continue;
54
+ const node = child;
55
+ if (node.type !== 'config')
56
+ continue;
57
+ const owner = codeConfigOwner(node.name);
58
+ if (!owner)
59
+ continue;
60
+ if (map === inherited)
61
+ map = { ...inherited };
62
+ map[owner] = (0, config_1.default)(node, {}).getAllValues('allow');
63
+ }
64
+ return map;
65
+ };
44
66
  const middle = () => tree => {
45
67
  const transformerBlocks = (0, makeTransformer_1.default)({
46
68
  ':para': (n, ctx, visiter) => {
69
+ const allowedIn = ctx.allowedIn;
47
70
  return (0, makeTransformer_1.default)({
48
71
  ':text': (n, ctx) => {
49
- return fcparser.parse(n.value, { parseAttributes: parseAttributes_1.parseAttributes });
72
+ return fcparser.parse(n.value, { allowedIn, parseAttributes: parseAttributes_1.parseAttributes });
50
73
  },
51
74
  ':verbatim': (n, ctx) => {
52
- return fcparser.parse(n.value, { parseAttributes: parseAttributes_1.parseAttributes });
75
+ return fcparser.parse(n.value, { allowedIn, parseAttributes: parseAttributes_1.parseAttributes });
53
76
  },
54
77
  })(n, { ...ctx });
55
78
  return n;
56
79
  },
57
80
  ':block': (n, ctx, visiter) => {
81
+ // a block is a lexical scope: a code configured inside it stays inside
82
+ const allowedIn = collectAllowedIn('content' in n ? n.content : undefined, ctx.allowedIn || {});
58
83
  // only =pod may have childs blocks
59
84
  if ('name' in n && n.name === 'pod')
60
85
  return {
61
86
  ...n,
62
- content: visiter(n.content, ctx, visiter),
87
+ content: visiter(n.content, { ...ctx, allowedIn }, visiter),
63
88
  };
64
89
  const conf = (0, config_1.default)(n, ctx);
65
90
  const name = 'name' in n ? n.name : '';
66
91
  // Blocks whose content is verbatim by default — fcode parsing only
67
92
  // kicks in when :allow opts in (per spec, "Formatting within code blocks").
68
93
  const isVerbatimDefault = ['code', 'data', 'markdown', 'picture', 'formula'].includes(name);
69
- const allowValues = conf.getAllValues('allow');
70
94
  if ((0, makeTransformer_2.isNamedBlock)(name))
71
95
  return n;
96
+ // A table owns rows, and the text sits in the cells, so :allow written on
97
+ // the table reaches them. The nearest declaration wins: cell, row, table.
98
+ const inheritsAllow = name === 'row' || name === 'cell';
99
+ const declared = conf.exists('allow')
100
+ ? conf.getAllValues('allow')
101
+ : inheritsAllow
102
+ ? ctx.allowFromTable
103
+ : undefined;
104
+ const allowValues = declared || [];
105
+ const passesAllow = name === 'table' || name === 'row' ? declared : ctx.allowFromTable;
72
106
  if (isVerbatimDefault && allowValues.length === 0)
73
107
  return n;
74
- // a cell built from data carries its own set; declared empty means no code acts
75
- if (name === 'cell' && conf.exists('allow') && allowValues.length === 0)
76
- return n;
77
- const allowed = allowValues.sort();
108
+ // declared empty means no code acts on this text; nested blocks still get
109
+ // their turn, since a cell may declare a set of its own
110
+ const literal = inheritsAllow && declared !== undefined && declared.length === 0;
111
+ const allowed = [...allowValues].sort();
112
+ const inner = { ...ctx, allowedIn, allowFromTable: passesAllow };
78
113
  const transformer = (0, makeTransformer_1.default)({
79
- ':verbatim': (n, ctx) => fcparser.parse(n.value, { allowed, parseAttributes: parseAttributes_1.parseAttributes }),
80
- ':text': (n, ctx) => fcparser.parse(n.value, { allowed, parseAttributes: parseAttributes_1.parseAttributes }),
81
- ':block': (n, ctx) => transformerBlocks(n, { ...ctx }),
114
+ ':verbatim': (node, ctx) => literal ? node : fcparser.parse(node.value, { allowed, allowedIn, parseAttributes: parseAttributes_1.parseAttributes }),
115
+ ':text': (node, ctx) => literal ? node : fcparser.parse(node.value, { allowed, allowedIn, parseAttributes: parseAttributes_1.parseAttributes }),
116
+ ':block': (node, ctx) => transformerBlocks(node, { ...ctx, allowedIn, allowFromTable: passesAllow }),
82
117
  });
83
- return { ...n, content: transformer(n.content, { ...ctx }) };
118
+ return { ...n, content: transformer(n.content, inner) };
84
119
  },
85
120
  });
86
- return transformerBlocks(tree, {});
121
+ // a document needs no enclosing block, so the top level is a scope of its own
122
+ return transformerBlocks(tree, { allowedIn: collectAllowedIn(tree, {}) });
87
123
  };
88
124
  exports.default = middle;
89
125
  //# sourceMappingURL=plugin-formatting-codes.js.map
@@ -7,6 +7,8 @@ export declare function parseMimeType(raw: any): {
7
7
  export declare function findDataBlockByKey(tree: any, key: any): any;
8
8
  export declare function extractDataText(dataNode: any): any;
9
9
  export declare function csvToTableContent(csvRows: any, hasHeader?: boolean, allow?: string[]): any;
10
- export declare function normalizeCellCounts(tableNode: any, source?: string): any;
11
- declare const _default: () => (tree: any) => any;
10
+ export type TableReport = (code: string, message: string, node: any) => void;
11
+ export declare function makeTableReport(opt: any): TableReport;
12
+ export declare function normalizeCellCounts(tableNode: any, source?: string, report?: TableReport): any;
13
+ declare const _default: (opt?: {}) => (tree: any) => any;
12
14
  export default _default;
@@ -9,6 +9,7 @@ exports.parseMimeType = parseMimeType;
9
9
  exports.findDataBlockByKey = findDataBlockByKey;
10
10
  exports.extractDataText = extractDataText;
11
11
  exports.csvToTableContent = csvToTableContent;
12
+ exports.makeTableReport = makeTableReport;
12
13
  exports.normalizeCellCounts = normalizeCellCounts;
13
14
  /**
14
15
  * Plugin for fill term's for defn. From S26:
@@ -216,16 +217,27 @@ function csvToTableContent(csvRows, hasHeader = false, allow = []) {
216
217
  return buildRowBlock(cells, hasHeader && i === 0);
217
218
  });
218
219
  }
219
- // ─── Error recovery (design notes Rules 2-4) ───────────────────────────────
220
+ function makeTableReport(opt) {
221
+ const diagnostics = opt && Array.isArray(opt.diagnostics) ? opt.diagnostics : null;
222
+ return (code, message, node) => {
223
+ const location = node && node.location;
224
+ if (!diagnostics || !location)
225
+ return;
226
+ const seen = diagnostics.some(d => d.message === message && d.location.start.offset === location.start.offset);
227
+ if (!seen)
228
+ diagnostics.push({ severity: 'warning', code, message, location });
229
+ };
230
+ }
231
+ const noReport = () => { };
220
232
  // Rule 2 — table-level cell count validation. Pad short rows with empty
221
- // cells; truncate long rows. Emit a warning whenever a row is changed.
233
+ // cells; truncate long rows. Report whenever a row is changed.
222
234
  // Expected count is taken from the `:header` row if present, otherwise from
223
235
  // the row with the maximum cell count.
224
236
  //
225
237
  // Skipped when any cell uses `:colspan` or `:rowspan`: a spanning cell
226
238
  // occupies multiple columns, so naive cell counting would misreport row
227
239
  // width and drop legitimate spanned cells.
228
- function normalizeCellCounts(tableNode, source = 'table') {
240
+ function normalizeCellCounts(tableNode, source = 'table', report = noReport) {
229
241
  if (!tableNode || !Array.isArray(tableNode.content))
230
242
  return tableNode;
231
243
  const rows = tableNode.content.filter(c => c && c.type === 'block' && c.name === 'row');
@@ -252,13 +264,13 @@ function normalizeCellCounts(tableNode, source = 'table') {
252
264
  const padding = [];
253
265
  for (let i = cells.length; i < expected; i++)
254
266
  padding.push(buildCellBlock(''));
255
- console.warn(`[${source}] row has ${cells.length} cells, expected ${expected} padded with ${padding.length} empty`);
267
+ report('table-row-cells', `${source} row has ${cells.length} of ${expected} cells, padded with ${padding.length} empty. A row continued on the next line needs a blank line before the next row`, tableNode);
256
268
  mutated = true;
257
269
  return { ...child, content: [...child.content, ...padding] };
258
270
  }
259
271
  // cells.length > expected → truncate
260
272
  const dropped = cells.length - expected;
261
- console.warn(`[${source}] row has ${cells.length} cells, expected ${expected} truncated ${dropped}`);
273
+ report('table-row-cells', `${source} row has ${cells.length} of ${expected} cells, dropped ${dropped}`, tableNode);
262
274
  mutated = true;
263
275
  // Keep non-cell entries (e.g. blanklines) and the first `expected` cells
264
276
  let keepCells = expected;
@@ -282,7 +294,7 @@ function normalizeCellCounts(tableNode, source = 'table') {
282
294
  // for visible separators (`|` / `+`) surrounded by whitespace; lines without
283
295
  // any visible separator fall back to whitespace separation. Warns when more
284
296
  // than one separator type is observed within a single table.
285
- function detectMixedSeparators(lines) {
297
+ function detectMixedSeparators(lines, report = noReport, node = null) {
286
298
  const seen = new Set();
287
299
  for (const line of lines) {
288
300
  if (!line || typeof line !== 'string')
@@ -297,7 +309,7 @@ function detectMixedSeparators(lines) {
297
309
  break;
298
310
  }
299
311
  if (seen.size > 1) {
300
- console.warn(`[table] mixed separator types detected: ${Array.from(seen).join(', ')} recommend a single style`);
312
+ report('table-mixed-separators', `table mixes separator styles: ${Array.from(seen).join(', ')}. Keep one`, node);
301
313
  }
302
314
  }
303
315
  // Rule 4 — replace =table with =code block. Used when a referenced =data
@@ -448,9 +460,41 @@ const wrapImplicitCells = rowNode => {
448
460
  });
449
461
  return { ...rowNode, content: wrapped };
450
462
  };
463
+ // An abbreviated `=for row` owns its own paragraph, so cells written under it
464
+ // stay at table level and both the row and the cells lose their meaning. They
465
+ // are attached to the row above, and the author is told, per the same recovery
466
+ // contract that pads and truncates rows.
467
+ const attachOrphanCells = (tableNode, report = noReport) => {
468
+ const content = tableNode.content;
469
+ if (!Array.isArray(content))
470
+ return tableNode;
471
+ if (!content.some(c => c && c.type === 'block' && c.name === 'cell'))
472
+ return tableNode;
473
+ const out = [];
474
+ let openRow = null;
475
+ for (const child of content) {
476
+ if (child && child.type === 'block' && child.name === 'row') {
477
+ openRow = { ...child, content: Array.isArray(child.content) ? [...child.content] : [] };
478
+ out.push(openRow);
479
+ continue;
480
+ }
481
+ if (child && child.type === 'block' && child.name === 'cell') {
482
+ if (!openRow) {
483
+ openRow = { type: 'block', name: 'row', margin: child.margin || '', content: [] };
484
+ out.push(openRow);
485
+ }
486
+ openRow.content.push(child);
487
+ continue;
488
+ }
489
+ out.push(child);
490
+ }
491
+ report('table-cell-outside-row', 'a cell written outside a row was attached to the row above it; an abbreviated =for row holds only its own paragraph, so write =begin row and =end row around the cells', tableNode);
492
+ return { ...tableNode, content: out };
493
+ };
451
494
  const isStructured = tableNode => Array.isArray(tableNode.content) &&
452
495
  tableNode.content.some(c => c && c.type === 'block' && (c.name === 'row' || c.name === 'cell'));
453
- exports.default = () => tree => {
496
+ exports.default = (opt = {}) => tree => {
497
+ const report = makeTableReport(opt);
454
498
  const transformer = (0, makeTransformer_1.default)({
455
499
  table: (node, ctx, visiter) => {
456
500
  // CSV/data source reference (spec §1672):
@@ -461,9 +505,12 @@ exports.default = () => tree => {
461
505
  const dataBlock = findDataBlockByKey(tree, ref.target);
462
506
  if (!dataBlock) {
463
507
  // Rule 4: source not found → empty table (still a =table block)
464
- console.warn(`[table] no =data block found for data:${ref.target} rendered as empty`);
508
+ report('table-source-unreadable', `no =data block found for data:${ref.target}, table rendered as empty`, node);
465
509
  return { ...node, content: [] };
466
510
  }
511
+ // whatever is built out of a covered =data block is covered as well,
512
+ // otherwise the table is a way to read what the cover hides
513
+ const carryCover = built => (0, config_1.default)(dataBlock, {}).getFirstValue('masked') ? { ...built, guarded: true } : built;
467
514
  const rawMime = (0, config_1.default)(dataBlock, {}).getFirstValue('mime-type');
468
515
  const { type: mimeType, params: mimeParams } = parseMimeType(rawMime);
469
516
  const isCsv = mimeType === 'text/csv';
@@ -472,22 +519,23 @@ exports.default = () => tree => {
472
519
  const text = extractDataText(dataBlock);
473
520
  const rows = isCsv ? parseCsv(text) : parseTsv(text);
474
521
  if (rows.length === 0) {
475
- console.warn(`[table] ${isCsv ? 'CSV' : 'TSV'} parse produced no rows for data:${ref.target} rendered as empty`);
522
+ report('table-source-unreadable', `${isCsv ? 'CSV' : 'TSV'} source data:${ref.target} has no rows, table rendered as empty`, node);
476
523
  return { ...node, content: [] };
477
524
  }
478
525
  const hasHeader = mimeParams.header === 'present';
479
526
  const allow = (0, config_1.default)(node, {}).getAllValues('allow');
480
527
  const filledNode = { ...node, content: csvToTableContent(rows, hasHeader, allow) };
481
- return normalizeCellCounts(filledNode, `table data:${ref.target}`);
528
+ return carryCover(normalizeCellCounts(filledNode, `table data:${ref.target}`, report));
482
529
  }
483
530
  // Rule 4: source not tabular → render as code block so content remains visible
484
- console.warn(`[table] =data :key<${ref.target}> has non-tabular mime-type ${rawMime || '(none)'} rendered as =code`);
485
- return buildCodeFromDataBlock(node, dataBlock);
531
+ report('table-source-unreadable', `=data :key<${ref.target}> has non-tabular mime-type ${rawMime || '(none)'}, rendered as =code`, node);
532
+ return carryCover(buildCodeFromDataBlock(node, dataBlock));
486
533
  }
487
534
  // structured mode: transform row children (wrap implicit cells), then
488
535
  // apply Rule 2 cell count normalization.
489
536
  if (isStructured(node)) {
490
- const transformedContent = (node.content || []).map(c => {
537
+ const attached = attachOrphanCells(node, report);
538
+ const transformedContent = (attached.content || []).map(c => {
491
539
  if (c && c.name === 'row')
492
540
  return wrapImplicitCells(c);
493
541
  return c;
@@ -495,7 +543,7 @@ exports.default = () => tree => {
495
543
  // a matched rule replaces the node without descending, so recurse
496
544
  // here or tables nested inside cells keep their raw text rows
497
545
  const recursed = visiter ? visiter(transformedContent, ctx) : transformedContent;
498
- return normalizeCellCounts({ ...node, content: recursed }, 'table');
546
+ return normalizeCellCounts({ ...attached, content: recursed }, 'table', report);
499
547
  }
500
548
  let rows = [];
501
549
  const collectValues = row => {
@@ -521,7 +569,7 @@ exports.default = () => tree => {
521
569
  const lines = flattenDeep(rows.map(splitToLines));
522
570
  const separators = flattenDeep(seps.map(splitToLines));
523
571
  // Rule 3: warn on mixed separator types within a single table
524
- detectMixedSeparators(lines);
572
+ detectMixedSeparators(lines, report, node);
525
573
  // collect text rows
526
574
  let textRows = [];
527
575
  (0, makeTransformer_1.default)({
@@ -560,7 +608,7 @@ exports.default = () => tree => {
560
608
  },
561
609
  'head:text': head => makeHeaderRow(splitToCells(head.value).map(makeCell)),
562
610
  })(node);
563
- return normalizeCellCounts(res, 'table');
611
+ return normalizeCellCounts(res, 'table', report);
564
612
  },
565
613
  });
566
614
  return transformer(tree, {});
@@ -31,4 +31,5 @@ export type ParsedSelector = {
31
31
  patterns: Pattern[];
32
32
  };
33
33
  export declare const parseSelector: (selector: string) => ParsedSelector | undefined;
34
+ export declare const filePathMatches: (docFile: string, target: string) => boolean;
34
35
  export declare const runSelector: <T extends SelectorDoc>(selector: string, docs: T[]) => T[] | PodNode[];
package/lib/selectors.js CHANGED
@@ -1,7 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.runSelector = exports.parseSelector = void 0;
3
+ exports.runSelector = exports.filePathMatches = exports.parseSelector = void 0;
4
4
  const index_1 = require("./index");
5
+ const parseAttributes_1 = require("./helpers/parseAttributes");
5
6
  // --- predicate parser ---------------------------------------------------
6
7
  const isIdentStart = (c) => /[a-zA-Z_]/.test(c);
7
8
  const isIdentCont = (c) => /[a-zA-Z0-9_-]/.test(c);
@@ -229,6 +230,39 @@ const parseSelector = (selector) => {
229
230
  return undefined;
230
231
  };
231
232
  exports.parseSelector = parseSelector;
233
+ // The operand is read by the same grammar as a declaration: the delimiters
234
+ // around it decide whether it is a string, a list or a number. Reading it as
235
+ // raw text would put values out of reach that a document can hold — a quoted
236
+ // string among them.
237
+ const readOperand = (raw) => {
238
+ const [item] = (0, parseAttributes_1.parseAttributes)(`:x<${raw}>`);
239
+ return item ? { value: item.value, type: item.type } : undefined;
240
+ };
241
+ // The declared value with its kind kept. makeAttrs flattens a list into the
242
+ // surrounding values, which loses the very thing equality compares.
243
+ const declaredValue = (node, name, ctx) => {
244
+ const anyNode = node;
245
+ const own = (Array.isArray(anyNode.config) ? anyNode.config : []).find(c => c && c.name === name);
246
+ if (own)
247
+ return { value: own.value, type: own.type };
248
+ const configured = (anyNode.name && ctx.config?.[anyNode.name]) || [];
249
+ const inherited = configured.find(c => c && c.name === name);
250
+ return inherited ? { value: inherited.value, type: inherited.type } : undefined;
251
+ };
252
+ const sameScalar = (a, b) => typeof a === typeof b && a === b;
253
+ // A list equals another list when their elements match one by one, in order; a
254
+ // value of one kind never equals a value of another.
255
+ const sameValue = (a, b) => {
256
+ if (Array.isArray(a.value) || Array.isArray(b.value)) {
257
+ if (!Array.isArray(a.value) || !Array.isArray(b.value))
258
+ return false;
259
+ return a.value.length === b.value.length && a.value.every((item, i) => sameScalar(item, b.value[i]));
260
+ }
261
+ return sameScalar(a.value, b.value);
262
+ };
263
+ // Membership runs over the values as declared. A string is one value, even
264
+ // when it holds spaces, so a word taken from its middle is not a member.
265
+ const valuesOf = (typed) => (Array.isArray(typed.value) ? typed.value : [typed.value]);
232
266
  const matchCondition = (node, cond, ctx) => {
233
267
  const attrs = (0, index_1.makeAttrs)(node, ctx);
234
268
  const exists = attrs.exists(cond.attrName);
@@ -244,30 +278,19 @@ const matchCondition = (node, cond, ctx) => {
244
278
  return !exists;
245
279
  }
246
280
  }
281
+ const declared = declaredValue(node, cond.attrName, ctx);
282
+ const operand = readOperand(cond.valueSpec.value);
247
283
  if (cond.valueSpec.kind === 'angle') {
248
- if (!exists)
284
+ if (!exists || !declared || !operand)
249
285
  return false;
250
- const equal = String(attrs.getFirstValue(cond.attrName)) === cond.valueSpec.value;
286
+ const equal = sameValue(declared, operand);
251
287
  return cond.modifier === '!' ? !equal : equal;
252
288
  }
253
289
  if (cond.valueSpec.kind === 'contains') {
254
- if (!exists)
290
+ if (!exists || !declared || !operand)
255
291
  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);
292
+ const values = valuesOf(declared);
293
+ const present = valuesOf(operand).every(wanted => values.some(held => sameScalar(held, wanted)));
271
294
  return cond.modifier === '!' ? !present : present;
272
295
  }
273
296
  return false;
@@ -388,6 +411,7 @@ const filePathMatches = (docFile, target) => {
388
411
  }
389
412
  return a === b || a.endsWith('/' + b) || b.endsWith('/' + a);
390
413
  };
414
+ exports.filePathMatches = filePathMatches;
391
415
  const getDocIDs = (doc) => {
392
416
  const ids = [];
393
417
  (0, index_1.getFromTree)(doc.node, 'NAME', 'TITLE').forEach(block => {
@@ -421,7 +445,7 @@ const runSelector = (selector, docs) => {
421
445
  matchedDocs = docs.filter(doc => getDocIDs(doc).includes(document));
422
446
  }
423
447
  else if (scheme === 'file' && document) {
424
- matchedDocs = docs.filter(doc => filePathMatches(doc.file, document));
448
+ matchedDocs = docs.filter(doc => (0, exports.filePathMatches)(doc.file, document));
425
449
  }
426
450
  else if (scheme && scheme !== 'doc' && scheme !== 'file') {
427
451
  return [];
package/lib/types.d.ts CHANGED
@@ -98,6 +98,7 @@ export interface Image {
98
98
  type: 'image';
99
99
  src: string;
100
100
  alt?: string;
101
+ link?: string;
101
102
  }
102
103
  export interface Toc {
103
104
  type: 'toc';
@@ -139,7 +140,7 @@ export interface RootBlock extends Omit<Block, 'location'> {
139
140
  }
140
141
  export interface ParseDiagnostic {
141
142
  severity: 'warning';
142
- code: 'value-unreadable' | 'directive-unreadable';
143
+ code: 'value-unreadable' | 'directive-unreadable' | 'table-row-cells' | 'table-cell-outside-row' | 'table-mixed-separators' | 'table-source-unreadable';
143
144
  message: string;
144
145
  location: Location;
145
146
  }
@@ -245,10 +246,12 @@ export interface Ambient {
245
246
  export interface Verbatim {
246
247
  type: 'verbatim';
247
248
  value: string;
249
+ guarded?: boolean;
248
250
  }
249
251
  export interface Text {
250
252
  type: 'text';
251
253
  value: string;
254
+ guarded?: boolean;
252
255
  }
253
256
  export interface Para {
254
257
  type: 'para';
@@ -308,6 +311,7 @@ export interface Block {
308
311
  margin: string;
309
312
  config?: Array<ConfigItem | BrokenConfigItem>;
310
313
  id?: string;
314
+ guarded?: boolean;
311
315
  }
312
316
  export interface BlockPod extends Block {
313
317
  name: 'pod';
@@ -364,7 +368,7 @@ export interface BlockTable extends Omit<Block, 'content'> {
364
368
  export interface TableCell {
365
369
  name: 'cell';
366
370
  type: 'block';
367
- content: Array<string>;
371
+ content: Array<PodNode>;
368
372
  }
369
373
  export interface TableRow {
370
374
  name: 'row';