@ng-native/components 0.14.0 → 0.16.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/dist/markdown.js CHANGED
@@ -9,12 +9,14 @@
9
9
  * import { Markdown } from '@ng-native/components/markdown';
10
10
  * ```
11
11
  */
12
- import { Component, Injector, computed, inject, input, linkedSignal, output } from '@angular/core';
12
+ import { Component, Injector, booleanAttribute, computed, inject, input, linkedSignal, output, } from '@angular/core';
13
13
  import { DeepLinks } from '@ng-native/device';
14
14
  import { nativePlatform } from '@ng-native/fabric';
15
15
  import { lexer } from 'marked';
16
16
  import { Image } from "./image.js";
17
- import { markdownBlocks, opensOnPress, } from "./markdown-blocks.js";
17
+ import { blockKey, markdownBlocks, opensOnPress, shareBlocks, } from "./markdown-blocks.js";
18
+ import { MarkdownColumns } from "./markdown-columns.js";
19
+ import { MarkdownStream } from "./markdown-stream.js";
18
20
  import { ScrollView } from "./scroll-view.js";
19
21
  import { TemplateSlot } from "./template-slot.js";
20
22
  import { Text } from "./text.js";
@@ -25,16 +27,16 @@ const ELEMENTS = [
25
27
  ...['pre', 'code', 'strong', 'em', 'del', 'a', 'hr', 'img', 'table', 'tr', 'th', 'td'],
26
28
  ];
27
29
  const DEFAULT_CLASSES = Object.fromEntries(ELEMENTS.map((element) => [element, `md-${element}`]));
28
- /** Every image address in `blocks`, however deep in quotes and lists. */
29
- function imageSources(blocks, into = new Set()) {
30
+ /** Every image in `blocks`, however deep in quotes and lists. */
31
+ function imageBlocks(blocks, into = new Set()) {
30
32
  for (const block of blocks) {
31
33
  if (block.kind === 'image')
32
- into.add(block.src);
34
+ into.add(block);
33
35
  else if (block.kind === 'blockquote')
34
- imageSources(block.children, into);
36
+ imageBlocks(block.children, into);
35
37
  else if (block.kind === 'list') {
36
38
  for (const item of block.items)
37
- imageSources(item.children, into);
39
+ imageBlocks(item.children, into);
38
40
  }
39
41
  }
40
42
  return into;
@@ -56,6 +58,15 @@ function imageSources(blocks, into = new Set()) {
56
58
  * dark. `classes` replaces one by its element's name, so a global stylesheet or Tailwind styles it
57
59
  * instead: `{ h1: 'text-3xl font-bold' }`. Put the default back beside your own to keep both.
58
60
  *
61
+ * For a source that grows as it streams in, an answer arriving a few words at a time, set
62
+ * `streaming` until it is complete. The blocks before the last one are then kept as they are and
63
+ * only the last is lexed and converted again, unless a chunk defines or changes a link reference,
64
+ * which can change a block already kept, and the whole source is lexed again. Syntax still open at its end, a code span, an emphasis, a link,
65
+ * an image or a table row, is held back until it closes. Of a code fence only an unfinished
66
+ * opening line is held back, and its code then streams as it arrives. Clearing `streaming` draws
67
+ * the whole source as written, and `tokens` are always drawn as given. A block that did not
68
+ * change keeps its views, streaming or not.
69
+ *
59
70
  * A pressed link is `(linkPress)`. An absolute target is then opened with `DeepLinks.open` unless
60
71
  * a handler calls `preventDefault()`; a relative one is only reported, for the app to route.
61
72
  */
@@ -69,33 +80,130 @@ export class Markdown {
69
80
  /** Tokens `marked.lexer` made already, drawn in place of `source`. */
70
81
  tokens = input(/* @ts-ignore */
71
82
  ...(ngDevMode ? [undefined, { debugName: "tokens" }] : /* istanbul ignore next */ []));
83
+ /**
84
+ * Whether `source` is still arriving, a chunk at a time. While it is, only its last block is
85
+ * lexed again, unless a link reference changes, and syntax still open at its end is held back.
86
+ * See the class note.
87
+ */
88
+ streaming = input(false, { ...(ngDevMode ? { debugName: "streaming" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
72
89
  /** A class for an element, in place of its default. See the class note. */
73
90
  classes = input({}, /* @ts-ignore */
74
91
  ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
75
92
  /** A link was pressed. See the class note. */
76
93
  linkPress = output();
77
- blocks = computed(() => markdownBlocks(this.tokens() ?? lexer(this.source() ?? '')), /* @ts-ignore */
78
- ...(ngDevMode ? [{ debugName: "blocks" }] : /* istanbul ignore next */ []));
94
+ stream = new MarkdownStream();
95
+ blockKey = blockKey;
96
+ /**
97
+ * The blocks each token the stream kept was drawn as. A kept token is the same object from one
98
+ * chunk to the next, so its blocks are not converted or compared again; a source lexed whole
99
+ * makes new tokens, and so new blocks.
100
+ */
101
+ streamed = new WeakMap();
102
+ /** The document's blocks, with each one that did not change kept, so it is not drawn again. */
103
+ blocks = linkedSignal({ ...(ngDevMode ? { debugName: "blocks" } : /* istanbul ignore next */ {}), source: () => {
104
+ const tokens = this.tokens();
105
+ if (tokens)
106
+ return { tokens, streamed: false };
107
+ const streamed = this.streaming();
108
+ return { tokens: this.lex(this.source() ?? '', streamed), streamed };
109
+ },
110
+ computation: ({ tokens, streamed }, previous) => streamed
111
+ ? this.streamedBlocks(tokens, previous?.value)
112
+ : shareBlocks(previous?.value, markdownBlocks(tokens)) });
79
113
  classOf = computed(() => ({ ...DEFAULT_CLASSES, ...this.classes() }), /* @ts-ignore */
80
114
  ...(ngDevMode ? [{ debugName: "classOf" }] : /* istanbul ignore next */ []));
81
115
  /**
82
- * Each image's width over its height, once it has loaded, so it is drawn at the page's width and
83
- * its own shape. An image the document no longer has is forgotten when the document changes.
116
+ * Each image's own width and its width over its height, once it has loaded, so it is drawn at its
117
+ * own size and shape, as a browser draws it, and no wider than the document. The width reaches
118
+ * the image as `--md-img-width`, which `.md-img` reads, so a class of the app's own still sets
119
+ * the width. An image the document no longer has is forgotten when the document changes.
84
120
  */
85
- ratios = linkedSignal({ ...(ngDevMode ? { debugName: "ratios" } : /* istanbul ignore next */ {}), source: this.blocks,
121
+ sizes = linkedSignal({ ...(ngDevMode ? { debugName: "sizes" } : /* istanbul ignore next */ {}), source: this.blocks,
86
122
  computation: (blocks, previous) => {
87
123
  const kept = new Map();
88
124
  if (!previous)
89
125
  return kept;
90
- const present = imageSources(blocks);
91
- for (const [src, ratio] of previous.value)
126
+ const present = new Set([...imageBlocks(blocks)].map((block) => block.src));
127
+ for (const [src, size] of previous.value)
92
128
  if (present.has(src))
93
- kept.set(src, ratio);
129
+ kept.set(src, size);
130
+ return kept;
131
+ } });
132
+ /**
133
+ * The width each image is laid out at where that is less than its own, narrowed by
134
+ * `max-width`. Yoga takes an image's height from its width before `max-width` narrows it, where
135
+ * CSS takes it after, so a narrowed image is given the height its ratio makes of the width it
136
+ * has. An image no longer in the document is forgotten when the document changes.
137
+ */
138
+ narrowed = linkedSignal({ ...(ngDevMode ? { debugName: "narrowed" } : /* istanbul ignore next */ {}), source: this.blocks,
139
+ computation: (blocks, previous) => {
140
+ const kept = new Map();
141
+ if (!previous)
142
+ return kept;
143
+ const present = imageBlocks(blocks);
144
+ for (const [block, width] of previous.value)
145
+ if (present.has(block))
146
+ kept.set(block, width);
94
147
  return kept;
95
148
  } });
149
+ lex(source, streaming) {
150
+ return streaming ? this.stream.lex(source) : lexer(source);
151
+ }
152
+ /** `tokens`' blocks, converting and sharing only those of tokens the stream had not kept. */
153
+ streamedBlocks(tokens, previous) {
154
+ const kept = [];
155
+ let at = 0;
156
+ for (; at < tokens.length; at++) {
157
+ const blocks = this.streamed.get(tokens[at]);
158
+ if (!blocks)
159
+ break;
160
+ kept.push(...blocks);
161
+ }
162
+ const rest = tokens.slice(at);
163
+ const converted = rest.map((token) => markdownBlocks([token]));
164
+ // The blocks drawn after the kept ones, when those are still the ones drawn first.
165
+ const drawn = !kept.length || previous?.[kept.length - 1] === kept.at(-1);
166
+ const was = drawn ? previous?.slice(kept.length) : undefined;
167
+ const shared = shareBlocks(was, converted.flat());
168
+ let from = 0;
169
+ rest.forEach((token, i) => {
170
+ const count = converted[i].length;
171
+ this.streamed.set(token, shared.slice(from, (from += count)));
172
+ });
173
+ return [...kept, ...shared];
174
+ }
96
175
  classFor(element) {
97
176
  return this.classOf()[element];
98
177
  }
178
+ imageWidth(src) {
179
+ const size = this.sizes().get(src);
180
+ return size ? `${size.width}px` : null;
181
+ }
182
+ /** The class's height until the image loads, then its ratio's, or a narrowed image's own. */
183
+ imageHeight(block) {
184
+ const size = this.sizes().get(block.src);
185
+ if (!size)
186
+ return null;
187
+ const width = this.narrowed().get(block);
188
+ return width === undefined ? 'auto' : width / size.ratio;
189
+ }
190
+ layOutImage(block, event) {
191
+ const width = event.nativeEvent?.layout?.width;
192
+ const size = this.sizes().get(block.src);
193
+ if (width === undefined || !size)
194
+ return;
195
+ const narrowed = width < size.width ? width : undefined;
196
+ if (this.narrowed().get(block) === narrowed)
197
+ return;
198
+ this.narrowed.update((all) => {
199
+ const next = new Map(all);
200
+ if (narrowed === undefined)
201
+ next.delete(block);
202
+ else
203
+ next.set(block, narrowed);
204
+ return next;
205
+ });
206
+ }
99
207
  sizeImage(src, event) {
100
208
  const source = event.nativeEvent?.source;
101
209
  if (!source)
@@ -103,7 +211,9 @@ export class Markdown {
103
211
  const { width, height } = source;
104
212
  if (!(width > 0 && height > 0))
105
213
  return;
106
- this.ratios.update((ratios) => new Map(ratios).set(src, width / height));
214
+ // A url's or a data url's scale is 1, so the load event's pixels are the points a browser's
215
+ // CSS pixels are.
216
+ this.sizes.update((sizes) => new Map(sizes).set(src, { width, ratio: width / height }));
107
217
  }
108
218
  followLink(link) {
109
219
  let prevented = false;
@@ -118,9 +228,9 @@ export class Markdown {
118
228
  this.injector.get(DeepLinks).open(link.href);
119
229
  }
120
230
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.2.0", ngImport: i0, type: Markdown, deps: [], target: i0.ɵɵFactoryTarget.Component });
121
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.2.0", type: Markdown, isStandalone: true, selector: "markdown", inputs: { source: { classPropertyName: "source", publicName: "source", isSignal: true, isRequired: false, transformFunction: null }, tokens: { classPropertyName: "tokens", publicName: "tokens", isSignal: true, isRequired: false, transformFunction: null }, classes: { classPropertyName: "classes", publicName: "classes", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { linkPress: "linkPress" }, host: { properties: { "class.md-ios": "platform === 'ios'", "class.md-android": "platform === 'android'" } }, ngImport: i0, template: `
231
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.2.0", type: Markdown, isStandalone: true, selector: "markdown", inputs: { source: { classPropertyName: "source", publicName: "source", isSignal: true, isRequired: false, transformFunction: null }, tokens: { classPropertyName: "tokens", publicName: "tokens", isSignal: true, isRequired: false, transformFunction: null }, streaming: { classPropertyName: "streaming", publicName: "streaming", isSignal: true, isRequired: false, transformFunction: null }, classes: { classPropertyName: "classes", publicName: "classes", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { linkPress: "linkPress" }, host: { properties: { "class.md-ios": "platform === 'ios'", "class.md-android": "platform === 'android'" } }, ngImport: i0, template: `
122
232
  <ng-template #blockTemplate let-list>
123
- @for (block of list; track $index) {
233
+ @for (block of list; track blockKey(block)) {
124
234
  @switch (block.kind) {
125
235
  @case ('heading') {
126
236
  <text [class]="classFor(block.element)" accessibilityRole="header"
@@ -138,9 +248,11 @@ export class Markdown {
138
248
  [alt]="block.alt || undefined"
139
249
  resizeMode="contain"
140
250
  [class]="classFor('img')"
141
- [style.aspectRatio]="ratios().get(block.src) ?? null"
142
- [style.height]="ratios().has(block.src) ? 'auto' : null"
251
+ [style.--md-img-width]="imageWidth(block.src)"
252
+ [style.aspectRatio]="sizes().get(block.src)?.ratio ?? null"
253
+ [style.height]="imageHeight(block)"
143
254
  (load)="sizeImage(block.src, $any($event))"
255
+ (layout)="layOutImage(block, $any($event))"
144
256
  />
145
257
  }
146
258
  @case ('blockquote') {
@@ -171,24 +283,39 @@ export class Markdown {
171
283
  <view [class]="classFor('hr')"></view>
172
284
  }
173
285
  @case ('table') {
174
- <view [class]="classFor('table')">
175
- <view [class]="classFor('tr')">
176
- @for (cell of block.header; track $index) {
177
- <text [class]="classFor('th')"
178
- ><ng-container *templateSlot="inlineTemplate; context: { $implicit: cell }"
179
- /></text>
180
- }
181
- </view>
182
- @for (row of block.rows; track $index) {
286
+ <scroll-view horizontal>
287
+ <view
288
+ [class]="classFor('table')"
289
+ [markdownColumns]="block"
290
+ #columns="markdownColumns"
291
+ >
183
292
  <view [class]="classFor('tr')">
184
- @for (cell of row; track $index) {
185
- <text [class]="classFor('td')"
186
- ><ng-container *templateSlot="inlineTemplate; context: { $implicit: cell }"
187
- /></text>
293
+ @for (cell of block.header; track $index; let column = $index) {
294
+ <view class="md-cell" [style.minWidth]="columns.width(column)">
295
+ <text
296
+ [class]="classFor('th')"
297
+ (layout)="columns.measure(0, column, $any($event))"
298
+ ><ng-container *templateSlot="inlineTemplate; context: { $implicit: cell }"
299
+ /></text>
300
+ </view>
188
301
  }
189
302
  </view>
190
- }
191
- </view>
303
+ @for (row of block.rows; track $index; let r = $index) {
304
+ <view [class]="classFor('tr')">
305
+ @for (cell of row; track $index; let column = $index) {
306
+ <view class="md-cell md-cell-body" [style.minWidth]="columns.width(column)">
307
+ <text
308
+ [class]="classFor('td')"
309
+ (layout)="columns.measure(r + 1, column, $any($event))"
310
+ ><ng-container
311
+ *templateSlot="inlineTemplate; context: { $implicit: cell }"
312
+ /></text>
313
+ </view>
314
+ }
315
+ </view>
316
+ }
317
+ </view>
318
+ </scroll-view>
192
319
  }
193
320
  }
194
321
  }
@@ -222,13 +349,13 @@ export class Markdown {
222
349
  </ng-template>
223
350
 
224
351
  <ng-container *templateSlot="blockTemplate; context: { $implicit: blocks() }" />
225
- `, isInline: true, styles: ["/* What a document looks like with no `classes` of the app's, in light and in dark. */\n\n:host {\n gap: 12px;\n color: #1c1c1e;\n}\n\n.md-h1,\n.md-h2,\n.md-h3,\n.md-h4,\n.md-h5,\n.md-h6 {\n font-weight: 700;\n}\n\n.md-h1 {\n font-size: 28px;\n line-height: 34px;\n}\n\n.md-h2 {\n font-size: 22px;\n line-height: 28px;\n}\n\n.md-h3 {\n font-size: 20px;\n line-height: 25px;\n}\n\n.md-h4 {\n font-size: 17px;\n line-height: 22px;\n}\n\n.md-h5 {\n font-size: 15px;\n line-height: 20px;\n}\n\n.md-h6 {\n font-size: 13px;\n line-height: 18px;\n color: #6c6c70;\n}\n\n.md-p,\n.md-marker,\n.md-th,\n.md-td {\n font-size: 16px;\n line-height: 23px;\n}\n\n.md-strong {\n font-weight: 700;\n}\n\n.md-em {\n font-style: italic;\n}\n\n.md-del {\n text-decoration-line: line-through;\n}\n\n.md-a {\n color: #0a66d8;\n text-decoration-line: underline;\n}\n\n.md-code {\n background-color: rgba(120, 120, 128, 0.16);\n}\n\n.md-code,\n.md-pre {\n font-family: Courier New;\n}\n\n:host(.md-ios) .md-code,\n:host(.md-ios) .md-pre {\n font-family: Menlo;\n}\n\n:host(.md-android) .md-code,\n:host(.md-android) .md-pre {\n font-family: monospace;\n}\n\n.md-blockquote {\n gap: 8px;\n padding-left: 12px;\n border-left-width: 3px;\n border-left-color: #d1d1d6;\n color: #6c6c70;\n}\n\n.md-ul,\n.md-ol {\n gap: 4px;\n}\n\n.md-li {\n flex-direction: row;\n gap: 8px;\n}\n\n.md-marker {\n min-width: 16px;\n}\n\n.md-li-body {\n flex: 1;\n gap: 4px;\n}\n\n.md-pre {\n flex-grow: 0;\n border-radius: 8px;\n background-color: #f2f2f7;\n}\n\n.md-pre-text {\n padding: 12px;\n font-size: 14px;\n line-height: 20px;\n}\n\n.md-hr {\n height: 1px;\n background-color: #d1d1d6;\n}\n\n.md-img {\n width: 100%;\n height: 200px;\n border-radius: 8px;\n}\n\n.md-table {\n border-width: 1px;\n border-color: #d1d1d6;\n border-radius: 8px;\n overflow: hidden;\n}\n\n.md-tr {\n flex-direction: row;\n}\n\n.md-th,\n.md-td {\n flex: 1;\n padding: 6px 8px;\n}\n\n.md-td {\n border-top-width: 1px;\n border-top-color: #d1d1d6;\n}\n\n.md-th {\n font-weight: 600;\n}\n\n@media (prefers-color-scheme: dark) {\n :host {\n color: #f2f2f7;\n }\n\n .md-h6,\n .md-blockquote {\n color: #98989f;\n }\n\n .md-a {\n color: #4c9bff;\n }\n\n .md-code {\n background-color: rgba(120, 120, 128, 0.32);\n }\n\n .md-pre {\n background-color: #1c1c1e;\n }\n\n .md-blockquote,\n .md-table,\n .md-td {\n border-color: #38383a;\n }\n\n .md-hr {\n background-color: #38383a;\n }\n}\n"], dependencies: [{ kind: "directive", type: Image, selector: "image", inputs: ["source", "src", "srcSet", "defaultSource", "loadingIndicatorSource", "resizeMode", "resizeMethod", "resizeMultiplier", "blurRadius", "capInsets", "tintColor", "fadeDuration", "progressiveRenderingEnabled", "alt", "crossOrigin", "referrerPolicy"] }, { kind: "component", type: ScrollView, selector: "scroll-view", inputs: ["contentContainerStyle", "contentContainerClass", "horizontal", "contentOffset", "maintainVisibleContentPosition", "keyboardShouldPersistTaps", "nestedScrollEnabled", "stickyHeaderIndices"], outputs: ["contentSizeChange"], exportAs: ["scrollView"] }, { kind: "directive", type: TemplateSlot, selector: "[templateSlot]", inputs: ["templateSlot", "templateSlotContext"] }, { kind: "directive", type: Text, selector: "text", inputs: ["numberOfLines", "ellipsizeMode", "selectable", "selectionColor", "allowFontScaling", "maxFontSizeMultiplier", "dynamicTypeRamp", "adjustsFontSizeToFit", "minimumFontScale", "lineBreakStrategyIOS", "textBreakStrategy", "dataDetectorType", "android_hyphenationFrequency", "pressable", "suppressHighlighting"], exportAs: ["text"] }, { kind: "directive", type: View, selector: "view" }] });
352
+ `, isInline: true, styles: ["/* What a document looks like with no `classes` of the app's, in light and in dark. */\n\n:host {\n gap: 12px;\n color: #1c1c1e;\n}\n\n.md-h1,\n.md-h2,\n.md-h3,\n.md-h4,\n.md-h5,\n.md-h6 {\n font-weight: 700;\n}\n\n.md-h1 {\n font-size: 28px;\n line-height: 34px;\n}\n\n.md-h2 {\n font-size: 22px;\n line-height: 28px;\n}\n\n.md-h3 {\n font-size: 20px;\n line-height: 25px;\n}\n\n.md-h4 {\n font-size: 17px;\n line-height: 22px;\n}\n\n.md-h5 {\n font-size: 15px;\n line-height: 20px;\n}\n\n.md-h6 {\n font-size: 13px;\n line-height: 18px;\n color: #6c6c70;\n}\n\n.md-p,\n.md-marker,\n.md-th,\n.md-td {\n font-size: 16px;\n line-height: 23px;\n}\n\n.md-strong {\n font-weight: 700;\n}\n\n.md-em {\n font-style: italic;\n}\n\n.md-del {\n text-decoration-line: line-through;\n}\n\n.md-a {\n color: #0a66d8;\n text-decoration-line: underline;\n}\n\n.md-code {\n background-color: rgba(120, 120, 128, 0.16);\n}\n\n.md-code,\n.md-pre {\n font-family: Courier New;\n}\n\n:host(.md-ios) .md-code,\n:host(.md-ios) .md-pre {\n font-family: Menlo;\n}\n\n:host(.md-android) .md-code,\n:host(.md-android) .md-pre {\n font-family: monospace;\n}\n\n.md-blockquote {\n gap: 8px;\n padding-left: 12px;\n border-left-width: 3px;\n border-left-color: #d1d1d6;\n color: #6c6c70;\n}\n\n.md-ul,\n.md-ol {\n gap: 4px;\n}\n\n.md-li {\n flex-direction: row;\n gap: 8px;\n}\n\n.md-marker {\n min-width: 16px;\n}\n\n.md-li-body {\n flex: 1;\n gap: 4px;\n}\n\n.md-pre {\n flex-grow: 0;\n border-radius: 8px;\n background-color: #f2f2f7;\n}\n\n.md-pre-text {\n padding: 12px;\n font-size: 14px;\n line-height: 20px;\n}\n\n.md-hr {\n height: 1px;\n background-color: #d1d1d6;\n}\n\n.md-img {\n width: var(--md-img-width, 100%);\n max-width: 100%;\n height: 200px;\n border-radius: 8px;\n}\n\n.md-table {\n border-width: 1px;\n border-color: #d1d1d6;\n border-radius: 8px;\n overflow: hidden;\n}\n\n.md-tr {\n flex-direction: row;\n}\n\n/* The text hugs its content inside the column's least width, so it can report a narrower one. */\n.md-cell {\n align-items: flex-start;\n}\n\n.md-th,\n.md-td {\n max-width: 280px;\n padding: 6px 8px;\n}\n\n.md-cell-body {\n border-top-width: 1px;\n border-top-color: #d1d1d6;\n}\n\n.md-th {\n font-weight: 600;\n}\n\n@media (prefers-color-scheme: dark) {\n :host {\n color: #f2f2f7;\n }\n\n .md-h6,\n .md-blockquote {\n color: #98989f;\n }\n\n .md-a {\n color: #4c9bff;\n }\n\n .md-code {\n background-color: rgba(120, 120, 128, 0.32);\n }\n\n .md-pre {\n background-color: #1c1c1e;\n }\n\n .md-blockquote,\n .md-table,\n .md-cell-body {\n border-color: #38383a;\n }\n\n .md-hr {\n background-color: #38383a;\n }\n}\n"], dependencies: [{ kind: "directive", type: Image, selector: "image", inputs: ["source", "src", "srcSet", "defaultSource", "loadingIndicatorSource", "resizeMode", "resizeMethod", "resizeMultiplier", "blurRadius", "capInsets", "tintColor", "fadeDuration", "progressiveRenderingEnabled", "alt", "crossOrigin", "referrerPolicy"] }, { kind: "directive", type: MarkdownColumns, selector: "[markdownColumns]", inputs: ["markdownColumns"], exportAs: ["markdownColumns"] }, { kind: "component", type: ScrollView, selector: "scroll-view", inputs: ["contentContainerStyle", "contentContainerClass", "horizontal", "contentOffset", "maintainVisibleContentPosition", "keyboardShouldPersistTaps", "nestedScrollEnabled", "stickyHeaderIndices"], outputs: ["contentSizeChange"], exportAs: ["scrollView"] }, { kind: "directive", type: TemplateSlot, selector: "[templateSlot]", inputs: ["templateSlot", "templateSlotContext"] }, { kind: "directive", type: Text, selector: "text", inputs: ["numberOfLines", "ellipsizeMode", "selectable", "selectionColor", "allowFontScaling", "maxFontSizeMultiplier", "dynamicTypeRamp", "adjustsFontSizeToFit", "minimumFontScale", "lineBreakStrategyIOS", "textBreakStrategy", "dataDetectorType", "android_hyphenationFrequency", "pressable", "suppressHighlighting"], exportAs: ["text"] }, { kind: "directive", type: View, selector: "view" }] });
226
353
  }
227
354
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.2.0", ngImport: i0, type: Markdown, decorators: [{
228
355
  type: Component,
229
- args: [{ selector: 'markdown', imports: [Image, ScrollView, TemplateSlot, Text, View], template: `
356
+ args: [{ selector: 'markdown', imports: [Image, MarkdownColumns, ScrollView, TemplateSlot, Text, View], template: `
230
357
  <ng-template #blockTemplate let-list>
231
- @for (block of list; track $index) {
358
+ @for (block of list; track blockKey(block)) {
232
359
  @switch (block.kind) {
233
360
  @case ('heading') {
234
361
  <text [class]="classFor(block.element)" accessibilityRole="header"
@@ -246,9 +373,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.2.0", ngImpor
246
373
  [alt]="block.alt || undefined"
247
374
  resizeMode="contain"
248
375
  [class]="classFor('img')"
249
- [style.aspectRatio]="ratios().get(block.src) ?? null"
250
- [style.height]="ratios().has(block.src) ? 'auto' : null"
376
+ [style.--md-img-width]="imageWidth(block.src)"
377
+ [style.aspectRatio]="sizes().get(block.src)?.ratio ?? null"
378
+ [style.height]="imageHeight(block)"
251
379
  (load)="sizeImage(block.src, $any($event))"
380
+ (layout)="layOutImage(block, $any($event))"
252
381
  />
253
382
  }
254
383
  @case ('blockquote') {
@@ -279,24 +408,39 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.2.0", ngImpor
279
408
  <view [class]="classFor('hr')"></view>
280
409
  }
281
410
  @case ('table') {
282
- <view [class]="classFor('table')">
283
- <view [class]="classFor('tr')">
284
- @for (cell of block.header; track $index) {
285
- <text [class]="classFor('th')"
286
- ><ng-container *templateSlot="inlineTemplate; context: { $implicit: cell }"
287
- /></text>
288
- }
289
- </view>
290
- @for (row of block.rows; track $index) {
411
+ <scroll-view horizontal>
412
+ <view
413
+ [class]="classFor('table')"
414
+ [markdownColumns]="block"
415
+ #columns="markdownColumns"
416
+ >
291
417
  <view [class]="classFor('tr')">
292
- @for (cell of row; track $index) {
293
- <text [class]="classFor('td')"
294
- ><ng-container *templateSlot="inlineTemplate; context: { $implicit: cell }"
295
- /></text>
418
+ @for (cell of block.header; track $index; let column = $index) {
419
+ <view class="md-cell" [style.minWidth]="columns.width(column)">
420
+ <text
421
+ [class]="classFor('th')"
422
+ (layout)="columns.measure(0, column, $any($event))"
423
+ ><ng-container *templateSlot="inlineTemplate; context: { $implicit: cell }"
424
+ /></text>
425
+ </view>
296
426
  }
297
427
  </view>
298
- }
299
- </view>
428
+ @for (row of block.rows; track $index; let r = $index) {
429
+ <view [class]="classFor('tr')">
430
+ @for (cell of row; track $index; let column = $index) {
431
+ <view class="md-cell md-cell-body" [style.minWidth]="columns.width(column)">
432
+ <text
433
+ [class]="classFor('td')"
434
+ (layout)="columns.measure(r + 1, column, $any($event))"
435
+ ><ng-container
436
+ *templateSlot="inlineTemplate; context: { $implicit: cell }"
437
+ /></text>
438
+ </view>
439
+ }
440
+ </view>
441
+ }
442
+ </view>
443
+ </scroll-view>
300
444
  }
301
445
  }
302
446
  }
@@ -333,6 +477,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.2.0", ngImpor
333
477
  `, host: {
334
478
  '[class.md-ios]': "platform === 'ios'",
335
479
  '[class.md-android]': "platform === 'android'",
336
- }, styles: ["/* What a document looks like with no `classes` of the app's, in light and in dark. */\n\n:host {\n gap: 12px;\n color: #1c1c1e;\n}\n\n.md-h1,\n.md-h2,\n.md-h3,\n.md-h4,\n.md-h5,\n.md-h6 {\n font-weight: 700;\n}\n\n.md-h1 {\n font-size: 28px;\n line-height: 34px;\n}\n\n.md-h2 {\n font-size: 22px;\n line-height: 28px;\n}\n\n.md-h3 {\n font-size: 20px;\n line-height: 25px;\n}\n\n.md-h4 {\n font-size: 17px;\n line-height: 22px;\n}\n\n.md-h5 {\n font-size: 15px;\n line-height: 20px;\n}\n\n.md-h6 {\n font-size: 13px;\n line-height: 18px;\n color: #6c6c70;\n}\n\n.md-p,\n.md-marker,\n.md-th,\n.md-td {\n font-size: 16px;\n line-height: 23px;\n}\n\n.md-strong {\n font-weight: 700;\n}\n\n.md-em {\n font-style: italic;\n}\n\n.md-del {\n text-decoration-line: line-through;\n}\n\n.md-a {\n color: #0a66d8;\n text-decoration-line: underline;\n}\n\n.md-code {\n background-color: rgba(120, 120, 128, 0.16);\n}\n\n.md-code,\n.md-pre {\n font-family: Courier New;\n}\n\n:host(.md-ios) .md-code,\n:host(.md-ios) .md-pre {\n font-family: Menlo;\n}\n\n:host(.md-android) .md-code,\n:host(.md-android) .md-pre {\n font-family: monospace;\n}\n\n.md-blockquote {\n gap: 8px;\n padding-left: 12px;\n border-left-width: 3px;\n border-left-color: #d1d1d6;\n color: #6c6c70;\n}\n\n.md-ul,\n.md-ol {\n gap: 4px;\n}\n\n.md-li {\n flex-direction: row;\n gap: 8px;\n}\n\n.md-marker {\n min-width: 16px;\n}\n\n.md-li-body {\n flex: 1;\n gap: 4px;\n}\n\n.md-pre {\n flex-grow: 0;\n border-radius: 8px;\n background-color: #f2f2f7;\n}\n\n.md-pre-text {\n padding: 12px;\n font-size: 14px;\n line-height: 20px;\n}\n\n.md-hr {\n height: 1px;\n background-color: #d1d1d6;\n}\n\n.md-img {\n width: 100%;\n height: 200px;\n border-radius: 8px;\n}\n\n.md-table {\n border-width: 1px;\n border-color: #d1d1d6;\n border-radius: 8px;\n overflow: hidden;\n}\n\n.md-tr {\n flex-direction: row;\n}\n\n.md-th,\n.md-td {\n flex: 1;\n padding: 6px 8px;\n}\n\n.md-td {\n border-top-width: 1px;\n border-top-color: #d1d1d6;\n}\n\n.md-th {\n font-weight: 600;\n}\n\n@media (prefers-color-scheme: dark) {\n :host {\n color: #f2f2f7;\n }\n\n .md-h6,\n .md-blockquote {\n color: #98989f;\n }\n\n .md-a {\n color: #4c9bff;\n }\n\n .md-code {\n background-color: rgba(120, 120, 128, 0.32);\n }\n\n .md-pre {\n background-color: #1c1c1e;\n }\n\n .md-blockquote,\n .md-table,\n .md-td {\n border-color: #38383a;\n }\n\n .md-hr {\n background-color: #38383a;\n }\n}\n"] }]
337
- }], propDecorators: { source: [{ type: i0.Input, args: [{ isSignal: true, alias: "source", required: false }] }], tokens: [{ type: i0.Input, args: [{ isSignal: true, alias: "tokens", required: false }] }], classes: [{ type: i0.Input, args: [{ isSignal: true, alias: "classes", required: false }] }], linkPress: [{ type: i0.Output, args: ["linkPress"] }] } });
480
+ }, styles: ["/* What a document looks like with no `classes` of the app's, in light and in dark. */\n\n:host {\n gap: 12px;\n color: #1c1c1e;\n}\n\n.md-h1,\n.md-h2,\n.md-h3,\n.md-h4,\n.md-h5,\n.md-h6 {\n font-weight: 700;\n}\n\n.md-h1 {\n font-size: 28px;\n line-height: 34px;\n}\n\n.md-h2 {\n font-size: 22px;\n line-height: 28px;\n}\n\n.md-h3 {\n font-size: 20px;\n line-height: 25px;\n}\n\n.md-h4 {\n font-size: 17px;\n line-height: 22px;\n}\n\n.md-h5 {\n font-size: 15px;\n line-height: 20px;\n}\n\n.md-h6 {\n font-size: 13px;\n line-height: 18px;\n color: #6c6c70;\n}\n\n.md-p,\n.md-marker,\n.md-th,\n.md-td {\n font-size: 16px;\n line-height: 23px;\n}\n\n.md-strong {\n font-weight: 700;\n}\n\n.md-em {\n font-style: italic;\n}\n\n.md-del {\n text-decoration-line: line-through;\n}\n\n.md-a {\n color: #0a66d8;\n text-decoration-line: underline;\n}\n\n.md-code {\n background-color: rgba(120, 120, 128, 0.16);\n}\n\n.md-code,\n.md-pre {\n font-family: Courier New;\n}\n\n:host(.md-ios) .md-code,\n:host(.md-ios) .md-pre {\n font-family: Menlo;\n}\n\n:host(.md-android) .md-code,\n:host(.md-android) .md-pre {\n font-family: monospace;\n}\n\n.md-blockquote {\n gap: 8px;\n padding-left: 12px;\n border-left-width: 3px;\n border-left-color: #d1d1d6;\n color: #6c6c70;\n}\n\n.md-ul,\n.md-ol {\n gap: 4px;\n}\n\n.md-li {\n flex-direction: row;\n gap: 8px;\n}\n\n.md-marker {\n min-width: 16px;\n}\n\n.md-li-body {\n flex: 1;\n gap: 4px;\n}\n\n.md-pre {\n flex-grow: 0;\n border-radius: 8px;\n background-color: #f2f2f7;\n}\n\n.md-pre-text {\n padding: 12px;\n font-size: 14px;\n line-height: 20px;\n}\n\n.md-hr {\n height: 1px;\n background-color: #d1d1d6;\n}\n\n.md-img {\n width: var(--md-img-width, 100%);\n max-width: 100%;\n height: 200px;\n border-radius: 8px;\n}\n\n.md-table {\n border-width: 1px;\n border-color: #d1d1d6;\n border-radius: 8px;\n overflow: hidden;\n}\n\n.md-tr {\n flex-direction: row;\n}\n\n/* The text hugs its content inside the column's least width, so it can report a narrower one. */\n.md-cell {\n align-items: flex-start;\n}\n\n.md-th,\n.md-td {\n max-width: 280px;\n padding: 6px 8px;\n}\n\n.md-cell-body {\n border-top-width: 1px;\n border-top-color: #d1d1d6;\n}\n\n.md-th {\n font-weight: 600;\n}\n\n@media (prefers-color-scheme: dark) {\n :host {\n color: #f2f2f7;\n }\n\n .md-h6,\n .md-blockquote {\n color: #98989f;\n }\n\n .md-a {\n color: #4c9bff;\n }\n\n .md-code {\n background-color: rgba(120, 120, 128, 0.32);\n }\n\n .md-pre {\n background-color: #1c1c1e;\n }\n\n .md-blockquote,\n .md-table,\n .md-cell-body {\n border-color: #38383a;\n }\n\n .md-hr {\n background-color: #38383a;\n }\n}\n"] }]
481
+ }], propDecorators: { source: [{ type: i0.Input, args: [{ isSignal: true, alias: "source", required: false }] }], tokens: [{ type: i0.Input, args: [{ isSignal: true, alias: "tokens", required: false }] }], streaming: [{ type: i0.Input, args: [{ isSignal: true, alias: "streaming", required: false }] }], classes: [{ type: i0.Input, args: [{ isSignal: true, alias: "classes", required: false }] }], linkPress: [{ type: i0.Output, args: ["linkPress"] }] } });
338
482
  //# sourceMappingURL=markdown.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"markdown.js","sourceRoot":"","sources":["../src/markdown.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Markdown, drawn as native views and text.\n *\n * Its own entry point because it parses with `marked`, an optional peer dependency an app installs\n * only when it renders Markdown: the barrel never imports it, so an app that does not is not asked\n * for it.\n *\n * ```ts\n * import { Markdown } from '@ng-native/components/markdown';\n * ```\n */\nimport { Component, Injector, computed, inject, input, linkedSignal, output } from '@angular/core';\nimport { DeepLinks } from '@ng-native/device';\nimport { nativePlatform } from '@ng-native/fabric';\nimport { lexer, type Token } from 'marked';\nimport type { ImageLoadEvent } from './events.ts';\nimport { Image } from './image.ts';\nimport {\n markdownBlocks,\n opensOnPress,\n type MarkdownBlock,\n type MarkdownElement,\n type MarkdownInline,\n} from './markdown-blocks.ts';\nimport { ScrollView } from './scroll-view.ts';\nimport { TemplateSlot } from './template-slot.ts';\nimport { Text } from './text.ts';\nimport { View } from './view.ts';\n\nexport type { MarkdownElement } from './markdown-blocks.ts';\n\n/** Classes for the elements a document is drawn with, each one in place of the default. */\nexport type MarkdownClasses = Partial<Readonly<Record<MarkdownElement, string>>>;\n\n/** `(linkPress)`: a link was pressed. */\nexport interface MarkdownLinkPress {\n /** The target as written, after its entities are decoded and its scheme was allowed. */\n readonly href: string;\n readonly title: string | null;\n /** Keep the link from being opened, to handle it in the app instead. */\n preventDefault(): void;\n}\n\nconst ELEMENTS: readonly MarkdownElement[] = [\n ...['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'blockquote', 'ul', 'ol', 'li', 'marker'],\n ...['pre', 'code', 'strong', 'em', 'del', 'a', 'hr', 'img', 'table', 'tr', 'th', 'td'],\n] as MarkdownElement[];\n\nconst DEFAULT_CLASSES = Object.fromEntries(\n ELEMENTS.map((element) => [element, `md-${element}`]),\n) as Readonly<Record<MarkdownElement, string>>;\n\ntype MarkdownLink = Extract<MarkdownInline, { kind: 'link' }>;\n\n/** Every image address in `blocks`, however deep in quotes and lists. */\nfunction imageSources(blocks: readonly MarkdownBlock[], into = new Set<string>()): Set<string> {\n for (const block of blocks) {\n if (block.kind === 'image') into.add(block.src);\n else if (block.kind === 'blockquote') imageSources(block.children, into);\n else if (block.kind === 'list') {\n for (const item of block.items) imageSources(item.children, into);\n }\n }\n return into;\n}\n\n/**\n * A Markdown document as `<text>`, `<view>`, `<image>` and `<scroll-view>`: headings, paragraphs,\n * emphasis, inline code, links, quotes, ordered, unordered and task lists, code blocks, rules,\n * tables and images.\n *\n * `source` is parsed with `marked.lexer`; `tokens` takes tokens lexed already, and wins when both\n * are set. A paragraph is one `<text>` with a nested `<text>` for each emphasis, code span and\n * link, so it wraps as one run; an image is lifted out to a block of its own.\n *\n * Nothing in the document is interpreted. Raw HTML is drawn as the text it is written as, a link\n * is followed only to `http`, `https`, `mailto`, `tel` or a relative target, and an image is\n * loaded only over `http` or `https`; anything else is drawn as its text.\n *\n * Each element carries a default class, `md-h1`, `md-p`, `md-a` and so on, styled in light and\n * dark. `classes` replaces one by its element's name, so a global stylesheet or Tailwind styles it\n * instead: `{ h1: 'text-3xl font-bold' }`. Put the default back beside your own to keep both.\n *\n * A pressed link is `(linkPress)`. An absolute target is then opened with `DeepLinks.open` unless\n * a handler calls `preventDefault()`; a relative one is only reported, for the app to route.\n */\n@Component({\n selector: 'markdown',\n imports: [Image, ScrollView, TemplateSlot, Text, View],\n template: `\n <ng-template #blockTemplate let-list>\n @for (block of list; track $index) {\n @switch (block.kind) {\n @case ('heading') {\n <text [class]=\"classFor(block.element)\" accessibilityRole=\"header\"\n ><ng-container *templateSlot=\"inlineTemplate; context: { $implicit: block.children }\"\n /></text>\n }\n @case ('paragraph') {\n <text [class]=\"classFor('p')\"\n ><ng-container *templateSlot=\"inlineTemplate; context: { $implicit: block.children }\"\n /></text>\n }\n @case ('image') {\n <image\n [src]=\"block.src\"\n [alt]=\"block.alt || undefined\"\n resizeMode=\"contain\"\n [class]=\"classFor('img')\"\n [style.aspectRatio]=\"ratios().get(block.src) ?? null\"\n [style.height]=\"ratios().has(block.src) ? 'auto' : null\"\n (load)=\"sizeImage(block.src, $any($event))\"\n />\n }\n @case ('blockquote') {\n <view [class]=\"classFor('blockquote')\">\n <ng-container *templateSlot=\"blockTemplate; context: { $implicit: block.children }\" />\n </view>\n }\n @case ('list') {\n <view [class]=\"classFor(block.element)\">\n @for (item of block.items; track $index) {\n <view [class]=\"classFor('li')\">\n <text [class]=\"classFor('marker')\">{{ item.marker }}</text>\n <view class=\"md-li-body\">\n <ng-container\n *templateSlot=\"blockTemplate; context: { $implicit: item.children }\"\n />\n </view>\n </view>\n }\n </view>\n }\n @case ('code') {\n <scroll-view horizontal [class]=\"classFor('pre')\">\n <text selectable class=\"md-pre-text\">{{ block.text }}</text>\n </scroll-view>\n }\n @case ('hr') {\n <view [class]=\"classFor('hr')\"></view>\n }\n @case ('table') {\n <view [class]=\"classFor('table')\">\n <view [class]=\"classFor('tr')\">\n @for (cell of block.header; track $index) {\n <text [class]=\"classFor('th')\"\n ><ng-container *templateSlot=\"inlineTemplate; context: { $implicit: cell }\"\n /></text>\n }\n </view>\n @for (row of block.rows; track $index) {\n <view [class]=\"classFor('tr')\">\n @for (cell of row; track $index) {\n <text [class]=\"classFor('td')\"\n ><ng-container *templateSlot=\"inlineTemplate; context: { $implicit: cell }\"\n /></text>\n }\n </view>\n }\n </view>\n }\n }\n }\n </ng-template>\n\n <ng-template #inlineTemplate let-runs>\n @for (run of runs; track $index) {\n @switch (run.kind) {\n @case ('text') {\n <ng-container>{{ run.text }}</ng-container>\n }\n @case ('code') {\n <text [class]=\"classFor('code')\">{{ run.text }}</text>\n }\n @case ('link') {\n <text\n [class]=\"classFor('a')\"\n pressable\n [accessibilityHint]=\"run.title ?? undefined\"\n (press)=\"followLink(run)\"\n ><ng-container *templateSlot=\"inlineTemplate; context: { $implicit: run.children }\"\n /></text>\n }\n @default {\n <text [class]=\"classFor(run.kind)\"\n ><ng-container *templateSlot=\"inlineTemplate; context: { $implicit: run.children }\"\n /></text>\n }\n }\n }\n </ng-template>\n\n <ng-container *templateSlot=\"blockTemplate; context: { $implicit: blocks() }\" />\n `,\n styleUrl: './markdown.css',\n host: {\n '[class.md-ios]': \"platform === 'ios'\",\n '[class.md-android]': \"platform === 'android'\",\n },\n})\nexport class Markdown {\n private readonly injector = inject(Injector);\n /** Which monospace font code is drawn in, as the platform names it. */\n protected readonly platform = nativePlatform();\n\n /** The Markdown to draw. */\n readonly source = input<string>();\n /** Tokens `marked.lexer` made already, drawn in place of `source`. */\n readonly tokens = input<readonly Token[]>();\n /** A class for an element, in place of its default. See the class note. */\n readonly classes = input<MarkdownClasses>({});\n /** A link was pressed. See the class note. */\n readonly linkPress = output<MarkdownLinkPress>();\n\n protected readonly blocks = computed(() =>\n markdownBlocks(this.tokens() ?? lexer(this.source() ?? '')),\n );\n\n private readonly classOf = computed(() => ({ ...DEFAULT_CLASSES, ...this.classes() }));\n\n /**\n * Each image's width over its height, once it has loaded, so it is drawn at the page's width and\n * its own shape. An image the document no longer has is forgotten when the document changes.\n */\n protected readonly ratios = linkedSignal<readonly MarkdownBlock[], ReadonlyMap<string, number>>({\n source: this.blocks,\n computation: (blocks, previous) => {\n const kept = new Map<string, number>();\n if (!previous) return kept;\n const present = imageSources(blocks);\n for (const [src, ratio] of previous.value) if (present.has(src)) kept.set(src, ratio);\n return kept;\n },\n });\n\n protected classFor(element: MarkdownElement): string {\n return this.classOf()[element];\n }\n\n protected sizeImage(src: string, event: Partial<ImageLoadEvent>): void {\n const source = event.nativeEvent?.source;\n if (!source) return;\n const { width, height } = source;\n if (!(width > 0 && height > 0)) return;\n this.ratios.update((ratios) => new Map(ratios).set(src, width / height));\n }\n\n protected followLink(link: MarkdownLink): void {\n let prevented = false;\n this.linkPress.emit({\n href: link.href,\n title: link.title,\n preventDefault: () => {\n prevented = true;\n },\n });\n if (!prevented && opensOnPress(link.href)) this.injector.get(DeepLinks).open(link.href);\n }\n}\n"]}
1
+ {"version":3,"file":"markdown.js","sourceRoot":"","sources":["../src/markdown.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,EACL,SAAS,EACT,QAAQ,EACR,gBAAgB,EAChB,QAAQ,EACR,MAAM,EACN,KAAK,EACL,YAAY,EACZ,MAAM,GACP,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAC9C,OAAO,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AACnD,OAAO,EAAE,KAAK,EAAc,MAAM,QAAQ,CAAC;AAE3C,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AACnC,OAAO,EACL,QAAQ,EACR,cAAc,EACd,YAAY,EACZ,WAAW,GAIZ,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AACxD,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;;AAgBjC,MAAM,QAAQ,GAA+B;IAC3C,GAAG,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,GAAG,EAAE,YAAY,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,QAAQ,CAAC;IACtF,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,EAAE,IAAI,EAAE,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC;CAClE,CAAC;AAEvB,MAAM,eAAe,GAAG,MAAM,CAAC,WAAW,CACxC,QAAQ,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,OAAO,EAAE,MAAM,OAAO,EAAE,CAAC,CAAC,CACT,CAAC;AAY/C,iEAAiE;AACjE,SAAS,WAAW,CAClB,MAAgC,EAChC,OAAO,IAAI,GAAG,EAAc;IAE5B,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;QAC3B,IAAI,KAAK,CAAC,IAAI,KAAK,OAAO;YAAE,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;aACvC,IAAI,KAAK,CAAC,IAAI,KAAK,YAAY;YAAE,WAAW,CAAC,KAAK,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;aACnE,IAAI,KAAK,CAAC,IAAI,KAAK,MAAM,EAAE,CAAC;YAC/B,KAAK,MAAM,IAAI,IAAI,KAAK,CAAC,KAAK;gBAAE,WAAW,CAAC,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;QACnE,CAAC;IACH,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAoIH,MAAM,OAAO,QAAQ;IACF,QAAQ,GAAG,MAAM,CAAC,QAAQ,CAAC,CAAC;IAC7C,uEAAuE;IACpD,QAAQ,GAAG,cAAc,EAAE,CAAC;IAE/C,4BAA4B;IACnB,MAAM,GAAG,KAAK;0FAAU,CAAC;IAClC,sEAAsE;IAC7D,MAAM,GAAG,KAAK;0FAAoB,CAAC;IAC5C;;;;OAIG;IACM,SAAS,GAAG,KAAK,CAAC,KAAK,iFAAI,SAAS,EAAE,gBAAgB,GAAG,CAAC;IACnE,2EAA2E;IAClE,OAAO,GAAG,KAAK,CAAkB,EAAE;gFAAC,CAAC;IAC9C,8CAA8C;IACrC,SAAS,GAAG,MAAM,EAAqB,CAAC;IAEhC,MAAM,GAAG,IAAI,cAAc,EAAE,CAAC;IAC5B,QAAQ,GAAG,QAAQ,CAAC;IAEvC;;;;OAIG;IACc,QAAQ,GAAG,IAAI,OAAO,EAAmC,CAAC;IAE3E,+FAA+F;IAC5E,MAAM,GAAG,YAAY,6EACtC,MAAM,EAAE,GAAG,EAAE;YACX,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC;YAC7B,IAAI,MAAM;gBAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC;YAC/C,MAAM,QAAQ,GAAG,IAAI,CAAC,SAAS,EAAE,CAAC;YAClC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,MAAM,EAAE,IAAI,EAAE,EAAE,QAAQ,CAAC,EAAE,QAAQ,EAAE,CAAC;QACvE,CAAC;QACD,WAAW,EAAE,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAE,EAAE,QAA8C,EAAE,EAAE,CACpF,QAAQ;YACN,CAAC,CAAC,IAAI,CAAC,cAAc,CAAC,MAAM,EAAE,QAAQ,EAAE,KAAK,CAAC;YAC9C,CAAC,CAAC,WAAW,CAAC,QAAQ,EAAE,KAAK,EAAE,cAAc,CAAC,MAAM,CAAC,CAAC,GAC1D,CAAC;IAEc,OAAO,GAAG,QAAQ,CAAC,GAAG,EAAE,CAAC,CAAC,EAAE,GAAG,eAAe,EAAE,GAAG,IAAI,CAAC,OAAO,EAAE,EAAE,CAAC;gFAAC,CAAC;IAEvF;;;;;OAKG;IACgB,KAAK,GAAG,YAAY,4EAEnC,MAAM,EAAE,IAAI,CAAC,MAAM;QACnB,WAAW,EAAE,CAAC,MAAM,EAAE,QAAQ,EAAE,EAAE;YAChC,MAAM,IAAI,GAAG,IAAI,GAAG,EAAqB,CAAC;YAC1C,IAAI,CAAC,QAAQ;gBAAE,OAAO,IAAI,CAAC;YAC3B,MAAM,OAAO,GAAG,IAAI,GAAG,CAAC,CAAC,GAAG,WAAW,CAAC,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC;YAC5E,KAAK,MAAM,CAAC,GAAG,EAAE,IAAI,CAAC,IAAI,QAAQ,CAAC,KAAK;gBAAE,IAAI,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC;oBAAE,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;YACpF,OAAO,IAAI,CAAC;QACd,CAAC,GAEJ,CAAC;IAEF;;;;;OAKG;IACgB,QAAQ,GAAG,YAAY,+EAIxC,MAAM,EAAE,IAAI,CAAC,MAAM;QACnB,WAAW,EAAE,CAAC,MAAM,EAAE,QAAQ,EAAE,EAAE;YAChC,MAAM,IAAI,GAAG,IAAI,GAAG,EAAsB,CAAC;YAC3C,IAAI,CAAC,QAAQ;gBAAE,OAAO,IAAI,CAAC;YAC3B,MAAM,OAAO,GAAG,WAAW,CAAC,MAAM,CAAC,CAAC;YACpC,KAAK,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,IAAI,QAAQ,CAAC,KAAK;gBAAE,IAAI,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC;oBAAE,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;YAC5F,OAAO,IAAI,CAAC;QACd,CAAC,GACD,CAAC;IAEK,GAAG,CAAC,MAAc,EAAE,SAAkB;QAC5C,OAAO,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;IAC7D,CAAC;IAED,6FAA6F;IACrF,cAAc,CACpB,MAAwB,EACxB,QAA8C;QAE9C,MAAM,IAAI,GAAoB,EAAE,CAAC;QACjC,IAAI,EAAE,GAAG,CAAC,CAAC;QACX,OAAO,EAAE,GAAG,MAAM,CAAC,MAAM,EAAE,EAAE,EAAE,EAAE,CAAC;YAChC,MAAM,MAAM,GAAG,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAE,CAAC,CAAC;YAC9C,IAAI,CAAC,MAAM;gBAAE,MAAM;YACnB,IAAI,CAAC,IAAI,CAAC,GAAG,MAAM,CAAC,CAAC;QACvB,CAAC;QACD,MAAM,IAAI,GAAG,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;QAC9B,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,cAAc,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;QAC/D,mFAAmF;QACnF,MAAM,KAAK,GAAG,CAAC,IAAI,CAAC,MAAM,IAAI,QAAQ,EAAE,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,KAAK,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;QAC1E,MAAM,GAAG,GAAG,KAAK,CAAC,CAAC,CAAC,QAAQ,EAAE,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;QAC7D,MAAM,MAAM,GAAG,WAAW,CAAC,GAAG,EAAE,SAAS,CAAC,IAAI,EAAE,CAAC,CAAC;QAClD,IAAI,IAAI,GAAG,CAAC,CAAC;QACb,IAAI,CAAC,OAAO,CAAC,CAAC,KAAK,EAAE,CAAC,EAAE,EAAE;YACxB,MAAM,KAAK,GAAG,SAAS,CAAC,CAAC,CAAE,CAAC,MAAM,CAAC;YACnC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,KAAK,EAAE,MAAM,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,IAAI,IAAI,KAAK,CAAC,CAAC,CAAC,CAAC;QAChE,CAAC,CAAC,CAAC;QACH,OAAO,CAAC,GAAG,IAAI,EAAE,GAAG,MAAM,CAAC,CAAC;IAC9B,CAAC;IAES,QAAQ,CAAC,OAAwB;QACzC,OAAO,IAAI,CAAC,OAAO,EAAE,CAAC,OAAO,CAAC,CAAC;IACjC,CAAC;IAES,UAAU,CAAC,GAAW;QAC9B,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QACnC,OAAO,IAAI,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,KAAK,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC;IACzC,CAAC;IAED,6FAA6F;IACnF,WAAW,CAAC,KAAiB;QACrC,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,EAAE,CAAC,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QACzC,IAAI,CAAC,IAAI;YAAE,OAAO,IAAI,CAAC;QACvB,MAAM,KAAK,GAAG,IAAI,CAAC,QAAQ,EAAE,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;QACzC,OAAO,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC;IAC3D,CAAC;IAES,WAAW,CAAC,KAAiB,EAAE,KAA2B;QAClE,MAAM,KAAK,GAAG,KAAK,CAAC,WAAW,EAAE,MAAM,EAAE,KAAK,CAAC;QAC/C,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,EAAE,CAAC,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QACzC,IAAI,KAAK,KAAK,SAAS,IAAI,CAAC,IAAI;YAAE,OAAO;QACzC,MAAM,QAAQ,GAAG,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC;QACxD,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC,GAAG,CAAC,KAAK,CAAC,KAAK,QAAQ;YAAE,OAAO;QACpD,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,EAAE;YAC3B,MAAM,IAAI,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,CAAC;YAC1B,IAAI,QAAQ,KAAK,SAAS;gBAAE,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;;gBAC1C,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,QAAQ,CAAC,CAAC;YAC/B,OAAO,IAAI,CAAC;QACd,CAAC,CAAC,CAAC;IACL,CAAC;IAES,SAAS,CAAC,GAAW,EAAE,KAA8B;QAC7D,MAAM,MAAM,GAAG,KAAK,CAAC,WAAW,EAAE,MAAM,CAAC;QACzC,IAAI,CAAC,MAAM;YAAE,OAAO;QACpB,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,MAAM,CAAC;QACjC,IAAI,CAAC,CAAC,KAAK,GAAG,CAAC,IAAI,MAAM,GAAG,CAAC,CAAC;YAAE,OAAO;QACvC,4FAA4F;QAC5F,kBAAkB;QAClB,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,IAAI,GAAG,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,GAAG,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,GAAG,MAAM,EAAE,CAAC,CAAC,CAAC;IAC1F,CAAC;IAES,UAAU,CAAC,IAAkB;QACrC,IAAI,SAAS,GAAG,KAAK,CAAC;QACtB,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC;YAClB,IAAI,EAAE,IAAI,CAAC,IAAI;YACf,KAAK,EAAE,IAAI,CAAC,KAAK;YACjB,cAAc,EAAE,GAAG,EAAE;gBACnB,SAAS,GAAG,IAAI,CAAC;YACnB,CAAC;SACF,CAAC,CAAC;QACH,IAAI,CAAC,SAAS,IAAI,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC;YAAE,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC1F,CAAC;uGAtKU,QAAQ;2FAAR,QAAQ,2tBAhIT;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyHT,6uFA1HS,KAAK,mTAAE,eAAe,0HAAE,UAAU,0TAAE,YAAY,4GAAE,IAAI,mYAAE,IAAI;;2FAiI3D,QAAQ;kBAnIpB,SAAS;+BACE,UAAU,WACX,CAAC,KAAK,EAAE,eAAe,EAAE,UAAU,EAAE,YAAY,EAAE,IAAI,EAAE,IAAI,CAAC,YAC7D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyHT,QAEK;wBACJ,gBAAgB,EAAE,oBAAoB;wBACtC,oBAAoB,EAAE,wBAAwB;qBAC/C","sourcesContent":["/**\n * Markdown, drawn as native views and text.\n *\n * Its own entry point because it parses with `marked`, an optional peer dependency an app installs\n * only when it renders Markdown: the barrel never imports it, so an app that does not is not asked\n * for it.\n *\n * ```ts\n * import { Markdown } from '@ng-native/components/markdown';\n * ```\n */\nimport {\n Component,\n Injector,\n booleanAttribute,\n computed,\n inject,\n input,\n linkedSignal,\n output,\n} from '@angular/core';\nimport { DeepLinks } from '@ng-native/device';\nimport { nativePlatform } from '@ng-native/fabric';\nimport { lexer, type Token } from 'marked';\nimport type { ImageLoadEvent, LayoutEvent } from './events.ts';\nimport { Image } from './image.ts';\nimport {\n blockKey,\n markdownBlocks,\n opensOnPress,\n shareBlocks,\n type MarkdownBlock,\n type MarkdownElement,\n type MarkdownInline,\n} from './markdown-blocks.ts';\nimport { MarkdownColumns } from './markdown-columns.ts';\nimport { MarkdownStream } from './markdown-stream.ts';\nimport { ScrollView } from './scroll-view.ts';\nimport { TemplateSlot } from './template-slot.ts';\nimport { Text } from './text.ts';\nimport { View } from './view.ts';\n\nexport type { MarkdownElement } from './markdown-blocks.ts';\n\n/** Classes for the elements a document is drawn with, each one in place of the default. */\nexport type MarkdownClasses = Partial<Readonly<Record<MarkdownElement, string>>>;\n\n/** `(linkPress)`: a link was pressed. */\nexport interface MarkdownLinkPress {\n /** The target as written, after its entities are decoded and its scheme was allowed. */\n readonly href: string;\n readonly title: string | null;\n /** Keep the link from being opened, to handle it in the app instead. */\n preventDefault(): void;\n}\n\nconst ELEMENTS: readonly MarkdownElement[] = [\n ...['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'blockquote', 'ul', 'ol', 'li', 'marker'],\n ...['pre', 'code', 'strong', 'em', 'del', 'a', 'hr', 'img', 'table', 'tr', 'th', 'td'],\n] as MarkdownElement[];\n\nconst DEFAULT_CLASSES = Object.fromEntries(\n ELEMENTS.map((element) => [element, `md-${element}`]),\n) as Readonly<Record<MarkdownElement, string>>;\n\ntype MarkdownLink = Extract<MarkdownInline, { kind: 'link' }>;\n\n/** A loaded image's own width in points, and its width over its height. */\ninterface ImageSize {\n readonly width: number;\n readonly ratio: number;\n}\n\ntype ImageBlock = Extract<MarkdownBlock, { kind: 'image' }>;\n\n/** Every image in `blocks`, however deep in quotes and lists. */\nfunction imageBlocks(\n blocks: readonly MarkdownBlock[],\n into = new Set<ImageBlock>(),\n): Set<ImageBlock> {\n for (const block of blocks) {\n if (block.kind === 'image') into.add(block);\n else if (block.kind === 'blockquote') imageBlocks(block.children, into);\n else if (block.kind === 'list') {\n for (const item of block.items) imageBlocks(item.children, into);\n }\n }\n return into;\n}\n\n/**\n * A Markdown document as `<text>`, `<view>`, `<image>` and `<scroll-view>`: headings, paragraphs,\n * emphasis, inline code, links, quotes, ordered, unordered and task lists, code blocks, rules,\n * tables and images.\n *\n * `source` is parsed with `marked.lexer`; `tokens` takes tokens lexed already, and wins when both\n * are set. A paragraph is one `<text>` with a nested `<text>` for each emphasis, code span and\n * link, so it wraps as one run; an image is lifted out to a block of its own.\n *\n * Nothing in the document is interpreted. Raw HTML is drawn as the text it is written as, a link\n * is followed only to `http`, `https`, `mailto`, `tel` or a relative target, and an image is\n * loaded only over `http` or `https`; anything else is drawn as its text.\n *\n * Each element carries a default class, `md-h1`, `md-p`, `md-a` and so on, styled in light and\n * dark. `classes` replaces one by its element's name, so a global stylesheet or Tailwind styles it\n * instead: `{ h1: 'text-3xl font-bold' }`. Put the default back beside your own to keep both.\n *\n * For a source that grows as it streams in, an answer arriving a few words at a time, set\n * `streaming` until it is complete. The blocks before the last one are then kept as they are and\n * only the last is lexed and converted again, unless a chunk defines or changes a link reference,\n * which can change a block already kept, and the whole source is lexed again. Syntax still open at its end, a code span, an emphasis, a link,\n * an image or a table row, is held back until it closes. Of a code fence only an unfinished\n * opening line is held back, and its code then streams as it arrives. Clearing `streaming` draws\n * the whole source as written, and `tokens` are always drawn as given. A block that did not\n * change keeps its views, streaming or not.\n *\n * A pressed link is `(linkPress)`. An absolute target is then opened with `DeepLinks.open` unless\n * a handler calls `preventDefault()`; a relative one is only reported, for the app to route.\n */\n@Component({\n selector: 'markdown',\n imports: [Image, MarkdownColumns, ScrollView, TemplateSlot, Text, View],\n template: `\n <ng-template #blockTemplate let-list>\n @for (block of list; track blockKey(block)) {\n @switch (block.kind) {\n @case ('heading') {\n <text [class]=\"classFor(block.element)\" accessibilityRole=\"header\"\n ><ng-container *templateSlot=\"inlineTemplate; context: { $implicit: block.children }\"\n /></text>\n }\n @case ('paragraph') {\n <text [class]=\"classFor('p')\"\n ><ng-container *templateSlot=\"inlineTemplate; context: { $implicit: block.children }\"\n /></text>\n }\n @case ('image') {\n <image\n [src]=\"block.src\"\n [alt]=\"block.alt || undefined\"\n resizeMode=\"contain\"\n [class]=\"classFor('img')\"\n [style.--md-img-width]=\"imageWidth(block.src)\"\n [style.aspectRatio]=\"sizes().get(block.src)?.ratio ?? null\"\n [style.height]=\"imageHeight(block)\"\n (load)=\"sizeImage(block.src, $any($event))\"\n (layout)=\"layOutImage(block, $any($event))\"\n />\n }\n @case ('blockquote') {\n <view [class]=\"classFor('blockquote')\">\n <ng-container *templateSlot=\"blockTemplate; context: { $implicit: block.children }\" />\n </view>\n }\n @case ('list') {\n <view [class]=\"classFor(block.element)\">\n @for (item of block.items; track $index) {\n <view [class]=\"classFor('li')\">\n <text [class]=\"classFor('marker')\">{{ item.marker }}</text>\n <view class=\"md-li-body\">\n <ng-container\n *templateSlot=\"blockTemplate; context: { $implicit: item.children }\"\n />\n </view>\n </view>\n }\n </view>\n }\n @case ('code') {\n <scroll-view horizontal [class]=\"classFor('pre')\">\n <text selectable class=\"md-pre-text\">{{ block.text }}</text>\n </scroll-view>\n }\n @case ('hr') {\n <view [class]=\"classFor('hr')\"></view>\n }\n @case ('table') {\n <scroll-view horizontal>\n <view\n [class]=\"classFor('table')\"\n [markdownColumns]=\"block\"\n #columns=\"markdownColumns\"\n >\n <view [class]=\"classFor('tr')\">\n @for (cell of block.header; track $index; let column = $index) {\n <view class=\"md-cell\" [style.minWidth]=\"columns.width(column)\">\n <text\n [class]=\"classFor('th')\"\n (layout)=\"columns.measure(0, column, $any($event))\"\n ><ng-container *templateSlot=\"inlineTemplate; context: { $implicit: cell }\"\n /></text>\n </view>\n }\n </view>\n @for (row of block.rows; track $index; let r = $index) {\n <view [class]=\"classFor('tr')\">\n @for (cell of row; track $index; let column = $index) {\n <view class=\"md-cell md-cell-body\" [style.minWidth]=\"columns.width(column)\">\n <text\n [class]=\"classFor('td')\"\n (layout)=\"columns.measure(r + 1, column, $any($event))\"\n ><ng-container\n *templateSlot=\"inlineTemplate; context: { $implicit: cell }\"\n /></text>\n </view>\n }\n </view>\n }\n </view>\n </scroll-view>\n }\n }\n }\n </ng-template>\n\n <ng-template #inlineTemplate let-runs>\n @for (run of runs; track $index) {\n @switch (run.kind) {\n @case ('text') {\n <ng-container>{{ run.text }}</ng-container>\n }\n @case ('code') {\n <text [class]=\"classFor('code')\">{{ run.text }}</text>\n }\n @case ('link') {\n <text\n [class]=\"classFor('a')\"\n pressable\n [accessibilityHint]=\"run.title ?? undefined\"\n (press)=\"followLink(run)\"\n ><ng-container *templateSlot=\"inlineTemplate; context: { $implicit: run.children }\"\n /></text>\n }\n @default {\n <text [class]=\"classFor(run.kind)\"\n ><ng-container *templateSlot=\"inlineTemplate; context: { $implicit: run.children }\"\n /></text>\n }\n }\n }\n </ng-template>\n\n <ng-container *templateSlot=\"blockTemplate; context: { $implicit: blocks() }\" />\n `,\n styleUrl: './markdown.css',\n host: {\n '[class.md-ios]': \"platform === 'ios'\",\n '[class.md-android]': \"platform === 'android'\",\n },\n})\nexport class Markdown {\n private readonly injector = inject(Injector);\n /** Which monospace font code is drawn in, as the platform names it. */\n protected readonly platform = nativePlatform();\n\n /** The Markdown to draw. */\n readonly source = input<string>();\n /** Tokens `marked.lexer` made already, drawn in place of `source`. */\n readonly tokens = input<readonly Token[]>();\n /**\n * Whether `source` is still arriving, a chunk at a time. While it is, only its last block is\n * lexed again, unless a link reference changes, and syntax still open at its end is held back.\n * See the class note.\n */\n readonly streaming = input(false, { transform: booleanAttribute });\n /** A class for an element, in place of its default. See the class note. */\n readonly classes = input<MarkdownClasses>({});\n /** A link was pressed. See the class note. */\n readonly linkPress = output<MarkdownLinkPress>();\n\n private readonly stream = new MarkdownStream();\n protected readonly blockKey = blockKey;\n\n /**\n * The blocks each token the stream kept was drawn as. A kept token is the same object from one\n * chunk to the next, so its blocks are not converted or compared again; a source lexed whole\n * makes new tokens, and so new blocks.\n */\n private readonly streamed = new WeakMap<Token, readonly MarkdownBlock[]>();\n\n /** The document's blocks, with each one that did not change kept, so it is not drawn again. */\n protected readonly blocks = linkedSignal({\n source: () => {\n const tokens = this.tokens();\n if (tokens) return { tokens, streamed: false };\n const streamed = this.streaming();\n return { tokens: this.lex(this.source() ?? '', streamed), streamed };\n },\n computation: ({ tokens, streamed }, previous?: { value: readonly MarkdownBlock[] }) =>\n streamed\n ? this.streamedBlocks(tokens, previous?.value)\n : shareBlocks(previous?.value, markdownBlocks(tokens)),\n });\n\n private readonly classOf = computed(() => ({ ...DEFAULT_CLASSES, ...this.classes() }));\n\n /**\n * Each image's own width and its width over its height, once it has loaded, so it is drawn at its\n * own size and shape, as a browser draws it, and no wider than the document. The width reaches\n * the image as `--md-img-width`, which `.md-img` reads, so a class of the app's own still sets\n * the width. An image the document no longer has is forgotten when the document changes.\n */\n protected readonly sizes = linkedSignal<readonly MarkdownBlock[], ReadonlyMap<string, ImageSize>>(\n {\n source: this.blocks,\n computation: (blocks, previous) => {\n const kept = new Map<string, ImageSize>();\n if (!previous) return kept;\n const present = new Set([...imageBlocks(blocks)].map((block) => block.src));\n for (const [src, size] of previous.value) if (present.has(src)) kept.set(src, size);\n return kept;\n },\n },\n );\n\n /**\n * The width each image is laid out at where that is less than its own, narrowed by\n * `max-width`. Yoga takes an image's height from its width before `max-width` narrows it, where\n * CSS takes it after, so a narrowed image is given the height its ratio makes of the width it\n * has. An image no longer in the document is forgotten when the document changes.\n */\n protected readonly narrowed = linkedSignal<\n readonly MarkdownBlock[],\n ReadonlyMap<ImageBlock, number>\n >({\n source: this.blocks,\n computation: (blocks, previous) => {\n const kept = new Map<ImageBlock, number>();\n if (!previous) return kept;\n const present = imageBlocks(blocks);\n for (const [block, width] of previous.value) if (present.has(block)) kept.set(block, width);\n return kept;\n },\n });\n\n private lex(source: string, streaming: boolean): readonly Token[] {\n return streaming ? this.stream.lex(source) : lexer(source);\n }\n\n /** `tokens`' blocks, converting and sharing only those of tokens the stream had not kept. */\n private streamedBlocks(\n tokens: readonly Token[],\n previous: readonly MarkdownBlock[] | undefined,\n ): readonly MarkdownBlock[] {\n const kept: MarkdownBlock[] = [];\n let at = 0;\n for (; at < tokens.length; at++) {\n const blocks = this.streamed.get(tokens[at]!);\n if (!blocks) break;\n kept.push(...blocks);\n }\n const rest = tokens.slice(at);\n const converted = rest.map((token) => markdownBlocks([token]));\n // The blocks drawn after the kept ones, when those are still the ones drawn first.\n const drawn = !kept.length || previous?.[kept.length - 1] === kept.at(-1);\n const was = drawn ? previous?.slice(kept.length) : undefined;\n const shared = shareBlocks(was, converted.flat());\n let from = 0;\n rest.forEach((token, i) => {\n const count = converted[i]!.length;\n this.streamed.set(token, shared.slice(from, (from += count)));\n });\n return [...kept, ...shared];\n }\n\n protected classFor(element: MarkdownElement): string {\n return this.classOf()[element];\n }\n\n protected imageWidth(src: string): string | null {\n const size = this.sizes().get(src);\n return size ? `${size.width}px` : null;\n }\n\n /** The class's height until the image loads, then its ratio's, or a narrowed image's own. */\n protected imageHeight(block: ImageBlock): number | 'auto' | null {\n const size = this.sizes().get(block.src);\n if (!size) return null;\n const width = this.narrowed().get(block);\n return width === undefined ? 'auto' : width / size.ratio;\n }\n\n protected layOutImage(block: ImageBlock, event: Partial<LayoutEvent>): void {\n const width = event.nativeEvent?.layout?.width;\n const size = this.sizes().get(block.src);\n if (width === undefined || !size) return;\n const narrowed = width < size.width ? width : undefined;\n if (this.narrowed().get(block) === narrowed) return;\n this.narrowed.update((all) => {\n const next = new Map(all);\n if (narrowed === undefined) next.delete(block);\n else next.set(block, narrowed);\n return next;\n });\n }\n\n protected sizeImage(src: string, event: Partial<ImageLoadEvent>): void {\n const source = event.nativeEvent?.source;\n if (!source) return;\n const { width, height } = source;\n if (!(width > 0 && height > 0)) return;\n // A url's or a data url's scale is 1, so the load event's pixels are the points a browser's\n // CSS pixels are.\n this.sizes.update((sizes) => new Map(sizes).set(src, { width, ratio: width / height }));\n }\n\n protected followLink(link: MarkdownLink): void {\n let prevented = false;\n this.linkPress.emit({\n href: link.href,\n title: link.title,\n preventDefault: () => {\n prevented = true;\n },\n });\n if (!prevented && opensOnPress(link.href)) this.injector.get(DeepLinks).open(link.href);\n }\n}\n"]}
@@ -74,6 +74,11 @@ export declare class TextInput extends ViewBase {
74
74
  readonly maxLength: import("@angular/core").InputSignalWithTransform<number | undefined, unknown>;
75
75
  /** Android: the height of a multiline field, in lines. */
76
76
  readonly numberOfLines: import("@angular/core").InputSignalWithTransform<number | undefined, unknown>;
77
+ /**
78
+ * How many lines of its `line-height` a multiline field is tall, where nothing sets its height,
79
+ * as a `<textarea rows>` is. Read by the engine, which sizes the field; never sent to native.
80
+ */
81
+ readonly rows: import("@angular/core").InputSignalWithTransform<number | undefined, unknown>;
77
82
  /** The selected range, or the caret when `start` equals `end`. */
78
83
  readonly selection: import("@angular/core").InputSignal<{
79
84
  readonly start: number;
@@ -164,5 +169,5 @@ export declare class TextInput extends ViewBase {
164
169
  isFocused(): boolean;
165
170
  protected onNativeChange(event: NativeSyntheticEvent<Partial<TextInputChangePayload>>): void;
166
171
  static ɵfac: i0.ɵɵFactoryDeclaration<TextInput, never>;
167
- static ɵdir: i0.ɵɵDirectiveDeclaration<TextInput, "text-input", ["textInput"], { "value": { "alias": "value"; "required": false; "isSignal": true; }; "disabled": { "alias": "disabled"; "required": false; "isSignal": true; }; "readonly": { "alias": "readonly"; "required": false; "isSignal": true; }; "invalid": { "alias": "invalid"; "required": false; "isSignal": true; }; "touched": { "alias": "touched"; "required": false; "isSignal": true; }; "multiline": { "alias": "multiline"; "required": false; "isSignal": true; }; "scrollEnabled": { "alias": "scrollEnabled"; "required": false; "isSignal": true; }; "editable": { "alias": "editable"; "required": false; "isSignal": true; }; "placeholder": { "alias": "placeholder"; "required": false; "isSignal": true; }; "placeholderTextColor": { "alias": "placeholderTextColor"; "required": false; "isSignal": true; }; "keyboardType": { "alias": "keyboardType"; "required": false; "isSignal": true; }; "returnKeyType": { "alias": "returnKeyType"; "required": false; "isSignal": true; }; "submitBehavior": { "alias": "submitBehavior"; "required": false; "isSignal": true; }; "secureTextEntry": { "alias": "secureTextEntry"; "required": false; "isSignal": true; }; "autoCapitalize": { "alias": "autoCapitalize"; "required": false; "isSignal": true; }; "autoCorrect": { "alias": "autoCorrect"; "required": false; "isSignal": true; }; "autoFocus": { "alias": "autoFocus"; "required": false; "isSignal": true; }; "maxLength": { "alias": "maxLength"; "required": false; "isSignal": true; }; "numberOfLines": { "alias": "numberOfLines"; "required": false; "isSignal": true; }; "selection": { "alias": "selection"; "required": false; "isSignal": true; }; "selectionColor": { "alias": "selectionColor"; "required": false; "isSignal": true; }; "selectTextOnFocus": { "alias": "selectTextOnFocus"; "required": false; "isSignal": true; }; "caretHidden": { "alias": "caretHidden"; "required": false; "isSignal": true; }; "contextMenuHidden": { "alias": "contextMenuHidden"; "required": false; "isSignal": true; }; "textAlign": { "alias": "textAlign"; "required": false; "isSignal": true; }; "allowFontScaling": { "alias": "allowFontScaling"; "required": false; "isSignal": true; }; "maxFontSizeMultiplier": { "alias": "maxFontSizeMultiplier"; "required": false; "isSignal": true; }; "inputAccessoryViewID": { "alias": "inputAccessoryViewID"; "required": false; "isSignal": true; }; "clearButtonMode": { "alias": "clearButtonMode"; "required": false; "isSignal": true; }; "clearTextOnFocus": { "alias": "clearTextOnFocus"; "required": false; "isSignal": true; }; "enablesReturnKeyAutomatically": { "alias": "enablesReturnKeyAutomatically"; "required": false; "isSignal": true; }; "keyboardAppearance": { "alias": "keyboardAppearance"; "required": false; "isSignal": true; }; "passwordRules": { "alias": "passwordRules"; "required": false; "isSignal": true; }; "spellCheck": { "alias": "spellCheck"; "required": false; "isSignal": true; }; "smartInsertDelete": { "alias": "smartInsertDelete"; "required": false; "isSignal": true; }; "dataDetectorTypes": { "alias": "dataDetectorTypes"; "required": false; "isSignal": true; }; "textContentType": { "alias": "textContentType"; "required": false; "isSignal": true; }; "autoComplete": { "alias": "autoComplete"; "required": false; "isSignal": true; }; "underlineColorAndroid": { "alias": "underlineColorAndroid"; "required": false; "isSignal": true; }; "cursorColor": { "alias": "cursorColor"; "required": false; "isSignal": true; }; "selectionHandleColor": { "alias": "selectionHandleColor"; "required": false; "isSignal": true; }; "textAlignVertical": { "alias": "textAlignVertical"; "required": false; "isSignal": true; }; "importantForAutofill": { "alias": "importantForAutofill"; "required": false; "isSignal": true; }; "showSoftInputOnFocus": { "alias": "showSoftInputOnFocus"; "required": false; "isSignal": true; }; "disableFullscreenUI": { "alias": "disableFullscreenUI"; "required": false; "isSignal": true; }; "inlineImageLeft": { "alias": "inlineImageLeft"; "required": false; "isSignal": true; }; "inlineImagePadding": { "alias": "inlineImagePadding"; "required": false; "isSignal": true; }; "textBreakStrategy": { "alias": "textBreakStrategy"; "required": false; "isSignal": true; }; }, { "value": "valueChange"; "changeText": "changeText"; "touch": "touch"; }, never, never, true, never>;
172
+ static ɵdir: i0.ɵɵDirectiveDeclaration<TextInput, "text-input", ["textInput"], { "value": { "alias": "value"; "required": false; "isSignal": true; }; "disabled": { "alias": "disabled"; "required": false; "isSignal": true; }; "readonly": { "alias": "readonly"; "required": false; "isSignal": true; }; "invalid": { "alias": "invalid"; "required": false; "isSignal": true; }; "touched": { "alias": "touched"; "required": false; "isSignal": true; }; "multiline": { "alias": "multiline"; "required": false; "isSignal": true; }; "scrollEnabled": { "alias": "scrollEnabled"; "required": false; "isSignal": true; }; "editable": { "alias": "editable"; "required": false; "isSignal": true; }; "placeholder": { "alias": "placeholder"; "required": false; "isSignal": true; }; "placeholderTextColor": { "alias": "placeholderTextColor"; "required": false; "isSignal": true; }; "keyboardType": { "alias": "keyboardType"; "required": false; "isSignal": true; }; "returnKeyType": { "alias": "returnKeyType"; "required": false; "isSignal": true; }; "submitBehavior": { "alias": "submitBehavior"; "required": false; "isSignal": true; }; "secureTextEntry": { "alias": "secureTextEntry"; "required": false; "isSignal": true; }; "autoCapitalize": { "alias": "autoCapitalize"; "required": false; "isSignal": true; }; "autoCorrect": { "alias": "autoCorrect"; "required": false; "isSignal": true; }; "autoFocus": { "alias": "autoFocus"; "required": false; "isSignal": true; }; "maxLength": { "alias": "maxLength"; "required": false; "isSignal": true; }; "numberOfLines": { "alias": "numberOfLines"; "required": false; "isSignal": true; }; "rows": { "alias": "rows"; "required": false; "isSignal": true; }; "selection": { "alias": "selection"; "required": false; "isSignal": true; }; "selectionColor": { "alias": "selectionColor"; "required": false; "isSignal": true; }; "selectTextOnFocus": { "alias": "selectTextOnFocus"; "required": false; "isSignal": true; }; "caretHidden": { "alias": "caretHidden"; "required": false; "isSignal": true; }; "contextMenuHidden": { "alias": "contextMenuHidden"; "required": false; "isSignal": true; }; "textAlign": { "alias": "textAlign"; "required": false; "isSignal": true; }; "allowFontScaling": { "alias": "allowFontScaling"; "required": false; "isSignal": true; }; "maxFontSizeMultiplier": { "alias": "maxFontSizeMultiplier"; "required": false; "isSignal": true; }; "inputAccessoryViewID": { "alias": "inputAccessoryViewID"; "required": false; "isSignal": true; }; "clearButtonMode": { "alias": "clearButtonMode"; "required": false; "isSignal": true; }; "clearTextOnFocus": { "alias": "clearTextOnFocus"; "required": false; "isSignal": true; }; "enablesReturnKeyAutomatically": { "alias": "enablesReturnKeyAutomatically"; "required": false; "isSignal": true; }; "keyboardAppearance": { "alias": "keyboardAppearance"; "required": false; "isSignal": true; }; "passwordRules": { "alias": "passwordRules"; "required": false; "isSignal": true; }; "spellCheck": { "alias": "spellCheck"; "required": false; "isSignal": true; }; "smartInsertDelete": { "alias": "smartInsertDelete"; "required": false; "isSignal": true; }; "dataDetectorTypes": { "alias": "dataDetectorTypes"; "required": false; "isSignal": true; }; "textContentType": { "alias": "textContentType"; "required": false; "isSignal": true; }; "autoComplete": { "alias": "autoComplete"; "required": false; "isSignal": true; }; "underlineColorAndroid": { "alias": "underlineColorAndroid"; "required": false; "isSignal": true; }; "cursorColor": { "alias": "cursorColor"; "required": false; "isSignal": true; }; "selectionHandleColor": { "alias": "selectionHandleColor"; "required": false; "isSignal": true; }; "textAlignVertical": { "alias": "textAlignVertical"; "required": false; "isSignal": true; }; "importantForAutofill": { "alias": "importantForAutofill"; "required": false; "isSignal": true; }; "showSoftInputOnFocus": { "alias": "showSoftInputOnFocus"; "required": false; "isSignal": true; }; "disableFullscreenUI": { "alias": "disableFullscreenUI"; "required": false; "isSignal": true; }; "inlineImageLeft": { "alias": "inlineImageLeft"; "required": false; "isSignal": true; }; "inlineImagePadding": { "alias": "inlineImagePadding"; "required": false; "isSignal": true; }; "textBreakStrategy": { "alias": "textBreakStrategy"; "required": false; "isSignal": true; }; }, { "value": "valueChange"; "changeText": "changeText"; "touch": "touch"; }, never, never, true, never>;
168
173
  }