@scalewing/react 1.40.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.
- package/CHANGELOG.md +38 -0
- package/README.md +274 -7
- package/dist/calendar-labels.d.ts +2 -0
- package/dist/calendar-labels.js +7 -0
- package/dist/components/Article.d.ts +60 -0
- package/dist/components/Article.js +42 -0
- package/dist/components/BarChart.d.ts +20 -4
- package/dist/components/BarChart.js +13 -6
- package/dist/components/ButtonGroup.d.ts +8 -1
- package/dist/components/ButtonGroup.js +11 -2
- package/dist/components/DateField.d.ts +14 -2
- package/dist/components/DateField.js +14 -11
- package/dist/components/DenominationGrid.d.ts +3 -2
- package/dist/components/DenominationGrid.js +21 -3
- package/dist/components/Dialog.d.ts +29 -13
- package/dist/components/Dialog.js +22 -20
- package/dist/components/DocumentViewer.d.ts +41 -0
- package/dist/components/DocumentViewer.js +72 -0
- package/dist/components/Field.d.ts +13 -2
- package/dist/components/Field.js +14 -38
- package/dist/components/FileField.d.ts +44 -0
- package/dist/components/FileField.js +66 -0
- package/dist/components/FilterChips.d.ts +7 -37
- package/dist/components/FilterChips.js +32 -24
- package/dist/components/ListRow.d.ts +33 -3
- package/dist/components/ListRow.js +18 -17
- package/dist/components/MarkdownView.d.ts +39 -0
- package/dist/components/MarkdownView.js +30 -0
- package/dist/components/NavCount.d.ts +15 -2
- package/dist/components/NavCount.js +16 -6
- package/dist/components/SegmentedControl.d.ts +17 -1
- package/dist/components/SegmentedControl.js +11 -3
- package/dist/components/Switch.d.ts +16 -1
- package/dist/components/Switch.js +17 -6
- package/dist/components/action-bar/ActionBarSummary.d.ts +10 -2
- package/dist/components/action-bar/ActionBarSummary.js +11 -2
- package/dist/components/chart/use-chart-width.js +2 -24
- package/dist/components/date-field/CalendarEntryButton.d.ts +24 -0
- package/dist/components/date-field/CalendarEntryButton.js +16 -0
- package/dist/components/date-field/TypedEntry.d.ts +26 -0
- package/dist/components/date-field/TypedEntry.js +13 -0
- package/dist/components/denomination-grid/CountCell.d.ts +16 -0
- package/dist/components/denomination-grid/CountCell.js +14 -0
- package/dist/components/denomination-grid/DenominationStrip.d.ts +4 -1
- package/dist/components/denomination-grid/DenominationStrip.js +2 -2
- package/dist/components/denomination-grid/EntryCell.d.ts +3 -1
- package/dist/components/denomination-grid/EntryCell.js +2 -2
- package/dist/components/denomination-grid/StripRow.js +1 -10
- package/dist/components/denomination-grid/TransposedStrip.d.ts +17 -0
- package/dist/components/denomination-grid/TransposedStrip.js +43 -0
- package/dist/components/denomination-grid/types.d.ts +31 -0
- package/dist/components/denomination-grid/types.js +7 -1
- package/dist/components/denomination-grid/validate-options.d.ts +7 -0
- package/dist/components/denomination-grid/validate-options.js +27 -0
- package/dist/components/dialog/DialogFooter.d.ts +6 -5
- package/dist/components/dialog/DialogFooter.js +6 -6
- package/dist/components/dialog/validate.d.ts +24 -0
- package/dist/components/dialog/validate.js +23 -0
- package/dist/components/document-viewer/document-viewer-labels.d.ts +23 -0
- package/dist/components/document-viewer/document-viewer-labels.js +23 -0
- package/dist/components/document-viewer/page-box.d.ts +8 -0
- package/dist/components/document-viewer/page-box.js +12 -0
- package/dist/components/document-viewer/pdf-document.d.ts +32 -0
- package/dist/components/document-viewer/pdf-document.js +66 -0
- package/dist/components/document-viewer/pdf-options.d.ts +36 -0
- package/dist/components/document-viewer/pdf-options.js +46 -0
- package/dist/components/document-viewer/render-pdf-page.d.ts +20 -0
- package/dist/components/document-viewer/render-pdf-page.js +38 -0
- package/dist/components/document-viewer/scroll-chain.d.ts +6 -0
- package/dist/components/document-viewer/scroll-chain.js +23 -0
- package/dist/components/document-viewer/use-error-report.d.ts +5 -0
- package/dist/components/document-viewer/use-error-report.js +13 -0
- package/dist/components/document-viewer/use-page-render.d.ts +21 -0
- package/dist/components/document-viewer/use-page-render.js +70 -0
- package/dist/components/document-viewer/use-pdf-document.d.ts +22 -0
- package/dist/components/document-viewer/use-pdf-document.js +47 -0
- package/dist/components/document-viewer/use-pinch-zoom.d.ts +12 -0
- package/dist/components/document-viewer/use-pinch-zoom.js +67 -0
- package/dist/components/document-viewer/use-zoom.d.ts +23 -0
- package/dist/components/document-viewer/use-zoom.js +70 -0
- package/dist/components/document-viewer/zoom.d.ts +29 -0
- package/dist/components/document-viewer/zoom.js +37 -0
- package/dist/components/field/field-checks.d.ts +34 -0
- package/dist/components/field/field-checks.js +42 -0
- package/dist/components/file-field/file-field-checks.d.ts +13 -0
- package/dist/components/file-field/file-field-checks.js +21 -0
- package/dist/components/file-field/use-file-drop.d.ts +34 -0
- package/dist/components/file-field/use-file-drop.js +78 -0
- package/dist/components/file-field/use-swap-focus.d.ts +15 -0
- package/dist/components/file-field/use-swap-focus.js +40 -0
- package/dist/components/filter-chips/FilterChip.d.ts +16 -0
- package/dist/components/filter-chips/FilterChip.js +20 -0
- package/dist/components/filter-chips/options.d.ts +75 -0
- package/dist/components/filter-chips/options.js +11 -0
- package/dist/components/filter-chips/validate.d.ts +9 -0
- package/dist/components/filter-chips/validate.js +48 -0
- package/dist/components/list/ListRowContent.d.ts +8 -2
- package/dist/components/list/ListRowContent.js +6 -3
- package/dist/components/list/ListRowItem.d.ts +10 -0
- package/dist/components/list/ListRowItem.js +17 -0
- package/dist/components/list/row-naming.d.ts +17 -0
- package/dist/components/list/row-naming.js +19 -0
- package/dist/components/list/row-slot.d.ts +8 -0
- package/dist/components/list/row-slot.js +12 -0
- package/dist/components/markdown/markdown-limits.d.ts +14 -0
- package/dist/components/markdown/markdown-limits.js +14 -0
- package/dist/components/markdown/markdown-model.d.ts +94 -0
- package/dist/components/markdown/markdown-model.js +8 -0
- package/dist/components/markdown/markdown-options.d.ts +18 -0
- package/dist/components/markdown/markdown-options.js +10 -0
- package/dist/components/markdown/markdown-rules.d.ts +28 -0
- package/dist/components/markdown/markdown-rules.js +81 -0
- package/dist/components/markdown/markdown-text.d.ts +17 -0
- package/dist/components/markdown/markdown-text.js +86 -0
- package/dist/components/markdown/parse-markdown.d.ts +4 -0
- package/dist/components/markdown/parse-markdown.js +21 -0
- package/dist/components/markdown/read-blocks.d.ts +8 -0
- package/dist/components/markdown/read-blocks.js +168 -0
- package/dist/components/markdown/read-inlines.d.ts +12 -0
- package/dist/components/markdown/read-inlines.js +126 -0
- package/dist/components/markdown/read-markdown.d.ts +9 -0
- package/dist/components/markdown/read-markdown.js +27 -0
- package/dist/components/markdown/reading-context.d.ts +18 -0
- package/dist/components/markdown/reading-context.js +35 -0
- package/dist/components/markdown/render-markdown.d.ts +10 -0
- package/dist/components/markdown/render-markdown.js +52 -0
- package/dist/components/use-element-width.d.ts +11 -0
- package/dist/components/use-element-width.js +38 -0
- package/dist/css/chart-tones.d.ts +2 -0
- package/dist/css/chart-tones.js +5 -1
- package/dist/css/css-article.d.ts +2 -0
- package/dist/css/css-article.js +110 -0
- package/dist/css/css-bar-chart.d.ts +17 -2
- package/dist/css/css-bar-chart.js +38 -7
- package/dist/css/css-button-group-attached.d.ts +3 -0
- package/dist/css/css-button-group-attached.js +18 -1
- package/dist/css/css-canvas-grouped.d.ts +17 -0
- package/dist/css/css-canvas-grouped.js +40 -0
- package/dist/css/css-components.js +18 -0
- package/dist/css/css-count-pill.js +14 -1
- package/dist/css/css-date-field-calendar-entry.d.ts +11 -0
- package/dist/css/css-date-field-calendar-entry.js +50 -0
- package/dist/css/css-date-field.js +4 -0
- package/dist/css/css-denomination-grid.js +4 -0
- package/dist/css/css-denomination-transposed.d.ts +4 -0
- package/dist/css/css-denomination-transposed.js +178 -0
- package/dist/css/css-dialog-canvas.d.ts +3 -0
- package/dist/css/css-dialog-canvas.js +20 -0
- package/dist/css/css-dialog-frame.js +19 -2
- package/dist/css/css-dialog-grabber.d.ts +4 -0
- package/dist/css/css-dialog-grabber.js +56 -0
- package/dist/css/css-dialog.js +3 -2
- package/dist/css/css-document-viewer.d.ts +2 -0
- package/dist/css/css-document-viewer.js +186 -0
- package/dist/css/css-document.js +3 -0
- package/dist/css/css-field-width.d.ts +12 -0
- package/dist/css/css-field-width.js +49 -0
- package/dist/css/css-field.js +4 -0
- package/dist/css/css-file-field.d.ts +2 -0
- package/dist/css/css-file-field.js +180 -0
- package/dist/css/css-filter-chips-layout.d.ts +2 -0
- package/dist/css/css-filter-chips-layout.js +42 -0
- package/dist/css/css-filter-chips.js +4 -0
- package/dist/css/css-list-row-trailing.d.ts +2 -0
- package/dist/css/css-list-row-trailing.js +91 -0
- package/dist/css/css-list-row.d.ts +6 -0
- package/dist/css/css-list-row.js +19 -3
- package/dist/css/css-list.js +5 -1
- package/dist/css/css-markdown.d.ts +2 -0
- package/dist/css/css-markdown.js +144 -0
- package/dist/css/css-segmented-sizing.d.ts +13 -0
- package/dist/css/css-segmented-sizing.js +66 -0
- package/dist/css/css-segmented.js +4 -0
- package/dist/css/css-switch.d.ts +6 -0
- package/dist/css/css-switch.js +39 -0
- package/dist/css/css-variables.js +1 -0
- package/dist/css/stacking.d.ts +1 -0
- package/dist/css/stacking.js +4 -1
- package/dist/css/stylesheet.js +12 -0
- package/dist/date-field-labels.d.ts +5 -0
- package/dist/date-field-labels.js +1 -0
- package/dist/document-viewer-worker.d.ts +9 -0
- package/dist/document-viewer-worker.js +9 -0
- package/dist/index.d.ts +15 -9
- package/dist/index.js +13 -7
- package/dist/list-options.d.ts +11 -7
- package/dist/list-options.js +20 -8
- package/dist/styles.css +889 -44
- package/dist/theme/ThemeProvider.d.ts +11 -1
- package/dist/theme/ThemeProvider.js +4 -2
- package/package.json +10 -2
|
@@ -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', ` ?? ''})`);
|
|
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,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', ``);
|
|
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[];
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { maxExcerpt, maxNesting } from './markdown-limits.js';
|
|
2
|
+
import { safeLinkHref } from './markdown-rules.js';
|
|
3
|
+
import { plainText } from './markdown-text.js';
|
|
4
|
+
/** A token's attribute, read without its methods (tokens may be copies). */
|
|
5
|
+
export function attr(token, name) {
|
|
6
|
+
return token.attrs?.find(([key]) => key === name)?.[1] ?? null;
|
|
7
|
+
}
|
|
8
|
+
function close(context, frame, parent) {
|
|
9
|
+
if (frame.kind === 'root')
|
|
10
|
+
return;
|
|
11
|
+
if (frame.kind !== 'link') {
|
|
12
|
+
parent.children.push({ kind: frame.kind, children: frame.children });
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
const href = safeLinkHref(frame.href ?? '');
|
|
16
|
+
if (href !== null) {
|
|
17
|
+
parent.children.push({ kind: 'link', href, children: frame.children });
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
context.omit('link', `[${plainText(frame.children)}](${frame.href ?? ''})`);
|
|
21
|
+
// Links never hold links, so each word moves up once.
|
|
22
|
+
for (const child of frame.children)
|
|
23
|
+
parent.children.push(child);
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* One block's inline tokens as the view's inlines: text, breaks, bold,
|
|
27
|
+
* italic and links. A link's address must pass `safeLinkHref`, or its
|
|
28
|
+
* words stay as text; raw HTML and images here are left out; code is its
|
|
29
|
+
* words; marks nested past `maxNesting` are their words.
|
|
30
|
+
*/
|
|
31
|
+
export function readInlines(context, tokens) {
|
|
32
|
+
const root = { kind: 'root', href: null, children: [] };
|
|
33
|
+
const stack = [root];
|
|
34
|
+
// Marks past `maxNesting` open no frame: their words go straight into
|
|
35
|
+
// the deepest drawn one, so each word is placed once at any depth.
|
|
36
|
+
let flat = 0;
|
|
37
|
+
let flatWords = '';
|
|
38
|
+
// Links never hold links: the open one's address, quoted when it shuts.
|
|
39
|
+
const flatLinks = [];
|
|
40
|
+
const quote = (text) => {
|
|
41
|
+
if (flatWords.length < maxExcerpt)
|
|
42
|
+
flatWords += text;
|
|
43
|
+
};
|
|
44
|
+
const open = (kind, href) => {
|
|
45
|
+
if (flat > 0 || stack.length > maxNesting) {
|
|
46
|
+
flat += 1;
|
|
47
|
+
// A link this deep is drawn as its words; the run's one omission
|
|
48
|
+
// quotes it as `[words](address)`.
|
|
49
|
+
flatLinks.push(kind === 'link' ? (href ?? '') : null);
|
|
50
|
+
if (kind === 'link')
|
|
51
|
+
quote('[');
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
stack.push({ kind, href, children: [] });
|
|
55
|
+
};
|
|
56
|
+
const shut = () => {
|
|
57
|
+
if (flat > 0) {
|
|
58
|
+
flat -= 1;
|
|
59
|
+
const link = flatLinks.pop();
|
|
60
|
+
if (link !== null && link !== undefined)
|
|
61
|
+
quote(`](${link})`);
|
|
62
|
+
if (flat === 0) {
|
|
63
|
+
context.omit('limit', flatWords);
|
|
64
|
+
flatWords = '';
|
|
65
|
+
}
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
if (stack.length > 1)
|
|
69
|
+
close(context, stack.pop(), stack[stack.length - 1]);
|
|
70
|
+
};
|
|
71
|
+
const words = (value) => {
|
|
72
|
+
stack[stack.length - 1].children.push({ kind: 'text', value });
|
|
73
|
+
if (flat > 0)
|
|
74
|
+
quote(value);
|
|
75
|
+
};
|
|
76
|
+
for (const token of tokens) {
|
|
77
|
+
const top = stack[stack.length - 1];
|
|
78
|
+
switch (token.type) {
|
|
79
|
+
case 'text':
|
|
80
|
+
case 'text_special':
|
|
81
|
+
words(token.content);
|
|
82
|
+
break;
|
|
83
|
+
case 'softbreak':
|
|
84
|
+
words('\n');
|
|
85
|
+
break;
|
|
86
|
+
case 'hardbreak':
|
|
87
|
+
top.children.push({ kind: 'break' });
|
|
88
|
+
break;
|
|
89
|
+
case 'code_inline':
|
|
90
|
+
context.omit('code', `\`${token.content}\``);
|
|
91
|
+
words(token.content);
|
|
92
|
+
break;
|
|
93
|
+
case 'html_inline':
|
|
94
|
+
context.omit('html', token.content);
|
|
95
|
+
break;
|
|
96
|
+
case 'image':
|
|
97
|
+
// An image in a link, a mark or a heading: never drawn there.
|
|
98
|
+
context.omit('image', ` ?? ''})`);
|
|
99
|
+
break;
|
|
100
|
+
case 'strong_open':
|
|
101
|
+
open('strong', null);
|
|
102
|
+
break;
|
|
103
|
+
case 'em_open':
|
|
104
|
+
open('emphasis', null);
|
|
105
|
+
break;
|
|
106
|
+
case 'link_open':
|
|
107
|
+
open('link', attr(token, 'href'));
|
|
108
|
+
break;
|
|
109
|
+
case 'strong_close':
|
|
110
|
+
case 'em_close':
|
|
111
|
+
case 'link_close':
|
|
112
|
+
shut();
|
|
113
|
+
break;
|
|
114
|
+
default:
|
|
115
|
+
// Nothing else is parsed (no strikethrough, no footnotes); any
|
|
116
|
+
// other token's words, if it has them, stay as text.
|
|
117
|
+
if (token.content)
|
|
118
|
+
words(token.content);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
while (flat > 0)
|
|
122
|
+
shut();
|
|
123
|
+
while (stack.length > 1)
|
|
124
|
+
close(context, stack.pop(), stack[stack.length - 1]);
|
|
125
|
+
return root.children;
|
|
126
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type MarkdownReading } from './markdown-model.js';
|
|
2
|
+
import { type MarkdownOptions } from './markdown-options.js';
|
|
3
|
+
/**
|
|
4
|
+
* Reads Markdown into what `MarkdownView` draws: its blocks, its headings
|
|
5
|
+
* (with the ids the view gives them) and what it leaves out. Pure: the
|
|
6
|
+
* same source and options always give the same reading. Refuses a source
|
|
7
|
+
* that is not text and options the view refuses.
|
|
8
|
+
*/
|
|
9
|
+
export declare function readMarkdown(source: string, options?: MarkdownOptions, owner?: string): MarkdownReading;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { maxParseNesting } from './markdown-limits.js';
|
|
2
|
+
import { assertMarkdownInput, } from './markdown-options.js';
|
|
3
|
+
import { parseMarkdown } from './parse-markdown.js';
|
|
4
|
+
import { readBlocks } from './read-blocks.js';
|
|
5
|
+
import { ReadingContext } from './reading-context.js';
|
|
6
|
+
/**
|
|
7
|
+
* Reads Markdown into what `MarkdownView` draws: its blocks, its headings
|
|
8
|
+
* (with the ids the view gives them) and what it leaves out. Pure: the
|
|
9
|
+
* same source and options always give the same reading. Refuses a source
|
|
10
|
+
* that is not text and options the view refuses.
|
|
11
|
+
*/
|
|
12
|
+
export function readMarkdown(source, options = {}, owner = 'readMarkdown') {
|
|
13
|
+
assertMarkdownInput(owner, source, options);
|
|
14
|
+
const tokens = parseMarkdown(source);
|
|
15
|
+
const context = new ReadingContext(source, options);
|
|
16
|
+
// The parser stops reading a block nested deeper than it reads.
|
|
17
|
+
const cut = tokens.find((token) => token.level >= maxParseNesting - 1);
|
|
18
|
+
if (cut)
|
|
19
|
+
context.omitBlock('limit', cut);
|
|
20
|
+
const blocks = readBlocks(context, tokens);
|
|
21
|
+
return {
|
|
22
|
+
blocks,
|
|
23
|
+
headings: context.headings,
|
|
24
|
+
omissions: context.omissions,
|
|
25
|
+
omissionCounts: context.omissionCounts,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type MarkdownHeading, type MarkdownOmission, type MarkdownOmissionCounts, type MarkdownOmissionKind } from './markdown-model.js';
|
|
2
|
+
import { type MarkdownOptions } from './markdown-options.js';
|
|
3
|
+
import { HeadingIds } from './markdown-rules.js';
|
|
4
|
+
import { type Token } from './parse-markdown.js';
|
|
5
|
+
/** One document's reading so far: its headings and what it left out. */
|
|
6
|
+
export declare class ReadingContext {
|
|
7
|
+
readonly options: MarkdownOptions;
|
|
8
|
+
readonly headings: MarkdownHeading[];
|
|
9
|
+
readonly omissions: MarkdownOmission[];
|
|
10
|
+
readonly omissionCounts: MarkdownOmissionCounts;
|
|
11
|
+
readonly ids: HeadingIds;
|
|
12
|
+
private readonly lines;
|
|
13
|
+
constructor(source: string, options: MarkdownOptions);
|
|
14
|
+
/** Notes something left out, by its words. */
|
|
15
|
+
omit(kind: MarkdownOmissionKind, text: string): void;
|
|
16
|
+
/** Notes a block left out, by the source lines it came from. */
|
|
17
|
+
omitBlock(kind: MarkdownOmissionKind, token: Token): void;
|
|
18
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { maxOmissionsListed } from './markdown-limits.js';
|
|
2
|
+
import { HeadingIds } from './markdown-rules.js';
|
|
3
|
+
import { excerpt } from './markdown-text.js';
|
|
4
|
+
/** One document's reading so far: its headings and what it left out. */
|
|
5
|
+
export class ReadingContext {
|
|
6
|
+
options;
|
|
7
|
+
headings = [];
|
|
8
|
+
omissions = [];
|
|
9
|
+
omissionCounts = {
|
|
10
|
+
html: 0,
|
|
11
|
+
table: 0,
|
|
12
|
+
image: 0,
|
|
13
|
+
link: 0,
|
|
14
|
+
code: 0,
|
|
15
|
+
limit: 0,
|
|
16
|
+
};
|
|
17
|
+
ids;
|
|
18
|
+
lines;
|
|
19
|
+
constructor(source, options) {
|
|
20
|
+
this.options = options;
|
|
21
|
+
this.ids = new HeadingIds(options.idPrefix ?? '');
|
|
22
|
+
this.lines = source.split(/\r\n?|\n/);
|
|
23
|
+
}
|
|
24
|
+
/** Notes something left out, by its words. */
|
|
25
|
+
omit(kind, text) {
|
|
26
|
+
this.omissionCounts[kind] += 1;
|
|
27
|
+
if (this.omissions.length < maxOmissionsListed)
|
|
28
|
+
this.omissions.push({ kind, source: excerpt(text) });
|
|
29
|
+
}
|
|
30
|
+
/** Notes a block left out, by the source lines it came from. */
|
|
31
|
+
omitBlock(kind, token) {
|
|
32
|
+
const [start, end] = token.map ?? [0, 0];
|
|
33
|
+
this.omit(kind, this.lines.slice(start, end).join('\n'));
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
import { type MarkdownBlock, type MarkdownInline } from './markdown-model.js';
|
|
3
|
+
/** What every drawn link carries besides its own words. */
|
|
4
|
+
export type LinkOptions = {
|
|
5
|
+
newTabLabel: string;
|
|
6
|
+
};
|
|
7
|
+
/** Draws a run of text as React elements: never HTML from a string. */
|
|
8
|
+
export declare function renderInlines(inlines: readonly MarkdownInline[], links: LinkOptions): ReactNode[];
|
|
9
|
+
/** Draws blocks in order. */
|
|
10
|
+
export declare function renderBlocks(blocks: readonly MarkdownBlock[], links: LinkOptions): ReactNode[];
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Separator } from '../Separator.js';
|
|
3
|
+
const headingElements = { 1: 'h2', 2: 'h3', 3: 'h4' };
|
|
4
|
+
/** Draws a run of text as React elements: never HTML from a string. */
|
|
5
|
+
export function renderInlines(inlines, links) {
|
|
6
|
+
return inlines.map((inline, index) => {
|
|
7
|
+
switch (inline.kind) {
|
|
8
|
+
case 'text':
|
|
9
|
+
return inline.value;
|
|
10
|
+
case 'break':
|
|
11
|
+
return _jsx("br", {}, index);
|
|
12
|
+
case 'strong':
|
|
13
|
+
return (_jsx("strong", { children: renderInlines(inline.children, links) }, index));
|
|
14
|
+
case 'emphasis':
|
|
15
|
+
return _jsx("em", { children: renderInlines(inline.children, links) }, index);
|
|
16
|
+
case 'link':
|
|
17
|
+
return (_jsxs("a", { className: "sw-markdown-link", href: inline.href, rel: "noopener noreferrer",
|
|
18
|
+
// Safari leaves a link out of the Tab order without one.
|
|
19
|
+
tabIndex: 0, target: "_blank", children: [renderInlines(inline.children, links), _jsxs("span", { className: "sw-sr-only", children: [" ", links.newTabLabel] })] }, index));
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
/** Draws a list item: a tight list's paragraphs are its words alone. */
|
|
24
|
+
function renderItem(blocks, tight, links) {
|
|
25
|
+
if (!tight)
|
|
26
|
+
return renderBlocks(blocks, links);
|
|
27
|
+
return blocks.map((block, index) => block.kind === 'paragraph' ? (_jsx("span", { className: "sw-markdown-item-text", children: renderInlines(block.children, links) }, index)) : (renderBlock(block, index, links)));
|
|
28
|
+
}
|
|
29
|
+
function renderBlock(block, key, links) {
|
|
30
|
+
switch (block.kind) {
|
|
31
|
+
case 'paragraph':
|
|
32
|
+
return (_jsx("p", { className: "sw-markdown-paragraph", children: renderInlines(block.children, links) }, key));
|
|
33
|
+
case 'heading': {
|
|
34
|
+
const Heading = headingElements[block.level];
|
|
35
|
+
return (_jsx(Heading, { className: `sw-markdown-heading sw-markdown-heading-${block.level}`, id: block.id, children: renderInlines(block.children, links) }, key));
|
|
36
|
+
}
|
|
37
|
+
case 'list': {
|
|
38
|
+
const items = block.items.map((item, index) => (_jsx("li", { children: renderItem(item, block.tight, links) }, index)));
|
|
39
|
+
return block.ordered ? (_jsx("ol", { className: "sw-markdown-list", start: block.start === null || block.start === 1 ? undefined : block.start, children: items }, key)) : (_jsx("ul", { className: "sw-markdown-list", children: items }, key));
|
|
40
|
+
}
|
|
41
|
+
case 'quote':
|
|
42
|
+
return (_jsx("blockquote", { className: "sw-markdown-quote", children: renderBlocks(block.children, links) }, key));
|
|
43
|
+
case 'figure':
|
|
44
|
+
return (_jsxs("figure", { className: "sw-markdown-figure", children: [_jsx("img", { alt: block.alt, className: "sw-markdown-image", src: block.src }), block.caption ? (_jsx("figcaption", { className: "sw-markdown-caption", children: block.caption })) : null] }, key));
|
|
45
|
+
case 'rule':
|
|
46
|
+
return _jsx(Separator, {}, key);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/** Draws blocks in order. */
|
|
50
|
+
export function renderBlocks(blocks, links) {
|
|
51
|
+
return blocks.map((block, index) => renderBlock(block, index, links));
|
|
52
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type RefObject } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* Follows the width an element is given, in whole CSS pixels: on mount,
|
|
4
|
+
* before the first paint, and whenever its box changes size. Before that,
|
|
5
|
+
* and on the server, it reports `fallback`; a browser with no
|
|
6
|
+
* ResizeObserver measures on window resizes instead. `content` measures
|
|
7
|
+
* the content box (inside the padding) rather than the border box.
|
|
8
|
+
*/
|
|
9
|
+
export declare function useElementWidth(ref: RefObject<HTMLElement | null>, fallback: number, { content }?: {
|
|
10
|
+
content?: boolean;
|
|
11
|
+
}): number;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { useLayoutEffect, useState } from 'react';
|
|
3
|
+
/**
|
|
4
|
+
* Follows the width an element is given, in whole CSS pixels: on mount,
|
|
5
|
+
* before the first paint, and whenever its box changes size. Before that,
|
|
6
|
+
* and on the server, it reports `fallback`; a browser with no
|
|
7
|
+
* ResizeObserver measures on window resizes instead. `content` measures
|
|
8
|
+
* the content box (inside the padding) rather than the border box.
|
|
9
|
+
*/
|
|
10
|
+
export function useElementWidth(ref, fallback, { content = false } = {}) {
|
|
11
|
+
const [width, setWidth] = useState(fallback);
|
|
12
|
+
useLayoutEffect(() => {
|
|
13
|
+
const box = ref.current;
|
|
14
|
+
if (!box)
|
|
15
|
+
return;
|
|
16
|
+
function measure() {
|
|
17
|
+
if (!box)
|
|
18
|
+
return;
|
|
19
|
+
const style = getComputedStyle(box);
|
|
20
|
+
const padding = content
|
|
21
|
+
? (parseFloat(style.paddingLeft) || 0) +
|
|
22
|
+
(parseFloat(style.paddingRight) || 0)
|
|
23
|
+
: 0;
|
|
24
|
+
const next = Math.round(box.getBoundingClientRect().width - padding);
|
|
25
|
+
if (next > 0)
|
|
26
|
+
setWidth(next);
|
|
27
|
+
}
|
|
28
|
+
measure();
|
|
29
|
+
if (typeof ResizeObserver === 'undefined') {
|
|
30
|
+
window.addEventListener('resize', measure);
|
|
31
|
+
return () => window.removeEventListener('resize', measure);
|
|
32
|
+
}
|
|
33
|
+
const observer = new ResizeObserver(measure);
|
|
34
|
+
observer.observe(box);
|
|
35
|
+
return () => observer.disconnect();
|
|
36
|
+
}, [ref, content]);
|
|
37
|
+
return width;
|
|
38
|
+
}
|
|
@@ -46,6 +46,8 @@ export declare const dangerFillShare = 0.4;
|
|
|
46
46
|
export declare const chartFillMixes: Record<ChartFillTone, ChartColorMix>;
|
|
47
47
|
/** Each stroke tone's colour: a semantic colour, or muted for neutral. */
|
|
48
48
|
export declare const chartStrokeColors: Record<ChartStrokeTone, ChartColorSource>;
|
|
49
|
+
/** A stroke tone's colour as CSS, such as a line's or a bar's. */
|
|
50
|
+
export declare function chartStroke(tone: ChartStrokeTone): string;
|
|
49
51
|
/**
|
|
50
52
|
* How much of a line's stroke the area under it takes, over transparency:
|
|
51
53
|
* a quiet tint behind the line, decorative (the line carries the values).
|