@scalewing/react 1.41.0 → 1.42.0

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 (100) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +120 -1
  3. package/dist/components/Article.d.ts +60 -0
  4. package/dist/components/Article.js +42 -0
  5. package/dist/components/DocumentViewer.d.ts +41 -0
  6. package/dist/components/DocumentViewer.js +72 -0
  7. package/dist/components/FileField.d.ts +44 -0
  8. package/dist/components/FileField.js +66 -0
  9. package/dist/components/ListRow.d.ts +23 -2
  10. package/dist/components/ListRow.js +17 -17
  11. package/dist/components/MarkdownView.d.ts +39 -0
  12. package/dist/components/MarkdownView.js +30 -0
  13. package/dist/components/NavCount.d.ts +15 -2
  14. package/dist/components/NavCount.js +16 -6
  15. package/dist/components/chart/use-chart-width.js +2 -24
  16. package/dist/components/document-viewer/document-viewer-labels.d.ts +23 -0
  17. package/dist/components/document-viewer/document-viewer-labels.js +23 -0
  18. package/dist/components/document-viewer/page-box.d.ts +8 -0
  19. package/dist/components/document-viewer/page-box.js +12 -0
  20. package/dist/components/document-viewer/pdf-document.d.ts +32 -0
  21. package/dist/components/document-viewer/pdf-document.js +66 -0
  22. package/dist/components/document-viewer/pdf-options.d.ts +36 -0
  23. package/dist/components/document-viewer/pdf-options.js +46 -0
  24. package/dist/components/document-viewer/render-pdf-page.d.ts +20 -0
  25. package/dist/components/document-viewer/render-pdf-page.js +38 -0
  26. package/dist/components/document-viewer/scroll-chain.d.ts +6 -0
  27. package/dist/components/document-viewer/scroll-chain.js +23 -0
  28. package/dist/components/document-viewer/use-error-report.d.ts +5 -0
  29. package/dist/components/document-viewer/use-error-report.js +13 -0
  30. package/dist/components/document-viewer/use-page-render.d.ts +21 -0
  31. package/dist/components/document-viewer/use-page-render.js +70 -0
  32. package/dist/components/document-viewer/use-pdf-document.d.ts +22 -0
  33. package/dist/components/document-viewer/use-pdf-document.js +47 -0
  34. package/dist/components/document-viewer/use-pinch-zoom.d.ts +12 -0
  35. package/dist/components/document-viewer/use-pinch-zoom.js +67 -0
  36. package/dist/components/document-viewer/use-zoom.d.ts +23 -0
  37. package/dist/components/document-viewer/use-zoom.js +70 -0
  38. package/dist/components/document-viewer/zoom.d.ts +29 -0
  39. package/dist/components/document-viewer/zoom.js +37 -0
  40. package/dist/components/file-field/file-field-checks.d.ts +13 -0
  41. package/dist/components/file-field/file-field-checks.js +21 -0
  42. package/dist/components/file-field/use-file-drop.d.ts +34 -0
  43. package/dist/components/file-field/use-file-drop.js +78 -0
  44. package/dist/components/file-field/use-swap-focus.d.ts +15 -0
  45. package/dist/components/file-field/use-swap-focus.js +40 -0
  46. package/dist/components/list/ListRowContent.d.ts +6 -2
  47. package/dist/components/list/ListRowContent.js +5 -3
  48. package/dist/components/list/ListRowItem.d.ts +10 -0
  49. package/dist/components/list/ListRowItem.js +17 -0
  50. package/dist/components/list/row-naming.d.ts +17 -0
  51. package/dist/components/list/row-naming.js +19 -0
  52. package/dist/components/list/row-slot.d.ts +8 -0
  53. package/dist/components/list/row-slot.js +12 -0
  54. package/dist/components/markdown/markdown-limits.d.ts +14 -0
  55. package/dist/components/markdown/markdown-limits.js +14 -0
  56. package/dist/components/markdown/markdown-model.d.ts +94 -0
  57. package/dist/components/markdown/markdown-model.js +8 -0
  58. package/dist/components/markdown/markdown-options.d.ts +18 -0
  59. package/dist/components/markdown/markdown-options.js +10 -0
  60. package/dist/components/markdown/markdown-rules.d.ts +28 -0
  61. package/dist/components/markdown/markdown-rules.js +81 -0
  62. package/dist/components/markdown/markdown-text.d.ts +17 -0
  63. package/dist/components/markdown/markdown-text.js +86 -0
  64. package/dist/components/markdown/parse-markdown.d.ts +4 -0
  65. package/dist/components/markdown/parse-markdown.js +21 -0
  66. package/dist/components/markdown/read-blocks.d.ts +8 -0
  67. package/dist/components/markdown/read-blocks.js +168 -0
  68. package/dist/components/markdown/read-inlines.d.ts +12 -0
  69. package/dist/components/markdown/read-inlines.js +126 -0
  70. package/dist/components/markdown/read-markdown.d.ts +9 -0
  71. package/dist/components/markdown/read-markdown.js +27 -0
  72. package/dist/components/markdown/reading-context.d.ts +18 -0
  73. package/dist/components/markdown/reading-context.js +35 -0
  74. package/dist/components/markdown/render-markdown.d.ts +10 -0
  75. package/dist/components/markdown/render-markdown.js +52 -0
  76. package/dist/components/use-element-width.d.ts +11 -0
  77. package/dist/components/use-element-width.js +38 -0
  78. package/dist/css/css-article.d.ts +2 -0
  79. package/dist/css/css-article.js +110 -0
  80. package/dist/css/css-components.js +12 -0
  81. package/dist/css/css-count-pill.js +14 -1
  82. package/dist/css/css-document-viewer.d.ts +2 -0
  83. package/dist/css/css-document-viewer.js +186 -0
  84. package/dist/css/css-file-field.d.ts +2 -0
  85. package/dist/css/css-file-field.js +180 -0
  86. package/dist/css/css-list-row-trailing.d.ts +2 -0
  87. package/dist/css/css-list-row-trailing.js +91 -0
  88. package/dist/css/css-list.js +5 -1
  89. package/dist/css/css-markdown.d.ts +2 -0
  90. package/dist/css/css-markdown.js +144 -0
  91. package/dist/css/css-variables.js +1 -0
  92. package/dist/css/stylesheet.js +8 -0
  93. package/dist/document-viewer-worker.d.ts +9 -0
  94. package/dist/document-viewer-worker.js +9 -0
  95. package/dist/index.d.ts +5 -1
  96. package/dist/index.js +5 -1
  97. package/dist/list-options.d.ts +5 -2
  98. package/dist/list-options.js +7 -2
  99. package/dist/styles.css +504 -0
  100. package/package.json +10 -2
@@ -0,0 +1,17 @@
1
+ import { type ReactNode } from 'react';
2
+ /**
3
+ * A pressable row's name (its title, the unread words and its value) and
4
+ * description (its detail lines and its tag), by the ids its parts carry.
5
+ */
6
+ export declare function rowNaming(id: string, row: {
7
+ 'aria-label'?: string;
8
+ value?: string;
9
+ detail?: string;
10
+ valueDetail?: string;
11
+ unread: boolean;
12
+ tag?: ReactNode;
13
+ }): {
14
+ 'aria-label': string | undefined;
15
+ 'aria-labelledby': string | undefined;
16
+ 'aria-describedby': string | undefined;
17
+ };
@@ -0,0 +1,19 @@
1
+ import { hasSlot } from './row-slot.js';
2
+ function ids(...parts) {
3
+ const joined = parts.filter(Boolean).join(' ');
4
+ return joined === '' ? undefined : joined;
5
+ }
6
+ /**
7
+ * A pressable row's name (its title, the unread words and its value) and
8
+ * description (its detail lines and its tag), by the ids its parts carry.
9
+ */
10
+ export function rowNaming(id, row) {
11
+ const hasTag = hasSlot(row.tag);
12
+ return {
13
+ 'aria-label': row['aria-label'],
14
+ 'aria-labelledby': row['aria-label']
15
+ ? undefined
16
+ : ids(`${id}-title`, row.unread && `${id}-unread`, row.value !== undefined && `${id}-value`),
17
+ 'aria-describedby': ids(Boolean(row.detail) && `${id}-detail`, Boolean(row.valueDetail) && `${id}-value-detail`, hasTag && `${id}-tag`),
18
+ };
19
+ }
@@ -0,0 +1,8 @@
1
+ import { type ReactNode } from 'react';
2
+ /**
3
+ * Whether a row's slot (`leading`, `tag`, `action`) holds something to
4
+ * draw. As React renders nothing for them, `undefined`, `null`, a boolean
5
+ * (the `false` of `canArchive && <ActionMenu …/>`) and an empty string are
6
+ * an empty slot, so they add no box and no gap.
7
+ */
8
+ export declare function hasSlot(node: ReactNode): boolean;
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Whether a row's slot (`leading`, `tag`, `action`) holds something to
3
+ * draw. As React renders nothing for them, `undefined`, `null`, a boolean
4
+ * (the `false` of `canArchive && <ActionMenu …/>`) and an empty string are
5
+ * an empty slot, so they add no box and no gap.
6
+ */
7
+ export function hasSlot(node) {
8
+ return (node !== undefined &&
9
+ node !== null &&
10
+ node !== '' &&
11
+ typeof node !== 'boolean');
12
+ }
@@ -0,0 +1,14 @@
1
+ /** How many quotes and lists the view draws inside one another. */
2
+ export declare const maxNesting = 16;
3
+ /**
4
+ * How deep the parser reads (a quote is one level, a list and its item
5
+ * two). markdown-it stops reading a block that nests deeper (its words are
6
+ * lost, and reported as `limit`). Its work on runs of link and image
7
+ * openers grows with this cap, so it stays low: 128 keeps 60-deep lists
8
+ * and reads 25,000 `![` in about 0.1 s.
9
+ */
10
+ export declare const maxParseNesting = 128;
11
+ /** The longest excerpt an omission keeps. */
12
+ export declare const maxExcerpt = 80;
13
+ /** How many omissions the reading lists; the counts keep the rest. */
14
+ export declare const maxOmissionsListed = 100;
@@ -0,0 +1,14 @@
1
+ /** How many quotes and lists the view draws inside one another. */
2
+ export const maxNesting = 16;
3
+ /**
4
+ * How deep the parser reads (a quote is one level, a list and its item
5
+ * two). markdown-it stops reading a block that nests deeper (its words are
6
+ * lost, and reported as `limit`). Its work on runs of link and image
7
+ * openers grows with this cap, so it stays low: 128 keeps 60-deep lists
8
+ * and reads 25,000 `![` in about 0.1 s.
9
+ */
10
+ export const maxParseNesting = 128;
11
+ /** The longest excerpt an omission keeps. */
12
+ export const maxExcerpt = 80;
13
+ /** How many omissions the reading lists; the counts keep the rest. */
14
+ export const maxOmissionsListed = 100;
@@ -0,0 +1,94 @@
1
+ /** A run of text and its marks, as the view draws it. */
2
+ export type MarkdownInline = {
3
+ kind: 'text';
4
+ value: string;
5
+ } | {
6
+ kind: 'strong' | 'emphasis';
7
+ children: MarkdownInline[];
8
+ } | {
9
+ kind: 'link';
10
+ href: string;
11
+ children: MarkdownInline[];
12
+ } | {
13
+ kind: 'break';
14
+ };
15
+ /** A block of the page, as the view draws it. */
16
+ export type MarkdownBlock = {
17
+ kind: 'paragraph';
18
+ children: MarkdownInline[];
19
+ } | {
20
+ kind: 'heading';
21
+ level: 1 | 2 | 3;
22
+ id: string;
23
+ children: MarkdownInline[];
24
+ } | {
25
+ kind: 'list';
26
+ ordered: boolean;
27
+ start: number | null;
28
+ tight: boolean;
29
+ items: MarkdownBlock[][];
30
+ } | {
31
+ kind: 'quote';
32
+ children: MarkdownBlock[];
33
+ } | {
34
+ kind: 'figure';
35
+ src: string;
36
+ alt: string;
37
+ caption: string;
38
+ } | {
39
+ kind: 'rule';
40
+ };
41
+ /** A heading of the page, for an "On this page" list. */
42
+ export type MarkdownHeading = {
43
+ /** The heading's element id, for a link to it (`#${id}`). */
44
+ id: string;
45
+ /** 1 for `#`, 2 for `##`, 3 for `###` (and deeper). */
46
+ level: 1 | 2 | 3;
47
+ text: string;
48
+ };
49
+ /** What the view leaves out of the Markdown it is given. */
50
+ export type MarkdownOmissionKind =
51
+ /** Raw HTML, scripts included: dropped (the words between tags stay). */
52
+ 'html'
53
+ /** A table: dropped. */
54
+ | 'table'
55
+ /** An image that is not a `photo:` the consumer resolves: dropped. */
56
+ | 'image'
57
+ /** A link whose scheme is not http, https or mailto: shown as its words. */
58
+ | 'link'
59
+ /** Code: shown as plain text, without its code formatting. */
60
+ | 'code'
61
+ /**
62
+ * Past the view's limits: quotes, lists and marks nested more than 16
63
+ * deep are shown as their words (a link there too, quoted in the run's
64
+ * one omission); a block reaching the parser's 128th level, past which
65
+ * it stops reading, is reported as it may be lost.
66
+ */
67
+ | 'limit';
68
+ export declare const markdownOmissionKinds: readonly ["html", "table", "image", "link", "code", "limit"];
69
+ /** How many of each kind the view left out, the ones past the list too. */
70
+ export type MarkdownOmissionCounts = Record<MarkdownOmissionKind, number>;
71
+ export type MarkdownOmission = {
72
+ kind: MarkdownOmissionKind;
73
+ /** The Markdown it came from, cut to 80 characters. */
74
+ source: string;
75
+ };
76
+ /** A `photo:` image as the consumer resolves it. */
77
+ export type MarkdownImage = {
78
+ /**
79
+ * The photo's address: relative, `https:`, `http:`, `blob:` or a
80
+ * `data:image/` of PNG, JPEG, GIF, WebP, AVIF or SVG; any other is left out.
81
+ */
82
+ src: string;
83
+ /** What the photo shows; the caption is used when this is left out. */
84
+ alt?: string;
85
+ };
86
+ export type MarkdownImageResolver = (id: string) => MarkdownImage | null;
87
+ /** The page read from Markdown: its blocks, its headings, what was left out. */
88
+ export type MarkdownReading = {
89
+ blocks: MarkdownBlock[];
90
+ headings: MarkdownHeading[];
91
+ /** The first 100 things left out, in the order they appear. */
92
+ omissions: MarkdownOmission[];
93
+ omissionCounts: MarkdownOmissionCounts;
94
+ };
@@ -0,0 +1,8 @@
1
+ export const markdownOmissionKinds = [
2
+ 'html',
3
+ 'table',
4
+ 'image',
5
+ 'link',
6
+ 'code',
7
+ 'limit',
8
+ ];
@@ -0,0 +1,18 @@
1
+ import { type MarkdownImageResolver } from './markdown-model.js';
2
+ export type MarkdownOptions = {
3
+ /**
4
+ * Turns `![caption](photo:<id>)` into a photo: its `src` and its `alt`
5
+ * (the caption when left out); null leaves it out. Any other image is
6
+ * left out. A new function reads the source again, so keep it stable
7
+ * (`useCallback`) where the view renders often.
8
+ */
9
+ resolveImage?: MarkdownImageResolver;
10
+ /**
11
+ * Put before every heading id, such as "page-", so its ids never clash
12
+ * with the page's or another view's (`''`, the default, is none). Pass
13
+ * the same one to the view and to `inspectMarkdown`.
14
+ */
15
+ idPrefix?: string;
16
+ };
17
+ /** Refuses what neither the view nor `inspectMarkdown` can read. */
18
+ export declare function assertMarkdownInput(owner: string, source: unknown, { resolveImage, idPrefix }: MarkdownOptions): void;
@@ -0,0 +1,10 @@
1
+ /** Refuses what neither the view nor `inspectMarkdown` can read. */
2
+ export function assertMarkdownInput(owner, source, { resolveImage, idPrefix }) {
3
+ if (typeof source !== 'string')
4
+ throw new RangeError(`${owner} source must be text`);
5
+ if (resolveImage !== undefined && typeof resolveImage !== 'function')
6
+ throw new RangeError(`${owner} resolveImage must be a function`);
7
+ if (idPrefix !== undefined &&
8
+ (typeof idPrefix !== 'string' || !/^([A-Za-z][\w-]*)?$/.test(idPrefix)))
9
+ throw new RangeError(`${owner} idPrefix must be empty or start with a letter and hold letters, digits, - or _`);
10
+ }
@@ -0,0 +1,28 @@
1
+ /** The link schemes a page may use; any other is shown as plain words. */
2
+ export declare const markdownLinkSchemes: readonly ["http:", "https:", "mailto:"];
3
+ /**
4
+ * The address a link may open, normalised as the browser reads it, or null
5
+ * for any scheme but http, https and mailto (`javascript:`, `data:`, a
6
+ * relative or protocol-relative address, anything that is not a URL).
7
+ */
8
+ export declare function safeLinkHref(url: string): string | null;
9
+ /** The id in `photo:<id>`, or null for any other image address. */
10
+ export declare function photoId(url: string): string | null;
11
+ /**
12
+ * Whether a resolved photo's address may be drawn: relative, `https:`,
13
+ * `http:`, `blob:` or a `data:` PNG, JPEG, GIF, WebP, AVIF or SVG (an
14
+ * `img` never runs an SVG's script); never any other scheme.
15
+ */
16
+ export declare function isSafeImageSrc(src: string): boolean;
17
+ /**
18
+ * Heading ids for one document: from a heading's words, `section` when
19
+ * nothing is left, after `prefix`; a repeat takes the next free `-2`,
20
+ * `-3`. Each base remembers its next number, so many repeats stay linear.
21
+ */
22
+ export declare class HeadingIds {
23
+ private readonly prefix;
24
+ private readonly taken;
25
+ private readonly next;
26
+ constructor(prefix: string);
27
+ take(text: string): string;
28
+ }
@@ -0,0 +1,81 @@
1
+ /** The link schemes a page may use; any other is shown as plain words. */
2
+ export const markdownLinkSchemes = ['http:', 'https:', 'mailto:'];
3
+ /**
4
+ * The address a link may open, normalised as the browser reads it, or null
5
+ * for any scheme but http, https and mailto (`javascript:`, `data:`, a
6
+ * relative or protocol-relative address, anything that is not a URL).
7
+ */
8
+ export function safeLinkHref(url) {
9
+ let parsed;
10
+ try {
11
+ parsed = new URL(url);
12
+ }
13
+ catch {
14
+ return null;
15
+ }
16
+ return markdownLinkSchemes.includes(parsed.protocol)
17
+ ? parsed.href
18
+ : null;
19
+ }
20
+ /** The id in `photo:<id>`, or null for any other image address. */
21
+ export function photoId(url) {
22
+ const match = /^photo:([A-Za-z0-9_-]{1,64})$/.exec(url.trim());
23
+ return match ? match[1] : null;
24
+ }
25
+ const imageSchemes = ['http:', 'https:', 'blob:'];
26
+ const imageData = /^data:image\/(png|jpeg|gif|webp|avif|svg\+xml)[;,]/i;
27
+ /**
28
+ * Whether a resolved photo's address may be drawn: relative, `https:`,
29
+ * `http:`, `blob:` or a `data:` PNG, JPEG, GIF, WebP, AVIF or SVG (an
30
+ * `img` never runs an SVG's script); never any other scheme.
31
+ */
32
+ export function isSafeImageSrc(src) {
33
+ if (typeof src !== 'string' || src.trim() === '')
34
+ return false;
35
+ let parsed;
36
+ try {
37
+ parsed = new URL(src, 'https://relative.invalid/');
38
+ }
39
+ catch {
40
+ return false;
41
+ }
42
+ if (parsed.protocol === 'data:')
43
+ return imageData.test(src.trim());
44
+ return imageSchemes.includes(parsed.protocol);
45
+ }
46
+ /** A heading's words as an id: lower case, accents dropped, hyphens. */
47
+ function slug(text) {
48
+ return text
49
+ .normalize('NFKD')
50
+ .replace(/[\u0300-\u036f]/g, '')
51
+ .toLowerCase()
52
+ .replace(/[^a-z0-9]+/g, '-')
53
+ .replace(/^-+|-+$/g, '');
54
+ }
55
+ /**
56
+ * Heading ids for one document: from a heading's words, `section` when
57
+ * nothing is left, after `prefix`; a repeat takes the next free `-2`,
58
+ * `-3`. Each base remembers its next number, so many repeats stay linear.
59
+ */
60
+ export class HeadingIds {
61
+ prefix;
62
+ taken = new Set();
63
+ next = new Map();
64
+ constructor(prefix) {
65
+ this.prefix = prefix;
66
+ }
67
+ take(text) {
68
+ const base = `${this.prefix}${slug(text) || 'section'}`;
69
+ let id = base;
70
+ let count = this.next.get(base) ?? 2;
71
+ if (this.taken.has(id)) {
72
+ do {
73
+ id = `${base}-${count}`;
74
+ count += 1;
75
+ } while (this.taken.has(id));
76
+ }
77
+ this.next.set(base, count);
78
+ this.taken.add(id);
79
+ return id;
80
+ }
81
+ }
@@ -0,0 +1,17 @@
1
+ import { type MarkdownInline } from './markdown-model.js';
2
+ import { type Token } from './parse-markdown.js';
3
+ import { type ReadingContext } from './reading-context.js';
4
+ /** Left-out text as an omission quotes it: trimmed, at most `maxExcerpt`. */
5
+ export declare function excerpt(text: string): string;
6
+ /** A run's words without its marks. */
7
+ export declare function plainText(inlines: readonly MarkdownInline[]): string;
8
+ /**
9
+ * The words of `tokens` (block tokens, or one block's inline children),
10
+ * in order, at any depth: a flat walk, so no nesting overflows the stack.
11
+ * Raw HTML, tables and images are never words: each is noted as left out.
12
+ */
13
+ export declare function tokenText(context: ReadingContext, tokens: readonly Token[]): string;
14
+ /** Code shown as plain lines, a break between each. */
15
+ export declare function lines(value: string): MarkdownInline[];
16
+ /** Drops the spaces an image left at a paragraph part's edges. */
17
+ export declare function trimEdges(inlines: MarkdownInline[]): MarkdownInline[];
@@ -0,0 +1,86 @@
1
+ import { maxExcerpt } from './markdown-limits.js';
2
+ /** Left-out text as an omission quotes it: trimmed, at most `maxExcerpt`. */
3
+ export function excerpt(text) {
4
+ const trimmed = text.trim();
5
+ return trimmed.length > maxExcerpt
6
+ ? `${trimmed.slice(0, maxExcerpt - 1)}…`
7
+ : trimmed;
8
+ }
9
+ /** A run's words without its marks. */
10
+ export function plainText(inlines) {
11
+ return inlines
12
+ .map((inline) => {
13
+ if (inline.kind === 'text')
14
+ return inline.value;
15
+ if (inline.kind === 'break')
16
+ return ' ';
17
+ return plainText(inline.children);
18
+ })
19
+ .join('');
20
+ }
21
+ /**
22
+ * The words of `tokens` (block tokens, or one block's inline children),
23
+ * in order, at any depth: a flat walk, so no nesting overflows the stack.
24
+ * Raw HTML, tables and images are never words: each is noted as left out.
25
+ */
26
+ export function tokenText(context, tokens) {
27
+ const words = [];
28
+ let table = null;
29
+ for (const token of tokens) {
30
+ if (table !== null) {
31
+ if (token.type === 'table_close' && token.level === table.level)
32
+ table = null;
33
+ continue;
34
+ }
35
+ switch (token.type) {
36
+ case 'table_open':
37
+ context.omitBlock('table', token);
38
+ table = token;
39
+ break;
40
+ case 'html_block':
41
+ case 'html_inline':
42
+ context.omit('html', token.content);
43
+ break;
44
+ case 'image':
45
+ context.omit('image', `![${token.content}](${token.attrGet('src') ?? ''})`);
46
+ break;
47
+ case 'inline':
48
+ words.push(tokenText(context, token.children ?? []));
49
+ break;
50
+ case 'text':
51
+ case 'code_inline':
52
+ case 'code_block':
53
+ case 'fence':
54
+ words.push(token.content);
55
+ break;
56
+ case 'softbreak':
57
+ case 'hardbreak':
58
+ words.push(' ');
59
+ break;
60
+ default:
61
+ break;
62
+ }
63
+ }
64
+ return words.join(' ').replace(/\s+/g, ' ').trim();
65
+ }
66
+ /** Code shown as plain lines, a break between each. */
67
+ export function lines(value) {
68
+ return value
69
+ .replace(/\n$/, '')
70
+ .split('\n')
71
+ .flatMap((line, index) => {
72
+ const text = { kind: 'text', value: line };
73
+ return index === 0 ? [text] : [{ kind: 'break' }, text];
74
+ });
75
+ }
76
+ /** Drops the spaces an image left at a paragraph part's edges. */
77
+ export function trimEdges(inlines) {
78
+ const result = [...inlines];
79
+ const first = result[0];
80
+ if (first?.kind === 'text')
81
+ result[0] = { kind: 'text', value: first.value.trimStart() };
82
+ const last = result[result.length - 1];
83
+ if (last?.kind === 'text')
84
+ result[result.length - 1] = { kind: 'text', value: last.value.trimEnd() };
85
+ return result;
86
+ }
@@ -0,0 +1,4 @@
1
+ import { type Token } from 'markdown-it';
2
+ export type { Token };
3
+ /** The block tokens of `source`; inline tokens are each block's `children`. */
4
+ export declare function parseMarkdown(source: string): Token[];
@@ -0,0 +1,21 @@
1
+ import MarkdownIt from 'markdown-it';
2
+ import { maxParseNesting } from './markdown-limits.js';
3
+ /*
4
+ * CommonMark with GFM tables (recognised only so they are left out whole)
5
+ * and raw HTML read as HTML tokens (so they are left out, never shown as
6
+ * text), as a token stream. Its renderer is never called: nothing here
7
+ * makes HTML. Every link is read as a link (the view decides on its
8
+ * address); links are never found in bare text.
9
+ */
10
+ // `maxNesting` is read by markdown-it's parsers; its types leave it out.
11
+ const options = {
12
+ html: true,
13
+ linkify: false,
14
+ maxNesting: maxParseNesting,
15
+ };
16
+ const parser = new MarkdownIt('commonmark', options).enable('table');
17
+ parser.validateLink = () => true;
18
+ /** The block tokens of `source`; inline tokens are each block's `children`. */
19
+ export function parseMarkdown(source) {
20
+ return parser.parse(source, {});
21
+ }
@@ -0,0 +1,8 @@
1
+ import { type MarkdownBlock } from './markdown-model.js';
2
+ import { type Token } from './parse-markdown.js';
3
+ import { type ReadingContext } from './reading-context.js';
4
+ /**
5
+ * Block tokens as the view draws them; `depth` counts the quotes and
6
+ * lists around. Past `maxNesting`, a quote or list is its words.
7
+ */
8
+ export declare function readBlocks(context: ReadingContext, tokens: readonly Token[], depth?: number): MarkdownBlock[];
@@ -0,0 +1,168 @@
1
+ import { maxNesting } from './markdown-limits.js';
2
+ import { isSafeImageSrc, photoId } from './markdown-rules.js';
3
+ import { lines, plainText, tokenText, trimEdges } from './markdown-text.js';
4
+ import { attr, readInlines } from './read-inlines.js';
5
+ /** The index of the token that closes the one at `index` (or `index`). */
6
+ function closeOf(tokens, index) {
7
+ const open = tokens[index];
8
+ if (open.nesting !== 1)
9
+ return index;
10
+ for (let at = index + 1; at < tokens.length; at += 1) {
11
+ const token = tokens[at];
12
+ if (token.nesting === -1 && token.level === open.level)
13
+ return at;
14
+ }
15
+ return tokens.length - 1;
16
+ }
17
+ /** Each part of `tokens` opened by a token of `type`, without its own open and close. */
18
+ function partsOf(tokens, type) {
19
+ const parts = [];
20
+ for (let at = 0; at < tokens.length; at += 1) {
21
+ if (tokens[at].type !== type)
22
+ continue;
23
+ const end = closeOf(tokens, at);
24
+ parts.push(tokens.slice(at + 1, end));
25
+ at = end;
26
+ }
27
+ return parts;
28
+ }
29
+ /**
30
+ * Block tokens as the view draws them; `depth` counts the quotes and
31
+ * lists around. Past `maxNesting`, a quote or list is its words.
32
+ */
33
+ export function readBlocks(context, tokens, depth = 0) {
34
+ const blocks = [];
35
+ for (let at = 0; at < tokens.length; at += 1) {
36
+ const token = tokens[at];
37
+ const end = closeOf(tokens, at);
38
+ const inner = tokens.slice(at + 1, end);
39
+ at = end;
40
+ switch (token.type) {
41
+ case 'paragraph_open':
42
+ blocks.push(...readParagraph(context, inner[0]?.children ?? []));
43
+ break;
44
+ case 'heading_open': {
45
+ const children = readInlines(context, inner[0]?.children ?? []);
46
+ const text = plainText(children).trim();
47
+ const level = Math.min(Number(token.tag.slice(1)), 3);
48
+ const id = context.ids.take(text);
49
+ context.headings.push({ id, level, text });
50
+ blocks.push({ kind: 'heading', level, id, children });
51
+ break;
52
+ }
53
+ case 'bullet_list_open':
54
+ case 'ordered_list_open':
55
+ case 'blockquote_open': {
56
+ if (depth >= maxNesting) {
57
+ context.omitBlock('limit', token);
58
+ const text = tokenText(context, inner);
59
+ if (text)
60
+ blocks.push({
61
+ kind: 'paragraph',
62
+ children: [{ kind: 'text', value: text }],
63
+ });
64
+ break;
65
+ }
66
+ if (token.type === 'blockquote_open') {
67
+ blocks.push({
68
+ kind: 'quote',
69
+ children: readBlocks(context, inner, depth + 1),
70
+ });
71
+ break;
72
+ }
73
+ const ordered = token.type === 'ordered_list_open';
74
+ blocks.push({
75
+ kind: 'list',
76
+ ordered,
77
+ start: ordered ? Number(attr(token, 'start') ?? 1) : null,
78
+ // A tight list's paragraphs are hidden: only their words show.
79
+ tight: inner.every((child) => child.type !== 'paragraph_open' ||
80
+ child.level !== token.level + 2 ||
81
+ child.hidden),
82
+ items: partsOf(inner, 'list_item_open').map((item) => readBlocks(context, item, depth + 1)),
83
+ });
84
+ break;
85
+ }
86
+ case 'code_block':
87
+ case 'fence':
88
+ context.omitBlock('code', token);
89
+ // An empty block draws nothing.
90
+ if (token.content.trim() !== '')
91
+ blocks.push({ kind: 'paragraph', children: lines(token.content) });
92
+ break;
93
+ case 'hr':
94
+ blocks.push({ kind: 'rule' });
95
+ break;
96
+ case 'html_block':
97
+ context.omit('html', token.content);
98
+ break;
99
+ case 'table_open':
100
+ context.omitBlock('table', token);
101
+ break;
102
+ default:
103
+ // Nothing else is parsed at this level.
104
+ break;
105
+ }
106
+ }
107
+ return blocks;
108
+ }
109
+ /**
110
+ * A `photo:` the consumer resolves to a safe address, with words for it
111
+ * (its `alt`, or the caption): anything else is left out.
112
+ */
113
+ function readFigure(context, image) {
114
+ const src = attr(image, 'src') ?? '';
115
+ const caption = plainText(readInlines(context, image.children ?? [])).trim();
116
+ const id = photoId(src);
117
+ const photo = id === null ? null : (context.options.resolveImage?.(id) ?? null);
118
+ // The caption names a photo without its own words, so it is never
119
+ // announced as decorative (screen readers then also read the caption).
120
+ const alt = (typeof photo?.alt === 'string' ? photo.alt.trim() : '') || caption;
121
+ if (!photo || !isSafeImageSrc(photo.src) || alt === '') {
122
+ context.omit('image', `![${image.content}](${src})`);
123
+ return null;
124
+ }
125
+ return { kind: 'figure', src: photo.src, alt, caption };
126
+ }
127
+ /**
128
+ * A paragraph, split around the photos in it (outside links and marks),
129
+ * which are figures of their own; an image left out leaves the paragraph
130
+ * whole, one space where it was.
131
+ */
132
+ function readParagraph(context, children) {
133
+ const blocks = [];
134
+ let run = [];
135
+ let level = 0;
136
+ let dropped = false;
137
+ const flush = () => {
138
+ const inlines = readInlines(context, run);
139
+ if (plainText(inlines).trim() !== '')
140
+ blocks.push({ kind: 'paragraph', children: trimEdges(inlines) });
141
+ run = [];
142
+ };
143
+ for (const child of children) {
144
+ if (child.type === 'image' && level === 0) {
145
+ const figure = readFigure(context, child);
146
+ if (figure) {
147
+ flush();
148
+ blocks.push(figure);
149
+ dropped = false;
150
+ }
151
+ else
152
+ dropped = true;
153
+ continue;
154
+ }
155
+ level += child.nesting;
156
+ const before = run[run.length - 1];
157
+ if (dropped &&
158
+ child.type === 'text' &&
159
+ before?.type === 'text' &&
160
+ /\s$/.test(before.content))
161
+ run.push({ ...child, content: child.content.trimStart() });
162
+ else
163
+ run.push(child);
164
+ dropped = false;
165
+ }
166
+ flush();
167
+ return blocks;
168
+ }
@@ -0,0 +1,12 @@
1
+ import { type MarkdownInline } from './markdown-model.js';
2
+ import { type Token } from './parse-markdown.js';
3
+ import { type ReadingContext } from './reading-context.js';
4
+ /** A token's attribute, read without its methods (tokens may be copies). */
5
+ export declare function attr(token: Token, name: string): string | null;
6
+ /**
7
+ * One block's inline tokens as the view's inlines: text, breaks, bold,
8
+ * italic and links. A link's address must pass `safeLinkHref`, or its
9
+ * words stay as text; raw HTML and images here are left out; code is its
10
+ * words; marks nested past `maxNesting` are their words.
11
+ */
12
+ export declare function readInlines(context: ReadingContext, tokens: readonly Token[]): MarkdownInline[];