@podlite/schema 0.0.67 → 0.0.68

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 (55) hide show
  1. package/CHANGELOG.podlite +32 -0
  2. package/esm/ast-helpers.d.ts +25 -1
  3. package/esm/ast-helpers.js +101 -2
  4. package/esm/ast-helpers.js.map +1 -1
  5. package/esm/attribute-names.d.ts +5 -0
  6. package/esm/attribute-names.js +36 -0
  7. package/esm/attribute-names.js.map +1 -0
  8. package/esm/exportAny.js +2 -2
  9. package/esm/exportAny.js.map +1 -1
  10. package/esm/exportHtml.js +50 -17
  11. package/esm/exportHtml.js.map +1 -1
  12. package/esm/exportMarkdown.js +28 -16
  13. package/esm/exportMarkdown.js.map +1 -1
  14. package/esm/folded-sections.d.ts +1 -0
  15. package/esm/folded-sections.js +80 -0
  16. package/esm/folded-sections.js.map +1 -0
  17. package/esm/grammarfc.js +6099 -15834
  18. package/esm/grammarfc.js.map +1 -1
  19. package/esm/helpers/corePlugins.js +9 -3
  20. package/esm/helpers/corePlugins.js.map +1 -1
  21. package/esm/helpers/html-attr.d.ts +1 -0
  22. package/esm/helpers/html-attr.js +6 -0
  23. package/esm/helpers/html-attr.js.map +1 -0
  24. package/esm/index.d.ts +4 -1
  25. package/esm/index.js +4 -1
  26. package/esm/index.js.map +1 -1
  27. package/esm/pluggableParser.d.ts +1 -1
  28. package/esm/pluggableParser.js +4 -1
  29. package/esm/pluggableParser.js.map +1 -1
  30. package/esm/types.d.ts +27 -16
  31. package/esm/version.d.ts +1 -1
  32. package/esm/version.js +1 -1
  33. package/lib/ast-helpers.d.ts +25 -1
  34. package/lib/ast-helpers.js +106 -3
  35. package/lib/attribute-names.d.ts +5 -0
  36. package/lib/attribute-names.js +41 -0
  37. package/lib/exportAny.js +1 -1
  38. package/lib/exportHtml.js +52 -19
  39. package/lib/exportMarkdown.js +27 -15
  40. package/lib/folded-sections.d.ts +1 -0
  41. package/lib/folded-sections.js +84 -0
  42. package/lib/grammarfc.js +6099 -15834
  43. package/lib/helpers/corePlugins.js +9 -3
  44. package/lib/helpers/html-attr.d.ts +1 -0
  45. package/lib/helpers/html-attr.js +10 -0
  46. package/lib/index.d.ts +4 -1
  47. package/lib/index.js +7 -1
  48. package/lib/pluggableParser.d.ts +1 -1
  49. package/lib/pluggableParser.js +5 -1
  50. package/lib/types.d.ts +27 -16
  51. package/lib/version.d.ts +1 -1
  52. package/lib/version.js +1 -1
  53. package/package.json +1 -1
  54. package/schema/AstTree.json +588 -436
  55. package/schema/PodliteDocument.json +588 -436
package/lib/exportHtml.js CHANGED
@@ -47,24 +47,45 @@ const plugin_clean_location_1 = __importDefault(require("./plugin-clean-location
47
47
  const ast_helpers_1 = require("./ast-helpers");
48
48
  const link_config_1 = require("./helpers/link-config");
49
49
  const entities_1 = require("entities");
50
- const quoteValue = (value) => value.replace(/"/g, '"');
50
+ const html_attr_1 = require("./helpers/html-attr");
51
+ // HTML gives an anchor without href to a link whose target the author never
52
+ // wrote; an empty href would claim the current document instead. The address is
53
+ // quoted like every other attribute value: a quote inside it would otherwise
54
+ // close the attribute and let the document write markup of its own.
55
+ const hrefAttr = (node, ctx) => {
56
+ const target = (0, ast_helpers_1.linkTarget)(node);
57
+ if (target === undefined)
58
+ return '';
59
+ const address = (0, ast_helpers_1.sameDocTarget)(target, ctx);
60
+ // A link level one refused points at nothing, and an anchor with no href is what
61
+ // html gives that case. An address shaped from a target that was never there only
62
+ // looks like a working link.
63
+ return address === undefined ? '' : ` href="${(0, html_attr_1.quoteAttribute)(String(address))}"`;
64
+ };
51
65
  const linkConfigAttrs = config => {
52
66
  const { newContext, title, lang, download } = (0, link_config_1.readLinkConfig)(config);
53
67
  const attrs = [];
54
68
  if (newContext)
55
69
  attrs.push(' target="_blank"');
56
70
  if (title !== undefined)
57
- attrs.push(` title="${quoteValue(title)}"`);
71
+ attrs.push(` title="${(0, html_attr_1.quoteAttribute)(title)}"`);
58
72
  if (lang !== undefined)
59
- attrs.push(` hreflang="${quoteValue(lang)}"`);
73
+ attrs.push(` hreflang="${(0, html_attr_1.quoteAttribute)(lang)}"`);
60
74
  if (download === true)
61
75
  attrs.push(' download');
62
76
  else if (typeof download === 'string')
63
- attrs.push(` download="${quoteValue(download)}"`);
77
+ attrs.push(` download="${(0, html_attr_1.quoteAttribute)(download)}"`);
64
78
  return attrs.join('');
65
79
  };
80
+ // Shaped by the same rule the address is: an anchor keeping a dot the address drops
81
+ // is how :id<v1.2> came to render an unreachable target. Every place that writes an
82
+ // anchor goes through here, or the two sides part again.
83
+ const anchorOf = (node, ctx) => {
84
+ const written = (0, ast_helpers_1.getExplicitNodeId)(node, ctx);
85
+ return written === null ? null : (ctx?.__anchors?.shape || ast_helpers_1.toFragment)(written);
86
+ };
66
87
  const openTag = (tag, node, ctx, attrs = '') => {
67
- const id = (0, ast_helpers_1.getExplicitNodeId)(node, ctx);
88
+ const id = anchorOf(node, ctx);
68
89
  return `<${tag}${id ? ` id="${id}"` : ''}${attrs}>`;
69
90
  };
70
91
  const rules = {
@@ -147,18 +168,12 @@ const rules = {
147
168
  'H<>': (0, handlers_1.wrapContent)('<sup>', '</sup>'),
148
169
  'J<>': (0, handlers_1.wrapContent)('<sub>', '</sub>'),
149
170
  'L<>': (0, handlers_1.setFn)((node, ctx) => {
150
- let { meta } = node;
151
- if (meta === null) {
152
- meta = node.content;
153
- }
154
- return (0, handlers_1.wrapContent)(`<a href="${(0, ast_helpers_1.sameDocTarget)(meta, ctx)}"${linkConfigAttrs((0, config_1.codeConfigWithDefaults)(node, ctx))}>`, `</a>`);
171
+ const attrs = linkConfigAttrs((0, config_1.codeConfigWithDefaults)(node, ctx));
172
+ return (0, handlers_1.wrapContent)(`<a${hrefAttr(node, ctx)}${attrs}>`, `</a>`);
155
173
  }),
156
174
  'W<>': (0, handlers_1.setFn)((node, ctx) => {
157
- let { meta } = node;
158
- if (meta === null) {
159
- meta = node.content;
160
- }
161
- return (0, handlers_1.wrapContent)(`<a href="${(0, ast_helpers_1.sameDocTarget)(meta, ctx)}"${linkConfigAttrs((0, config_1.codeConfigWithDefaults)(node, ctx))} class="backlink">`, `</a>`);
175
+ const attrs = linkConfigAttrs((0, config_1.codeConfigWithDefaults)(node, ctx));
176
+ return (0, handlers_1.wrapContent)(`<a${hrefAttr(node, ctx)}${attrs} class="backlink">`, `</a>`);
162
177
  }),
163
178
  /**
164
179
  * CSS rules for footnotes
@@ -244,6 +259,19 @@ const rules = {
244
259
  pod: handlers_1.content,
245
260
  ':code': (0, handlers_1.wrapContent)('<pre><code>', '</code></pre>'),
246
261
  code: (0, handlers_1.handleNested)((0, handlers_1.setFn)((node, ctx) => (0, handlers_1.wrapContent)(`${openTag('pre', node, ctx)}<code>`, '</code></pre>'))),
262
+ // a folded section is a heading plus the nodes under it; the disclosure is
263
+ // native so the reader needs no script to open it
264
+ _folded_section: (writer, processor) => (node, ctx, interator) => {
265
+ const [heading, ...rest] = (node.content || []);
266
+ const open = node.foldedState === false || node.foldedState === 0 || node.foldedState === '0';
267
+ writer.writeRaw(`<details class="folded-section"${open ? ' open' : ''}>`);
268
+ writer.writeRaw('<summary class="folded-section-summary">');
269
+ if (heading)
270
+ interator([heading], ctx);
271
+ writer.writeRaw('</summary><div class="folded-section-content">');
272
+ interator(rest, ctx);
273
+ writer.writeRaw('</div></details>');
274
+ },
247
275
  data: handlers_1.emptyContent,
248
276
  ':verbatim': (writer, processor) => (node, ctx, interator) => {
249
277
  if (node.error) {
@@ -277,7 +305,7 @@ const rules = {
277
305
  // block =para
278
306
  // With an :id the block owns the <p> so the anchor lands on it; without one
279
307
  // the inner paragraph renders as before.
280
- para: (0, handlers_1.handleNested)((0, handlers_1.setFn)((node, ctx) => (0, ast_helpers_1.getExplicitNodeId)(node, ctx)
308
+ para: (0, handlers_1.handleNested)((0, handlers_1.setFn)((node, ctx) => anchorOf(node, ctx)
281
309
  ? (0, handlers_1.subUse)({ ':para': handlers_1.content }, (0, handlers_1.wrapContent)(openTag('p', node, ctx), '</p>'))
282
310
  : handlers_1.content)),
283
311
  ':para': (0, handlers_1.setFn)((node, ctx) => (0, handlers_1.wrapContent)(openTag('p', node, ctx), '</p>')),
@@ -316,7 +344,7 @@ const rules = {
316
344
  'comment:block': handlers_1.emptyContent,
317
345
  'boundary:block': (writer, processor) => (node, ctx) => {
318
346
  const conf = (0, config_1.default)(node, ctx);
319
- const id = (0, ast_helpers_1.getExplicitNodeId)(node, ctx);
347
+ const id = anchorOf(node, ctx);
320
348
  const idAttr = id ? ` id="${id}"` : '';
321
349
  if (conf.exists('caption')) {
322
350
  writer.writeRaw(`<hr${idAttr} title="`);
@@ -329,7 +357,7 @@ const rules = {
329
357
  },
330
358
  // The term opens the pair, so an :id on the definition lands on its <dt>.
331
359
  defn: (0, handlers_1.setFn)((node, ctx) => {
332
- const id = (0, ast_helpers_1.getExplicitNodeId)(node, ctx);
360
+ const id = anchorOf(node, ctx);
333
361
  return id
334
362
  ? (0, handlers_1.subUse)({ 'term:para': (0, handlers_1.wrapContent)(`<dt id="${id}">`, '</dt><dd>') }, (0, handlers_1.wrapContent)('', '</dd>'))
335
363
  : (0, handlers_1.wrapContent)('', '</dd>');
@@ -421,7 +449,12 @@ const rules = {
421
449
  ':toc-list': (0, handlers_1.setFn)((node, ctx) => (0, handlers_1.wrapContent)(`<ul class="toc-list listlevel${node.level}">`, '</ul>')),
422
450
  ':toc-item': (0, handlers_1.setFn)((node, ctx) => (0, handlers_1.wrapContent)('<li class="toc-item">', '</li>')),
423
451
  ':image': (writer, processor) => (node, ctx, interator) => {
424
- writer.writeRaw(`<img src="${(0, image_base_1.applyImageBase)(node.src, ctx?.base)}" alt="${node.alt || ''}"/>`);
452
+ const src = (0, html_attr_1.quoteAttribute)(String((0, image_base_1.applyImageBase)(node.src, ctx?.base) ?? ''));
453
+ // html reads a missing alt as "this image is part of the content" and an empty
454
+ // one as "decorative", so an alternative text the author never wrote is left out
455
+ const altText = (0, ast_helpers_1.writtenValue)(node.alt);
456
+ const alt = altText === undefined ? '' : ` alt="${(0, html_attr_1.quoteAttribute)(altText)}"`;
457
+ writer.writeRaw(`<img src="${src}"${alt}/>`);
425
458
  },
426
459
  };
427
460
  const toHtml = opt => (0, exportAny_1.default)({
@@ -52,6 +52,26 @@ const linkTitle = config => {
52
52
  const { title } = (0, link_config_1.readLinkConfig)(config);
53
53
  return title === undefined ? '' : ` "${title.replace(/"/g, '\\"')}"`;
54
54
  };
55
+ // Markdown has no form for a link whose target the author never wrote — an empty
56
+ // address there claims the current document — so the text is written on its own.
57
+ // A bracket or a space inside the address would end it where markdown looks for
58
+ // the closing one, so such an address is written in angle brackets, the form
59
+ // markdown keeps for exactly that. An angle of its own is percent-encoded rather
60
+ // than dropped, so the address survives the round trip — no source text reaches
61
+ // here holding one today, since the parser ends the code on it.
62
+ const markdownAddress = (address) => /[()\s]/.test(address) ? `<${address.replace(/</g, '%3C').replace(/>/g, '%3E')}>` : address;
63
+ const linkWrap = (node, ctx) => {
64
+ const target = (0, ast_helpers_1.linkTarget)(node);
65
+ if (target === undefined)
66
+ return (0, handlers_1.wrapContent)('', '');
67
+ const resolved = (0, ast_helpers_1.sameDocTarget)(target, ctx, markdownAnchors(ctx));
68
+ // Markdown has no anchor without an address either, so a refused link keeps its
69
+ // text and loses the brackets.
70
+ if (resolved === undefined)
71
+ return (0, handlers_1.wrapContent)('', '');
72
+ const address = String(resolved);
73
+ return (0, handlers_1.wrapContent)(`[`, `](${markdownAddress(address)}${linkTitle((0, config_1.codeConfigWithDefaults)(node, ctx))})`);
74
+ };
55
75
  // A cell's own text is written whole. Trimming each fragment on its own eats the
56
76
  // spaces that sit between them — between a word and a markup code, or around a
57
77
  // sign the parser split the text on — so only the cell edges are stripped.
@@ -122,20 +142,8 @@ const rules = {
122
142
  },
123
143
  'H<>': (0, handlers_1.wrapContent)('<sup>', '</sup>'),
124
144
  'J<>': (0, handlers_1.wrapContent)('<sub>', '</sub>'),
125
- 'L<>': (0, handlers_1.setFn)((node, ctx) => {
126
- let { meta } = node;
127
- if (meta === null) {
128
- meta = node.content;
129
- }
130
- return (0, handlers_1.wrapContent)(`[`, `](${(0, ast_helpers_1.sameDocTarget)(meta, ctx, markdownAnchors(ctx))}${linkTitle((0, config_1.codeConfigWithDefaults)(node, ctx))})`);
131
- }),
132
- 'W<>': (0, handlers_1.setFn)((node, ctx) => {
133
- let { meta } = node;
134
- if (meta === null) {
135
- meta = node.content;
136
- }
137
- return (0, handlers_1.wrapContent)(`[`, `](${(0, ast_helpers_1.sameDocTarget)(meta, ctx, markdownAnchors(ctx))}${linkTitle((0, config_1.codeConfigWithDefaults)(node, ctx))})`);
138
- }),
145
+ 'L<>': (0, handlers_1.setFn)((node, ctx) => linkWrap(node, ctx)),
146
+ 'W<>': (0, handlers_1.setFn)((node, ctx) => linkWrap(node, ctx)),
139
147
  'N<>': (writer, processor) => {
140
148
  writer.addListener('end', () => {
141
149
  if (!writer.hasOwnProperty('FOOTNOTES')) {
@@ -457,7 +465,11 @@ const rules = {
457
465
  ':toc-list': handlers_1.emptyContent,
458
466
  ':toc-item': handlers_1.emptyContent,
459
467
  ':image': (writer, processor) => (node, ctx, interator) => {
460
- writer.writeRaw(`![${node.alt || ''}](${(0, image_base_1.applyImageBase)(node.src, ctx?.base)})`);
468
+ // the alternative text sits between square brackets, where a `]` of its own
469
+ // would end it, and the address between round ones — the same shape a link has
470
+ const alt = ((0, ast_helpers_1.writtenValue)(node.alt) ?? '').replace(/([[\]\\])/g, '\\$1');
471
+ const src = markdownAddress(String((0, image_base_1.applyImageBase)(node.src, ctx?.base) ?? ''));
472
+ writer.writeRaw(`![${alt}](${src})`);
461
473
  },
462
474
  };
463
475
  const toMarkdown = opt => (0, exportAny_1.default)({
@@ -0,0 +1 @@
1
+ export declare const applyFoldedSections: (node: any) => any;
@@ -0,0 +1,84 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.applyFoldedSections = void 0;
4
+ // Folding of a section belongs to the tree, not to one renderer: the command
5
+ // line and every build-time producer read the same document as the editor does.
6
+ // Moved here from @podlite/to-jsx, where it lived and left exportHtml and
7
+ // exportMarkdown unable to fold anything.
8
+ // `:folded` on a heading folds the whole section — the heading plus every
9
+ // following node up to the next same-or-higher-level heading. Detect the
10
+ // attribute on =head nodes in a container's content array and wrap that
11
+ // range in a synthetic `_folded_section` block so the JSX renderer can emit
12
+ // <details> around it.
13
+ const getFoldedAttr = (node) => {
14
+ const config = node && node.config;
15
+ if (!Array.isArray(config))
16
+ return null;
17
+ const entry = config.find((c) => c && c.name === 'folded');
18
+ return entry ? entry.value : null;
19
+ };
20
+ const isHeadBlock = (node) => node && node.type === 'block' && node.name === 'head' && node.level !== undefined && node.level !== null;
21
+ const headLevel = (node) => Number(node.level);
22
+ const groupFoldedSections = (content) => {
23
+ if (!Array.isArray(content))
24
+ return content;
25
+ const result = [];
26
+ let i = 0;
27
+ while (i < content.length) {
28
+ const node = content[i];
29
+ if (isHeadBlock(node)) {
30
+ const folded = getFoldedAttr(node);
31
+ if (folded !== null) {
32
+ const level = headLevel(node);
33
+ const sectionNodes = [node];
34
+ let j = i + 1;
35
+ while (j < content.length) {
36
+ const next = content[j];
37
+ if (isHeadBlock(next) && headLevel(next) <= level)
38
+ break;
39
+ sectionNodes.push(next);
40
+ j++;
41
+ }
42
+ result.push({
43
+ type: 'block',
44
+ name: '_folded_section',
45
+ content: sectionNodes,
46
+ foldedState: folded,
47
+ location: node.location,
48
+ });
49
+ i = j;
50
+ continue;
51
+ }
52
+ }
53
+ result.push(node);
54
+ i++;
55
+ }
56
+ return result;
57
+ };
58
+ // Walk the AST and apply `groupFoldedSections` to every block's `content`
59
+ // array, so :folded heads inside named blocks, defn, nested etc. fold the
60
+ // same way they do at the pod-level. Inside an already-built
61
+ // `_folded_section`, the first element is the heading the wrapper belongs
62
+ // to — skip it when grouping the rest, otherwise the same head would be
63
+ // re-wrapped on every recursion. Nested folds (a folded heading inside a
64
+ // folded section's body) are still picked up by grouping the remainder.
65
+ const applyFoldedSections = (node) => {
66
+ if (!node || typeof node !== 'object')
67
+ return node;
68
+ if (Array.isArray(node))
69
+ return node.map(exports.applyFoldedSections);
70
+ if (!Array.isArray(node.content))
71
+ return node;
72
+ if (node.name === '_folded_section') {
73
+ const [head, ...rest] = node.content;
74
+ const grouped = groupFoldedSections(rest);
75
+ return {
76
+ ...node,
77
+ content: [(0, exports.applyFoldedSections)(head), ...grouped.map(exports.applyFoldedSections)],
78
+ };
79
+ }
80
+ const grouped = groupFoldedSections(node.content);
81
+ return { ...node, content: grouped.map(exports.applyFoldedSections) };
82
+ };
83
+ exports.applyFoldedSections = applyFoldedSections;
84
+ //# sourceMappingURL=folded-sections.js.map