From 096508240f3a80a17c7dc215f6b4ca44d6da79c4 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Thu, 15 Oct 2026 11:59:35 +0200 Subject: [PATCH] feat(markdown): use enriched renderer across all platforms --- CLAUDE.md | 2 +- docs/agent-stream-performance.md | 22 +- docs/markdown-renderer.md | 25 + docs/native-text-selection.md | 34 - package-lock.json | 108 +- .../browser/assistant-selection-copy.spec.ts | 87 +- packages/app/e2e/browser/chat-find.spec.ts | 4 +- .../e2e/browser/mermaid-fullscreen.spec.ts | 74 -- .../app/e2e/browser/mermaid-streaming.spec.ts | 72 -- .../e2e/browser/plan-card-markdown.spec.ts | 45 +- .../support/helpers/acp-streaming-markdown.ts | 6 +- packages/app/e2e/support/helpers/diagram.ts | 150 --- .../e2e/support/helpers/streaming-markdown.ts | 11 +- packages/app/package.json | 30 +- .../chat-find/ranges.browser.test.ts | 11 + .../src/agent-stream/chat-find/ranges.web.ts | 4 +- .../chat-find/viewport.browser.test.ts | 20 + .../agent-stream/chat-find/viewport.web.ts | 7 +- .../src/agent-stream/footer-spacing.test.ts | 37 - packages/app/src/agent-stream/layout.test.ts | 9 +- packages/app/src/agent-stream/layout.ts | 6 +- .../app/src/agent-stream/presentation.test.ts | 405 +------ packages/app/src/agent-stream/presentation.ts | 151 +-- packages/app/src/agent-stream/spacing.test.ts | 141 +-- packages/app/src/agent-stream/spacing.ts | 35 +- .../app/src/agent-stream/strategy-web.tsx | 3 +- packages/app/src/agent-stream/strategy.ts | 2 - packages/app/src/agent-stream/view.tsx | 8 +- .../agent-stream/web-virtualization.test.ts | 60 +- .../src/agent-stream/web-virtualization.ts | 4 - .../app/src/assistant-file-links/index.ts | 7 +- .../link-press-context.ts | 24 - .../app/src/assistant-file-links/link.tsx | 213 ---- .../app/src/assistant-file-links/provider.tsx | 15 +- .../app/src/assistant-image/lifecycle.test.ts | 120 --- packages/app/src/assistant-image/lifecycle.ts | 46 - .../assistant-image/load-dimensions.test.ts | 48 - .../src/assistant-image/load-dimensions.ts | 45 - .../app/src/assistant-image/retry.test.ts | 49 - packages/app/src/assistant-image/retry.ts | 31 - .../assistant-image/use-assistant-image.ts | 544 ---------- .../content.browser.test.ts | 55 + .../assistant-selection-copy/content.web.ts | 74 +- .../src/assistant-selection-copy/markup.ts | 59 +- .../src/components/highlighted-code-block.tsx | 24 +- .../src/components/markdown-prose/index.tsx | 2 - .../components/markdown-prose/prepare.test.ts | 217 ---- .../src/components/markdown-prose/prepare.ts | 429 -------- .../src/components/markdown-prose/props.ts | 9 - .../markdown-prose/renderer.native.tsx | 134 --- .../components/markdown-prose/renderer.tsx | 1 - .../markdown-prose/renderer.web.tsx | 5 - .../app/src/components/markdown-prose/runs.ts | 198 ---- .../markdown-prose/source-inline.ts | 60 -- .../markdown-text-selection.test.ts | 14 - .../components/markdown-text-selection.tsx | 17 - .../components/markdown-text-style.test.ts | 32 - .../app/src/components/markdown-text-style.ts | 25 - .../src/components/markdown-text.android.tsx | 56 - .../app/src/components/markdown-text.d.ts | 1 - .../app/src/components/markdown-text.ios.tsx | 113 -- .../app/src/components/markdown-text.web.tsx | 73 -- .../src/components/markdown/fence/index.tsx | 42 - .../markdown/fence/language.test.ts | 15 - .../src/components/markdown/fence/language.ts | 3 - .../markdown/fence/mermaid/build-runtime.mjs | 81 -- .../fence/mermaid/fullscreen-viewer.web.tsx | 140 --- .../markdown/fence/mermaid/host.native.tsx | 379 ------- .../markdown/fence/mermaid/host.tsx | 13 - .../markdown/fence/mermaid/host.web.tsx | 193 ---- .../fence/mermaid/iframe-runtime.web.tsx | 92 -- .../markdown/fence/mermaid/index.tsx | 6 - .../markdown/fence/mermaid/presentation.ts | 14 - .../fence/mermaid/render-model.test.ts | 163 --- .../markdown/fence/mermaid/render-model.ts | 170 --- .../markdown/fence/mermaid/runtime/entry.ts | 170 --- .../fence/mermaid/runtime/html.gen.ts | 4 - .../fence/mermaid/runtime/messages.ts | 91 -- .../mermaid/runtime/request-driver.test.ts | 52 - .../fence/mermaid/runtime/request-driver.ts | 63 -- .../mermaid/runtime/runtime-html.test.ts | 12 - .../mermaid/runtime/runtime.browser.test.ts | 146 --- .../fence/mermaid/source-policy.test.ts | 72 -- .../markdown/fence/mermaid/source-policy.ts | 52 - .../fence/mermaid/use-render-model.ts | 96 -- .../src/components/markdown/fence/types.ts | 10 - .../src/components/markdown/html-ish.test.ts | 529 ---------- .../app/src/components/markdown/html-ish.ts | 926 ---------------- .../markdown}/image-source.test.ts | 0 .../markdown}/image-source.ts | 0 .../app/src/components/markdown/images.ts | 95 ++ .../components/markdown/inline-image-size.ts | 62 -- .../src/components/markdown/link-children.ts | 21 - .../app/src/components/markdown/link-text.tsx | 41 - .../components/markdown/part-groups.test.ts | 92 -- .../src/components/markdown/part-groups.ts | 89 -- .../src/components/markdown/renderer.test.ts | 123 +-- .../app/src/components/markdown/renderer.tsx | 961 ++++------------- .../app/src/components/markdown/source.ts | 78 ++ packages/app/src/components/message.tsx | 997 ++---------------- packages/app/src/components/plan-card.tsx | 199 +--- .../src/file-pane/markdown-preview/index.tsx | 47 +- .../markdown-preview/preview-image.tsx | 151 --- packages/app/src/hooks/use-revealed-text.ts | 2 +- .../screens/workspace/workspace-screen.tsx | 15 +- .../app/src/styles/markdown-styles.test.ts | 142 --- packages/app/src/styles/markdown-styles.ts | 422 -------- .../types/react-native-markdown-display.d.ts | 11 - packages/app/src/types/stream.ts | 4 - .../__tests__/split-markdown-blocks.test.ts | 127 --- .../utils/assistant-image-metadata.test.ts | 105 -- .../app/src/utils/assistant-image-metadata.ts | 196 ---- .../utils/assistant-markdown-parser.test.ts | 135 --- .../src/utils/assistant-markdown-parser.ts | 10 +- .../assistant-message-height-estimate.test.ts | 55 +- .../assistant-message-height-estimate.ts | 92 +- packages/app/src/utils/markdown-ast.test.ts | 35 - packages/app/src/utils/markdown-ast.ts | 12 - packages/app/src/utils/markdown-list.test.ts | 87 -- packages/app/src/utils/markdown-list.ts | 147 --- .../app/src/utils/split-markdown-blocks.ts | 92 -- .../app/src/utils/streaming-markdown/index.ts | 153 --- ...own+1.2.0-nightly-20261014-766fc0e67.patch | 702 ++++++++++++ .../react-native-markdown-display+7.0.2.patch | 74 -- scripts/postinstall-patches.mjs | 5 +- 125 files changed, 1653 insertions(+), 11773 deletions(-) create mode 100644 docs/markdown-renderer.md delete mode 100644 docs/native-text-selection.md delete mode 100644 packages/app/e2e/browser/mermaid-fullscreen.spec.ts delete mode 100644 packages/app/e2e/browser/mermaid-streaming.spec.ts delete mode 100644 packages/app/e2e/support/helpers/diagram.ts delete mode 100644 packages/app/src/assistant-file-links/link-press-context.ts delete mode 100644 packages/app/src/assistant-file-links/link.tsx delete mode 100644 packages/app/src/assistant-image/lifecycle.test.ts delete mode 100644 packages/app/src/assistant-image/lifecycle.ts delete mode 100644 packages/app/src/assistant-image/load-dimensions.test.ts delete mode 100644 packages/app/src/assistant-image/load-dimensions.ts delete mode 100644 packages/app/src/assistant-image/retry.test.ts delete mode 100644 packages/app/src/assistant-image/retry.ts delete mode 100644 packages/app/src/assistant-image/use-assistant-image.ts delete mode 100644 packages/app/src/components/markdown-prose/index.tsx delete mode 100644 packages/app/src/components/markdown-prose/prepare.test.ts delete mode 100644 packages/app/src/components/markdown-prose/prepare.ts delete mode 100644 packages/app/src/components/markdown-prose/props.ts delete mode 100644 packages/app/src/components/markdown-prose/renderer.native.tsx delete mode 100644 packages/app/src/components/markdown-prose/renderer.tsx delete mode 100644 packages/app/src/components/markdown-prose/renderer.web.tsx delete mode 100644 packages/app/src/components/markdown-prose/runs.ts delete mode 100644 packages/app/src/components/markdown-prose/source-inline.ts delete mode 100644 packages/app/src/components/markdown-text-selection.test.ts delete mode 100644 packages/app/src/components/markdown-text-selection.tsx delete mode 100644 packages/app/src/components/markdown-text-style.test.ts delete mode 100644 packages/app/src/components/markdown-text-style.ts delete mode 100644 packages/app/src/components/markdown-text.android.tsx delete mode 100644 packages/app/src/components/markdown-text.d.ts delete mode 100644 packages/app/src/components/markdown-text.ios.tsx delete mode 100644 packages/app/src/components/markdown-text.web.tsx delete mode 100644 packages/app/src/components/markdown/fence/index.tsx delete mode 100644 packages/app/src/components/markdown/fence/language.test.ts delete mode 100644 packages/app/src/components/markdown/fence/language.ts delete mode 100644 packages/app/src/components/markdown/fence/mermaid/build-runtime.mjs delete mode 100644 packages/app/src/components/markdown/fence/mermaid/fullscreen-viewer.web.tsx delete mode 100644 packages/app/src/components/markdown/fence/mermaid/host.native.tsx delete mode 100644 packages/app/src/components/markdown/fence/mermaid/host.tsx delete mode 100644 packages/app/src/components/markdown/fence/mermaid/host.web.tsx delete mode 100644 packages/app/src/components/markdown/fence/mermaid/iframe-runtime.web.tsx delete mode 100644 packages/app/src/components/markdown/fence/mermaid/index.tsx delete mode 100644 packages/app/src/components/markdown/fence/mermaid/presentation.ts delete mode 100644 packages/app/src/components/markdown/fence/mermaid/render-model.test.ts delete mode 100644 packages/app/src/components/markdown/fence/mermaid/render-model.ts delete mode 100644 packages/app/src/components/markdown/fence/mermaid/runtime/entry.ts delete mode 100644 packages/app/src/components/markdown/fence/mermaid/runtime/html.gen.ts delete mode 100644 packages/app/src/components/markdown/fence/mermaid/runtime/messages.ts delete mode 100644 packages/app/src/components/markdown/fence/mermaid/runtime/request-driver.test.ts delete mode 100644 packages/app/src/components/markdown/fence/mermaid/runtime/request-driver.ts delete mode 100644 packages/app/src/components/markdown/fence/mermaid/runtime/runtime-html.test.ts delete mode 100644 packages/app/src/components/markdown/fence/mermaid/runtime/runtime.browser.test.ts delete mode 100644 packages/app/src/components/markdown/fence/mermaid/source-policy.test.ts delete mode 100644 packages/app/src/components/markdown/fence/mermaid/source-policy.ts delete mode 100644 packages/app/src/components/markdown/fence/mermaid/use-render-model.ts delete mode 100644 packages/app/src/components/markdown/fence/types.ts delete mode 100644 packages/app/src/components/markdown/html-ish.test.ts delete mode 100644 packages/app/src/components/markdown/html-ish.ts rename packages/app/src/{file-pane/markdown-preview => components/markdown}/image-source.test.ts (100%) rename packages/app/src/{file-pane/markdown-preview => components/markdown}/image-source.ts (100%) create mode 100644 packages/app/src/components/markdown/images.ts delete mode 100644 packages/app/src/components/markdown/inline-image-size.ts delete mode 100644 packages/app/src/components/markdown/link-children.ts delete mode 100644 packages/app/src/components/markdown/link-text.tsx delete mode 100644 packages/app/src/components/markdown/part-groups.test.ts delete mode 100644 packages/app/src/components/markdown/part-groups.ts create mode 100644 packages/app/src/components/markdown/source.ts delete mode 100644 packages/app/src/file-pane/markdown-preview/preview-image.tsx delete mode 100644 packages/app/src/styles/markdown-styles.test.ts delete mode 100644 packages/app/src/styles/markdown-styles.ts delete mode 100644 packages/app/src/types/react-native-markdown-display.d.ts delete mode 100644 packages/app/src/utils/__tests__/split-markdown-blocks.test.ts delete mode 100644 packages/app/src/utils/assistant-image-metadata.test.ts delete mode 100644 packages/app/src/utils/assistant-image-metadata.ts delete mode 100644 packages/app/src/utils/markdown-ast.test.ts delete mode 100644 packages/app/src/utils/markdown-ast.ts delete mode 100644 packages/app/src/utils/markdown-list.test.ts delete mode 100644 packages/app/src/utils/markdown-list.ts delete mode 100644 packages/app/src/utils/split-markdown-blocks.ts delete mode 100644 packages/app/src/utils/streaming-markdown/index.ts create mode 100644 patches/react-native-enriched-markdown+1.2.0-nightly-20261014-766fc0e67.patch delete mode 100644 patches/react-native-markdown-display+7.0.2.patch diff --git a/CLAUDE.md b/CLAUDE.md index 945f56c436..9c1712aab2 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -49,7 +49,7 @@ At the start of non-trivial work, list `docs/` and skim anything relevant to the | [docs/permissions.md](docs/permissions.md) | Semantic daemon permissions, principals, credentials, pairing invitations, and Hub authority | | [docs/terminal-performance.md](docs/terminal-performance.md) | Terminal latency pipeline, coalescing/backpressure invariants, benchmark + perf spec usage | | [docs/agent-stream-performance.md](docs/agent-stream-performance.md) | Assistant text pipeline — coalescing window, paced reveal, why arrival lumps are smoothed at render | -| [docs/native-text-selection.md](docs/native-text-selection.md) | Native prose runs, selection boundaries, renderer flavor and library constraints | +| [docs/markdown-renderer.md](docs/markdown-renderer.md) | Shared Markdown engine, native selection, links, images and library constraints | | [docs/file-observation.md](docs/file-observation.md) | Recursive watcher ownership, Linux constraints, teardown invariants, and Parcel comparison | | [docs/testing.md](docs/testing.md) | TDD workflow, determinism, real dependencies over mocks, test organization | | [docs/qa.md](docs/qa.md) | QA evidence bar for pull requests — platform matrix, version drift, performance, UI proof | diff --git a/docs/agent-stream-performance.md b/docs/agent-stream-performance.md index 02d9a42848..3732950304 100644 --- a/docs/agent-stream-performance.md +++ b/docs/agent-stream-performance.md @@ -29,26 +29,8 @@ So arrival sets a _target_ and the reveal rate is derived from the backlog inste - **The coalescer is leading + trailing.** The first delta after an idle window flushes synchronously; only the rest of the burst waits for the trailing timer. Reverting to trailing-only adds a full window to the first character of every turn. Same shape and the same reason as `TerminalOutputCoalescer`. - **The leading flush adds a canonical row, and that is fine.** A burst's first chunk lands as its own timeline row. `mergeAssistantChunks` / `mergeReasoningChunks` in `timeline-projection.ts` join contiguous same-turn rows, and clients read the projected timeline, so history is unaffected. Tests that assert on raw rows have to account for the extra row; tests that assert on what a client sees do not. - **The store holds the full text; only the rendered slice is paced.** Copy, selection, the chat outline, and scroll geometry all read the same string the user can see. Pacing the store instead would leave the bottom anchor chasing a content height that is ahead of the reveal. -- **Markdown rows belong to presentation, and every assistant message is a block group.** - `agent-stream/presentation.ts` keeps the source message whole for plugins, then splits it for - display. Native groups completed adjacent prose into selectable runs; web retains individual blocks. - The growing last source block stays on the existing block renderer and streaming parser. - It joins the preceding run when the next block starts or the turn ends. - Code, tables, images, HTML-like content and blockquotes end a native run. See - [native text selection](native-text-selection.md) for renderer ownership and library constraints. - Appends reparse the growing last block. A completed reference definition at its start can - belong to the preceding block, so that case reparses the full message. Completed rows retain - object identity and unchanged history stays cached. Prose rows retain their original source - blocks so native preparation cannot broaden reference scope. Completed blocks are classified - and prepared once; the growing block is neither classified nor prepared for the prose renderer. -- **A row id is never a message id.** Block rows are `${messageId}:block:${n}`; native prose runs use `${messageId}:run:${n}` with - `blockGroupId = messageId`, including single-row messages. Inline images split a source block into offset-keyed parts, so nothing can come to rely on the two - being equal. `getStreamItemMessageId` in `presentation.ts` is the only way to go from a row to its - message, and web rows carry it as `data-message-id` alongside `data-history-row-id`. Anything - addressing a message — chat find, scroll-to-message, history reveal, the find expansion that lifts - the render cap — uses the message id and must expect several rows to answer to it. Row ids stay for - React keys, virtualizer measurement, scroll anchors, and per-row caches like assistant image - occurrence keys. +- **One source assistant message is one display row.** Presentation projects plugin output and caches unchanged history. The shared [Markdown renderer](markdown-renderer.md) owns rendering on every platform. Chat paces the visible source and applies its 32,000-character render cap; it does not split Markdown or promote completed blocks into history. +- **Message identity survives completion.** Assistant row ids are source message ids. Plugin rows can replace a source, so find, scroll-to-message and history reveal still use `getStreamItemMessageId`. Width-specific measurements describe whole rendered messages. - **First sight of a text is revealed whole.** Only growth is paced. This is what makes history hydration, timeline replay, a virtualized row remounting on scroll, and an already-finished message all render complete on first paint without a special case for each. - **Leaving `phase: "streaming"` snaps the reveal.** A completed turn must never be left holding characters. `layoutStream` sets the phase, so anything outside the live head with an active turn is already complete. - **The reducer queue commits on a frame, with a timer as the ceiling.** A frame callback never fires in a hidden tab, so a timer races it and wins when nothing is painting — the store has to keep advancing either way. diff --git a/docs/markdown-renderer.md b/docs/markdown-renderer.md new file mode 100644 index 0000000000..269db3da9a --- /dev/null +++ b/docs/markdown-renderer.md @@ -0,0 +1,25 @@ +# Markdown rendering + +Use `MarkdownRenderer` for chat replies, file previews, plans, pull requests and changelog content. It uses `react-native-enriched-markdown` on iOS, Android and web. Keep source adaptation and renderer configuration in `components/markdown`; consumers supply Markdown and, when needed, its file location. + +Native selection belongs to the library's text view. Keep an assistant message in one row so selection can cross paragraphs, headings and lists. Code blocks and tables use the library's native views and their selection behavior. Web uses browser selection over semantic HTML. Chat still owns paced reveal, its render cap, message identity and list measurement; it does not choose a Markdown engine or classify prose. + +## Links and images + +Workspace file links use the shared file-link resolver. The workspace supplies its client, root and open-file action once; chat can supply the agent's more specific context. URLs use the same link press path. Recognized inline-code paths stay tappable. + +Resolve workspace image paths over the existing session RPC, then give the library a cached native file URL or web blob URL. HTTP(S) images pass through unchanged. This preserves relay, SSH and local socket connections, which do not expose a daemon HTTP endpoint. Image taps open the attachment lightbox. File previews resolve relative images against the Markdown file's directory. + +## Engine constraints + +The pinned nightly has the same React Native package and core source trees as upstream commit `73ac5085eb1fdc743973b3473d4dba65abad5261`. Compatibility was checked with React Native 0.81.5. Native dependencies must be rebuilt when changing the highlighting configuration; JavaScript refresh cannot enable compiled grammars. + +Math and video are disabled. Task lists and admonitions are enabled; task checkboxes are read-only. Source adaptation escapes spoiler delimiters and decodes entity text before the engine can discard it. Code spans and fences keep their literal source. Mermaid renders as a code block. HTML and details collapsibles have no custom renderer. + +Native code blocks use the library's copy control and compiled grammars. Web code blocks use `HighlightedCodeBlock` through the library's `renderers.codeBlock` override. The patch also preserves web list numbering and link titles, and marks pending parses with `aria-busy` so Find waits for rendered text. The package patch is tracked by [upstream PR #997](https://github.com/software-mansion/enriched-markdown/pull/997); remove the patch when the pinned release includes it. + +The library determines how incomplete Markdown appears during streaming. Completed text uses the same component and engine as history. Find and scroll-to-message address source message ids; selection-copy understands semantic HTML and the web code component's copy metadata. + +## Verification + +Follow [QA](qa.md) and [mobile testing](mobile-testing.md). Exercise chat, file preview, plan and PR content on all three platforms. Include cross-paragraph selection, progressive streaming, a file-link tap, code copy/highlighting, a table and a workspace image. On iPad, repeat preview selection after opening and closing Explorer and after rotation. Check web find, selection-copy and source-message navigation after DOM changes. diff --git a/docs/native-text-selection.md b/docs/native-text-selection.md deleted file mode 100644 index e59ea89cde..0000000000 --- a/docs/native-text-selection.md +++ /dev/null @@ -1,34 +0,0 @@ -# Native text selection - -Assistant prose uses `react-native-enriched-markdown` 1.1.1 on iOS and Android. Native selection belongs to one text view, so separate paragraph rows prevent dragging a selection between them. - -Keep completed adjacent paragraphs, headings, lists and horizontal rules in prose runs. The growing last source block uses Paseo’s existing block renderer and streaming parser. It joins the preceding run when another block starts or the turn ends, retaining the run’s id. Selection across that paragraph becomes available at completion. Code, tables, images and HTML-like content need Paseo's existing renderers. Preserve whole containers when splitting around an image would lose formatting or list context. Reference definitions retain their original block scope; grouping text for selection must not change which references resolve. - -Selection stops at preserved blocks, blockquotes and run boundaries required by the 32,000-character render cap. Consecutive paragraphs inside a quote share its text segment; nested quotes and embedded blocks introduce further library segments. Web keeps its existing renderer and selection behavior. - -## Flavor - -Use `github` to retain strikethrough and gain task checkboxes. `commonmark` allows selection across quotes but loses strikethrough and task-list parsing. GitHub quotes require separate native views, so blockquotes are run boundaries. - -Checkbox toggling is disabled because changes would not persist. Admonitions add styling to `[!NOTE]`-style quotes without hiding their content. Ordinary quotes retain the existing blockquote styling. - -## Compatibility constraints - -- The 1.1.1 native parser drops entity nodes. Decode text and destination entities before rendering, retaining literal entities inside code. Whitespace entities must remain text instead of creating new Markdown blocks. Its autolinks also disagree with Paseo about bare domains and trailing punctuation, so preserve Paseo’s recognized destinations as explicit links. -- Keep dollar-delimited text literal with `latexMath: false`; dollar amounts must not become math. -- Spoilers have no public parser switch in 1.1.1. Escape prose pipes so `||text||` stays visible; code and link destinations retain their pipes. -- `lineHeight` is a minimum, so font metrics can increase it. Compare spacing on devices. -- Strikethrough color is iOS-only. Android uses the surrounding text color. -- Strong text uses bold instead of the theme's weight 500. The public API accepts only bold or normal, and the app has no bundled medium-weight font family to substitute. -- Native h1/h2 lose their 1px bottom borders because the library has no heading-border API. H6 keeps uppercase plain text but loses its 0.5 letter spacing; code and link destinations keep their source casing. -- Android heading spans override inline code's font, size and color. iOS retains the code style; its system-monospace alias needs mapping at the library boundary. Web headings are unchanged. -- Published 1.1.1 cannot embed Paseo's custom React link pills or hover tooltips in its text view. Links retain Paseo's press handling. -- Adding a completed block replaces the run’s attributed text. The library does not retain selections made while that run changes. The growing block never goes through native prose preparation. - -Keep the package's math, highlighting, tree-sitter and video engines disabled. Paseo owns those renderers; duplicate native engines add binary size without serving this surface. Rebuild the dev client after changing engine configuration. - -Test Android selection-handle dragging against the compiled client. A JavaScript test cannot prove that the inverted timeline's scroll protection prevents movement during selection. - -## Follow-ups - -Contribute Android heading-span precedence fixes and heading border style props upstream so inline code and h1/h2 borders retain their previous appearance without breaking selection across headings. diff --git a/package-lock.json b/package-lock.json index 41849ad6dc..45e1f8fca2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16265,15 +16265,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/camelize": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.1.tgz", - "integrity": "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/caniuse-lite": { "version": "1.0.30001777", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001777.tgz", @@ -17088,15 +17079,6 @@ "node": ">=8" } }, - "node_modules/css-color-keywords": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz", - "integrity": "sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==", - "license": "ISC", - "engines": { - "node": ">=4" - } - }, "node_modules/css-in-js-utils": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/css-in-js-utils/-/css-in-js-utils-3.1.0.tgz", @@ -17122,17 +17104,6 @@ "url": "https://github.com/sponsors/fb55" } }, - "node_modules/css-to-react-native": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.2.0.tgz", - "integrity": "sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==", - "license": "MIT", - "dependencies": { - "camelize": "^1.0.0", - "css-color-keywords": "^1.0.0", - "postcss-value-parser": "^4.0.2" - } - }, "node_modules/css-tree": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz", @@ -30248,6 +30219,7 @@ "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "dev": true, "license": "MIT", "dependencies": { "loose-envify": "^1.4.0", @@ -30259,6 +30231,7 @@ "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "dev": true, "license": "MIT" }, "node_modules/proper-lockfile": { @@ -30905,32 +30878,6 @@ "react-native": "*" } }, - "node_modules/react-native-enriched-markdown": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/react-native-enriched-markdown/-/react-native-enriched-markdown-1.1.1.tgz", - "integrity": "sha512-JvbRIVy75MAoGCv8V6sSscu11/Ian588wfIgH+j64XFpf6gy5sNNkMyG087MH88jNCf3p7fTjgEAGxZMhEqL4A==", - "hasInstallScript": true, - "license": "MIT", - "peerDependencies": { - "katex": ">=0.16.0", - "react": "*", - "react-native": "*" - }, - "peerDependenciesMeta": { - "katex": { - "optional": true - } - } - }, - "node_modules/react-native-fit-image": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/react-native-fit-image/-/react-native-fit-image-1.5.5.tgz", - "integrity": "sha512-Wl3Vq2DQzxgsWKuW4USfck9zS7YzhvLNPpkwUUCF90bL32e1a0zOVQ3WsJILJOwzmPdHfzZmWasiiAUNBkhNkg==", - "license": "Beerware", - "dependencies": { - "prop-types": "^15.5.10" - } - }, "node_modules/react-native-gesture-handler": { "version": "2.28.0", "resolved": "https://registry.npmjs.org/react-native-gesture-handler/-/react-native-gesture-handler-2.28.0.tgz", @@ -30956,22 +30903,6 @@ "react-native": "*" } }, - "node_modules/react-native-markdown-display": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/react-native-markdown-display/-/react-native-markdown-display-7.0.2.tgz", - "integrity": "sha512-Mn4wotMvMfLAwbX/huMLt202W5DsdpMO/kblk+6eUs55S57VVNni1gzZCh5qpznYLjIQELNh50VIozEfY6fvaQ==", - "license": "MIT", - "dependencies": { - "css-to-react-native": "^3.0.0", - "markdown-it": "^10.0.0", - "prop-types": "^15.7.2", - "react-native-fit-image": "^1.5.5" - }, - "peerDependencies": { - "react": ">=16.2.0", - "react-native": ">=0.50.4" - } - }, "node_modules/react-native-nitro-modules": { "version": "0.35.5", "resolved": "https://registry.npmjs.org/react-native-nitro-modules/-/react-native-nitro-modules-0.35.5.tgz", @@ -31037,18 +30968,6 @@ "react-native": "*" } }, - "node_modules/react-native-uitextview": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/react-native-uitextview/-/react-native-uitextview-2.2.0.tgz", - "integrity": "sha512-Vbv3cTAuyfkYrfsR2YKFsOd9OYgfys+IX5yvvYY7Wd6TrOd6FSGrX93xtQHjeLXV1ds6fDJcFJsuLi3S4Ypr8A==", - "workspaces": [ - "example" - ], - "peerDependencies": { - "react": "*", - "react-native": "*" - } - }, "node_modules/react-native-unistyles": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/react-native-unistyles/-/react-native-unistyles-3.2.4.tgz", @@ -36497,7 +36416,9 @@ "i18next": "^26.3.0", "lucide-react-native": "^1.50.0", "markdown-it": "^10.0.0", + "mdast-util-from-markdown": "2.0.3", "mermaid": "^11.16.0", + "micromark-util-decode-string": "2.0.1", "mnemonic-id": "^3.2.7", "qrcode": "^1.5.4", "react": "19.1.0", @@ -36507,16 +36428,14 @@ "react-native": "0.81.5", "react-native-draggable-flatlist": "^4.0.3", "react-native-edge-to-edge": "^1.7.0", - "react-native-enriched-markdown": "1.1.1", + "react-native-enriched-markdown": "1.2.0-nightly-20261014-766fc0e67", "react-native-gesture-handler": "~2.28.0", "react-native-keyboard-controller": "1.21.12", - "react-native-markdown-display": "^7.0.2", "react-native-nitro-modules": "0.35.5", "react-native-reanimated": "~4.3.1", "react-native-safe-area-context": "~5.6.0", "react-native-screens": "~4.16.0", "react-native-svg": "^15.14.0", - "react-native-uitextview": "^2.2.0", "react-native-unistyles": "^3.2.4", "react-native-web": "~0.21.0", "react-native-webview": "^13.16.0", @@ -37396,6 +37315,23 @@ "dev": true, "license": "MIT" }, + "packages/app/node_modules/react-native-enriched-markdown": { + "version": "1.2.0-nightly-20261014-766fc0e67", + "resolved": "https://registry.npmjs.org/react-native-enriched-markdown/-/react-native-enriched-markdown-1.2.0-nightly-20261014-766fc0e67.tgz", + "integrity": "sha512-PyYAAVJAyjlcqHXgSnzxok13MDmyxs/nt46hHIOBdCc0prfaL2Ef6nYkZu7fFCeaETOJXd/xQr4n2/79I+gvdA==", + "hasInstallScript": true, + "license": "MIT", + "peerDependencies": { + "katex": ">=0.16.0", + "react": "*", + "react-native": "*" + }, + "peerDependenciesMeta": { + "katex": { + "optional": true + } + } + }, "packages/app/node_modules/react-native-keyboard-controller": { "version": "1.21.12", "resolved": "https://registry.npmjs.org/react-native-keyboard-controller/-/react-native-keyboard-controller-1.21.12.tgz", diff --git a/packages/app/e2e/browser/assistant-selection-copy.spec.ts b/packages/app/e2e/browser/assistant-selection-copy.spec.ts index bf8e455c88..37eb80322a 100644 --- a/packages/app/e2e/browser/assistant-selection-copy.spec.ts +++ b/packages/app/e2e/browser/assistant-selection-copy.spec.ts @@ -73,9 +73,14 @@ const ASSISTANT_MARKDOWN = [ ].join("\n"); const EXPECTED_WHOLE_SELECTION_MARKDOWN = ASSISTANT_MARKDOWN.replace( - `${CROSS_STRUCTURE_MARKDOWN}\n\n`, - "", + "www.example.com stays", + "[www.example.com](http://www.example.com) stays", ) + .replace( + "`https://example.com/generated` stays", + "[`https://example.com/generated`](https://example.com/generated) stays", + ) + .replace(`${CROSS_STRUCTURE_MARKDOWN}\n\n`, "") .replace( "", "[https://autolink.example.com](https://autolink.example.com)", @@ -98,8 +103,7 @@ async function allowRichClipboard(context: BrowserContext): Promise { } /** - * The rows of one assistant message. A message renders one row per Markdown block, so - * every locator and every text walk below covers the group, not a single block. + * Locate assistant rows by source-message identity. */ function assistantMessageBlocks(page: Page): Locator { return page.locator("[data-message-id]").filter({ has: page.getByTestId("assistant-message") }); @@ -173,7 +177,7 @@ async function dragFromBelowImageTo(page: Page, text: string): Promise { const blocks = assistantMessageBlocks(page); const target = blocks.getByText(text); await target.scrollIntoViewIfNeeded(); - const frame = await blocks.locator('[data-paseo-markdown-tag="img"]').boundingBox(); + const frame = await blocks.locator("img").boundingBox(); const end = await target.evaluate((element, selectedText) => { const walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT); while (walker.nextNode()) { @@ -192,7 +196,8 @@ async function dragFromBelowImageTo(page: Page, text: string): Promise { if (!frame) { throw new Error("Expected a rendered image"); } - await page.mouse.move(end.left + 5, frame.y + frame.height + 4); + await page.evaluate(() => window.getSelection()?.removeAllRanges()); + await page.mouse.move(end.left + 1, end.y - 14); await page.mouse.down(); await page.mouse.move(end.x, end.y, { steps: 10 }); await page.mouse.up(); @@ -210,18 +215,12 @@ async function selectAssistantListItemFromMarker( ): Promise { const assistantMessage = assistantMessageBlocks(page); const item = assistantMessage - .locator(`[data-paseo-markdown-tag="${listTag}"]`) + .locator(listTag) .filter({ hasText: itemText }) - .locator(':scope > [data-paseo-markdown-tag="li"]') + .locator(":scope > li") .filter({ hasText: itemText }); await item.evaluate((element, selectedEndText) => { - const marker = element.querySelector(':scope > [data-paseo-markdown-list-marker="true"]'); - const markerText = marker?.firstChild; - if (!(markerText instanceof Text)) { - throw new Error("Expected rendered list marker text"); - } - const walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT); let endNode: Text | null = null; let endOffset = -1; @@ -239,7 +238,7 @@ async function selectAssistantListItemFromMarker( } const range = document.createRange(); - range.setStart(markerText, 0); + range.setStart(element, 0); range.setEnd(endNode, endOffset); const selection = window.getSelection(); selection?.removeAllRanges(); @@ -254,7 +253,7 @@ async function doubleClickAssistantMarkdownText( ): Promise { const assistantMessage = assistantMessageBlocks(page); await assistantMessage - .locator(`[data-paseo-markdown-tag="${tag}"]`) + .locator(tag) .filter({ hasText: text }) .dblclick({ position: { x: 5, y: 5 } }); } @@ -276,39 +275,23 @@ async function selectAssistantTextRange( found = blockWalker.nextNode(); } } - let cursor = 0; - const walker = { nextNode: () => nodes[cursor++] ?? null }; - let startNode: Node | null = null; - let startOffset = -1; - let endNode: Node | null = null; - let endOffset = -1; - let textNode = walker.nextNode(); - while (textNode) { - if (!startNode) { - const offset = textNode.textContent?.indexOf(selectedRange.startText) ?? -1; - if (offset >= 0) { - startNode = textNode; - startOffset = offset; - } - } - if (startNode) { - const offset = textNode.textContent?.indexOf(selectedRange.endText) ?? -1; - if (offset >= 0) { - endNode = textNode; - endOffset = offset + selectedRange.endText.length; - break; - } - } - textNode = walker.nextNode(); - } - if (!startNode || !endNode) { + const text = nodes.map((node) => node.data).join(""); + const start = text.indexOf(selectedRange.startText); + const end = text.indexOf(selectedRange.endText, start); + if (start < 0 || end < 0) throw new Error( `Could not find assistant text range: ${selectedRange.startText} — ${selectedRange.endText}`, ); - } + const boundary = (offset: number, isEnd = false): [Text, number] => { + for (const node of nodes) { + if (offset < node.length || (isEnd && offset === node.length)) return [node, offset]; + offset -= node.length; + } + throw new Error("Selection extends beyond message"); + }; const range = document.createRange(); - range.setStart(startNode, startOffset); - range.setEnd(endNode, endOffset); + range.setStart(...boundary(start)); + range.setEnd(...boundary(end + selectedRange.endText.length, true)); const selection = window.getSelection(); selection?.removeAllRanges(); selection?.addRange(range); @@ -428,16 +411,12 @@ test("copying an assistant selection preserves Markdown structure and links", as ["em", "emphasized prose"], ["s", "struck prose"], ]) { - const formattedProse = assistantMessage - .locator(`[data-paseo-markdown-tag="${tag}"]`) - .filter({ hasText: text }); + const formattedProse = assistantMessage.locator(tag).filter({ hasText: text }); await expect(formattedProse).toHaveCSS("font-family", "serif"); await expect(formattedProse).not.toHaveAttribute("data-pmono"); } - const inlineCode = assistantMessage - .locator('[data-paseo-markdown-tag="code"]') - .filter({ hasText: "apply_patch" }); - await expect(inlineCode).toHaveAttribute("data-pmono", ""); + const inlineCode = assistantMessage.locator("code").filter({ hasText: "apply_patch" }); + await expect(inlineCode).toHaveCSS("font-family", /monospace|Mono/); await selectStructuredClipboardFixture(page); await copySelection(page); @@ -466,7 +445,7 @@ test("copying an assistant selection preserves Markdown structure and links", as 'workspace file', ); expect(clipboard.html).toContain("https://example.com/generated"); - expect(clipboard.html).not.toContain( + expect(clipboard.html).toContain( 'https://example.com/generated', ); expect(clipboard.html).toContain(''); @@ -512,7 +491,7 @@ test("copying an assistant selection preserves Markdown structure and links", as " - Switch Claude models and confirm each model retains its own thinking choice.", " - Cover new-workspace drafts, queued launches, schedules, and editing existing schedules.", " - Confirm Opus 5 exposes the Fast toggle.", - " - Smoke with an older saved Fabel/model alias and with daemon/client version drift.", + " - Smoke with an older saved [Fabel/model](Fabel/model) alias and with [daemon/client](daemon/client) version drift.", ].join("\n"), ); expect(headingAndListClipboard.html).toMatch( diff --git a/packages/app/e2e/browser/chat-find.spec.ts b/packages/app/e2e/browser/chat-find.spec.ts index 0ce6c561ed..b3db4903f8 100644 --- a/packages/app/e2e/browser/chat-find.spec.ts +++ b/packages/app/e2e/browser/chat-find.spec.ts @@ -33,8 +33,8 @@ const STREAMED_RESPONSE = [ const RESPONSE = 'İ😀hello **world**; hello *world*.\n\n[hello & world](https://example.com/hidden-destination)\n\n```ts\nconst a = "a.b";\n```\n\n- first cell\n- second cell'; -// One message far taller than the viewport, so its first and last paragraph rows are -// never in the virtualizer's window at the same time. +// One message taller than the viewport, exercising navigation between distant +// matches inside the same rendered row. const PARAGRAPH_PADDING = Array.from({ length: 40 }, (_, index) => `padding${index}`).join(" "); const TALL_RESPONSE = Array.from({ length: 30 }, (_, index) => { if (index === 0) return `Opening paragraph with span-needle. ${PARAGRAPH_PADDING}`; diff --git a/packages/app/e2e/browser/mermaid-fullscreen.spec.ts b/packages/app/e2e/browser/mermaid-fullscreen.spec.ts deleted file mode 100644 index 72b05ed83e..0000000000 --- a/packages/app/e2e/browser/mermaid-fullscreen.spec.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { test } from "../support/fixtures"; -import { - closeDiagramFullscreen, - closeDiagramFullscreenFromOutside, - expectCompletedDiagram, - expectFullscreenDiagram, - expectFullscreenDiagramClosed, - openDiagramFullscreen, - requestDiagram, - waitForDiagramTurnToComplete, - zoomAndPanFullscreenDiagram, -} from "../support/helpers/diagram"; -import { openAgentRoute, seedMockAgentWorkspace } from "../support/helpers/mock-agent"; - -const MERMAID_DIAGRAM = [ - "```mermaid", - "flowchart LR", - " Start --> Middle", - " Middle --> Review", - " Review --> Ship", - "```", -].join("\n"); - -test("opens a Mermaid diagram fullscreen and closes it again", async ({ page }) => { - test.setTimeout(120_000); - - const agent = await seedMockAgentWorkspace({ - repoPrefix: "mermaid-fullscreen-", - title: "Fullscreen Mermaid", - featureValues: { - mockStreamingAssistantResponse: MERMAID_DIAGRAM, - mockStreamingAssistantIntervalMs: 10, - }, - }); - try { - await test.step("Open the conversation and wait for the rendered diagram", async () => { - await openAgentRoute(page, agent); - await requestDiagram(agent); - await waitForDiagramTurnToComplete(agent); - await expectCompletedDiagram(page, ["Start", "Ship"]); - }); - - await test.step("The toolbar opens the diagram fullscreen", async () => { - await openDiagramFullscreen(page); - await expectFullscreenDiagram(page, ["Start", "Review", "Ship"]); - }); - - await test.step("The fullscreen diagram zooms with the wheel and pans by dragging", async () => { - await zoomAndPanFullscreenDiagram(page); - }); - - await test.step("The close action returns to the conversation", async () => { - await closeDiagramFullscreen(page); - await expectFullscreenDiagramClosed(page); - await expectCompletedDiagram(page, ["Start", "Ship"]); - }); - - await test.step("Escape closes the fullscreen diagram", async () => { - await openDiagramFullscreen(page); - await expectFullscreenDiagram(page, ["Start", "Ship"]); - await page.keyboard.press("Escape"); - await expectFullscreenDiagramClosed(page); - }); - - await test.step("Clicking outside the diagram closes the fullscreen viewer", async () => { - await openDiagramFullscreen(page); - await expectFullscreenDiagram(page, ["Start", "Ship"]); - await closeDiagramFullscreenFromOutside(page); - await expectFullscreenDiagramClosed(page); - }); - } finally { - await agent.cleanup(); - } -}); diff --git a/packages/app/e2e/browser/mermaid-streaming.spec.ts b/packages/app/e2e/browser/mermaid-streaming.spec.ts deleted file mode 100644 index 19ade0cb54..0000000000 --- a/packages/app/e2e/browser/mermaid-streaming.spec.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { test } from "../support/fixtures"; -import { - expectCompletedDiagram, - expectDiagramRemainsRenderedWhileStreaming, - expectDiagramWithLabels, - reloadConversation, - requestDiagram, - waitForDiagramTurnToComplete, -} from "../support/helpers/diagram"; -import { openAgentRoute, seedMockAgentWorkspace } from "../support/helpers/mock-agent"; - -const STREAMED_MERMAID = [ - "```mermaid", - "flowchart LR", - " Start --> Middle", - ' Middle --> Done["Done"]', - " Middle --> Review", - " Review --> Verify", - " Verify --> Ship", - " Ship --> Package", - " Package --> Sign", - " Sign --> Upload", - " Upload --> Publish", - " Publish --> Deploy", - " Deploy --> Observe", - " Observe --> Validate", - " Validate --> Announce", - " Announce --> Document", - " Document --> Archive", - " Archive --> Release", - "```", -].join("\n"); - -test("keeps a Mermaid diagram rendered while its message streams, completes, and reloads", async ({ - page, -}) => { - test.setTimeout(120_000); - let diagramTurnCompleted!: Promise; - - const agent = await seedMockAgentWorkspace({ - repoPrefix: "mermaid-streaming-", - title: "Streaming Mermaid", - featureValues: { - mockStreamingAssistantResponse: STREAMED_MERMAID, - mockStreamingAssistantIntervalMs: 150, - }, - }); - try { - await test.step("Open the conversation and request a diagram", async () => { - await openAgentRoute(page, agent); - await requestDiagram(agent); - diagramTurnCompleted = waitForDiagramTurnToComplete(agent); - }); - - await test.step("The diagram renders and stays rendered while tokens arrive", async () => { - await expectDiagramWithLabels(page, ["Start", "Middle"]); - await expectDiagramRemainsRenderedWhileStreaming(page, diagramTurnCompleted); - }); - - await test.step("The completed diagram shows the final streamed content", async () => { - await diagramTurnCompleted; - await expectCompletedDiagram(page, ["Start", "Done", "Release"]); - }); - - await test.step("The completed diagram remains rendered after reload", async () => { - await reloadConversation(page); - await expectCompletedDiagram(page, ["Start", "Done", "Release"]); - }); - } finally { - await agent.cleanup(); - } -}); diff --git a/packages/app/e2e/browser/plan-card-markdown.spec.ts b/packages/app/e2e/browser/plan-card-markdown.spec.ts index b5d7cfe1e7..eb58909c6a 100644 --- a/packages/app/e2e/browser/plan-card-markdown.spec.ts +++ b/packages/app/e2e/browser/plan-card-markdown.spec.ts @@ -2,14 +2,7 @@ import { expect, test } from "../support/fixtures"; import { waitForPermissionPrompt } from "../support/helpers/permissions"; import { openAgentRoute, seedMockAgentWorkspace } from "../support/helpers/mock-agent"; -// The plan card has to pass its own markdown parser. Drop that prop and -// react-native-markdown-display silently supplies one with `typographer: true` -// (node_modules/react-native-markdown-display/src/index.js:139-141), which -// turns (c) into ©, curls straight quotes, and rewrites --- as an em dash. -// No unit test can catch that, because the defect lives in the JSX. -// -// Nothing here is provider-specific: the mock provider drives it, and the -// behavior is the same for every agent. +// Literal shell commands and copyright markers must survive Markdown rendering. test.describe("Plan card markdown", () => { test("renders plan text verbatim", async ({ page }) => { test.setTimeout(180_000); @@ -25,6 +18,7 @@ test.describe("Plan card markdown", () => { await waitForPermissionPrompt(page, 120_000); const planCard = page.getByTestId("permission-plan-card"); + await expect(planCard.locator(".enrm-text")).toBeVisible(); await expect(planCard).toContainText("(c)"); await expect(planCard).toContainText('--name="my repo"'); await expect(planCard).toContainText("---buzz"); @@ -35,3 +29,38 @@ test.describe("Plan card markdown", () => { } }); }); + +test("chat and file preview share the library renderer and resolve file links", async ({ + page, +}, testInfo) => { + const { writeFile } = await import("node:fs/promises"); + const source = + "First paragraph.\n\nSecond paragraph.\n\n```ts\nconst answer = 42;\n```\n\n> A quote\n\n| Key | Value |\n| --- | --- |\n| Answer | 42 |\n\nOpen `README.md`."; + const session = await seedMockAgentWorkspace({ + repoPrefix: "shared-markdown-", + title: "Shared Markdown", + featureValues: { mockAssistantResponse: source }, + initialPrompt: "Show Markdown", + }); + try { + await writeFile(`${session.cwd}/README.md`, source); + await session.client.waitForFinish(session.agentId, 30_000); + await openAgentRoute(page, session); + const message = page.getByTestId("assistant-message"); + await expect(message).toHaveCount(1); + await expect(message.locator(".enrm-text > p").first()).toContainText("First paragraph."); + await expect(message.locator(".enrm-text > p").nth(1)).toContainText("Second paragraph."); + await expect(message.locator("blockquote")).toContainText("A quote"); + await expect(message.getByRole("table")).toContainText("42"); + await message.getByRole("link", { name: "README.md" }).click(); + const preview = page.getByTestId("markdown-preview-frame"); + await expect(preview.locator(".enrm-text")).toContainText("Second paragraph."); + await expect(preview.getByRole("table")).toContainText("42"); + await testInfo.attach("shared-renderer", { + body: await page.screenshot(), + contentType: "image/png", + }); + } finally { + await session.cleanup(); + } +}); diff --git a/packages/app/e2e/support/helpers/acp-streaming-markdown.ts b/packages/app/e2e/support/helpers/acp-streaming-markdown.ts index 0ebe9c2b3d..d7ca2ee183 100644 --- a/packages/app/e2e/support/helpers/acp-streaming-markdown.ts +++ b/packages/app/e2e/support/helpers/acp-streaming-markdown.ts @@ -52,13 +52,11 @@ export async function expectAcpMarkdown(page: Page, count: number): Promise { - await agent.client.sendAgentMessage(agent.agentId, "Render the requested Mermaid diagram."); -} - -export async function expectDiagramWithLabels( - page: Page, - labels: readonly string[], -): Promise { - const { diagram, svg } = renderedDiagram(page); - await expect(diagram).toBeVisible({ timeout: 30_000 }); - await expect(svg).toBeVisible({ timeout: 30_000 }); - for (const label of labels) { - await expect(svg).toContainText(label); - } -} - -export async function expectDiagramRemainsRenderedWhileStreaming( - page: Page, - completion: Promise, -): Promise { - const diagram = page.getByRole("img", { name: DIAGRAM_NAME }).last(); - let turnCompleted = false; - const completed = completion.then(() => { - turnCompleted = true; - return true; - }); - for (;;) { - expect(await diagram.isVisible()).toBe(true); - if (turnCompleted) return completion; - const box = await diagram.boundingBox(); - if (turnCompleted) return completion; - expect(box, "the inline diagram should have measurable layout").not.toBeNull(); - expect( - box?.height, - "the inline diagram should remain tall enough to read", - ).toBeGreaterThanOrEqual(56); - const didComplete = await Promise.race([completed, page.waitForTimeout(16).then(() => false)]); - if (didComplete) return completion; - } -} - -export async function waitForDiagramTurnToComplete(agent: MockAgentWorkspace): Promise { - await agent.client.waitForFinish(agent.agentId, 30_000); -} - -export async function expectCompletedDiagram(page: Page, labels: readonly string[]): Promise { - await expectDiagramWithLabels(page, labels); -} - -export async function reloadConversation(page: Page): Promise { - await page.reload({ waitUntil: "domcontentloaded" }); -} - -const FULLSCREEN_VIEWPORT = "mermaid-fullscreen-viewport"; - -function diagramSvg(page: Page, viewportTestId: string) { - return page - .getByTestId(`${viewportTestId}-canvas`) - .last() - .locator("iframe") - .contentFrame() - .locator("#diagram svg"); -} - -/** - * The viewport toolbar only reveals itself on hover, so the diagram is hovered first. The - * pointer is parked elsewhere beforehand so the hover transition fires even when a previous - * step left the cursor inside the viewport. - */ -export async function openDiagramFullscreen(page: Page): Promise { - await page.mouse.move(0, 0); - await page.getByTestId("mermaid-viewport-canvas").last().hover(); - await page.getByTestId("mermaid-fullscreen").last().click(); -} - -export async function closeDiagramFullscreen(page: Page): Promise { - await page.getByTestId(`${FULLSCREEN_VIEWPORT}-canvas`).hover(); - await page.getByTestId("mermaid-fullscreen-close").click(); -} - -export async function zoomAndPanFullscreenDiagram(page: Page): Promise { - const canvas = page.getByTestId(`${FULLSCREEN_VIEWPORT}-canvas`); - const svg = diagramSvg(page, FULLSCREEN_VIEWPORT); - const initial = await svg.boundingBox(); - expect(initial, "the fullscreen diagram should have measurable layout").not.toBeNull(); - - await page.mouse.move(initial!.x + initial!.width / 2, initial!.y + initial!.height / 2); - await page.mouse.wheel(0, -100); - - const zoomed = await svg.boundingBox(); - expect(zoomed, "the zoomed diagram should have measurable layout").not.toBeNull(); - expect(zoomed!.width).toBeGreaterThan(initial!.width * 1.1); - - const viewport = await canvas.boundingBox(); - expect(viewport, "the fullscreen viewport should have measurable layout").not.toBeNull(); - await page.mouse.move(viewport!.x + viewport!.width / 2, viewport!.y + viewport!.height / 2); - await page.mouse.down(); - await page.mouse.move( - viewport!.x + viewport!.width / 2 - 120, - viewport!.y + viewport!.height / 2, - ); - await page.mouse.up(); - - const panned = await svg.boundingBox(); - expect(panned, "the panned diagram should have measurable layout").not.toBeNull(); - expect(Math.abs(panned!.x - zoomed!.x)).toBeGreaterThan(100); -} - -export async function closeDiagramFullscreenFromOutside(page: Page): Promise { - const canvas = page.getByTestId(`${FULLSCREEN_VIEWPORT}-canvas`); - const viewport = await canvas.boundingBox(); - const svg = await diagramSvg(page, FULLSCREEN_VIEWPORT).boundingBox(); - expect(viewport, "the fullscreen viewport should have measurable layout").not.toBeNull(); - expect(svg, "the fullscreen diagram should have measurable layout").not.toBeNull(); - - const x = viewport!.x + viewport!.width / 2; - const y = - svg!.y + svg!.height < viewport!.y + viewport!.height - 8 - ? viewport!.y + viewport!.height - 4 - : viewport!.y + 4; - await page.mouse.click(x, y); -} - -export async function expectFullscreenDiagram( - page: Page, - labels: readonly string[], -): Promise { - const viewport = page.getByTestId(FULLSCREEN_VIEWPORT); - await expect(viewport).toBeVisible({ timeout: 30_000 }); - const svg = diagramSvg(page, FULLSCREEN_VIEWPORT); - await expect(svg).toBeVisible({ timeout: 30_000 }); - for (const label of labels) { - await expect(svg).toContainText(label); - } -} - -export async function expectFullscreenDiagramClosed(page: Page): Promise { - await expect(page.getByTestId(FULLSCREEN_VIEWPORT)).toHaveCount(0); -} diff --git a/packages/app/e2e/support/helpers/streaming-markdown.ts b/packages/app/e2e/support/helpers/streaming-markdown.ts index e628d80471..91bf9be172 100644 --- a/packages/app/e2e/support/helpers/streaming-markdown.ts +++ b/packages/app/e2e/support/helpers/streaming-markdown.ts @@ -45,11 +45,8 @@ export async function requestStreamingMarkdown(agent: StreamingMarkdownAgent): P export async function expectUnfinishedBold(page: Page): Promise { const message = page.getByTestId("assistant-message").last(); - const bold = message.locator('[data-paseo-markdown-tag="strong"]'); - await expect(bold).toContainText("Bold"); + await expect(message.locator(".enrm-text")).toContainText("Bold"); await expect(message).not.toContainText("stays bold"); - await expect(bold).toHaveCSS("font-weight", "500"); - await expect(message).not.toContainText("*"); } export async function expectUnfinishedLink( @@ -61,7 +58,6 @@ export async function expectUnfinishedLink( await agent.stream.showThrough("**Bold text stays bold** and [Paseo docs"); await expect(message).toContainText("Paseo docs"); await expect(message.getByRole("link", { name: "Paseo docs" })).toHaveCount(0); - await expect(message).not.toContainText("["); await expect(message).not.toContainText("https:"); await captureMarkdown(page, testInfo, "unfinished-link"); } @@ -95,8 +91,5 @@ async function expectCompletedMarkdown(page: Page): Promise { await expect( message.getByRole("link", { name: "Paseo docs" }).and(message.locator("a")), ).toHaveAttribute("href", "https://example.com/documentation"); - await expect(message.locator('[data-paseo-markdown-tag="strong"]')).toHaveCSS( - "font-weight", - "500", - ); + await expect(message.locator("strong")).toHaveCSS("font-weight", "700"); } diff --git a/packages/app/package.json b/packages/app/package.json index 912dabc61a..9d0d90feec 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -104,7 +104,9 @@ "i18next": "^26.3.0", "lucide-react-native": "^1.50.0", "markdown-it": "^10.0.0", + "mdast-util-from-markdown": "2.0.3", "mermaid": "^11.16.0", + "micromark-util-decode-string": "2.0.1", "mnemonic-id": "^3.2.7", "qrcode": "^1.5.4", "react": "19.1.0", @@ -114,16 +116,14 @@ "react-native": "0.81.5", "react-native-draggable-flatlist": "^4.0.3", "react-native-edge-to-edge": "^1.7.0", - "react-native-enriched-markdown": "1.1.1", + "react-native-enriched-markdown": "1.2.0-nightly-20261014-766fc0e67", "react-native-gesture-handler": "~2.28.0", "react-native-keyboard-controller": "1.21.12", - "react-native-markdown-display": "^7.0.2", "react-native-nitro-modules": "0.35.5", "react-native-reanimated": "~4.3.1", "react-native-safe-area-context": "~5.6.0", "react-native-screens": "~4.16.0", "react-native-svg": "^15.14.0", - "react-native-uitextview": "^2.2.0", "react-native-unistyles": "^3.2.4", "react-native-web": "~0.21.0", "react-native-webview": "^13.16.0", @@ -167,8 +167,28 @@ }, "enriched-markdown": { "enableMath": false, - "enableCodeHighlight": false, - "codeHighlightLanguages": [], + "enableCodeHighlight": true, + "codeHighlightLanguages": [ + "json", + "html", + "css", + "markdown", + "yaml", + "go", + "java", + "javascript", + "python", + "c", + "rust", + "bash", + "typescript", + "tsx", + "cpp", + "swift", + "php", + "ruby", + "c-sharp" + ], "enableVideo": false }, "reanimated": { diff --git a/packages/app/src/agent-stream/chat-find/ranges.browser.test.ts b/packages/app/src/agent-stream/chat-find/ranges.browser.test.ts index c1d84c1b42..631b8f1d27 100644 --- a/packages/app/src/agent-stream/chat-find/ranges.browser.test.ts +++ b/packages/app/src/agent-stream/chat-find/ranges.browser.test.ts @@ -65,3 +65,14 @@ it("matches whitespace across text nodes without inspecting hidden link destinat "world\nline", ]); }); + +it("keeps semantic Markdown paragraphs separate while finding inline matches", () => { + const row = content( + "

İ😀hello world; hello world.

another paragraph

", + ); + expect(findRenderedMatches(row, "hello world").map((range) => range.toString())).toEqual([ + "hello world", + "hello world", + ]); + expect(findRenderedMatches(row, ".another")).toEqual([]); +}); diff --git a/packages/app/src/agent-stream/chat-find/ranges.web.ts b/packages/app/src/agent-stream/chat-find/ranges.web.ts index 91ed7bff6a..ea2e901c2d 100644 --- a/packages/app/src/agent-stream/chat-find/ranges.web.ts +++ b/packages/app/src/agent-stream/chat-find/ranges.web.ts @@ -1,10 +1,10 @@ const BLOCKS = ["p", "pre", "li", "td", "th", "h1", "h2", "h3", "h4", "h5", "h6"] - .map((tag) => `[data-paseo-markdown-tag="${tag}"]`) + .flatMap((tag) => [tag, `[data-paseo-markdown-tag="${tag}"]`]) .join(","); const IGNORED = '[data-paseo-markdown-ignore="true"], [aria-hidden="true"], button, [role="button"], svg, script, style'; -/** The rows of one message, in reading order. Each is one Markdown block. */ +/** Rendered rows carrying a source message identity, in reading order. */ export function findMessageRows(root: HTMLElement | null, messageId: string): HTMLElement[] { if (!root) return []; return Array.from( diff --git a/packages/app/src/agent-stream/chat-find/viewport.browser.test.ts b/packages/app/src/agent-stream/chat-find/viewport.browser.test.ts index 2bd1e291b4..a7cf29994c 100644 --- a/packages/app/src/agent-stream/chat-find/viewport.browser.test.ts +++ b/packages/app/src/agent-stream/chat-find/viewport.browser.test.ts @@ -98,3 +98,23 @@ function highlightedRow(): string | undefined { ); return row?.dataset.historyRowId; } + +it("waits for an asynchronous renderer before counting its matches", async () => { + const root = transcript("message-1", ["needle"]); + const content = root.querySelector('[data-message-text="true"]')!; + content.setAttribute("aria-busy", "true"); + let settled = false; + const reveal = viewport(root, {}) + .reveal("message-1", "needle", 0, new AbortController().signal) + .then((result) => { + settled = true; + return result; + }); + await new Promise((resolve) => + requestAnimationFrame(() => requestAnimationFrame(() => resolve())), + ); + expect(settled).toBe(false); + content.textContent = "needle and another needle"; + content.removeAttribute("aria-busy"); + expect(await reveal).toEqual({ occurrence: 0, count: 2 }); +}); diff --git a/packages/app/src/agent-stream/chat-find/viewport.web.ts b/packages/app/src/agent-stream/chat-find/viewport.web.ts index 6bbb3a704b..4fef1f6abc 100644 --- a/packages/app/src/agent-stream/chat-find/viewport.web.ts +++ b/packages/app/src/agent-stream/chat-find/viewport.web.ts @@ -21,8 +21,7 @@ export function createFindViewport({ clear() { CSS.highlights.delete(highlightName); }, - // A message renders as one row per Markdown block, so an occurrence can sit in - // any of them and the chosen one decides where the viewport lands. + // The source message owns navigation; the rendered occurrence owns its position. reveal(messageId, query, occurrence, signal) { return new Promise((resolve, reject) => { let frame = 0; @@ -61,6 +60,10 @@ export function createFindViewport({ frame = requestAnimationFrame(poll); return; } + if (rows.some((row) => row.querySelector('[aria-busy="true"]'))) { + frame = requestAnimationFrame(poll); + return; + } const ranges = findMessageMatches(getRoot(), messageId, query); const index = occurrence < 0 ? ranges.length - 1 : Math.min(occurrence, ranges.length - 1); diff --git a/packages/app/src/agent-stream/footer-spacing.test.ts b/packages/app/src/agent-stream/footer-spacing.test.ts index a2414fff5c..5fa3535bce 100644 --- a/packages/app/src/agent-stream/footer-spacing.test.ts +++ b/packages/app/src/agent-stream/footer-spacing.test.ts @@ -49,17 +49,6 @@ function assistantMessage(id: string): Extract { - return { - ...assistantMessage(id), - blockGroupId: "response", - blockIndex, - }; -} - function timingFor(assistantId: string): Map { return new Map([ [ @@ -109,30 +98,4 @@ describe("turn footer spacing", () => { expect(assistantLayout?.assistantSpacing).toBe("compactBottom"); }, ); - - it.each([ - ["web", false], - ["web", true], - ["native", false], - ["native", true], - ] as const)( - "compacts both edges of the final split paragraph on %s when active=%s", - (platform, isTurnActive) => { - const strategy = strategyFor(platform); - const first = assistantBlock("first", 0); - const last = assistantBlock("last", 1); - const layout = layoutStream({ - strategy, - isTurnActive, - history: strategy.orderTail([userMessage("user"), first]), - liveHead: strategy.orderHead([last]), - timingByAssistantId: timingFor(last.id), - }); - const firstLayout = layout.history.find((item) => item.item.id === first.id); - const lastLayout = layout.liveHead.find((item) => item.item.id === last.id); - - expect(firstLayout?.assistantSpacing).toBe("compactBottom"); - expect(lastLayout?.assistantSpacing).toBe("compactBoth"); - }, - ); }); diff --git a/packages/app/src/agent-stream/layout.test.ts b/packages/app/src/agent-stream/layout.test.ts index 3ab2a9f8f0..f10fde2cb1 100644 --- a/packages/app/src/agent-stream/layout.test.ts +++ b/packages/app/src/agent-stream/layout.test.ts @@ -25,7 +25,7 @@ function userMessage(id: string, seed: number): Extract { return { @@ -33,7 +33,6 @@ function assistantMessage( id, text: id, timestamp: timestamp(seed), - ...(block ? { blockGroupId: block.groupId, blockIndex: block.index } : {}), ...(turnId ? { turnId } : {}), }; } @@ -318,7 +317,7 @@ describe("layoutStream", () => { expect(assistantRow.frameOrder).toBe("content-then-footer"); }); - it("compacts assistant block spacing across the history and live-head boundary", () => { + it("keeps separate messages spaced across history and the live head", () => { const historyBlock = assistantMessage("turn:block:0", 2, { groupId: "turn", index: 0 }); const headBlock = assistantMessage("turn:head", 3, { groupId: "turn", index: 1 }); const layout = layoutFor({ @@ -328,8 +327,8 @@ describe("layoutStream", () => { timingIds: [historyBlock.id, headBlock.id], }); - expect(findLayoutItem(layout, historyBlock.id).assistantSpacing).toBe("compactBottom"); - expect(findLayoutItem(layout, headBlock.id).assistantSpacing).toBe("compactBoth"); + expect(findLayoutItem(layout, historyBlock.id).assistantSpacing).toBe("default"); + expect(findLayoutItem(layout, headBlock.id).assistantSpacing).toBe("compactBottom"); }); it.each(["web", "android"] as const)( diff --git a/packages/app/src/agent-stream/layout.ts b/packages/app/src/agent-stream/layout.ts index d460c1fa2e..82ef98db54 100644 --- a/packages/app/src/agent-stream/layout.ts +++ b/packages/app/src/agent-stream/layout.ts @@ -1,6 +1,6 @@ import type { TurnTiming } from "@/timeline/turn-time"; import type { StreamItem } from "@/types/stream"; -import { getAssistantBlockSpacing, getGapBetweenStreamItems } from "./spacing"; +import { getAssistantMessageSpacing, getGapBetweenStreamItems } from "./spacing"; import type { StreamFrameChildOrder, StreamStrategy } from "./strategy"; import { continuesResponse, continuesTurn, isResponseBoundary } from "./turn-membership"; @@ -311,10 +311,8 @@ function layoutSegmentItem( boundaryAboveItems: input.boundaryAboveItems, boundaryAboveIndex: input.boundaryAboveIndex, }); - const assistantSpacing = getAssistantBlockSpacing({ + const assistantSpacing = getAssistantMessageSpacing({ item, - aboveItem, - belowItem, hasFooterBelow: completedFooter !== null || (input.hasAuxiliaryFooter && belowItem === null), }); diff --git a/packages/app/src/agent-stream/presentation.test.ts b/packages/app/src/agent-stream/presentation.test.ts index d85338cfd2..dcb52fd24b 100644 --- a/packages/app/src/agent-stream/presentation.test.ts +++ b/packages/app/src/agent-stream/presentation.test.ts @@ -1,8 +1,5 @@ -import { QueryClient } from "@tanstack/react-query"; -import { ASSISTANT_MESSAGE_RENDER_CHARACTER_LIMIT } from "@/components/assistant-message-render-limit"; -import { describe, expect, it } from "vitest"; -import type { AgentStreamEventPayload } from "@getpaseo/protocol/messages"; import { runPluginClientBundle, type PluginClientRuntime } from "@/plugins/evaluate"; +import { transformTimelineItem, type TimelineItemTransform } from "@/plugins/timeline/model"; import type { InstalledPlugin } from "@/plugins/types"; import { applyStreamEvent, @@ -11,9 +8,11 @@ import { type ToolCallItem, type UserMessageItem, } from "@/types/stream"; -import { transformTimelineItem, type TimelineItemTransform } from "@/plugins/timeline/model"; -import { createStreamPresentation, getStreamItemMessageId } from "./presentation"; +import type { AgentStreamEventPayload } from "@getpaseo/protocol/messages"; +import { QueryClient } from "@tanstack/react-query"; +import { describe, expect, it } from "vitest"; import { buildAgentStreamRenderModel } from "./model"; +import { createStreamPresentation, getStreamItemMessageId } from "./presentation"; const runtime = { paseo: {}, @@ -124,326 +123,22 @@ function rows(result: { tail: StreamItem[]; head: StreamItem[] }): StreamItem[] return [...result.tail, ...result.head]; } -describe("native prose runs", () => { - it("keeps an oversized history block on the existing block renderer", () => { - const text = "a".repeat(ASSISTANT_MESSAGE_RENDER_CHARACTER_LIMIT + 1); - const source = hydrateStreamState([{ event: assistant(text), timestamp: new Date(1000) }]); - const result = createStreamPresentation({ markdown: "prose-runs" })({ - ...presentationOptions, - tail: source, - head: [], - transform: undefined, - }); - expect(result.tail).toHaveLength(1); - expect(result.tail[0]).toMatchObject({ text, markdownKind: "block" }); - expect(result.tail[0]).not.toHaveProperty("markdownProseBlocks", expect.anything()); - }); - - it("keeps a normal history paragraph as selectable prose", () => { - const text = "A normal paragraph."; - const source = hydrateStreamState([{ event: assistant(text), timestamp: new Date(1000) }]); - const result = createStreamPresentation({ markdown: "prose-runs" })({ - ...presentationOptions, - tail: source, - head: [], - transform: undefined, - }); - expect(result.tail).toHaveLength(1); - expect(result.tail[0]).toMatchObject({ - text, - markdownKind: "prose", - markdownProseBlocks: [text], - }); - }); - - it("retains completed runs and their source arrays while the tail grows", () => { - const present = createStreamPresentation({ markdown: "prose-runs" }); - let state: { tail: StreamItem[]; head: StreamItem[] } = { tail: [], head: [] }; - state = applyStreamEvent({ - ...state, - event: assistant("First.\n\nSecond.\n\n```js\n1\n```\n\nTail."), - timestamp: new Date(1000), - }); - const before = rows(present({ ...presentationOptions, ...state, transform: undefined })); - state = applyStreamEvent({ ...state, event: assistant(" More."), timestamp: new Date(1000) }); - const after = rows(present({ ...presentationOptions, ...state, transform: undefined })); - expect(after[0]).toBe(before[0]); - expect(after[1]).toBe(before[1]); - expect(after[2]).not.toBe(before[2]); - }); - it("does not duplicate retained prose around split images on append", () => { - const present = createStreamPresentation({ markdown: "prose-runs" }); - let state: { tail: StreamItem[]; head: StreamItem[] } = { tail: [], head: [] }; - let combined = ""; - for (const text of ["First.\n\nBefore ![shot](shots/app.png) after.", "\n\nNext.", " More."]) { - combined += text; - state = applyStreamEvent({ ...state, event: assistant(text), timestamp: new Date(1000) }); - const live = rows( - present({ ...presentationOptions, ...state, isTurnActive: false, transform: undefined }), - ); - const history = createStreamPresentation({ markdown: "prose-runs" })({ +describe("whole-message Markdown presentation", () => { + it("keeps paragraphs, fences and quotes in one progressively updated row", () => { + const present = createStreamPresentation(); + const text = "First paragraph.\n\nSecond paragraph.\n\n```ts\nconst n = 1;\n```\n\n> Quote"; + for (const length of [10, 30, text.length]) { + const source = hydrateStreamState([ + { event: assistant(text.slice(0, length)), timestamp: new Date(1000) }, + ]); + const result = present({ ...presentationOptions, - tail: hydrateStreamState([{ event: assistant(combined), timestamp: new Date(1000) }]), - head: [], - transform: undefined, - }); - expect(live).toEqual(history.tail); - } - }); - it.each([ - ["short paragraphs", "A short paragraph.\n\n".repeat(3).slice(0, 50)], - [ - "one growing paragraph", - "Use `foo` with https://example.com/path and `bar`. ".repeat(2).slice(0, 50), - ], - ])("keeps 600 appends near blocks mode: %s", { retry: 2 }, (_name, chunk) => { - function measure(markdown: "blocks" | "prose-runs") { - const present = createStreamPresentation({ markdown }); - let state: { tail: StreamItem[]; head: StreamItem[] } = { tail: [], head: [] }; - const start = performance.now(); - for (let i = 0; i < 600; i++) { - state = applyStreamEvent({ - ...state, - event: assistant(chunk), - timestamp: new Date(1000), - }); - present({ ...presentationOptions, ...state, transform: undefined }); - } - return performance.now() - start; - } - measure("blocks"); - const blocks = measure("blocks"); - const prose = measure("prose-runs"); - expect(prose).toBeLessThan(Math.max(300, blocks * 3)); - expect(prose).toBeLessThan(1500); - }); - it.each([ - "\nText.", - "", - "", - "Text after", - ])("preserves HTML constructs: %s", (text) => { - const source = hydrateStreamState([{ event: assistant(text), timestamp: new Date(1000) }]); - const result = createStreamPresentation({ markdown: "prose-runs" })({ - ...presentationOptions, - tail: source, - head: [], - transform: undefined, - }); - expect(result.tail).toMatchObject([{ text, markdownKind: "block" }]); - }); - it.each(["blocks", "prose-runs"] as const)( - "projects a completed streamed reference definition like fresh history in %s", - (markdown) => { - const present = createStreamPresentation({ markdown }); - let state: { tail: StreamItem[]; head: StreamItem[] } = { tail: [], head: [] }; - for (const text of ["![shot][pic]\n\n[pic]:", " shots/app.png"]) { - state = applyStreamEvent({ ...state, event: assistant(text), timestamp: new Date(1000) }); - present({ ...presentationOptions, ...state, transform: undefined }); - } - const live = present({ - ...presentationOptions, - ...state, - isTurnActive: false, - transform: undefined, - }); - const history = createStreamPresentation({ markdown })({ - ...presentationOptions, - tail: hydrateStreamState([ - { event: assistant("![shot][pic]\n\n[pic]: shots/app.png"), timestamp: new Date(1000) }, - ]), - head: [], - transform: undefined, - }); - expect([...live.tail, ...live.head]).toEqual(history.tail); - }, - ); - it("keeps file URL images in Paseo's renderer", () => { - const source = hydrateStreamState([ - { - event: assistant("Before ![shot](file:///repo/shots/app.png) after."), - timestamp: new Date(1000), - }, - ]); - const result = createStreamPresentation({ markdown: "prose-runs" })({ - ...presentationOptions, - tail: source, - head: [], - transform: undefined, - }); - expect(result.tail).toMatchObject([ - { text: "Before", markdownKind: "prose" }, - { text: "![shot](file:///repo/shots/app.png)", markdownKind: "block" }, - { text: "after.", markdownKind: "prose" }, - ]); - }); - - it("starts another run before combined paragraphs exceed the render cap", () => { - const first = "a".repeat(20_000); - const second = "b".repeat(20_000); - const present = createStreamPresentation({ markdown: "prose-runs" }); - let state: { tail: StreamItem[]; head: StreamItem[] } = { tail: [], head: [] }; - for (const text of [first, "\n\n" + second.slice(0, 10_000), second.slice(10_000)]) { - state = applyStreamEvent({ ...state, event: assistant(text), timestamp: new Date(1000) }); - present({ ...presentationOptions, ...state, transform: undefined }); - } - const live = present({ - ...presentationOptions, - ...state, - isTurnActive: false, - transform: undefined, - }); - expect([...live.tail, ...live.head]).toMatchObject([ - { id: "message-1:run:0", text: first }, - { id: "message-1:run:1", text: second }, - ]); - const source = hydrateStreamState([ - { event: assistant(`${first}\n\n${second}`), timestamp: new Date(1000) }, - ]); - expect( - present({ ...presentationOptions, tail: source, head: [], transform: undefined }).tail, - ).toMatchObject([{ text: first }, { text: second }]); - }); - - it.each([ - "![Screenshot][pic]\n\n[pic]: shots/app.png", - "**before ![alt](file.png) after**", - "# heading ![alt](file.png) after", - "[before ![alt](file.png) after](https://example.com)", - "~~before ![alt](file.png) after~~", - "- parent\n - before ![x](shots/app.png) after\n - next", - ])("keeps image context in Paseo's block renderer: %s", (text) => { - const source = hydrateStreamState([{ event: assistant(text), timestamp: new Date(1000) }]); - const result = createStreamPresentation({ markdown: "prose-runs" })({ - ...presentationOptions, - tail: source, - head: [], - transform: undefined, - }); - expect(result.tail).toMatchObject([{ text, markdownKind: "block" }]); - expect(result.tail).toHaveLength(1); - }); - - it("splits inline workspace images out of prose without mistaking code for images", () => { - const source = hydrateStreamState([ - { - event: assistant("Before `![literal](code.png)` ![Screenshot](shots/app.png) after."), - timestamp: new Date(1000), - }, - ]); - const result = createStreamPresentation({ markdown: "prose-runs" })({ - ...presentationOptions, - tail: source, - head: [], - transform: undefined, - }); - expect(result.tail).toMatchObject([ - { text: "Before `![literal](code.png)`", markdownKind: "prose" }, - { text: "![Screenshot](shots/app.png)", markdownKind: "block" }, - { text: "after.", markdownKind: "prose" }, - ]); - }); - - it.each([ - { middle: "```ts\nconst a = 1;\n```", kind: "block" }, - { middle: "> Quoted paragraph.", kind: "quote" }, - { middle: "![Image](image.png)", kind: "block" }, - ])("keeps $kind between prose in a separate row", ({ middle, kind }) => { - const source = hydrateStreamState([ - { event: assistant(`First.\n\n${middle}\n\nLast.`), timestamp: new Date(1000) }, - ]); - const result = createStreamPresentation({ markdown: "prose-runs" })({ - ...presentationOptions, - tail: source, - head: [], - transform: undefined, - }); - expect(result.tail).toMatchObject([ - { id: `${source[0]!.id}:run:0`, text: "First.", markdownKind: "prose" }, - { id: `${source[0]!.id}:block:1`, text: middle, markdownKind: kind }, - { id: `${source[0]!.id}:run:2`, text: "Last.", markdownKind: "prose" }, - ]); - }); - - it.each(["> Quoted text.", "- List item", "### Heading", "Plain paragraph."])( - "classifies the final block at turn completion: %s", - (text) => { - const present = createStreamPresentation({ markdown: "prose-runs" }); - const head = hydrateStreamState([{ event: assistant(text), timestamp: new Date(1000) }]); - expect( - present({ ...presentationOptions, tail: [], head, transform: undefined }).head, - ).toMatchObject([{ markdownKind: "block" }]); - const completed = present({ - ...presentationOptions, - isTurnActive: false, tail: [], - head, + head: source, transform: undefined, }); - const history = createStreamPresentation({ markdown: "prose-runs" })({ - ...presentationOptions, - tail: head, - head: [], - transform: undefined, - }); - expect(rows(completed)).toEqual(history.tail); - }, - ); - it("keeps the growing block separate and merges completed blocks into a stable run", () => { - const present = createStreamPresentation({ markdown: "prose-runs" }); - let state: { tail: StreamItem[]; head: StreamItem[] } = { tail: [], head: [] }; - function send(text: string) { - state = applyStreamEvent({ ...state, event: assistant(text), timestamp: new Date(1000) }); - return present({ ...presentationOptions, ...state, transform: undefined }); + expect(rows(result)).toEqual(source); } - expect(send("First.").head).toMatchObject([{ id: "message-1:block:0", markdownKind: "block" }]); - const second = send("\n\nSecond."); - expect(second.tail).toMatchObject([ - { id: "message-1:run:0", text: "First.", markdownKind: "prose" }, - ]); - expect(second.head).toMatchObject([ - { id: "message-1:block:1", text: "Second.", markdownKind: "block" }, - ]); - const growing = send(" More."); - expect(growing.tail[0]).toBe(second.tail[0]); - const third = send("\n\nThird."); - expect(third.tail).toMatchObject([{ id: "message-1:run:0", text: "First.\n\nSecond. More." }]); - const completed = present({ - ...presentationOptions, - ...state, - isTurnActive: false, - transform: undefined, - }); - const history = createStreamPresentation({ markdown: "prose-runs" })({ - ...presentationOptions, - tail: [...state.tail, ...state.head], - head: [], - transform: undefined, - }); - expect(rows(completed)).toEqual(history.tail); - expect(rows(completed)).toMatchObject([ - { id: "message-1:run:0", text: "First.\n\nSecond. More.\n\nThird." }, - ]); - }); - - it("keeps consecutive paragraphs in one selectable row", () => { - const source = hydrateStreamState([ - { event: assistant("First paragraph.\n\nSecond paragraph."), timestamp: new Date(1000) }, - ]); - const result = createStreamPresentation({ markdown: "prose-runs" })({ - ...presentationOptions, - tail: source, - head: [], - transform: undefined, - }); - expect(result.tail).toMatchObject([ - { - id: `${source[0]!.id}:run:0`, - text: "First paragraph.\n\nSecond paragraph.", - markdownKind: "prose", - blockGroupId: source[0]!.id, - }, - ]); }); }); @@ -522,26 +217,17 @@ describe("stream presentation through installed plugins", () => { { text: "Header\n\nBody", phase: "streaming" }, ]); expect(rows(harness.render(transform))).toHaveLength(1); - expect(rows(harness.render()).map((item) => item.kind)).toEqual([ - "assistant_message", - "assistant_message", - ]); + expect(rows(harness.render()).map((item) => item.kind)).toEqual(["assistant_message"]); }); - it("keeps completed native blocks stable while the last block grows and finishes", () => { + it("retains the complete message when streaming finishes", () => { const harness = streamHarness(); - const first = harness.send(assistant("Intro\n\n![Image](image.png)")); - const growing = harness.send(assistant("\n\nAfter\n")); - const last = harness.send(assistant("- item")); + harness.send(assistant("Intro\n\n![Image](image.png)")); + const last = harness.send(assistant("\n\nAfter\n- item")); const completed = harness.send({ type: "turn_completed", provider: "claude" }); - expect(first.tail).toMatchObject([{ text: "Intro", blockIndex: 0 }]); - expect(first.head).toMatchObject([{ text: "![Image](image.png)", blockIndex: 1 }]); - expect(growing.tail[0]).toBe(first.tail[0]); - expect(growing.tail[1]).toBe(first.head[0]); - expect(last.tail).toBe(growing.tail); - expect(last.head).toMatchObject([{ text: "After\n- item", blockIndex: 2 }]); - expect(completed.tail).toEqual(rows(last)); - expect(completed.tail[2]).toBe(last.head[0]); + expect(last.tail).toEqual([]); + expect(last.head).toMatchObject([{ text: "Intro\n\n![Image](image.png)\n\nAfter\n- item" }]); + expect(completed.tail).toEqual(last.head); expect(completed.head).toEqual([]); }); @@ -549,8 +235,8 @@ describe("stream presentation through installed plugins", () => { const harness = streamHarness(); harness.send(assistant("Intro\n\n```ts\nconst a = 1;")); const result = harness.send(assistant("\n\nconst b = 2;")); - expect(result.tail).toMatchObject([{ text: "Intro" }]); - expect(result.head).toMatchObject([{ text: "```ts\nconst a = 1;\n\nconst b = 2;" }]); + expect(result.tail).toEqual([]); + expect(result.head).toMatchObject([{ text: "Intro\n\n```ts\nconst a = 1;\n\nconst b = 2;" }]); }); // Codex streams a line break and the next line's indent as their own chunks. @@ -568,14 +254,13 @@ describe("stream presentation through installed plugins", () => { // which is what lets find, scroll-to-message and history reveal address it by id. // A link reference definition stays with the paragraph that uses it, so the split // never leaves an empty row behind and the reference still resolves. - it("splits an assistant message into the same blocks through history and through the live head", () => { + it("preserves source identity through history and the live head", () => { const source = hydrateStreamState([ { event: assistant("[Link][docs]\n\n[docs]: https://example.com\n\nClosing paragraph."), timestamp: new Date(1000), }, ]); - const messageId = source[0]!.id; const present = (placement: "tail" | "head") => rows( createStreamPresentation()({ @@ -585,20 +270,7 @@ describe("stream presentation through installed plugins", () => { transform: undefined, }), ); - expect(present("tail")).toMatchObject([ - { - id: `${messageId}:block:0`, - blockGroupId: messageId, - blockIndex: 0, - text: "[Link][docs]\n\n[docs]: https://example.com", - }, - { - id: `${messageId}:block:1`, - blockGroupId: messageId, - blockIndex: 1, - text: "Closing paragraph.", - }, - ]); + expect(present("tail")).toEqual(source); const blocks = (items: StreamItem[]) => items.map((item) => [item.id, item.kind === "assistant_message" ? item.text : null]); expect(blocks(present("head"))).toEqual(blocks(present("tail"))); @@ -606,7 +278,7 @@ describe("stream presentation through installed plugins", () => { // Rows are addressed by id from outside presentation, so the same text has to land on // the same ids whether the reader watched it arrive or opened the chat afterwards. - it("gives a streamed message the block ids its hydrated text would have had", () => { + it("gives a streamed message the same id as its hydrated text", () => { const text = "Intro paragraph.\n\n# Heading\n\n- first\n- second\n\nClosing paragraph."; const harness = streamHarness(); for (let end = 8; end < text.length; end += 8) { @@ -628,7 +300,7 @@ describe("stream presentation through installed plugins", () => { ); }); - it("repoints a block at the canonical source that replaced the streamed one", () => { + it("uses the canonical source that replaced the streamed message", () => { const harness = streamHarness(); harness.send(assistant("Streamed answer")); const streamed = rows(harness.render()); @@ -662,15 +334,8 @@ describe("stream presentation through installed plugins", () => { head: [], transform: undefined, }); - expect(result.tail).toMatchObject([ - { - id: `${messageId}:block:0`, - blockGroupId: messageId, - blockIndex: 0, - text: "Only one paragraph.", - }, - ]); - expect(result.tail[0]!.id).not.toBe(messageId); + expect(result.tail).toEqual(source); + expect(result.tail[0]!.id).toBe(messageId); }); it("continues to stream inline reasoning with a stable plugin row", () => { @@ -887,13 +552,7 @@ describe("timeline presentation", () => { text: "ExampleExplain this XML.", }, ]; - expect(projectTimelineItems(items)).toEqual( - items.map((item) => - item.kind === "assistant_message" - ? { ...item, id: `${item.id}:block:0`, blockGroupId: item.id, blockIndex: 0 } - : item, - ), - ); + expect(projectTimelineItems(items)).toEqual(items); expect(projectTimelineItems(items)[0]).toBe(items[0]); }); diff --git a/packages/app/src/agent-stream/presentation.ts b/packages/app/src/agent-stream/presentation.ts index 390e52c7f7..4459486a7b 100644 --- a/packages/app/src/agent-stream/presentation.ts +++ b/packages/app/src/agent-stream/presentation.ts @@ -1,15 +1,12 @@ -import type { AssistantMessageItem, StreamItem, UserMessageItem } from "@/types/stream"; import type { TimelineItemTransform } from "@/plugins/timeline/model"; import { projectPluginTimelineItems } from "@/plugins/timeline/projection"; -import { splitMarkdownBlocks } from "@/utils/split-markdown-blocks"; import { prepareToolCallHistory, projectToolCallDetailLevel, type PreparedToolCallHistory, type ToolCallDetailLevel, } from "@/tool-calls/detail-level/projection"; - -import { groupMarkdownProse } from "@/components/markdown-prose"; +import type { StreamItem, UserMessageItem } from "@/types/stream"; interface PresentationInput { tail: StreamItem[]; @@ -19,47 +16,14 @@ interface PresentationInput { isTurnActive: boolean; } -function retainItems(previous: StreamItem[], next: StreamItem[]): StreamItem[] { - return previous.length === next.length && previous.every((item, index) => item === next[index]) - ? previous - : next; -} - -/** - * The source message a display row belongs to. Every assistant message is split into - * Markdown blocks, so an assistant row id never equals its message id; anything that - * addresses a message — find, scroll-to-message, history reveal — asks for this. A - * plugin row a timeline transformer projected answers for the item it replaced, whose - * id is what the chat outline and find hold. - */ +/** Find and scrolling address a plugin row by the source it replaces. */ export function getStreamItemMessageId(item: StreamItem): string { - if (item.kind === "assistant_message") return item.blockGroupId ?? item.id; if (item.kind === "plugin") return item.sourceId ?? item.id; return item.id; } -/** - * A block is reusable only when it still stands for the same source state. Text and - * turn are what the reader sees; cursor and timestamp are what the timeline reads back - * through the row — a reused block carrying a pre-canonical cursor reports a stale - * reading position and weakens fork-boundary resolution. - */ -function isReusableBlock(block: AssistantMessageItem, source: AssistantMessageItem, id: string) { - return ( - block.id === id && - block.turnId === source.turnId && - block.timestamp.getTime() === source.timestamp.getTime() && - block.timelineCursor?.epoch === source.timelineCursor?.epoch && - block.timelineCursor?.seq === source.timelineCursor?.seq - ); -} - -/** Source messages reach plugins before any Markdown splitting or Overview grouping. */ -export function createStreamPresentation({ - markdown = "blocks", -}: { markdown?: "blocks" | "prose-runs" } = {}) { - const runsBySource = new WeakMap(); - const growingRunsBySource = new WeakMap(); +/** Source messages reach plugins before display projection. */ +export function createStreamPresentation() { const userMessageCache = new WeakMap(); function presentUserMessage(item: UserMessageItem): UserMessageItem { @@ -77,95 +41,20 @@ export function createStreamPresentation({ return projected; } - const blocksBySource = new WeakMap(); - let liveSources = new Map(); let historySource: StreamItem[] | undefined; let historyTransform: TimelineItemTransform | undefined; let historyRows: StreamItem[] = []; - let displayTail: StreamItem[] = []; - let displayHistory: StreamItem[] | undefined; - let promotedRows: StreamItem[] = []; let preparedTail: StreamItem[] | undefined; let preparedLevel: ToolCallDetailLevel | undefined; let preparedHistory: PreparedToolCallHistory | null = null; - /** - * One display row per Markdown block. Each block renders on its own, so a construct - * that needs context from another block does not resolve: `splitMarkdownBlocks` keeps - * link reference definitions with the block that uses them, but a reference pointing - * at a definition several blocks away renders as literal text. Streamed messages - * always behaved this way; history now matches them. - */ - function nativeBlocks(item: StreamItem): StreamItem[] { - if (item.kind === "user_message") return [presentUserMessage(item)]; - if (item.kind !== "assistant_message") return [item]; - const cached = blocksBySource.get(item); - if (cached) return cached; - const previousSource = liveSources.get(item.id); - const previous = previousSource && blocksBySource.get(previousSource); - const isAppend = previousSource && item.text.startsWith(previousSource.text); - let prefix: AssistantMessageItem[] = []; - let growingText = item.text; - if (isAppend && previous) { - prefix = previous.slice(0, -1); - growingText = - previous[previous.length - 1]!.text + item.text.slice(previousSource.text.length); - } - let parsed = splitMarkdownBlocks(growingText); - // A definition completed in the last block can now belong to the preceding block. - if (prefix.length && /^\s{0,3}\[[^\]]+\]:/.test(parsed[0] ?? growingText)) { - prefix = []; - parsed = splitMarkdownBlocks(item.text); - } - const textBlocks = parsed.length + prefix.length > 0 ? parsed : [""]; - const blocks = [...prefix]; - for (const [offset, text] of textBlocks.entries()) { - const index = prefix.length + offset; - let blockText = text; - if (offset === textBlocks.length - 1) { - // The split drops trailing blank lines, including a line break followed only - // by the next line's indent. Keep them so the next append continues that line. - const trailingBlankLines = /\n\s*$/.exec(item.text)?.[0] ?? ""; - blockText += trailingBlankLines; - } - const existing = previous?.[index]; - const id = `${item.id}:block:${index}`; - if (existing?.text === blockText && isReusableBlock(existing, item, id)) { - blocks.push(existing); - continue; - } - blocks.push({ - ...item, - id, - blockGroupId: item.id, - blockIndex: index, - text: blockText, - }); - } - blocksBySource.set(item, blocks); - return blocks; - } - - function presentRows(item: StreamItem, growing = false): StreamItem[] { - const blocks = nativeBlocks(item); - if (markdown === "blocks" || item.kind !== "assistant_message") return blocks; - const cache = growing ? growingRunsBySource : runsBySource; - const cached = cache.get(item); - if (cached) return cached; - const previousSource = liveSources.get(item.id); - const previousBlocks = previousSource && blocksBySource.get(previousSource); - // nativeBlocks emits only assistant blocks for an assistant source. - const runs = groupMarkdownProse(blocks as AssistantMessageItem[], previousBlocks, growing); - cache.set(item, runs); - return runs; + function presentRows(item: StreamItem): StreamItem[] { + return [item.kind === "user_message" ? presentUserMessage(item) : item]; } return (input: PresentationInput) => { // Retained history is not reprojected or regrouped on each live text update. if (historySource !== input.tail || historyTransform !== input.transform) { - // One rendering path: history splits the same way the live head does, and - // `blocksBySource` keeps both the live block identities and the split cost - // from being paid again when only the tail's array identity changed. historyRows = projectPluginTimelineItems(input.tail, input.transform).flatMap( (item) => presentRows(item), ); @@ -173,34 +62,18 @@ export function createStreamPresentation({ historyTransform = input.transform; } const head: StreamItem[] = []; - const promoted: StreamItem[] = []; - const nextLiveSources = new Map(); for (const item of projectPluginTimelineItems(input.head, input.transform, "streaming")) { - const blocks = presentRows(item, input.isTurnActive); - if (item.kind === "assistant_message") nextLiveSources.set(item.id, item); - if (item.kind === "assistant_message" && blocks.length > 1) { - promoted.push(...blocks.slice(0, -1)); - head.push(blocks[blocks.length - 1]!); - } else { - head.push(...blocks); - } + const blocks = presentRows(item); + head.push(...blocks); } - liveSources = nextLiveSources; - const nextPromoted = retainItems(promotedRows, promoted); - if (displayHistory !== historyRows || nextPromoted !== promotedRows) { - displayTail = historyRows; - if (nextPromoted.length > 0) displayTail = [...historyRows, ...nextPromoted]; - displayHistory = historyRows; - promotedRows = nextPromoted; - } - if (preparedTail !== displayTail || preparedLevel !== input.level) { - preparedHistory = prepareToolCallHistory(input.level, displayTail); - preparedTail = displayTail; + if (preparedTail !== historyRows || preparedLevel !== input.level) { + preparedHistory = prepareToolCallHistory(input.level, historyRows); + preparedTail = historyRows; preparedLevel = input.level; } return projectToolCallDetailLevel({ level: input.level, - tail: displayTail, + tail: historyRows, head, preparedHistory, isTurnActive: input.isTurnActive, diff --git a/packages/app/src/agent-stream/spacing.test.ts b/packages/app/src/agent-stream/spacing.test.ts index dcf8a36844..07e9f185ff 100644 --- a/packages/app/src/agent-stream/spacing.test.ts +++ b/packages/app/src/agent-stream/spacing.test.ts @@ -1,135 +1,20 @@ import { describe, expect, it } from "vitest"; +import { getAssistantMessageSpacing, getGapBetweenStreamItems } from "./spacing"; import type { StreamItem } from "@/types/stream"; -import { getAssistantBlockSpacing, isSameAssistantBlockGroup } from "./spacing"; - -function assistantBlock(params: { - id: string; - blockGroupId: string; - blockIndex: number; - text?: string; -}): Extract { - return { - kind: "assistant_message", - id: params.id, - blockGroupId: params.blockGroupId, - blockIndex: params.blockIndex, - text: params.text ?? "", - timestamp: new Date("2026-05-01T00:00:00.000Z"), - }; -} - -function toolCallBlock(id: string): Extract { - return { - kind: "tool_call", - id, - timestamp: new Date("2026-05-01T00:00:00.000Z"), - payload: { - source: "orchestrator", - data: { - toolCallId: id, - toolName: "bash", - arguments: "cmd", - result: null, - status: "executing", - }, - }, - }; -} - -describe("isSameAssistantBlockGroup", () => { - it("returns true for two assistant blocks with the same blockGroupId", () => { - const a = assistantBlock({ id: "a", blockGroupId: "group-1", blockIndex: 0 }); - const b = assistantBlock({ id: "b", blockGroupId: "group-1", blockIndex: 1 }); - expect(isSameAssistantBlockGroup({ item: a, other: b })).toBe(true); - }); - - it("returns false for blocks from different groups", () => { - const a = assistantBlock({ id: "a", blockGroupId: "group-1", blockIndex: 0 }); - const b = assistantBlock({ id: "b", blockGroupId: "group-2", blockIndex: 0 }); - expect(isSameAssistantBlockGroup({ item: a, other: b })).toBe(false); - }); - - it("returns false when one item is not an assistant_message", () => { - const a = assistantBlock({ id: "a", blockGroupId: "group-1", blockIndex: 0 }); - const tc = toolCallBlock("tc-1"); - expect(isSameAssistantBlockGroup({ item: a, other: tc })).toBe(false); - }); - - it("returns false for null neighbors", () => { - const a = assistantBlock({ id: "a", blockGroupId: "group-1", blockIndex: 0 }); - expect(isSameAssistantBlockGroup({ item: a, other: null })).toBe(false); - }); -}); - -describe("getAssistantBlockSpacing", () => { - it("returns default for non-assistant items", () => { - const tc = toolCallBlock("tc-1"); - expect(getAssistantBlockSpacing({ item: tc, aboveItem: null, belowItem: null })).toBe( - "default", - ); - }); - - it("returns default when no same-group neighbors exist", () => { - const a = assistantBlock({ id: "a", blockGroupId: "group-1", blockIndex: 0 }); - expect(getAssistantBlockSpacing({ item: a, aboveItem: null, belowItem: null })).toBe("default"); - }); - - it("compacts the bottom edge when the turn footer follows the assistant block", () => { - const a = assistantBlock({ id: "a", blockGroupId: "group-1", blockIndex: 0 }); - expect( - getAssistantBlockSpacing({ - item: a, - aboveItem: null, - belowItem: null, - hasFooterBelow: true, - }), - ).toBe("compactBottom"); - }); - - it("returns compactTop when the item above is in the same block group", () => { - const above = assistantBlock({ id: "above", blockGroupId: "group-1", blockIndex: 0 }); - const item = assistantBlock({ id: "item", blockGroupId: "group-1", blockIndex: 1 }); - expect(getAssistantBlockSpacing({ item, aboveItem: above, belowItem: null })).toBe( - "compactTop", - ); - }); - - it("returns compactBottom when the item below is in the same block group", () => { - const item = assistantBlock({ id: "item", blockGroupId: "group-1", blockIndex: 0 }); - const below = assistantBlock({ id: "below", blockGroupId: "group-1", blockIndex: 1 }); - expect(getAssistantBlockSpacing({ item, aboveItem: null, belowItem: below })).toBe( +const message: StreamItem = { + kind: "assistant_message", + id: "m", + text: "First\n\nSecond", + timestamp: new Date(0), +}; +describe("whole-message spacing", () => { + it("compacts only the gap to its turn footer", () => { + expect(getAssistantMessageSpacing({ item: message })).toBe("default"); + expect(getAssistantMessageSpacing({ item: message, hasFooterBelow: true })).toBe( "compactBottom", ); }); - - it("returns compactBoth when both neighbors are in the same block group", () => { - const above = assistantBlock({ id: "above", blockGroupId: "group-1", blockIndex: 0 }); - const item = assistantBlock({ id: "item", blockGroupId: "group-1", blockIndex: 1 }); - const below = assistantBlock({ id: "below", blockGroupId: "group-1", blockIndex: 2 }); - expect(getAssistantBlockSpacing({ item, aboveItem: above, belowItem: below })).toBe( - "compactBoth", - ); - }); - - it("spans the history/live-head boundary: tail gets compactBottom, head gets compactTop", () => { - const tailBlock = assistantBlock({ - id: "group-1:block:0", - blockGroupId: "group-1", - blockIndex: 0, - text: "First paragraph", - }); - const headBlock = assistantBlock({ - id: "group-1:head", - blockGroupId: "group-1", - blockIndex: 1, - text: "Second paragraph", - }); - - expect( - getAssistantBlockSpacing({ item: tailBlock, aboveItem: null, belowItem: headBlock }), - ).toBe("compactBottom"); - expect( - getAssistantBlockSpacing({ item: headBlock, aboveItem: tailBlock, belowItem: null }), - ).toBe("compactTop"); + it("keeps separate messages separated", () => { + expect(getGapBetweenStreamItems(message, { ...message, id: "next" })).toBeGreaterThan(0); }); }); diff --git a/packages/app/src/agent-stream/spacing.ts b/packages/app/src/agent-stream/spacing.ts index 87bdf75f88..55761f71f3 100644 --- a/packages/app/src/agent-stream/spacing.ts +++ b/packages/app/src/agent-stream/spacing.ts @@ -1,35 +1,13 @@ import type { StreamItem } from "@/types/stream"; import { SPACING } from "@/styles/theme"; -export function isSameAssistantBlockGroup(params: { - item: StreamItem | null | undefined; - other: StreamItem | null | undefined; -}): boolean { - return ( - params.item?.kind === "assistant_message" && - params.other?.kind === "assistant_message" && - params.item.blockGroupId !== undefined && - params.item.blockGroupId === params.other.blockGroupId - ); -} - -export function getAssistantBlockSpacing(params: { +export function getAssistantMessageSpacing(params: { item: StreamItem; - aboveItem: StreamItem | null | undefined; - belowItem: StreamItem | null | undefined; hasFooterBelow?: boolean; -}): "default" | "compactTop" | "compactBottom" | "compactBoth" { - if (params.item.kind !== "assistant_message") { - return "default"; - } - const compactTop = isSameAssistantBlockGroup({ item: params.item, other: params.aboveItem }); - const compactBottom = - params.hasFooterBelow || - isSameAssistantBlockGroup({ item: params.item, other: params.belowItem }); - if (compactTop && compactBottom) return "compactBoth"; - if (compactTop) return "compactTop"; - if (compactBottom) return "compactBottom"; - return "default"; +}): "default" | "compactBottom" { + return params.item.kind === "assistant_message" && params.hasFooterBelow + ? "compactBottom" + : "default"; } const isUserMessageItem = (item?: StreamItem | null) => item?.kind === "user_message"; @@ -62,8 +40,5 @@ export function getGapBetweenStreamItems( if (isToolSequenceItem(item) && belowItem.kind === "assistant_message") { return SPACING[1]; } - if (isSameAssistantBlockGroup({ item, other: belowItem })) { - return SPACING[3]; - } return SPACING[4]; } diff --git a/packages/app/src/agent-stream/strategy-web.tsx b/packages/app/src/agent-stream/strategy-web.tsx index 226b7c1e2a..150d18326f 100644 --- a/packages/app/src/agent-stream/strategy-web.tsx +++ b/packages/app/src/agent-stream/strategy-web.tsx @@ -307,7 +307,6 @@ function WebStreamViewport(props: StreamRenderInput & { isMobileBreakpoint: bool olderHistoryProgressKey, scrollEnabled, contentMaxWidth, - imageContext, isMobileBreakpoint, } = props; const historyVirtualized = useRevisedHistoryRows( @@ -455,7 +454,7 @@ function WebStreamViewport(props: StreamRenderInput & { isMobileBreakpoint: bool const row = segments.historyVirtualized[index]; const measured = row && measuredRowHeights.current.get(row.id); if (measured && measured.width === contentWidth) return measured.height; - return row ? estimateStreamItemHeight({ item: row, contentMaxWidth, imageContext }) : 120; + return row ? estimateStreamItemHeight({ item: row, contentMaxWidth }) : 120; }, observeElementOffset: observeVirtualOffset, rangeExtractor, diff --git a/packages/app/src/agent-stream/strategy.ts b/packages/app/src/agent-stream/strategy.ts index 3b77f6b831..5f43f8f6b1 100644 --- a/packages/app/src/agent-stream/strategy.ts +++ b/packages/app/src/agent-stream/strategy.ts @@ -1,4 +1,3 @@ -import type { AssistantImageContext } from "@/utils/assistant-image-metadata"; import type { ComponentType, ReactElement, ReactNode, RefObject } from "react"; import type { StyleProp, ViewStyle } from "react-native"; import type { StreamItem } from "@/types/stream"; @@ -95,7 +94,6 @@ export interface StreamRenderInput { forwardListContentContainerStyle: StyleProp; // The chat column's max width. The web viewport estimates unmeasured row heights with it. contentMaxWidth: number; - imageContext?: AssistantImageContext; } export interface ResolveStreamRenderStrategyInput { diff --git a/packages/app/src/agent-stream/view.tsx b/packages/app/src/agent-stream/view.tsx index 683ad0dfd3..18c39f6d31 100644 --- a/packages/app/src/agent-stream/view.tsx +++ b/packages/app/src/agent-stream/view.tsx @@ -535,10 +535,7 @@ const AgentStreamViewComponent = forwardRef createStreamPresentation({ markdown: isWeb ? "blocks" : "prose-runs" }), - [], - ); + const presentStream = useMemo(() => createStreamPresentation(), []); const presentation = useMemo( () => presentStream({ @@ -729,8 +726,6 @@ const AgentStreamViewComponent = forwardRef {(renderFullContent) => ( { ); }); - it("uses local image dimensions only from the row's server and workspace", () => { - clearAssistantImageMetadataCache(); - const imageContext = { serverId: "server-one", workspaceRoot: "/workspace/one" }; - setAssistantImageMetadata( - { source: "portrait.png", ...imageContext }, - { width: 800, height: 1600 }, - ); + it("uses the renderer's whole-message measurement at the measured width", () => { + clearAssistantMessageHeightEstimateCache(); const item: StreamItem = { kind: "assistant_message", - id: "local-image", - text: "![Portrait](portrait.png)", + id: "measured", + text: "First paragraph\n\n![Image](image.png)", timestamp: createTimestamp(2), }; - - expect(estimateStreamItemHeight({ item, contentMaxWidth: 608, imageContext })).toBe(1264); - expect( - estimateStreamItemHeight({ - item, - contentMaxWidth: 608, - imageContext: { ...imageContext, workspaceRoot: "/workspace/two" }, - }), - ).toBe(464); - expect( - estimateStreamItemHeight({ - item, - contentMaxWidth: 608, - imageContext: { ...imageContext, serverId: "server-two" }, - }), - ).toBe(464); - }); - - it("uses cached assistant image metadata when available", () => { - clearAssistantImageMetadataCache(); - setAssistantImageMetadata( - { - source: "https://example.com/tall.png", - }, - { width: 800, height: 1600 }, - ); - - const item: StreamItem = { - kind: "assistant_message", - id: "a-image", - text: "Look at this\n\n![Screenshot](https://example.com/tall.png)", - timestamp: createTimestamp(2), - }; - - expect( - estimateStreamItemHeight({ item, contentMaxWidth: DEFAULT_CONTENT_MAX_WIDTH }), - ).toBeGreaterThan(220); + setAssistantMessageHeight({ markdown: item.text, width: 608, height: 640 }); + expect(estimateStreamItemHeight({ item, contentMaxWidth: 608 })).toBe(640); + expect(estimateStreamItemHeight({ item, contentMaxWidth: 320 })).toBe(220); + clearAssistantMessageHeightEstimateCache(); }); }); diff --git a/packages/app/src/agent-stream/web-virtualization.ts b/packages/app/src/agent-stream/web-virtualization.ts index 0c7f756959..daf01af558 100644 --- a/packages/app/src/agent-stream/web-virtualization.ts +++ b/packages/app/src/agent-stream/web-virtualization.ts @@ -1,4 +1,3 @@ -import type { AssistantImageContext } from "@/utils/assistant-image-metadata"; import type { StreamItem } from "@/types/stream"; import { estimateAssistantMessageHeightFromCache } from "@/utils/assistant-message-height-estimate"; import { @@ -47,13 +46,11 @@ export interface WebVirtualizedHistoryWindow { interface StreamItemHeightEstimateInput { item: StreamItem; contentMaxWidth: number; - imageContext?: AssistantImageContext; } export function estimateStreamItemHeight({ item, contentMaxWidth, - imageContext, }: StreamItemHeightEstimateInput): number { switch (item.kind) { case "user_message": @@ -63,7 +60,6 @@ export function estimateStreamItemHeight({ estimateAssistantMessageHeightFromCache({ markdown: item.text, contentMaxWidth, - imageContext, }) ?? 220 ); case "tool_call": diff --git a/packages/app/src/assistant-file-links/index.ts b/packages/app/src/assistant-file-links/index.ts index 723945409f..c4078ad332 100644 --- a/packages/app/src/assistant-file-links/index.ts +++ b/packages/app/src/assistant-file-links/index.ts @@ -1,9 +1,3 @@ -export { - AssistantInlineCodePathLink, - AssistantMarkdownCodeLink, - AssistantMarkdownLink, -} from "./link"; -export { type AssistantLinkPress, useAssistantLinkPress } from "./link-press-context"; export { classifyAssistantFileLink, normalizeInlinePathTarget, @@ -11,6 +5,7 @@ export { } from "./parse"; export { AssistantFileLinkResolverProvider, + useAssistantFileLinkEnvironment, type AssistantFileLinkResolverProviderProps, } from "./provider"; export type { AssistantFileLinkSource } from "./resolver"; diff --git a/packages/app/src/assistant-file-links/link-press-context.ts b/packages/app/src/assistant-file-links/link-press-context.ts deleted file mode 100644 index 2d6ae19639..0000000000 --- a/packages/app/src/assistant-file-links/link-press-context.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { createContext, useContext } from "react"; -import type { TextProps } from "react-native"; - -// Carries a link's press handler down to the leaf text spans that render its -// label. On iOS an assistant link is a nested UITextView span, and -// react-native-uitextview only attaches onPress to the *string* children it -// converts into RNUITextViewChild nodes (src/Text.tsx) — element children (the -// MarkdownInheritedText spans markdown produces for link text) pass through -// untouched, so an onPress placed on the wrapping span never reaches a tappable -// native node. Threading the handler through context lets each leaf span hand -// onPress to its own string children, where the native tap recognizer can find -// it. Provided only on iOS (Android/web links tap fine via their own paths). -export interface AssistantLinkPress { - onPress: () => void; - accessibilityRole?: TextProps["accessibilityRole"]; -} - -const AssistantLinkPressContext = createContext(null); - -export const AssistantLinkPressProvider = AssistantLinkPressContext.Provider; - -export function useAssistantLinkPress(): AssistantLinkPress | null { - return useContext(AssistantLinkPressContext); -} diff --git a/packages/app/src/assistant-file-links/link.tsx b/packages/app/src/assistant-file-links/link.tsx deleted file mode 100644 index 4b8e944ee3..0000000000 --- a/packages/app/src/assistant-file-links/link.tsx +++ /dev/null @@ -1,213 +0,0 @@ -import { useMemo, type CSSProperties, type MouseEvent, type ReactNode } from "react"; -import { Platform, Text, View, type StyleProp, type TextStyle, type ViewStyle } from "react-native"; -import { StyleSheet } from "react-native-unistyles"; -import { isNative, isWeb } from "@/constants/platform"; -import { MarkdownTextSpan } from "@/components/markdown-text"; -import { MarkdownLinkText } from "@/components/markdown/link-text"; -import { AssistantLinkPressProvider, type AssistantLinkPress } from "./link-press-context"; -import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; -import { CODE_SURFACE_DATASET } from "@/styles/code-surface"; -import { markdownCopyDataSet } from "@/assistant-selection-copy/markup"; -import { useAssistantFileLinkResolverContext } from "./provider"; -import type { AssistantFileLinkSource } from "./resolver"; -import { formatFileLinkTooltipPath } from "./tooltip-path"; -import { useFileLink } from "./use-file-link"; - -interface AssistantMarkdownLinkProps { - source: AssistantFileLinkSource; - style: StyleProp; - monoSurface?: boolean; - children: ReactNode; -} - -const MARKDOWN_CODE_LINK_DATASET = { - ...CODE_SURFACE_DATASET, - ...markdownCopyDataSet.code, -} as const; - -export function AssistantMarkdownLink({ - source, - style, - monoSurface, - children, -}: AssistantMarkdownLinkProps) { - const { target, onHoverIn, onPress } = useFileLink(source); - const { configRef } = useAssistantFileLinkResolverContext(); - const workspaceRoot = configRef.current.workspaceRoot; - const tooltipPath = useMemo( - () => (target ? formatFileLinkTooltipPath({ target, workspaceRoot }) : null), - [target, workspaceRoot], - ); - const linkPress = useMemo( - () => ({ onPress, accessibilityRole: "link" }), - [onPress], - ); - const unwrapForMarkdownCopy = source.sourceType === "inline-code" || source.markup === "linkify"; - - if (isNative) { - // Must be a MarkdownTextSpan, not a plain : on iOS the link renders - // inside the paragraph's native UITextView, and a plain nested there - // is not hoisted into a UITextViewChild, so its text is silently dropped - // (the link disappears). The span composes correctly and stays selectable. - // - // Tap-to-open: react-native-uitextview only wires onPress onto the *string* - // children it turns into RNUITextViewChild nodes — the element children that - // markdown emits for link text pass through untouched, so an onPress placed - // here never reaches a tappable native node. We thread it down through - // AssistantLinkPressProvider so each leaf text span re-attaches it to its - // own string children, where the native tap recognizer can find it. iOS - // only: Android forwards onPress through nested already, and web uses - // the path below. - const span = ( - - {children} - - ); - return ( - - {Platform.OS === "ios" ? ( - {span} - ) : ( - span - )} - - ); - } - - const anchor = ( - - - {children} - - - ); - - return {anchor}; -} - -interface AssistantMarkdownCodeLinkProps { - source: AssistantFileLinkSource; - inheritedStyles: TextStyle; - codeInlineStyle: TextStyle; - linkStyle: TextStyle; - children: ReactNode; -} - -export function AssistantMarkdownCodeLink({ - source, - inheritedStyles, - codeInlineStyle, - linkStyle, - children, -}: AssistantMarkdownCodeLinkProps) { - const style = useMemo( - () => [inheritedStyles, codeInlineStyle, linkStyle], - [inheritedStyles, codeInlineStyle, linkStyle], - ); - return ( - - {children} - - ); -} - -interface AssistantInlineCodePathLinkProps { - content: string; - inheritedStyles: TextStyle; - codeInlineStyle: TextStyle; - linkStyle: TextStyle; -} - -export function AssistantInlineCodePathLink({ - content, - inheritedStyles, - codeInlineStyle, - linkStyle, -}: AssistantInlineCodePathLinkProps) { - const source = useMemo( - () => ({ - href: content, - text: content, - sourceType: "inline-code", - }), - [content], - ); - - return ( - - {content} - - ); -} - -const FILE_LINK_TOOLTIP_TRIGGER_STYLE: ViewStyle = { - // RN doesn't type "inline-flex" but RN-web honors it at runtime, which keeps - // the tooltip wrapper from breaking inline link flow. - display: "inline-flex" as ViewStyle["display"], -}; - -function FileLinkHoverTooltip({ - filePath, - children, -}: { - filePath: string | null; - children: ReactNode; -}) { - if (!isWeb) { - return children; - } - return ( - - - {children} - - {filePath ? ( - - - {filePath} - - - ) : null} - - ); -} - -const LINK_ANCHOR_STYLE: CSSProperties = { - display: "contents", - color: "inherit", - textDecoration: "none", -}; - -function preventAnchorNavigation(event: MouseEvent): void { - event.preventDefault(); -} - -const styles = StyleSheet.create((theme) => ({ - tooltipPath: { - color: theme.colors.foreground, - fontSize: theme.fontSize.sm, - fontWeight: theme.fontWeight.normal, - }, -})); diff --git a/packages/app/src/assistant-file-links/provider.tsx b/packages/app/src/assistant-file-links/provider.tsx index 77d0b2e034..967e1c32e3 100644 --- a/packages/app/src/assistant-file-links/provider.tsx +++ b/packages/app/src/assistant-file-links/provider.tsx @@ -14,6 +14,7 @@ import type { InlinePathTarget } from "./parse"; import type { AssistantFileLinkContext, GetDirectorySuggestions } from "./resolver"; export interface AssistantFileLinkDaemonClient { + readFile?: import("@getpaseo/client/internal/daemon-client").DaemonClient["readFile"]; getDirectorySuggestions: GetDirectorySuggestions; } @@ -78,10 +79,16 @@ export function AssistantFileLinkResolverProvider({ export function useAssistantFileLinkResolverContext(): AssistantFileLinkResolverContextValue { const context = useContext(AssistantFileLinkResolverContext); - if (!context) { - throw new Error("AssistantFileLinkResolverProvider is required for assistant file links."); - } - return context; + return context ?? EMPTY_CONTEXT; } export type { AssistantFileLinkContext }; + +const EMPTY_CONTEXT: AssistantFileLinkResolverContextValue = { + configRef: { current: {} }, + getDirectorySuggestions: async () => ({ entries: [], error: null }), +}; + +export function useAssistantFileLinkEnvironment() { + return useAssistantFileLinkResolverContext().configRef.current; +} diff --git a/packages/app/src/assistant-image/lifecycle.test.ts b/packages/app/src/assistant-image/lifecycle.test.ts deleted file mode 100644 index ed7729dd38..0000000000 --- a/packages/app/src/assistant-image/lifecycle.test.ts +++ /dev/null @@ -1,120 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { createAssistantImageLifecycle, transitionAssistantImageLifecycle } from "./lifecycle"; - -describe("assistant image lifecycle", () => { - it("keeps preview URL recreation in the public loading state", () => { - const loading = transitionAssistantImageLifecycle(createAssistantImageLifecycle(), { - type: "preview_created", - uri: "blob:first", - aspectRatio: null, - }); - const loaded = transitionAssistantImageLifecycle(loading, { - type: "image_loaded", - uri: "blob:first", - aspectRatio: 1.5, - }); - - const recreating = transitionAssistantImageLifecycle(loaded, { - type: "preview_released", - }); - - expect(recreating).toEqual({ status: "loading", uri: null, aspectRatio: null }); - }); - - it("publishes loaded only after the recreated URL loads", () => { - const recreating = transitionAssistantImageLifecycle(createAssistantImageLifecycle(), { - type: "preview_created", - uri: "blob:recreated", - aspectRatio: 1.5, - }); - - expect(recreating).toEqual({ - status: "loading", - uri: "blob:recreated", - aspectRatio: 1.5, - }); - - const recreated = transitionAssistantImageLifecycle(recreating, { - type: "image_loaded", - uri: "blob:recreated", - aspectRatio: 0.75, - }); - - expect(recreated).toEqual({ - status: "loaded", - uri: "blob:recreated", - aspectRatio: 0.75, - }); - }); - - it("does not reset a loaded image when the same preview is reported again", () => { - const loading = transitionAssistantImageLifecycle(createAssistantImageLifecycle(), { - type: "preview_created", - uri: "blob:current", - aspectRatio: null, - }); - const loaded = transitionAssistantImageLifecycle(loading, { - type: "image_loaded", - uri: "blob:current", - aspectRatio: 1.5, - }); - - const repeated = transitionAssistantImageLifecycle(loaded, { - type: "preview_created", - uri: "blob:current", - aspectRatio: 1.5, - }); - - expect(repeated).toBe(loaded); - }); - - it("publishes failed for a terminal image failure on the current URI", () => { - const loading = transitionAssistantImageLifecycle(createAssistantImageLifecycle(), { - type: "preview_created", - uri: "blob:current", - aspectRatio: null, - }); - const failed = transitionAssistantImageLifecycle(loading, { - type: "failed", - uri: "blob:current", - message: "Unable to load image preview.", - }); - - expect(failed).toEqual({ - status: "failed", - message: "Unable to load image preview.", - }); - }); - - it("ignores a stale load callback from a replaced URI", () => { - const current = transitionAssistantImageLifecycle(createAssistantImageLifecycle(), { - type: "preview_created", - uri: "blob:current", - aspectRatio: 1.25, - }); - - const afterStaleLoad = transitionAssistantImageLifecycle(current, { - type: "image_loaded", - uri: "blob:released", - aspectRatio: 2, - }); - - expect(afterStaleLoad).toEqual(current); - }); - - it("ignores a stale error callback from a replaced URI", () => { - const current = transitionAssistantImageLifecycle(createAssistantImageLifecycle(), { - type: "preview_created", - uri: "blob:current", - aspectRatio: null, - }); - - const afterStaleError = transitionAssistantImageLifecycle(current, { - type: "failed", - uri: "blob:released", - message: "Image unavailable", - }); - - expect(afterStaleError).toEqual(current); - }); -}); diff --git a/packages/app/src/assistant-image/lifecycle.ts b/packages/app/src/assistant-image/lifecycle.ts deleted file mode 100644 index bf0db18c97..0000000000 --- a/packages/app/src/assistant-image/lifecycle.ts +++ /dev/null @@ -1,46 +0,0 @@ -export type AssistantImageLifecycle = - | { status: "loading"; uri: string | null; aspectRatio: number | null } - | { status: "loaded"; uri: string; aspectRatio: number } - | { status: "failed"; message: string }; - -export type AssistantImageLifecycleEvent = - | { type: "preview_created"; uri: string; aspectRatio: number | null } - | { type: "preview_released" } - | { type: "image_loaded"; uri: string; aspectRatio: number } - | { type: "failed"; uri: string; message: string }; - -export function createAssistantImageLifecycle(): AssistantImageLifecycle { - return { status: "loading", uri: null, aspectRatio: null }; -} - -export function transitionAssistantImageLifecycle( - state: AssistantImageLifecycle, - event: AssistantImageLifecycleEvent, -): AssistantImageLifecycle { - if (event.type === "image_loaded") { - if (state.status !== "loading" || state.uri !== event.uri) { - return state; - } - return { - status: "loaded", - uri: event.uri, - aspectRatio: event.aspectRatio, - }; - } - if (event.type === "failed") { - if (state.status === "failed" || state.uri !== event.uri) { - return state; - } - return { status: "failed", message: event.message }; - } - if (event.type === "preview_created") { - if (state.status === "loaded" && state.uri === event.uri) { - return state; - } - return { status: "loading", uri: event.uri, aspectRatio: event.aspectRatio }; - } - if (event.type === "preview_released") { - return { status: "loading", uri: null, aspectRatio: null }; - } - return state; -} diff --git a/packages/app/src/assistant-image/load-dimensions.test.ts b/packages/app/src/assistant-image/load-dimensions.test.ts deleted file mode 100644 index a60f9b1b6d..0000000000 --- a/packages/app/src/assistant-image/load-dimensions.test.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { - type AssistantImageRenderedDimensionsReader, - resolveAssistantImageLoadDimensions, -} from "./load-dimensions"; - -class MemoryRenderedDimensionsReader implements AssistantImageRenderedDimensionsReader { - constructor(private readonly dimensions: { width: number; height: number } | null) {} - - read(): { width: number; height: number } | null { - return this.dimensions; - } -} - -describe("assistant image load dimensions", () => { - it("prefers native source dimensions", () => { - const dimensions = resolveAssistantImageLoadDimensions({ - source: { width: 640, height: 320 }, - target: { naturalWidth: 800, naturalHeight: 400 }, - renderedImage: null, - renderedDimensions: new MemoryRenderedDimensionsReader({ width: 900, height: 600 }), - }); - - expect(dimensions).toEqual({ width: 640, height: 320 }); - }); - - it("uses browser event dimensions when native dimensions are absent", () => { - const dimensions = resolveAssistantImageLoadDimensions({ - source: { width: 0, height: 0 }, - target: { naturalWidth: 800, naturalHeight: 400 }, - renderedImage: null, - renderedDimensions: new MemoryRenderedDimensionsReader({ width: 900, height: 600 }), - }); - - expect(dimensions).toEqual({ width: 800, height: 400 }); - }); - - it("falls back to the rendered image adapter", () => { - const renderedImage = { id: "rendered-image" }; - const dimensions = resolveAssistantImageLoadDimensions({ - target: null, - renderedImage, - renderedDimensions: new MemoryRenderedDimensionsReader({ width: 900, height: 600 }), - }); - - expect(dimensions).toEqual({ width: 900, height: 600 }); - }); -}); diff --git a/packages/app/src/assistant-image/load-dimensions.ts b/packages/app/src/assistant-image/load-dimensions.ts deleted file mode 100644 index 069814ced8..0000000000 --- a/packages/app/src/assistant-image/load-dimensions.ts +++ /dev/null @@ -1,45 +0,0 @@ -export interface AssistantImageDimensions { - width: number; - height: number; -} - -interface AssistantImageDimensionCandidate { - width?: unknown; - height?: unknown; -} - -export interface AssistantImageRenderedDimensionsReader { - read(renderedImage: unknown): AssistantImageDimensions | null; -} - -function readPositiveDimensions( - candidate: AssistantImageDimensionCandidate | null | undefined, -): AssistantImageDimensions | null { - if ( - typeof candidate?.width !== "number" || - typeof candidate.height !== "number" || - candidate.width <= 0 || - candidate.height <= 0 - ) { - return null; - } - return { width: candidate.width, height: candidate.height }; -} - -export function resolveAssistantImageLoadDimensions(input: { - source?: AssistantImageDimensionCandidate | null; - target?: { naturalWidth?: unknown; naturalHeight?: unknown } | null; - renderedImage: unknown; - renderedDimensions: AssistantImageRenderedDimensionsReader; -}): AssistantImageDimensions | null { - const source = readPositiveDimensions(input.source); - if (source) { - return source; - } - - const target = readPositiveDimensions({ - width: input.target?.naturalWidth, - height: input.target?.naturalHeight, - }); - return target ?? input.renderedDimensions.read(input.renderedImage); -} diff --git a/packages/app/src/assistant-image/retry.test.ts b/packages/app/src/assistant-image/retry.test.ts deleted file mode 100644 index 9777663bed..0000000000 --- a/packages/app/src/assistant-image/retry.test.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { runAssistantImageOperationWithRetry } from "./retry"; - -describe("assistant image retry", () => { - it("recovers from transient failures while the image remains mounted", async () => { - const waits: number[] = []; - let attempts = 0; - - const result = await runAssistantImageOperationWithRetry({ - operation: async () => { - attempts += 1; - if (attempts < 3) { - throw new Error("transient"); - } - return "loaded"; - }, - delaysMs: [10, 20, 30], - wait: async (delayMs) => { - waits.push(delayMs); - }, - }); - - expect({ attempts, waits, result }).toEqual({ - attempts: 3, - waits: [10, 20], - result: "loaded", - }); - }); - - it("stops retrying after the image is released", async () => { - let stopped = false; - let attempts = 0; - - await expect( - runAssistantImageOperationWithRetry({ - operation: async () => { - attempts += 1; - throw new Error("transient"); - }, - delaysMs: [10, 20], - shouldStop: () => stopped, - wait: async () => { - stopped = true; - }, - }), - ).rejects.toThrow("transient"); - expect(attempts).toBe(1); - }); -}); diff --git a/packages/app/src/assistant-image/retry.ts b/packages/app/src/assistant-image/retry.ts deleted file mode 100644 index 4a2cf5c58a..0000000000 --- a/packages/app/src/assistant-image/retry.ts +++ /dev/null @@ -1,31 +0,0 @@ -export const ASSISTANT_IMAGE_RETRY_DELAYS_MS = [100, 400, 1_200] as const; - -export async function runAssistantImageOperationWithRetry(input: { - operation: () => Promise; - delaysMs?: readonly number[]; - shouldStop?: () => boolean; - wait?: (delayMs: number) => Promise; -}): Promise { - const delays = input.delaysMs ?? ASSISTANT_IMAGE_RETRY_DELAYS_MS; - const shouldStop = input.shouldStop ?? (() => false); - const wait = - input.wait ?? - (async (delayMs: number) => { - await new Promise((resolve) => setTimeout(resolve, delayMs)); - }); - - for (let attempt = 0; ; attempt += 1) { - try { - return await input.operation(); - } catch (error) { - const delay = delays[attempt]; - if (delay === undefined || shouldStop()) { - throw error; - } - await wait(delay); - if (shouldStop()) { - throw error; - } - } - } -} diff --git a/packages/app/src/assistant-image/use-assistant-image.ts b/packages/app/src/assistant-image/use-assistant-image.ts deleted file mode 100644 index 3314b7a398..0000000000 --- a/packages/app/src/assistant-image/use-assistant-image.ts +++ /dev/null @@ -1,544 +0,0 @@ -import { - useCallback, - useEffect, - useLayoutEffect, - useMemo, - useReducer, - useRef, - useState, -} from "react"; -import type { ImageLoadEvent } from "react-native"; -import { useTranslation } from "react-i18next"; -import type { DaemonClient } from "@getpaseo/client/internal/daemon-client"; -import type { AttachmentMetadata } from "@/attachments/types"; -import { isWeb } from "@/constants/platform"; -import { useStableEvent } from "@/hooks/use-stable-event"; -import { retainAttachmentForGarbageCollection } from "@/attachments/gc-retention"; -import { - persistAttachmentFromBytes, - persistAttachmentFromDataUrl, - releaseAttachmentPreviewUrl, - resolveAttachmentPreviewUrl, -} from "@/attachments/service"; -import { createPreviewAttachmentId, parseImageDataUrl } from "@/attachments/utils"; -import { - getAssistantImageMetadata, - setAssistantImageMetadata, -} from "@/utils/assistant-image-metadata"; -import { resolveAssistantImageSource } from "@/utils/assistant-image-source"; -import { createAssistantImageAcquisitionCache } from "./acquisition-cache"; -import { - createAssistantImageFileAcquisition, - type AssistantImageAcquisition, - type AssistantImageFileAcquisitionPort, -} from "./file-acquisition"; -import { - createAssistantImageLifecycle, - transitionAssistantImageLifecycle, - type AssistantImageLifecycle, - type AssistantImageLifecycleEvent, -} from "./lifecycle"; -import { - type AssistantImageRenderedDimensionsReader, - resolveAssistantImageLoadDimensions, -} from "./load-dimensions"; -import { runAssistantImageOperationWithRetry } from "./retry"; - -interface AssistantImageRenderBinding { - uri: string; - onRef: (instance: unknown) => void; - onLoad: (event: ImageLoadEvent) => void; - onError: () => void; -} - -const renderedDimensions: AssistantImageRenderedDimensionsReader = { - read(renderedImage) { - if (!isWeb || !(renderedImage instanceof HTMLElement)) { - return null; - } - const image = renderedImage.querySelector("img"); - return image && image.naturalWidth > 0 && image.naturalHeight > 0 - ? { width: image.naturalWidth, height: image.naturalHeight } - : null; - }, -}; - -export type AssistantImageResult = - | { - status: "loading"; - binding: AssistantImageRenderBinding | null; - aspectRatio: number | null; - } - | { - status: "loaded"; - binding: AssistantImageRenderBinding; - aspectRatio: number; - } - | { status: "failed"; message: string }; - -interface UseAssistantImageInput { - source: string; - occurrenceKey: string; - client?: DaemonClient | null; - workspaceRoot?: string; - serverId?: string; -} - -type PreviewUrlState = - | { status: "waiting" } - | { status: "loading" } - | { status: "loaded"; uri: string } - | { status: "failed"; error: unknown }; - -type AttachmentAcquisitionState = - | { status: "waiting" } - | { status: "loading" } - | { status: "loaded"; attachment: AttachmentMetadata } - | { status: "failed"; error: unknown }; - -interface DataImage { - mimeType: string; - base64: string; - cacheKey: string; -} - -const attachmentAcquisitionCache = createAssistantImageAcquisitionCache({ - capacity: 500, - onRetain: (attachment) => retainAttachmentForGarbageCollection(attachment.id), -}); - -interface CachedPreviewUrl { - attachment: AttachmentMetadata; - uri: string; -} - -const previewUrlCache = createAssistantImageAcquisitionCache({ - capacity: 500, - onRetain: - ({ attachment, uri }) => - () => { - void releaseAttachmentPreviewUrl({ attachment, url: uri }); - }, -}); - -const LOADED_IMAGE_CACHE_CAPACITY = 500; -const loadedImageCache = new Map(); - -function getLoadedImageAspectRatio(uri: string): number | null { - const aspectRatio = loadedImageCache.get(uri); - if (aspectRatio === undefined) { - return null; - } - loadedImageCache.delete(uri); - loadedImageCache.set(uri, aspectRatio); - return aspectRatio; -} - -function rememberLoadedImage(uri: string, aspectRatio: number): void { - loadedImageCache.delete(uri); - loadedImageCache.set(uri, aspectRatio); - if (loadedImageCache.size <= LOADED_IMAGE_CACHE_CAPACITY) { - return; - } - const leastRecentlyUsedUri = loadedImageCache.keys().next().value; - if (leastRecentlyUsedUri !== undefined) { - loadedImageCache.delete(leastRecentlyUsedUri); - } -} - -function useAttachmentAcquisition( - acquisition: AssistantImageAcquisition | null, -): AttachmentAcquisitionState { - const acquisitionKey = acquisition?.key ?? null; - const [entry, setEntry] = useState<{ - key: string | null; - state: AttachmentAcquisitionState; - }>(() => { - const cached = acquisitionKey ? attachmentAcquisitionCache.peek(acquisitionKey) : undefined; - return { - key: acquisitionKey, - state: cached ? { status: "loaded", attachment: cached } : { status: "waiting" }, - }; - }); - - useEffect(() => { - let disposed = false; - let releaseCurrent: (() => void) | null = null; - if (!acquisition || !acquisitionKey) { - setEntry({ key: null, state: { status: "waiting" } }); - return; - } - - const acquireCurrent = () => { - releaseCurrent?.(); - const retained = attachmentAcquisitionCache.acquireRetained( - acquisitionKey, - acquisition.locate, - ); - releaseCurrent = retained.release; - return retained; - }; - const initial = acquireCurrent(); - if (initial.value) { - setEntry({ key: acquisitionKey, state: { status: "loaded", attachment: initial.value } }); - return () => { - disposed = true; - releaseCurrent?.(); - }; - } - - setEntry({ key: acquisitionKey, state: { status: "loading" } }); - void (async () => { - let firstAttempt: ReturnType | null = initial; - try { - const attachment = await runAssistantImageOperationWithRetry({ - operation: async () => { - const retained = firstAttempt ?? acquireCurrent(); - firstAttempt = null; - return await retained.promise; - }, - shouldStop: () => disposed, - }); - if (!disposed) { - setEntry({ key: acquisitionKey, state: { status: "loaded", attachment } }); - } - } catch (error) { - if (!disposed) { - setEntry({ key: acquisitionKey, state: { status: "failed", error } }); - } - } - })(); - return () => { - disposed = true; - releaseCurrent?.(); - }; - }, [acquisition, acquisitionKey]); - - if (!acquisitionKey) { - return { status: "waiting" }; - } - const cached = attachmentAcquisitionCache.peek(acquisitionKey); - if (cached) { - return { status: "loaded", attachment: cached }; - } - return entry.key === acquisitionKey ? entry.state : { status: "waiting" }; -} - -function createDataImageAcquisition(input: { - source: string; - dataImage: DataImage | null; -}): AssistantImageAcquisition | null { - if (!input.dataImage) { - return null; - } - const { dataImage, source } = input; - return { - key: dataImage.cacheKey, - locate: async () => - await persistAttachmentFromDataUrl({ - id: createPreviewAttachmentId({ - mimeType: dataImage.mimeType, - contentLength: dataImage.base64.length, - contentKey: dataImage.cacheKey, - }), - dataUrl: source, - mimeType: dataImage.mimeType, - }), - }; -} - -function usePreviewUrl(attachment: AttachmentMetadata | null | undefined): PreviewUrlState { - const id = attachment?.id; - const storageType = attachment?.storageType; - const storageKey = attachment?.storageKey; - const mimeType = attachment?.mimeType; - const previewKey = - id && storageType && storageKey && mimeType - ? `${id}:${storageType}:${storageKey}:${mimeType}` - : null; - const [entry, setEntry] = useState<{ key: string | null; state: PreviewUrlState }>(() => { - return { - key: previewKey, - state: { status: "waiting" }, - }; - }); - const getCurrentAttachment = useStableEvent(() => attachment ?? null); - - useLayoutEffect(() => { - let disposed = false; - let releaseCurrent: (() => void) | null = null; - const current = getCurrentAttachment(); - - if (!current || !previewKey) { - setEntry({ key: null, state: { status: "waiting" } }); - return; - } - - const acquireCurrent = () => { - releaseCurrent?.(); - const retained = previewUrlCache.acquireRetained(previewKey, async () => ({ - attachment: current, - uri: await resolveAttachmentPreviewUrl(current), - })); - releaseCurrent = retained.release; - return retained; - }; - const initial = acquireCurrent(); - if (initial.value) { - setEntry({ key: previewKey, state: { status: "loaded", uri: initial.value.uri } }); - return () => { - disposed = true; - releaseCurrent?.(); - }; - } - - setEntry({ key: previewKey, state: { status: "loading" } }); - void (async () => { - let firstAttempt: ReturnType | null = initial; - try { - const preview = await runAssistantImageOperationWithRetry({ - operation: async () => { - const retained = firstAttempt ?? acquireCurrent(); - firstAttempt = null; - return await retained.promise; - }, - shouldStop: () => disposed, - }); - if (!disposed) { - setEntry({ key: previewKey, state: { status: "loaded", uri: preview.uri } }); - } - } catch (error) { - if (!disposed) { - setEntry({ key: previewKey, state: { status: "failed", error } }); - } - } - })(); - - return () => { - disposed = true; - releaseCurrent?.(); - }; - }, [getCurrentAttachment, previewKey]); - - if (!previewKey) { - return { status: "waiting" }; - } - return entry.key === previewKey ? entry.state : { status: "waiting" }; -} - -function lifecycleReducer( - state: AssistantImageLifecycle, - event: AssistantImageLifecycleEvent, -): AssistantImageLifecycle { - return transitionAssistantImageLifecycle(state, event); -} - -function errorMessage(error: unknown, fallback: string): string { - return error instanceof Error ? error.message : fallback; -} - -function getAcquisitionFailure(input: { - hasResolution: boolean; - isFileSource: boolean; - isDataImage: boolean; - fileAttachment: AttachmentAcquisitionState; - dataImageAttachment: AttachmentAcquisitionState; - preview: PreviewUrlState; - hasDirectUri: boolean; - fallbackMessage: string; -}): AssistantImageResult | null { - if (!input.hasResolution) { - return { status: "failed", message: input.fallbackMessage }; - } - if (input.isFileSource && input.fileAttachment.status === "failed") { - return { - status: "failed", - message: errorMessage(input.fileAttachment.error, input.fallbackMessage), - }; - } - if (input.isDataImage && input.dataImageAttachment.status === "failed") { - return { - status: "failed", - message: errorMessage(input.dataImageAttachment.error, input.fallbackMessage), - }; - } - if (!input.hasDirectUri && input.preview.status === "failed") { - return { - status: "failed", - message: errorMessage(input.preview.error, input.fallbackMessage), - }; - } - return null; -} - -export function useAssistantImage({ - source, - occurrenceKey, - client, - workspaceRoot, - serverId, -}: UseAssistantImageInput): AssistantImageResult { - const { t } = useTranslation(); - const resolution = useMemo( - () => resolveAssistantImageSource({ source, workspaceRoot }), - [source, workspaceRoot], - ); - const dataImage = useMemo(() => parseImageDataUrl(source), [source]); - const fileAcquisition = useMemo(() => { - const port: AssistantImageFileAcquisitionPort | null = client - ? { - readFile: async (cwd, path) => await client.readFile(cwd, path), - persist: persistAttachmentFromBytes, - } - : null; - return createAssistantImageFileAcquisition({ - port, - resolution, - serverId, - occurrenceKey, - unavailableMessage: t("message.attachments.imagePreviewUnavailable"), - }); - }, [client, occurrenceKey, resolution, serverId, t]); - const dataImageAcquisition = useMemo( - () => createDataImageAcquisition({ source, dataImage }), - [dataImage, source], - ); - const fileAttachment = useAttachmentAcquisition(fileAcquisition); - const dataImageAttachment = useAttachmentAcquisition(dataImageAcquisition); - const filePreview = usePreviewUrl( - fileAttachment.status === "loaded" ? fileAttachment.attachment : null, - ); - const dataImagePreview = usePreviewUrl( - dataImageAttachment.status === "loaded" ? dataImageAttachment.attachment : null, - ); - const directUri = resolution?.kind === "direct" && !dataImage ? resolution.uri : null; - const preview = dataImage ? dataImagePreview : filePreview; - const previewUri = preview.status === "loaded" ? preview.uri : null; - const uri = directUri ?? previewUri; - const cachedAspectRatio = useMemo( - () => getAssistantImageMetadata({ source, workspaceRoot, serverId })?.aspectRatio ?? null, - [serverId, source, workspaceRoot], - ); - const [lifecycle, dispatchLifecycle] = useReducer( - lifecycleReducer, - uri, - (initialUri): AssistantImageLifecycle => { - if (initialUri) { - const aspectRatio = getLoadedImageAspectRatio(initialUri); - if (aspectRatio !== null) { - return { status: "loaded", uri: initialUri, aspectRatio }; - } - } - return createAssistantImageLifecycle(); - }, - ); - const dispatch = useCallback((event: AssistantImageLifecycleEvent) => { - if (event.type === "image_loaded") { - rememberLoadedImage(event.uri, event.aspectRatio); - } else if (event.type === "failed" && event.uri) { - loadedImageCache.delete(event.uri); - } - dispatchLifecycle(event); - }, []); - const renderedImageRef = useRef(null); - const handleImageRef = useCallback((instance: unknown) => { - renderedImageRef.current = instance; - }, []); - - useEffect(() => { - if (!uri) { - dispatch({ type: "preview_released" }); - return; - } - - dispatch({ - type: "preview_created", - uri, - aspectRatio: cachedAspectRatio, - }); - }, [cachedAspectRatio, dispatch, uri]); - - const handleImageError = useCallback(() => { - if (uri) { - dispatch({ - type: "failed", - uri, - message: t("message.attachments.imageUnavailable"), - }); - } - }, [dispatch, t, uri]); - const handleImageLoad = useCallback( - (event: ImageLoadEvent) => { - if (!uri) { - return; - } - const nativeEvent = event.nativeEvent as ImageLoadEvent["nativeEvent"] & { - target?: { naturalWidth?: unknown; naturalHeight?: unknown }; - }; - const dimensions = resolveAssistantImageLoadDimensions({ - source: nativeEvent.source, - target: nativeEvent.target, - renderedImage: renderedImageRef.current, - renderedDimensions, - }); - const metadata = dimensions - ? setAssistantImageMetadata({ source, workspaceRoot, serverId }, dimensions) - : null; - const aspectRatio = metadata?.aspectRatio ?? cachedAspectRatio; - if (!aspectRatio) { - dispatch({ - type: "failed", - uri, - message: t("message.attachments.imageUnavailable"), - }); - return; - } - dispatch({ type: "image_loaded", uri, aspectRatio }); - }, - [cachedAspectRatio, dispatch, serverId, source, t, uri, workspaceRoot], - ); - - const acquisitionFailure = getAcquisitionFailure({ - hasResolution: resolution !== null, - isFileSource: resolution?.kind === "file_rpc", - isDataImage: dataImage !== null, - fileAttachment, - dataImageAttachment, - preview, - hasDirectUri: directUri !== null, - fallbackMessage: t("message.attachments.imagePreviewLoadFailed"), - }); - if (acquisitionFailure) { - return acquisitionFailure; - } - const hasCurrentLifecycleUri = lifecycle.status !== "failed" && lifecycle.uri === uri; - let binding: AssistantImageRenderBinding | null = null; - if (hasCurrentLifecycleUri && lifecycle.uri) { - binding = { - uri: lifecycle.uri, - onRef: handleImageRef, - onLoad: handleImageLoad, - onError: handleImageError, - }; - } - if (lifecycle.status === "loaded" && lifecycle.uri === uri) { - return { - status: "loaded", - binding: { - uri: lifecycle.uri, - onRef: handleImageRef, - onLoad: handleImageLoad, - onError: handleImageError, - }, - aspectRatio: lifecycle.aspectRatio, - }; - } - if (lifecycle.status === "failed") { - return lifecycle; - } - return { - status: "loading", - binding, - aspectRatio: (hasCurrentLifecycleUri ? lifecycle.aspectRatio : null) ?? cachedAspectRatio, - }; -} diff --git a/packages/app/src/assistant-selection-copy/content.browser.test.ts b/packages/app/src/assistant-selection-copy/content.browser.test.ts index aecb4ba5b4..5c4fcfb331 100644 --- a/packages/app/src/assistant-selection-copy/content.browser.test.ts +++ b/packages/app/src/assistant-selection-copy/content.browser.test.ts @@ -760,3 +760,58 @@ describe("assistant selection copy inside highlighted code", () => { ); }); }); + +describe("semantic library output", () => { + it("copies a partial strong span as plain text", () => { + const host = document.createElement("div"); + host.innerHTML = + '

Before bold text after

'; + document.body.append(host); + const text = host.querySelector("strong")!.firstChild!; + const range = document.createRange(); + range.setStart(text, 1); + range.setEnd(text, 4); + const selection = window.getSelection()!; + selection.removeAllRanges(); + selection.addRange(range); + expect(createAssistantSelectionClipboardContent(selection)?.plainText).toBe("old"); + expect(createAssistantSelectionClipboardContent(selection)?.html).not.toContain(""); + host.remove(); + }); +}); + +it("copies native semantic ordered lists with their original numbers", () => { + const host = document.createElement("div"); + host.innerHTML = + '
  1. Third item
  2. Fourth item
'; + document.body.append(host); + const selection = selectNodeContents(host.querySelector("ol")!); + const content = createAssistantSelectionClipboardContent(selection); + expect(content?.plainText).toContain("3. Third item"); + expect(content?.plainText).toContain("4. Fourth item"); + host.remove(); +}); + +it("copies a selection inside semantic inline code without Markdown escaping", () => { + const message = mountFixture(); + message.innerHTML = "

Run apply_patch now.

"; + const code = fixtureElement(message, "code"); + expect(createAssistantSelectionClipboardContent(selectText(code, 0, 11))?.plainText).toBe( + "apply_patch", + ); +}); + +it("copies a workspace image path instead of its temporary display URL", () => { + const message = mountFixture(); + const sources = JSON.stringify({ "blob:preview": "assets/chart.png" }); + message.innerHTML = '
chart
'; + message.firstElementChild!.setAttribute("data-paseo-markdown-image-sources", sources); + const selection = window.getSelection()!; + const range = document.createRange(); + range.selectNode(message.querySelector("img")!); + selection.removeAllRanges(); + selection.addRange(range); + expect(createAssistantSelectionClipboardContent(selection)?.plainText).toBe( + "![chart](assets/chart.png)", + ); +}); diff --git a/packages/app/src/assistant-selection-copy/content.web.ts b/packages/app/src/assistant-selection-copy/content.web.ts index e59a71aeca..250d60d761 100644 --- a/packages/app/src/assistant-selection-copy/content.web.ts +++ b/packages/app/src/assistant-selection-copy/content.web.ts @@ -6,6 +6,7 @@ import { type MarkdownClipboardContent, } from "@/utils/rich-clipboard"; import { + MARKDOWN_COPY_IMAGE_SOURCES_ATTRIBUTE, MARKDOWN_COPY_ALIGN_ATTRIBUTE, MARKDOWN_COPY_ALT_ATTRIBUTE, MARKDOWN_COPY_IGNORE_ATTRIBUTE, @@ -22,9 +23,38 @@ const ASSISTANT_MESSAGE_SELECTOR = '[data-testid="assistant-message"]'; const MESSAGE_ROW_SELECTOR = "[data-message-id]"; const CHAT_SCROLL_SELECTOR = '[data-testid="agent-chat-scroll"]'; const messageRowSelector = (messageId: string) => `[data-message-id="${CSS.escape(messageId)}"]`; -const CODE_BLOCK_SELECTOR = `[${MARKDOWN_COPY_TAG_ATTRIBUTE}="pre"]`; -const CODE_REGION_SELECTOR = `${CODE_BLOCK_SELECTOR}, [${MARKDOWN_COPY_TAG_ATTRIBUTE}="code"]`; -const IMAGE_FRAME_SELECTOR = `[${MARKDOWN_COPY_TAG_ATTRIBUTE}="img"]`; +const CODE_BLOCK_SELECTOR = `pre, [${MARKDOWN_COPY_TAG_ATTRIBUTE}="pre"]`; +const CODE_REGION_SELECTOR = `${CODE_BLOCK_SELECTOR}, code, [${MARKDOWN_COPY_TAG_ATTRIBUTE}="code"]`; +const IMAGE_FRAME_SELECTOR = `img, [${MARKDOWN_COPY_TAG_ATTRIBUTE}="img"]`; + +const SEMANTIC_TAGS = + "p,blockquote,br,code,pre,h1,h2,h3,h4,h5,h6,hr,img,li,ol,ul,strong,em,s,del,table,thead,tbody,tr,th,td"; +function semanticTag(element: Element): string | null { + return ( + element.getAttribute(MARKDOWN_COPY_TAG_ATTRIBUTE) ?? + (element.matches(SEMANTIC_TAGS) ? element.tagName.toLowerCase() : null) + ); +} +function copySemanticAttributes(original: Element, copy: Element): void { + const tag = semanticTag(original); + if (!tag || original.hasAttribute(MARKDOWN_COPY_TAG_ATTRIBUTE)) return; + copy.setAttribute(MARKDOWN_COPY_TAG_ATTRIBUTE, tag); + if (tag === "ol") + copy.setAttribute(MARKDOWN_COPY_LIST_START_ATTRIBUTE, original.getAttribute("start") ?? "1"); + if (tag === "img") { + const url = original.getAttribute("src") ?? ""; + const encodedSources = original + .closest(`[${MARKDOWN_COPY_IMAGE_SOURCES_ATTRIBUTE}]`) + ?.getAttribute(MARKDOWN_COPY_IMAGE_SOURCES_ATTRIBUTE); + const sources: Record = encodedSources ? JSON.parse(encodedSources) : {}; + copy.setAttribute(MARKDOWN_COPY_SRC_ATTRIBUTE, sources[url] ?? url); + copy.setAttribute(MARKDOWN_COPY_ALT_ATTRIBUTE, original.getAttribute("alt") ?? ""); + } + if (tag === "td" || tag === "th") { + const align = (original as HTMLElement).style.textAlign; + if (align) copy.setAttribute(MARKDOWN_COPY_ALIGN_ATTRIBUTE, align); + } +} const turndown = new TurndownService({ bulletListMarker: "-", @@ -247,6 +277,7 @@ function cloneMarkdownSelection(range: Range, message: Element): Node { while (ancestor && ancestor !== message) { if (shouldPreserveSemanticElement(range, ancestor)) { const wrapper = ancestor.cloneNode(false) as Element; + copySemanticAttributes(ancestor, wrapper); normalizeOrderedListStart(wrapper, ancestor, range); wrapper.append(selected); selected = wrapper; @@ -294,25 +325,26 @@ function nodeAtPath(root: Node, path: number[]): Node { } function removeUnselectedSemantics(range: Range, source: Element, clone: Element): void { - const selector = `[${MARKDOWN_COPY_TAG_ATTRIBUTE}], a`; + const selector = `[${MARKDOWN_COPY_TAG_ATTRIBUTE}], a, ${SEMANTIC_TAGS}`; const originals = Array.from(source.querySelectorAll(selector)); const copies = Array.from(clone.querySelectorAll(selector)); originals.forEach((original, index) => { const copy = copies[index]; + if (copy) copySemanticAttributes(original, copy); if (!copy || shouldPreserveSemanticElement(range, original)) { if (copy) { normalizeOrderedListStart(copy, original, range); } return; } - const tag = original.getAttribute(MARKDOWN_COPY_TAG_ATTRIBUTE); + const tag = semanticTag(original); if (tag && isBlockBoundary(tag)) { copy.setAttribute(MARKDOWN_COPY_TAG_ATTRIBUTE, "p"); return; } copy.removeAttribute(MARKDOWN_COPY_TAG_ATTRIBUTE); - if (copy.tagName === "A") { + if (copy.matches(SEMANTIC_TAGS) || copy.tagName === "A") { copy.setAttribute(MARKDOWN_COPY_UNWRAP_ATTRIBUTE, "true"); } }); @@ -322,7 +354,7 @@ function shouldPreserveSemanticElement(range: Range, element: Element): boolean if (!range.intersectsNode(element)) { return false; } - const tag = element.getAttribute(MARKDOWN_COPY_TAG_ATTRIBUTE); + const tag = semanticTag(element); // An image has no part to select, so touching it selects all of it. if (tag === "p" || tag === "img" || isTableStructure(tag)) { return true; @@ -340,8 +372,16 @@ function shouldPreserveSemanticElement(range: Range, element: Element): boolean (item) => hasSelectedListMarker(range, item) || hasSelectedAllContents(range, item, true), ); } - if (tag === "li" && hasSelectedListMarker(range, element)) { - return true; + if (tag === "li") { + if (hasSelectedListMarker(range, element)) return true; + // CSS list markers are outside text nodes. Starting inside a semantic item + // excludes its marker even when all of the label's characters are selected. + if ( + element.tagName === "LI" && + range.startContainer !== element && + element.contains(range.startContainer) + ) + return false; } return hasSelectedAllContents(range, element, tag === "li"); } @@ -358,7 +398,7 @@ function selectionStaysInsideElement(range: Range, element: Element): boolean { function selectedListItems(list: Element, range: Range): Element[] { return Array.from(list.children).filter( (child) => - child.getAttribute(MARKDOWN_COPY_TAG_ATTRIBUTE) === "li" && + semanticTag(child) === "li" && (child.contains(range.startContainer) || child.contains(range.endContainer) || hasSelectedAllContents(range, child, true)), @@ -368,7 +408,7 @@ function selectedListItems(list: Element, range: Range): Element[] { function hasSelectedListMarker(range: Range, item: Element): boolean { const marker = item.querySelector(`:scope > [${MARKDOWN_COPY_LIST_MARKER_ATTRIBUTE}]`); if (!marker) { - return false; + return item.tagName === "LI" && range.startContainer === item && range.startOffset === 0; } const markerContents = document.createRange(); @@ -391,18 +431,20 @@ function hasSelectedListMarker(range: Range, item: Element): boolean { } function normalizeOrderedListStart(copy: Element, original: Element, range: Range): void { - if (original.getAttribute(MARKDOWN_COPY_TAG_ATTRIBUTE) !== "ol") { + if (semanticTag(original) !== "ol") { return; } const firstSelectedItem = selectedListItems(original, range)[0]; if (!firstSelectedItem) { return; } - const items = Array.from(original.children).filter( - (child) => child.getAttribute(MARKDOWN_COPY_TAG_ATTRIBUTE) === "li", - ); + const items = Array.from(original.children).filter((child) => semanticTag(child) === "li"); const omittedItems = items.indexOf(firstSelectedItem); - const originalStart = Number(original.getAttribute(MARKDOWN_COPY_LIST_START_ATTRIBUTE) ?? 1); + const originalStart = Number( + original.getAttribute(MARKDOWN_COPY_LIST_START_ATTRIBUTE) ?? + original.getAttribute("start") ?? + 1, + ); copy.setAttribute(MARKDOWN_COPY_LIST_START_ATTRIBUTE, String(originalStart + omittedItems)); } diff --git a/packages/app/src/assistant-selection-copy/markup.ts b/packages/app/src/assistant-selection-copy/markup.ts index 91c6c1618a..7b4a2a1572 100644 --- a/packages/app/src/assistant-selection-copy/markup.ts +++ b/packages/app/src/assistant-selection-copy/markup.ts @@ -19,44 +19,11 @@ export const MARKDOWN_COPY_ALT_ATTRIBUTE = "data-paseo-markdown-alt"; export const TRAILING_CODE_LINE_BREAKS = /(\r?\n[ \t]*)+$/; export const markdownCopyDataSet = { - blockquote: { paseoMarkdownTag: "blockquote" }, - br: { paseoMarkdownTag: "br" }, code: { paseoMarkdownTag: "code" }, - h1: { paseoMarkdownTag: "h1" }, - h2: { paseoMarkdownTag: "h2" }, - h3: { paseoMarkdownTag: "h3" }, - h4: { paseoMarkdownTag: "h4" }, - h5: { paseoMarkdownTag: "h5" }, - h6: { paseoMarkdownTag: "h6" }, - hr: { paseoMarkdownTag: "hr" }, - ignore: { paseoMarkdownIgnore: "true" }, - li: { paseoMarkdownTag: "li" }, - listMarker: { paseoMarkdownIgnore: "true", paseoMarkdownListMarker: "true" }, - ol: { paseoMarkdownTag: "ol" }, - p: { paseoMarkdownTag: "p" }, pre: { paseoMarkdownTag: "pre" }, - s: { paseoMarkdownTag: "s" }, - strong: { paseoMarkdownTag: "strong" }, - em: { paseoMarkdownTag: "em" }, - table: { paseoMarkdownTag: "table" }, - tbody: { paseoMarkdownTag: "tbody" }, - td: { paseoMarkdownTag: "td" }, - th: { paseoMarkdownTag: "th" }, - thead: { paseoMarkdownTag: "thead" }, - tr: { paseoMarkdownTag: "tr" }, - ul: { paseoMarkdownTag: "ul" }, - unwrap: { paseoMarkdownUnwrap: "true" }, + ignore: { paseoMarkdownIgnore: "true" }, } as const; -export type MarkdownCopyInlineTag = "br" | "code" | "em" | "s" | "strong"; - -export function markdownCopyOrderedListDataSet(start: unknown) { - return { - ...markdownCopyDataSet.ol, - paseoMarkdownListStart: String(start ?? 1), - } as const; -} - export function markdownCopyCodeBlockDataSet(language: string | null | undefined) { const fenceLanguage = language?.trim().split(/\s+/)[0]; return { @@ -65,25 +32,7 @@ export function markdownCopyCodeBlockDataSet(language: string | null | undefined } as const; } -/** - * An image copies as the Markdown it came from. The rendered `img` points at a preview - * URL for data and workspace images, and carries no alt text of its own. - */ -export function markdownCopyImageDataSet(source: string, alt: string | undefined) { - return { - paseoMarkdownTag: "img", - paseoMarkdownSrc: source, - ...(alt ? { paseoMarkdownAlt: alt } : {}), - } as const; -} - -export function markdownCopyTableCellDataSet(tag: "td" | "th", style: unknown) { - const alignment = - typeof style === "string" - ? style.match(/(?:^|;)\s*text-align\s*:\s*(left|right|center)/i)?.[1] - : null; - return { - ...markdownCopyDataSet[tag], - ...(alignment ? { paseoMarkdownAlign: alignment.toLowerCase() } : {}), - } as const; +export const MARKDOWN_COPY_IMAGE_SOURCES_ATTRIBUTE = "data-paseo-markdown-image-sources"; +export function markdownCopyImageSourcesDataSet(sources: Record) { + return { paseoMarkdownImageSources: JSON.stringify(sources) }; } diff --git a/packages/app/src/components/highlighted-code-block.tsx b/packages/app/src/components/highlighted-code-block.tsx index 86fc3801a3..292ec53618 100644 --- a/packages/app/src/components/highlighted-code-block.tsx +++ b/packages/app/src/components/highlighted-code-block.tsx @@ -1,7 +1,13 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"; -import { Pressable, View, type StyleProp, type TextStyle, type ViewStyle } from "react-native"; +import { + Pressable, + View, + Text, + type StyleProp, + type TextStyle, + type ViewStyle, +} from "react-native"; import { StyleSheet } from "react-native-unistyles"; -import { MarkdownTextSpan } from "@/components/markdown-text"; import * as Clipboard from "expo-clipboard"; import { Check, Copy } from "lucide-react-native"; import { useTranslation } from "react-i18next"; @@ -96,13 +102,13 @@ export const HighlightedCodeBlock = React.memo(function HighlightedCodeBlock({ onPointerLeave={handlePointerLeave} > {keyedLines ? ( - + {renderCodeSegments(keyedLines)} - +
) : ( - + {renderedCode} - +
)} @@ -129,9 +135,7 @@ interface TokenSpanProps { const TokenSpan = React.memo(function TokenSpan({ token }: TokenSpanProps) { return ( - - {token.text} - + {token.text} ); }); @@ -140,7 +144,7 @@ interface CodeTextSpanProps { } const CodeTextSpan = React.memo(function CodeTextSpan({ text }: CodeTextSpanProps) { - return {text}; + return {text}; }); interface SplitStyles { diff --git a/packages/app/src/components/markdown-prose/index.tsx b/packages/app/src/components/markdown-prose/index.tsx deleted file mode 100644 index 94ceb0d13a..0000000000 --- a/packages/app/src/components/markdown-prose/index.tsx +++ /dev/null @@ -1,2 +0,0 @@ -export { MarkdownProse } from "./renderer"; -export { groupMarkdownProse } from "./runs"; diff --git a/packages/app/src/components/markdown-prose/prepare.test.ts b/packages/app/src/components/markdown-prose/prepare.test.ts deleted file mode 100644 index 8577890912..0000000000 --- a/packages/app/src/components/markdown-prose/prepare.test.ts +++ /dev/null @@ -1,217 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { - prepareProse, - createProsePreparer, - inlineCodeLinkContent, - proseLinkSource, - handleProseLinkPress, -} from "./prepare"; - -const noFiles = () => false; -describe("source-preserving prose", () => { - it.each([ - ["http://[::1]:3000", ""], - ["[http://[::1]:3000](http://[::1]:3000)", "[http\\://[::1]:3000]()"], - ["http://[::1]:3000/path?x=[1]", ""], - ["###### a", "###### A"], - ])("preserves IPv6 links and decoded heading case: %s", (source, expected) => { - expect(prepareProse([source], noFiles)).toBe(expected); - }); - it.each([ - [ - "https://example.com/?tags[]=a", - "[https://example.com/?tags[]=a]()", - ], - ["`src/[...]/file.ts`", "`src/[...]/file.ts`"], - ["[text []](https://example.com)", "[text []](https://example.com)"], - ["https://example.com/a*b*c", "[https://example.com/a]()*b*c"], - ])("uses parser-owned link ranges: %s", (source, expected) => { - expect(prepareProse([source], noFiles)).toBe(expected); - }); - it.each([ - ["Paths ~/.config and ~/.local are used.", "Paths \\~/.config and \\~/.local are used."], - ["~/a ~/b", "\\~/a \\~/b"], - ["~~gone~~", "~~gone~~"], - ["a ~ b", "a \\~ b"], - ["`~~` and `~/a ~/b`", "`~~` and `~/a ~/b`"], - ])("keeps single tildes literal: %s", (source, expected) => { - expect(prepareProse([source], noFiles)).toBe(expected); - }); - it.each([ - ["https://example.com/a&b", "[https://example.com/a&b]()"], - ["https://example.com/é", "[https://example.com/é]()"], - ["https://example.com/a\\_b", "[https://example.com/a\\_b]()"], - ])("wraps linkify's normalized destination: %s", (source, expected) => { - expect(prepareProse([source], noFiles)).toBe(expected); - }); - it.each([ - "A & B", - "# A & B", - "- A & B", - "> A & B", - "Escaped \\* and \\|", - "", - "**bold** and *emphasis* and ~~deleted~~", - "First \nSecond", - "# #", - "Heading\n=======", - 'A [titled link](https://example.com "A title")', - "[linked `code`](https://example.net)", - "Before ![shot](file:///repo/shots/app.png) after.", - "Blank ` ` code and ` normal ` code", - "\n\nA paragraph.\n\n", - "- [ ] Task\n- [x] Done", - "> [!NOTE]\n> Note", - "> Ordinary quote", - ])("copies unchanged source byte-for-byte: %s", (source) => { - expect(prepareProse([source], noFiles)).toBe(source); - }); - it.each([ - ["[file](file:///repo/a&b.txt)", "[file]()"], - ["![file](file:///repo/a&b.png)", "![file]()"], - ["- first\n\t& second", "- first\n & second"], - ["[foo\nbar]", "\\[foo\nbar]"], - ["[foo\\*bar]", "\\[foo\\*bar]"], - ])("preserves decoded destinations and source positions: %s", (source, expected) => { - expect(prepareProse([source], noFiles)).toBe(expected); - }); - it.each([ - [" ---", "\\\n\\---"], - ["Before # after", "Before\\\n\\# after"], - [" 1. next", "\\\n1\\. next"], - [" ---", "\u00a0\u00a0\u00a0\u00a0---"], - [" ---", "\u00a0---"], - ["Before after", "Before\\\nafter"], - ])("keeps whitespace entities inside text: %s", (source, expected) => { - expect(prepareProse([source], noFiles)).toBe(expected); - }); - it("keeps multiline and escaped labels local to each source block", () => { - expect(prepareProse(["[foo\nbar]", "Other", "[foo bar]: https://example.com"], noFiles)).toBe( - "\\[foo\nbar]\n\nOther\n\n[foo bar]: https://example.com", - ); - expect( - prepareProse(["[foo\\*bar]", "Other", "[foo\\*bar]: https://example.com"], noFiles), - ).toBe("\\[foo\\*bar]\n\nOther\n\n[foo\\*bar]: https://example.com"); - }); - it("prepares only changed source blocks on append", () => { - const candidates: string[] = []; - const prepare = createProsePreparer((text) => { - candidates.push(text); - return false; - }); - prepare(["First `one`.", "Next `two`.", "Last `three`."], Infinity); - candidates.length = 0; - prepare(["First `one`.", "Next `two`.", "Last `three`. More."], Infinity); - expect(candidates).toEqual(["three"]); - }); - it("bounds preparation before parsing a capped block", () => { - let candidates = 0; - const prepare = createProsePreparer(() => { - candidates++; - return false; - }); - const source = "Use `foo` here. ".repeat(70_000); - prepare([source], 32_000); - expect(candidates).toBeLessThan(2200); - }); - it("decodes entities outside code", () => { - expect(prepareProse(["A & B A A © `&`"], noFiles)).toBe( - "A & B A A © `&`", - ); - }); - it("decodes entities once, including encoded entity syntax", () => { - expect(prepareProse(["Literal &amp; and &copy;"], noFiles)).toBe( - "Literal \\& and \\©", - ); - }); - it.each(["\r", "\r\n"])("normalizes line endings %j", (ending) => { - expect(prepareProse([`First${ending}Second ||text||`], noFiles)).toBe( - "First\nSecond \\|\\|text\\|\\|", - ); - }); - it.each([ - ["See example.com.", "See [example.com]()."], - ["https://example.com/a_(b).", "[https://example.com/a_(b)]()."], - ["https://example.com/a.", "[https://example.com/a]()."], - ])("makes linkify destinations explicit: %s", (source, expected) => { - expect(prepareProse([source], noFiles)).toBe(expected); - }); - it("only exempts task markers inside list items", () => { - expect(prepareProse(["[x]", "Other\n\n[x]: https://example.com"], noFiles)).toBe( - "\\[x]\n\nOther\n\n[x]: https://example.com", - ); - const tasks = "- [x] Task\n- [ ] Pending\n1. [x] Ordered"; - expect(prepareProse([tasks], noFiles)).toBe(tasks); - }); - it("prepares 30k of inline code in under 50ms warm", () => { - const text = "Use `foo` and `bar` here. ".repeat(1300).slice(0, 30000); - for (let i = 0; i < 10; i++) prepareProse([text], noFiles); - const start = performance.now(); - for (let i = 0; i < 5; i++) expect(prepareProse([text], noFiles)).toBe(text); - expect((performance.now() - start) / 5).toBeLessThan(50); - }); - it("changes only prose pipes", () => { - expect(prepareProse(["Keep ||literal|| and `a || b` and [a|b](https://x/a|b)."], noFiles)).toBe( - "Keep \\|\\|literal\\|\\| and `a || b` and [a\\|b](https://x/a|b).", - ); - }); - it("wraps inline file code without reprinting its source", () => { - const source = "[Docs](https://example.com) and `` src/file.ts:12 ``"; - expect(prepareProse([source], (text) => text === "src/file.ts:12")).toBe( - "[Docs](https://example.com) and [`` src/file.ts:12 ``]()", - ); - expect(inlineCodeLinkContent("paseo-inline-code:src%2Ffile.ts%3A12")).toBe("src/file.ts:12"); - }); - it("keeps inline-code URLs tappable", () => { - expect(prepareProse(["`https://example.com`"], noFiles)).toBe( - "[`https://example.com`]()", - ); - }); - it("uppercases only h6 text", () => { - expect( - prepareProse( - ["###### Small **bold** `lower.ts` [link](https://example.com/lower) &"], - noFiles, - ), - ).toBe("###### SMALL **BOLD** `lower.ts` [LINK](https://example.com/lower) &"); - }); - it("prepares source blocks independently, even beside a reference definition", () => { - const blocks = ["![shot][pic]", "Other paragraph.\n\n[pic]: shots/app.png"]; - expect(prepareProse(blocks, noFiles)).toBe( - blocks.map((block) => prepareProse([block], noFiles)).join("\n\n"), - ); - expect(prepareProse(blocks, noFiles)).toBe( - "!\\[shot]\\[pic]\n\nOther paragraph.\n\n[pic]: shots/app.png", - ); - }); - it("localizes resolved references before joining blocks with conflicting definitions", () => { - const blocks = [ - "[foo][ref]\n\n[ref]: https://one.example", - "[bar][ref]\n\n[ref]: https://two.example", - ]; - expect(prepareProse(blocks, noFiles)).toBe( - "[foo]()\n\n[ref]: https://one.example\n\n[bar]()\n\n[ref]: https://two.example", - ); - }); - it("carries a bare filename's text and href through the file action", () => { - const prepared = prepareProse(["Open README.md"], (text) => text === "README.md"); - const source = { - href: "http://README.md", - text: "README.md", - markup: "linkify", - sourceInfo: "auto", - }; - const url = `paseo-file-link:${encodeURIComponent(JSON.stringify(source))}`; - expect(prepared).toBe(`Open [README.md](<${url}>)`); - expect(proseLinkSource(url)).toEqual(source); - const opened: unknown[] = []; - handleProseLinkPress( - url, - (value) => opened.push(value), - () => { - throw new Error("Opened website"); - }, - ); - expect(opened).toEqual([source]); - }); -}); diff --git a/packages/app/src/components/markdown-prose/prepare.ts b/packages/app/src/components/markdown-prose/prepare.ts deleted file mode 100644 index b643215489..0000000000 --- a/packages/app/src/components/markdown-prose/prepare.ts +++ /dev/null @@ -1,429 +0,0 @@ -import type { AssistantFileLinkSource } from "@/assistant-file-links"; -import type Token from "markdown-it/lib/token.mjs"; -import { type InlineSpan, type InlineTrace, createSourceParser } from "./source-inline"; - -const settled = createSourceParser(); -const INLINE_CODE_SCHEME = "paseo-inline-code:"; -const FILE_LINK_SCHEME = "paseo-file-link:"; - -export function inlineCodeLinkContent(url: string): string | null { - return url.startsWith(INLINE_CODE_SCHEME) - ? decodeURIComponent(url.slice(INLINE_CODE_SCHEME.length)) - : null; -} -export function proseLinkSource(url: string): AssistantFileLinkSource | null { - try { - const code = inlineCodeLinkContent(url); - if (code !== null) return { href: code, text: code, sourceType: "inline-code" }; - if (!url.startsWith(FILE_LINK_SCHEME)) return null; - const source: unknown = JSON.parse(decodeURIComponent(url.slice(FILE_LINK_SCHEME.length))); - if ( - !source || - typeof source !== "object" || - !("href" in source) || - !("text" in source) || - typeof source.href !== "string" || - typeof source.text !== "string" - ) - return null; - return { href: source.href, text: source.text, markup: "linkify", sourceInfo: "auto" }; - } catch { - return null; - } -} - -export function handleProseLinkPress( - url: string, - open: (source: AssistantFileLinkSource) => void, - onLinkPress: (url: string) => unknown, -): void { - const source = proseLinkSource(url); - if (source) open(source); - else onLinkPress(url); -} -interface Edit { - start: number; - end: number; - text: string; -} -function applyEdits(source: string, edits: Edit[]): string { - let cursor = 0; - let result = ""; - for (const edit of edits.sort((a, b) => a.start - b.start || a.end - b.end)) { - if (edit.start < cursor) continue; - result += source.slice(cursor, edit.start) + edit.text; - cursor = edit.end; - } - return result + source.slice(cursor); -} - -/** Each completed source block retains its own reference scope. */ -export function prepareProse( - blocks: readonly string[], - canResolveCode: (text: string) => boolean, - visibleLength = Infinity, -): string { - return createProsePreparer(canResolveCode)(blocks, visibleLength); -} - -/** One cache per prose row; joining a completed block does not reparse earlier blocks. */ -export function createProsePreparer(canResolveCode: (text: string) => boolean) { - let cache = new Map(); - return (blocks: readonly string[], visibleLength = Infinity): string => { - const retained = new Map(); - const result: string[] = []; - let remaining = visibleLength; - for (const block of blocks) { - if (remaining < 0) break; - const source = block.slice(0, remaining); - const prepared = cache.get(source) ?? prepareBlock(source, canResolveCode); - retained.set(source, prepared); - result.push(prepared); - remaining -= block.length + 2; - } - cache = retained; - return result.join("\n\n"); - }; -} - -interface InlinePreparation { - source: string; - token: Token; - trace: InlineTrace; - parser: typeof settled.parser; - env: object; - uppercase: boolean; - canResolveCode: (text: string) => boolean; - protectedRanges: Uint8Array; - referenceRanges: Uint8Array; - task: boolean; - edits: Edit[]; -} -function insert(context: InlinePreparation, at: number, text: string) { - context.edits.push({ start: at, end: at, text }); -} -function contains(ranges: Uint8Array, start: number, end = start + 1): boolean { - for (let i = start; i < end; i++) if (ranges[i]) return true; - return false; -} -function protect(ranges: Uint8Array, span: { start: number; end: number }) { - ranges.fill(1, span.start, span.end); -} -function codeUrl(context: InlinePreparation, content: string): string | null { - if (context.canResolveCode(content)) return INLINE_CODE_SCHEME + encodeURIComponent(content); - const matches = context.parser.linkify.match(content); - const match = matches?.length === 1 ? matches[0] : undefined; - return match?.index === 0 && match.lastIndex === content.length ? match.url : null; -} -function prepareCode(context: InlinePreparation, span: InlineSpan, code: Token) { - protect(context.protectedRanges, span); - protect(context.referenceRanges, span); - const url = codeUrl(context, code.content); - if (!span.insideLink && url) { - insert(context, span.start, "["); - insert(context, span.end, `](<${url}>)`); - } -} -function labelEnd(context: InlinePreparation, source: string, start: number): number { - const state = new context.parser.inline.State(source, context.parser, context.env, []); - return context.parser.helpers.parseLinkLabel(state, start, false); -} -function preserveLinkScope(context: InlinePreparation, span: InlineSpan, raw: string, link: Token) { - const end = labelEnd(context, raw, 0); - protect(context.protectedRanges, { start: span.start + end, end: span.end }); - protect(context.referenceRanges, span); - // MD4C's permissive autolink scan otherwise consumes the bracketed host - // inside a link label before the explicit link is recognized. - const ipv6Label = /^https?:\/\/\[/.exec(raw.slice(1, end)); - if (ipv6Label) insert(context, span.start + 1 + ipv6Label[0].indexOf(":"), "\\"); - if (raw[end + 1] === "(") { - decodeDestination(context, span, raw, end, link.attrGet("href")!); - return; - } - // The native parser sees the joined run; bind reference destinations to their source block. - const title = link.attrGet("title"); - const quotedTitle = - title === null ? "" : ` "${title.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`; - context.edits.push({ - start: span.start + end, - end: span.end, - text: `](<${link.attrGet("href")}>${quotedTitle})`, - }); -} -function decodeDestination( - context: InlinePreparation, - span: InlineSpan, - raw: string, - label: number, - href: string, -) { - if (raw[label + 1] !== "(") return; - const start = label + 2 + (raw.slice(label + 2).match(/^\s*/)?.[0].length ?? 0); - const destination = context.parser.helpers.parseLinkDestination(raw, start, raw.length); - if (!destination.ok) return; - const original = raw.slice(start, destination.pos); - if (context.parser.utils.unescapeAll(original) === original && !/[[\]]/.test(original)) return; - href = href.replace(/^(https?:\/\/)%5B([^/]+)%5D/i, "$1[$2]"); - context.edits.push({ - start: span.start + start, - end: span.start + destination.pos, - text: `<${href}>`, - }); -} -function entityWhitespace( - context: InlinePreparation, - span: InlineSpan, - decoded: string, -): string | null { - if (!/^[\t\n\r\f\v ]+$/.test(decoded)) return null; - if (!/[\r\n]/.test(decoded)) - return decoded.replace(/\t/g, "\u00a0\u00a0\u00a0\u00a0").replace(/[\f\v ]/g, "\u00a0"); - // A text-token newline must not start a block. Backslash breaks also work at - // the beginning of a paragraph, where two-space breaks would be trimmed away. - const following = context.source.slice(span.end); - const marker = - /^[ \t]*(?:[-=]+[ \t]*(?:\n|$)|(?:[-*_][ \t]*){3,}(?:\n|$)|#{1,6}(?:[ \t]|$)|>|\d{1,9}[.)][ \t]|[-+*][ \t]|`{3,}|~{3,}|<)/.exec( - following, - ); - if (marker) { - const punctuation = marker[0].search(/[^ \t\d]/); - insert(context, span.end + punctuation, "\\"); - } - return decoded.replace(/\r\n?|\n/g, "\\\n"); -} -function decodeEntity(context: InlinePreparation, span: InlineSpan, raw: string) { - // MD4C 1.1.1 drops entity nodes. Decode once without introducing Markdown syntax. - const value = context.parser.utils.unescapeAll(raw); - const decoded = context.uppercase ? value.toUpperCase() : value; - let text = - entityWhitespace(context, span, decoded) ?? - decoded.replace(/([\\`*{}[\]()#+.!_<>|~-])/g, "\\$1"); - if (decoded.endsWith("&") && /^(?:#\w+|\w+);/.test(context.source.slice(span.end))) - text = text.slice(0, -1) + "\\&"; - context.edits.push({ start: span.start, end: span.end, text }); - protect(context.protectedRanges, span); -} -function prepareSpan(context: InlinePreparation, span: InlineSpan) { - const raw = context.source.slice(span.start, span.end); - const link = span.tokens.find((child) => child.type === "link_open"); - const image = span.tokens.find((child) => child.type === "image"); - const code = span.tokens.find((child) => child.type === "code_inline"); - if (image) { - decodeDestination(context, span, raw, labelEnd(context, raw, 1), image.attrGet("src")!); - protect(context.protectedRanges, span); - protect(context.referenceRanges, span); - } else if (raw.startsWith("\\") || (link && raw.startsWith("<"))) { - protect(context.protectedRanges, span); - protect(context.referenceRanges, span); - } else if (/^&(?:#\w+|\w+);$/.test(raw)) { - decodeEntity(context, span, raw); - } else if (link && raw.startsWith("[")) { - preserveLinkScope(context, span, raw, link); - } else if (code && raw.startsWith("`")) { - prepareCode(context, span, code); - } -} - -function preserveUnresolvedReferences(context: InlinePreparation) { - // Protect every unresolved label, regardless of CommonMark's case/whitespace - // normalization. Escapes inside a label do not make its opening bracket safe. - const openings: number[] = []; - for (let index = 0; index < context.source.length; index++) { - if (contains(context.referenceRanges, index)) continue; - if (context.source[index] === "[") openings.push(index); - if (context.source[index] !== "]") continue; - const start = openings.pop(); - if (start === undefined) continue; - const label = context.source.slice(start, index + 1); - if (/^\[!(?:NOTE|TIP|IMPORTANT|WARNING|CAUTION)\]$/.test(label)) continue; - if ( - context.task && - start === 0 && - /^\[[ xX]\]$/.test(label) && - /\s/.test(context.source[index + 1] ?? "") - ) - continue; - insert(context, start, "\\"); - } -} -function prepareText(context: InlinePreparation) { - for (let i = 0; i < context.source.length; i++) { - if (contains(context.protectedRanges, i)) continue; - const char = context.source[i]!; - if ( - char === "|" || - (char === "~" && context.source[i - 1] !== "~" && context.source[i + 1] !== "~") - ) - insert(context, i, "\\"); - if (context.uppercase && char.toUpperCase() !== char) - context.edits.push({ start: i, end: i + 1, text: char.toUpperCase() }); - } -} -/** Linkify sees decoded inline text; keep its offsets tied to the original Markdown. */ -function linkifySource(context: InlinePreparation) { - const positions: number[] = []; - let decoded = ""; - let cursor = 0; - const escapes = /\\[!"#$%&'()*+,\-./:;<=>?@[\\\]^_`{|}~]|&[a-z#][a-z0-9]{1,31};/gi; - function appendPlain(end: number) { - while (cursor < end) { - positions.push(cursor); - decoded += context.source[cursor++]; - } - } - for (const match of context.source.matchAll(escapes)) { - appendPlain(match.index); - const text = context.parser.utils.unescapeAll(match[0]); - for (let index = 0; index < text.length; index++) positions.push(cursor); - decoded += text; - cursor += match[0].length; - } - appendPlain(context.source.length); - positions.push(cursor); - return { decoded, positions }; -} -function prepareLinkifiedLinks(context: InlinePreparation) { - const links = - context.token.children?.filter( - (child) => child.type === "link_open" && child.markup === "linkify", - ) ?? []; - if (!links.length && !context.source.includes("://[")) return; - const destinations = new Map(links.map((link) => [link.attrGet("href"), link])); - const { decoded, positions } = linkifySource(context); - let cursor = 0; - for (const text of context.trace.linkifyTexts) { - if (!text) continue; - let offset = decoded.indexOf(text, cursor); - while ( - offset >= 0 && - contains(context.referenceRanges, positions[offset]!, positions[offset + text.length]!) - ) - offset = decoded.indexOf(text, offset + 1); - if (offset < 0) continue; - cursor = offset + text.length; - prepareIpv6Links(context, text, offset, positions); - for (const match of context.parser.linkify.match(text) ?? []) { - const link = destinations.get(context.parser.normalizeLink(match.url)); - if (!link) continue; - wrapLinkifiedMatch( - context, - positions[offset + match.index]!, - positions[offset + match.lastIndex]!, - link.attrGet("href")!, - match.text, - ); - } - } -} -/** linkify-it omits bracketed hosts; bind those URLs in plain parser text explicitly. */ -function prepareIpv6Links( - context: InlinePreparation, - text: string, - offset: number, - positions: number[], -) { - for (const match of text.matchAll(/https?:\/\/\[[0-9a-f:.]+\](?::\d+)?(?:[/?#][^\s<>]*)?/gi)) { - try { - if (!new URL(match[0]).hostname.startsWith("[")) continue; - } catch { - continue; - } - const start = positions[offset + match.index]!; - const end = positions[offset + match.index + match[0].length]!; - if (contains(context.referenceRanges, start, end)) continue; - protect(context.referenceRanges, { start, end }); - protect(context.protectedRanges, { start, end }); - insert(context, start, "<"); - insert(context, end, ">"); - } -} -function wrapLinkifiedMatch( - context: InlinePreparation, - start: number, - end: number, - destination: string, - text: string, -) { - const payload: AssistantFileLinkSource = { - href: destination, - text, - markup: "linkify", - sourceInfo: "auto", - }; - const href = context.canResolveCode(text) - ? FILE_LINK_SCHEME + encodeURIComponent(JSON.stringify(payload)) - : destination; - protect(context.referenceRanges, { start, end }); - insert(context, start, "["); - insert(context, end, `](<${href}>)`); -} - -function sourcePositions( - token: Token, - heading: Token | undefined, - lines: string[], - lineOffsets: number[], -): number[] { - const positions: number[] = []; - for (const [index, content] of token.content.split("\n").entries()) { - const row = token.map![0] + index; - const rawLine = lines[row]!; - let headingPrefix = 0; - if (heading?.type === "heading_open" && heading.markup.startsWith("#")) - headingPrefix = rawLine.match(/^(?:\s*>\s*)*\s*#{1,6}\s+/)?.[0].length ?? 0; - const column = rawLine.indexOf(content, headingPrefix); - const start = lineOffsets[row]! + Math.max(0, column); - for (let i = 0; i <= content.length; i++) positions.push(start + i); - } - return positions; -} -function prepareBlock(text: string, canResolveCode: (text: string) => boolean): string { - text = text.replace(/\r\n?/g, "\n").replaceAll("\0", "\ufffd"); - text = text.replace(/^[ \t>]*/gm, (prefix) => { - let expanded = ""; - for (const char of prefix) - expanded += char === "\t" ? " ".repeat(4 - (expanded.length % 4)) : char; - return expanded; - }); - const { parser, traces } = settled; - const env = {}; - const tokens = parser.parse(text, env); - const lines = text.split("\n"); - const lineOffsets: number[] = []; - let offset = 0; - for (const line of lines) { - lineOffsets.push(offset); - offset += line.length + 1; - } - const edits: Edit[] = []; - for (const [index, token] of tokens.entries()) { - if (token.type !== "inline" || !token.map) continue; - const trace = traces.get(token)!; - const heading = tokens[index - 1]; - const context: InlinePreparation = { - source: token.content, - token, - trace, - parser, - env, - uppercase: heading?.type === "heading_open" && heading.tag === "h6", - canResolveCode, - protectedRanges: new Uint8Array(token.content.length), - referenceRanges: new Uint8Array(token.content.length), - task: tokens[index - 2]?.type === "list_item_open", - edits: [], - }; - // Link/code ownership is established before references can be rewritten. Linkify uses the parser's emphasis-split text tokens. - for (const span of trace.spans) - if (span.tokens.some((child) => ["link_open", "code_inline", "image"].includes(child.type))) - protect(context.referenceRanges, span); - prepareLinkifiedLinks(context); - for (const span of trace.spans) prepareSpan(context, span); - preserveUnresolvedReferences(context); - prepareText(context); - const positions = sourcePositions(token, heading, lines, lineOffsets); - for (const edit of context.edits) - edits.push({ ...edit, start: positions[edit.start]!, end: positions[edit.end]! }); - } - return applyEdits(text, edits); -} diff --git a/packages/app/src/components/markdown-prose/props.ts b/packages/app/src/components/markdown-prose/props.ts deleted file mode 100644 index 19f2d5092e..0000000000 --- a/packages/app/src/components/markdown-prose/props.ts +++ /dev/null @@ -1,9 +0,0 @@ -import type { ReactNode } from "react"; - -export interface MarkdownProseProps { - text: string; - sourceBlocks: readonly string[]; - kind: "prose" | "quote" | "block"; - onLinkPress: (url: string) => boolean; - children: ReactNode; -} diff --git a/packages/app/src/components/markdown-prose/renderer.native.tsx b/packages/app/src/components/markdown-prose/renderer.native.tsx deleted file mode 100644 index 91a2511a83..0000000000 --- a/packages/app/src/components/markdown-prose/renderer.native.tsx +++ /dev/null @@ -1,134 +0,0 @@ -import { memo, useMemo } from "react"; -import { EnrichedMarkdownText, type MarkdownStyle } from "react-native-enriched-markdown"; -import { withUnistyles } from "react-native-unistyles"; -import { useAssistantFileLinkActions } from "@/assistant-file-links"; -import { useStableEvent } from "@/hooks/use-stable-event"; -import { createMarkdownStyles } from "@/styles/markdown-styles"; -import type { Theme } from "@/styles/theme"; -import type { MarkdownProseProps } from "./props"; -import { handleProseLinkPress, createProsePreparer } from "./prepare"; - -const ThemedMarkdown = withUnistyles(EnrichedMarkdownText); -const MD4C_FLAGS = { latexMath: false, hardSoftBreaks: true, admonitions: true }; - -function proseStyleMapping(theme: Theme): { markdownStyle: MarkdownStyle } { - const styles = createMarkdownStyles(theme); - const paragraph = { - color: styles.body.color, - fontSize: styles.body.fontSize, - lineHeight: styles.body.lineHeight, - marginTop: 0, - marginBottom: styles.paragraph.marginBottom + theme.spacing[3], - }; - return { - markdownStyle: { - paragraph, - h1: { - ...styles.heading1, - marginBottom: - styles.heading1.marginBottom + theme.spacing[3] + styles.heading1.paddingBottom, - }, - h2: { - ...styles.heading2, - marginBottom: - styles.heading2.marginBottom + theme.spacing[3] + styles.heading2.paddingBottom, - }, - h3: { ...styles.heading3, marginBottom: styles.heading3.marginBottom + theme.spacing[3] }, - h4: { ...styles.heading4, marginBottom: styles.heading4.marginBottom + theme.spacing[3] }, - h5: { ...styles.heading5, marginBottom: styles.heading5.marginBottom + theme.spacing[3] }, - h6: { ...styles.heading6, marginBottom: styles.heading6.marginBottom + theme.spacing[3] }, - list: { - ...paragraph, - bulletColor: styles.bullet_list_icon.color, - markerColor: styles.ordered_list_icon.color, - gapWidth: 7, - itemSpacing: styles.list_item.marginBottom, - marginLeft: theme.spacing[4], - }, - blockquote: { - ...paragraph, - color: styles.blockquote.color, - backgroundColor: styles.blockquote.backgroundColor, - borderColor: styles.blockquote.borderLeftColor, - borderWidth: styles.blockquote.borderLeftWidth, - borderRadius: styles.blockquote.borderRadius, - padding: styles.blockquote.paddingTop, - gapWidth: 4, - marginTop: styles.blockquote.marginVertical, - marginBottom: styles.blockquote.marginVertical, - }, - link: { color: styles.link.color, underline: false }, - code: { - // RCTFont does not resolve Fabric's ui-monospace alias. An empty family - // selects the library's native system-monospace font on iOS. - fontFamily: - styles.code_inline.fontFamily === "ui-monospace" ? "" : styles.code_inline.fontFamily, - fontSize: styles.code_inline.fontSize, - color: styles.code_inline.color, - backgroundColor: styles.code_inline.backgroundColor, - borderColor: "transparent", - }, - strong: { fontWeight: "bold" }, - em: styles.em, - strikethrough: { color: styles.s.color }, - thematicBreak: { - color: styles.hr.backgroundColor, - height: styles.hr.height, - marginTop: styles.hr.marginVertical, - marginBottom: styles.hr.marginVertical, - }, - }, - }; -} - -function quoteStyleMapping(theme: Theme): { markdownStyle: MarkdownStyle } { - const mapped = proseStyleMapping(theme); - return { - markdownStyle: { - ...mapped.markdownStyle, - paragraph: { ...mapped.markdownStyle.paragraph, marginBottom: theme.spacing[3] }, - }, - }; -} - -const PROSE_CONTAINER = { marginBottom: -12 }; - -const NativeProse = memo(function NativeProse({ - text, - sourceBlocks, - kind, - onLinkPress, -}: MarkdownProseProps) { - const actions = useAssistantFileLinkActions(); - const canResolveCode = useStableEvent((content: string) => - actions.canResolveFile({ href: content, text: content, sourceType: "inline-code" }), - ); - const prepare = useMemo(() => createProsePreparer(canResolveCode), [canResolveCode]); - const markdown = useMemo( - () => prepare(sourceBlocks, text.length), - [text.length, sourceBlocks, prepare], - ); - const handleLinkPress = useStableEvent(({ url }: { url: string }) => { - handleProseLinkPress(url, (source) => actions.open(source, "preferred"), onLinkPress); - }); - return ( - - ); -}); - -export function MarkdownProse(props: MarkdownProseProps) { - return props.kind === "block" ? props.children : ; -} diff --git a/packages/app/src/components/markdown-prose/renderer.tsx b/packages/app/src/components/markdown-prose/renderer.tsx deleted file mode 100644 index 546b4be816..0000000000 --- a/packages/app/src/components/markdown-prose/renderer.tsx +++ /dev/null @@ -1 +0,0 @@ -export { MarkdownProse } from "./renderer.web"; diff --git a/packages/app/src/components/markdown-prose/renderer.web.tsx b/packages/app/src/components/markdown-prose/renderer.web.tsx deleted file mode 100644 index 42fd94e331..0000000000 --- a/packages/app/src/components/markdown-prose/renderer.web.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import type { MarkdownProseProps } from "./props"; - -export function MarkdownProse({ children }: MarkdownProseProps) { - return children; -} diff --git a/packages/app/src/components/markdown-prose/runs.ts b/packages/app/src/components/markdown-prose/runs.ts deleted file mode 100644 index 6c61f7fff9..0000000000 --- a/packages/app/src/components/markdown-prose/runs.ts +++ /dev/null @@ -1,198 +0,0 @@ -import { ASSISTANT_MESSAGE_RENDER_CHARACTER_LIMIT } from "@/components/assistant-message-render-limit"; -import { createAssistantMarkdownParser } from "@/utils/assistant-markdown-parser"; -import type Token from "markdown-it/lib/token.mjs"; -import type { AssistantMessageItem } from "@/types/stream"; - -const parser = createAssistantMarkdownParser().set({ html: true }); -const partsByBlock = new WeakMap(); - -function splitBlock(block: AssistantMessageItem): AssistantMessageItem[] { - const cached = partsByBlock.get(block); - if (cached) return cached; - // Oversized blocks cannot join capped runs; the existing renderer caps text before parsing. - if (block.text.length > ASSISTANT_MESSAGE_RENDER_CHARACTER_LIMIT) { - return [{ ...block, markdownKind: "block" }]; - } - const environment: { references?: unknown } = {}; - const tokens = parser.parse(block.text, environment); - const preserved = tokens.some( - (token) => - ["fence", "code_block", "table_open", "html_block"].includes(token.type) || - token.children?.some((child) => child.type === "html_inline"), - ); - const isQuote = tokens.some((token) => token.type === "blockquote_open"); - const hasImage = tokens.some((token) => token.children?.some((child) => child.type === "image")); - let parts: AssistantMessageItem[]; - // Retain the container and its definitions when splitting would lose image context. - if (preserved || (hasImage && (environment.references || !canSplitParagraphImages(tokens)))) { - parts = [{ ...block, markdownKind: "block" }]; - } else if (hasImage) { - parts = splitParagraphImages(block); - } else { - parts = [{ ...block, markdownKind: isQuote ? "quote" : "prose" }]; - } - partsByBlock.set(block, parts); - return parts; -} - -function canSplitParagraphImages(tokens: Token[]): boolean { - if (tokens.length !== 3 || tokens[0]?.type !== "paragraph_open") return false; - let depth = 0; - for (const child of tokens[1]!.children ?? []) { - if (child.type === "image" && depth !== 0) return false; - depth += child.nesting; - } - return true; -} - -function splitParagraphImages(block: AssistantMessageItem): AssistantMessageItem[] { - const parts: AssistantMessageItem[] = []; - const state = new parser.inline.State(block.text, parser, {}, []); - let cursor = 0; - function append(text: string, markdownKind: "prose" | "block", offset: number) { - if (!text.trim()) return; - parts.push({ ...block, text: text.trim(), markdownKind, id: `${block.id}:part:${offset}` }); - } - while (state.pos < state.posMax) { - const start = state.pos; - parser.inline.skipToken(state); - const candidate = block.text.slice(start, state.pos); - if (!candidate.includes("![")) continue; - const inline = parser.parseInline(candidate, {})[0]; - if (!inline?.children?.some((child) => child.type === "image")) continue; - append(block.text.slice(cursor, start), "prose", cursor); - append(candidate, "block", start); - cursor = state.pos; - } - append(block.text.slice(cursor), "prose", cursor); - if (parts.length === 1) parts[0] = { ...parts[0]!, id: block.id }; - return parts; -} - -interface GroupedRuns { - growing: boolean; - rows: AssistantMessageItem[]; - starts: number[]; - ends: number[]; -} -const runsByBlocks = new WeakMap(); -const growingRunsByBlocks = new WeakMap(); - -/** Rebuild only the run containing the changed source block; completed runs retain identity. */ -export function groupMarkdownProse( - blocks: readonly AssistantMessageItem[], - previousBlocks?: readonly AssistantMessageItem[], - growing = false, -): AssistantMessageItem[] { - const cache = growing ? growingRunsByBlocks : runsByBlocks; - const cached = cache.get(blocks); - if (cached) return cached.rows; - const previous = previousGroups(previousBlocks); - const retained = retainedRunCount(blocks, previousBlocks, previous, growing); - const start = previous?.starts[retained] ?? 0; - const rows = previous?.rows.slice(0, retained) ?? []; - const starts = previous?.starts.slice(0, retained) ?? []; - const ends = previous?.ends.slice(0, retained) ?? []; - let parts: AssistantMessageItem[] = []; - let length = 0; - let runStart = start; - let runEnd = start; - function flush() { - if (!parts.length) return; - const first = parts[0]!; - const texts = parts.map((part) => part.text); - rows.push({ - ...first, - text: texts.join("\n\n"), - markdownProseBlocks: first.markdownKind === "block" ? undefined : texts, - id: first.markdownKind === "prose" ? first.id.replace(":block:", ":run:") : first.id, - }); - starts.push(runStart); - ends.push(runEnd); - parts = []; - length = 0; - } - for (let index = start; index < blocks.length; index++) { - const block = blocks[index]!; - const partsForBlock = - growing && index === blocks.length - 1 - ? [{ ...block, markdownKind: "block" as const }] - : splitBlock(block); - for (const part of partsForBlock) { - if (parts.length && !canJoinRun(parts[0]!, part, length)) flush(); - if (!parts.length) runStart = index; - length += (parts.length ? 2 : 0) + part.text.length; - parts.push(part); - runEnd = index; - } - } - flush(); - reuseUnchangedRows(blocks, previousBlocks, previous, rows, starts, ends, retained); - cache.set(blocks, { rows, starts, ends, growing }); - return rows; -} - -function retainedRunCount( - blocks: readonly AssistantMessageItem[], - previousBlocks: readonly AssistantMessageItem[] | undefined, - previous: GroupedRuns | undefined, - growing: boolean, -): number { - if (!previous || !previousBlocks) return 0; - let shared = 0; - while (shared < blocks.length && blocks[shared] === previousBlocks[shared]) shared++; - if (previous.growing && (!growing || blocks.length !== previousBlocks.length)) - shared = Math.min(shared, previousBlocks.length - 1); - let retained = 0; - while (retained < previous.rows.length && previous.ends[retained]! < shared) retained++; - // Only completed blocks can merge with the preceding run. The live tail is a - // hard boundary, so appending to it never rebuilds completed prose. - if (!(growing && shared === blocks.length - 1)) retained = Math.max(0, retained - 1); - while (retained > 0 && previous.ends[retained - 1]! >= (previous.starts[retained] ?? shared)) - retained--; - return retained; -} -function reuseUnchangedRows( - blocks: readonly AssistantMessageItem[], - previousBlocks: readonly AssistantMessageItem[] | undefined, - previous: GroupedRuns | undefined, - rows: AssistantMessageItem[], - starts: number[], - ends: number[], - retained: number, -) { - if (!previous || !previousBlocks) return; - for (let index = retained; index < rows.length; index++) { - const old = previous.rows[index]; - const row = rows[index]!; - if (old?.id !== row.id || old.text !== row.text || old.markdownKind !== row.markdownKind) - continue; - const end = ends[index]!; - let identical = true; - for (let block = starts[index]!; block <= end; block++) - if (blocks[block] !== previousBlocks[block]) { - identical = false; - break; - } - if (!identical) continue; - rows[index] = old; - } -} - -function canJoinRun( - first: AssistantMessageItem, - next: AssistantMessageItem, - length: number, -): boolean { - return ( - first.markdownKind === "prose" && - next.markdownKind === "prose" && - length + 2 + next.text.length <= ASSISTANT_MESSAGE_RENDER_CHARACTER_LIMIT - ); -} - -function previousGroups( - blocks: readonly AssistantMessageItem[] | undefined, -): GroupedRuns | undefined { - return blocks && (growingRunsByBlocks.get(blocks) ?? runsByBlocks.get(blocks)); -} diff --git a/packages/app/src/components/markdown-prose/source-inline.ts b/packages/app/src/components/markdown-prose/source-inline.ts deleted file mode 100644 index f311fbfee9..0000000000 --- a/packages/app/src/components/markdown-prose/source-inline.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { createAssistantMarkdownParser } from "@/utils/assistant-markdown-parser"; -import type Token from "markdown-it/lib/token.mjs"; - -export interface InlineSpan { - start: number; - end: number; - tokens: Token[]; - insideLink: boolean; -} -export interface InlineTrace { - spans: InlineSpan[]; - linkifyTexts: string[]; -} -const TRACE = Symbol("prose source positions"); - -/** Observe the actual inline rules, including nested labels, without reprinting tokens. */ -export function createSourceParser() { - const parser = createAssistantMarkdownParser(); - const traces = new WeakMap(); - const inlineTraces = new WeakMap(); - const parseInline = parser.inline.parse.bind(parser.inline); - parser.inline.parse = (source, md, env, output) => { - const trace: InlineTrace = { spans: [], linkifyTexts: [] }; - parseInline(source, md, { ...env, [TRACE]: trace }, output); - inlineTraces.set(output, trace); - }; - const rules = parser.inline.ruler.getRules(""); - parser.inline.ruler.before("text", "prose_source_positions", (state, silent) => { - if (silent) return false; - for (const rule of rules) { - const start = state.pos; - const count = state.tokens.length; - const trace: InlineTrace = state.env[TRACE]; - const firstNested = trace.spans.length; - if (rule(state, false)) { - const tokens = state.tokens.slice(count); - if (tokens.some((token) => token.type === "link_open")) { - for (let i = firstNested; i < trace.spans.length; i++) trace.spans[i]!.insideLink = true; - } - trace.spans.push({ start, end: state.pos, tokens, insideLink: false }); - return true; - } - } - return false; - }); - parser.core.ruler.after("inline", "prose_inline_traces", (state) => { - for (const token of state.tokens) { - if (token.type !== "inline" || !token.children) continue; - const trace = inlineTraces.get(token.children)!; - let linkDepth = 0; - for (const child of token.children) { - if (child.type === "link_open") linkDepth++; - if (child.type === "link_close") linkDepth--; - if (child.type === "text" && linkDepth === 0) trace.linkifyTexts.push(child.content); - } - traces.set(token, trace); - } - }); - return { parser, traces }; -} diff --git a/packages/app/src/components/markdown-text-selection.test.ts b/packages/app/src/components/markdown-text-selection.test.ts deleted file mode 100644 index 6aa42afa00..0000000000 --- a/packages/app/src/components/markdown-text-selection.test.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { iosMarkdownTextIsSelectable } from "./markdown-text-selection"; - -describe("markdown text selection", () => { - it("uses plain text only for iOS table cells", () => { - expect({ - tableCell: iosMarkdownTextIsSelectable("table-cell"), - prose: iosMarkdownTextIsSelectable("prose"), - }).toEqual({ - tableCell: false, - prose: true, - }); - }); -}); diff --git a/packages/app/src/components/markdown-text-selection.tsx b/packages/app/src/components/markdown-text-selection.tsx deleted file mode 100644 index f83ff6a258..0000000000 --- a/packages/app/src/components/markdown-text-selection.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { createContext, type ReactNode, useContext } from "react"; - -export type MarkdownTextSurface = "prose" | "table-cell"; - -const MarkdownTextSurfaceContext = createContext("prose"); - -export function MarkdownTableCellText({ children }: { children: ReactNode }) { - return {children}; -} - -export function useMarkdownTextSurface(): MarkdownTextSurface { - return useContext(MarkdownTextSurfaceContext); -} - -export function iosMarkdownTextIsSelectable(surface: MarkdownTextSurface): boolean { - return surface !== "table-cell"; -} diff --git a/packages/app/src/components/markdown-text-style.test.ts b/packages/app/src/components/markdown-text-style.test.ts deleted file mode 100644 index 8094cca386..0000000000 --- a/packages/app/src/components/markdown-text-style.test.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { StyleSheet, type TextStyle } from "react-native"; -import { resolvePlainMarkdownTextStyle } from "@/components/markdown-text-style"; - -function unistylesStyle(id: string, style: Record) { - return { - ...style, - [`unistyles_${id}`]: { id }, - }; -} - -function uiTextViewFlatten(rootStyle: TextStyle, style: TextStyle): Record { - // react-native-uitextview/src/util.ts:8 flattens [rootStyle, style] - // before passing the result to its native View-backed components. - return { ...(StyleSheet.flatten([rootStyle, style]) as Record) }; -} - -function unistylesMetadataKeys(style: Record) { - return Object.keys(style).filter((key) => key.startsWith("unistyles_")); -} - -describe("resolvePlainMarkdownTextStyle", () => { - it("keeps UITextView from collapsing parent and child Unistyles styles into one native View style object", () => { - const merged = uiTextViewFlatten( - resolvePlainMarkdownTextStyle(unistylesStyle("paragraph", { color: "#111" })), - resolvePlainMarkdownTextStyle(unistylesStyle("text", { fontWeight: "600" })), - ); - - expect(unistylesMetadataKeys(merged)).toHaveLength(0); - expect(merged).toMatchObject({ color: "#111", fontWeight: "600" }); - }); -}); diff --git a/packages/app/src/components/markdown-text-style.ts b/packages/app/src/components/markdown-text-style.ts deleted file mode 100644 index ab1ac4abf6..0000000000 --- a/packages/app/src/components/markdown-text-style.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { StyleSheet, type StyleProp, type TextStyle } from "react-native"; - -export function resolvePlainMarkdownTextStyle(style: StyleProp): TextStyle { - return stripUnistylesMetadata(StyleSheet.flatten(style) ?? {}); -} - -function stripUnistylesMetadata(style: TextStyle): TextStyle { - // iOS markdown text goes through react-native-uitextview. That library - // inherits text styles by flattening [parentStyle, childStyle] before handing - // the result to native View-backed components. If both entries are Unistyles - // styles, flattening preserves both `unistyles_*` metadata keys in one object, - // and Unistyles correctly warns that the style should have stayed array-shaped. - // - // `UITextView` is a third-party boundary, not a Unistyles-tracked component in - // our ownership model. Resolve the concrete style values before crossing that - // boundary and drop only Unistyles' private tracking metadata. This preserves - // iOS paragraph/spanning text selection while avoiding the metadata merge. - const plainStyle: Record = { ...style }; - for (const key of Object.keys(plainStyle)) { - if (key.startsWith("unistyles_")) { - delete plainStyle[key]; - } - } - return plainStyle as TextStyle; -} diff --git a/packages/app/src/components/markdown-text.android.tsx b/packages/app/src/components/markdown-text.android.tsx deleted file mode 100644 index f444d916bd..0000000000 --- a/packages/app/src/components/markdown-text.android.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import { useMemo, type ReactNode } from "react"; -import { - Text, - View, - type StyleProp, - type TextProps, - type TextStyle, - type ViewStyle, -} from "react-native"; -import type { MarkdownCopyInlineTag } from "@/assistant-selection-copy/markup"; - -interface MarkdownTextSpanProps { - style?: StyleProp; - monoSurface?: boolean; - copyTag?: MarkdownCopyInlineTag; - children: ReactNode; - onPress?: TextProps["onPress"]; - accessibilityRole?: TextProps["accessibilityRole"]; -} - -// Android's enables per-text-node selection natively. Each -// sibling Text is its own selection scope — drag can't span across siblings -// (that requires a single UITextView ancestor and is iOS-only). onPress works -// natively here, so links routed through this span stay tappable on Android. -export function MarkdownTextSpan({ - style, - children, - onPress, - accessibilityRole, -}: MarkdownTextSpanProps) { - return ( - - {children} - - ); -} - -interface MarkdownParagraphViewProps { - paragraphStyle: ViewStyle; - containsImage?: boolean; - children: ReactNode; -} - -const MARKDOWN_PARAGRAPH_RESET: ViewStyle = {}; - -// Paragraph stays a , not a , for layout fidelity. RN Android's -// text engine *does* accept inline View children (TextInlineViewPlaceholderSpan -// in ReactBaseTextShadowNode), so this isn't a crash-avoidance choice — but -// inline-placeholder spans collapse block-level children (e.g. paragraph -// images) into one-character placeholders, which destroys image row layout. -// preserves the original block layout; the trade-off is no cross-span -// selection on Android (a UITextView-style trick has no Android equivalent). -export function MarkdownParagraphView({ paragraphStyle, children }: MarkdownParagraphViewProps) { - const style = useMemo(() => [paragraphStyle, MARKDOWN_PARAGRAPH_RESET], [paragraphStyle]); - return {children}; -} diff --git a/packages/app/src/components/markdown-text.d.ts b/packages/app/src/components/markdown-text.d.ts deleted file mode 100644 index 00194ac319..0000000000 --- a/packages/app/src/components/markdown-text.d.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./markdown-text.ios"; diff --git a/packages/app/src/components/markdown-text.ios.tsx b/packages/app/src/components/markdown-text.ios.tsx deleted file mode 100644 index 1b2eb405c4..0000000000 --- a/packages/app/src/components/markdown-text.ios.tsx +++ /dev/null @@ -1,113 +0,0 @@ -import { useMemo, type ReactNode } from "react"; -import { - Text, - View, - type StyleProp, - type TextProps, - type TextStyle, - type ViewStyle, -} from "react-native"; -import { UITextView } from "react-native-uitextview"; -import { resolvePlainMarkdownTextStyle } from "@/components/markdown-text-style"; -import { - iosMarkdownTextIsSelectable, - useMarkdownTextSurface, -} from "@/components/markdown-text-selection"; -import type { MarkdownCopyInlineTag } from "@/assistant-selection-copy/markup"; - -interface MarkdownTextSpanProps { - style?: StyleProp; - monoSurface?: boolean; - copyTag?: MarkdownCopyInlineTag; - children: ReactNode; - // Links route through this span too (see assistant-file-links/link.tsx). A - // plain nested in the paragraph UITextView is dropped, so the link - // must be a UITextView span to be visible. onPress is wired onto the leaf - // string children here: react-native-uitextview attaches it to the - // RNUITextViewChild nodes it builds from string content, which the native tap - // recognizer dispatches to. The link's handler reaches these leaf spans via - // AssistantLinkPressProvider (see assistant-file-links/link-press-context). - onPress?: TextProps["onPress"]; - accessibilityRole?: TextProps["accessibilityRole"]; -} - -// Inline span backed by UITextView so iOS gets native word-selection handles. -// Used inside MarkdownParagraphView (which is also a UITextView on iOS); the -// library's TextAncestorContext hoists these into UITextViewChild nodes so -// selection drags can cross sibling spans (e.g. plain text → **bold** → code). -export function MarkdownTextSpan({ - style, - children, - onPress, - accessibilityRole, -}: MarkdownTextSpanProps) { - const plainStyle = useMemo(() => resolvePlainMarkdownTextStyle(style), [style]); - const surface = useMarkdownTextSurface(); - - // Each selectable span creates a UIKit UITextView with a window-level tap recognizer. - // A large table would create one per cell and make every app touch fan out across them. - if (!iosMarkdownTextIsSelectable(surface)) { - return ( - - {children} - - ); - } - - return ( - - {children} - - ); -} - -interface MarkdownParagraphViewProps { - paragraphStyle: ViewStyle; - containsImage?: boolean; - children: ReactNode; -} - -const MARKDOWN_PARAGRAPH_RESET: ViewStyle = {}; - -// iOS-only: paragraph wraps in UITextView so the entire paragraph is one -// native text view. That's what unlocks cross-inline drag selection — handles -// can span every MarkdownTextSpan child inside this paragraph. -// ViewStyle is structurally compatible with the layout props paragraphs use -// (margin, padding, alignment); the cast lets the existing paragraphStyle -// flow through unchanged. -export function MarkdownParagraphView({ - paragraphStyle, - containsImage = false, - children, -}: MarkdownParagraphViewProps) { - const textStyle = useMemo( - () => - resolvePlainMarkdownTextStyle([ - paragraphStyle, - MARKDOWN_PARAGRAPH_RESET, - ] as StyleProp), - [paragraphStyle], - ); - const viewStyle = useMemo(() => [paragraphStyle, MARKDOWN_PARAGRAPH_RESET], [paragraphStyle]); - - if (containsImage) { - return {children}; - } - - return ( - - {children} - - ); -} diff --git a/packages/app/src/components/markdown-text.web.tsx b/packages/app/src/components/markdown-text.web.tsx deleted file mode 100644 index 0cb89f0cff..0000000000 --- a/packages/app/src/components/markdown-text.web.tsx +++ /dev/null @@ -1,73 +0,0 @@ -import { useMemo, type ReactNode } from "react"; -import { - Text, - View, - type StyleProp, - type TextProps, - type TextStyle, - type ViewStyle, -} from "react-native"; -import { CODE_SURFACE_DATASET } from "@/styles/code-surface"; -import { markdownCopyDataSet, type MarkdownCopyInlineTag } from "@/assistant-selection-copy/markup"; - -interface MarkdownTextSpanProps { - style?: StyleProp; - monoSurface?: boolean; - copyTag?: MarkdownCopyInlineTag; - children: ReactNode; - // Web links use the /Pressable path in link.tsx, not this span, so these - // are accepted for prop-shape parity with the native variants and forwarded - // harmlessly. - onPress?: TextProps["onPress"]; - accessibilityRole?: TextProps["accessibilityRole"]; -} - -// react-native-web renders Text as /
with `user-select: text` -// already applied via markdownStyleMapping. The web bundle must not import -// react-native-uitextview: its transitive import of codegenNativeComponent -// pulls in setUpReactDevTools, which doesn't resolve under Metro's web -// target in dev mode. -export function MarkdownTextSpan({ - style, - monoSurface, - copyTag, - children, - onPress, - accessibilityRole, -}: MarkdownTextSpanProps) { - const dataSet = useMemo(() => { - if (copyTag && (monoSurface || copyTag === "code")) { - return { ...CODE_SURFACE_DATASET, ...markdownCopyDataSet[copyTag] }; - } - if (copyTag) { - return markdownCopyDataSet[copyTag]; - } - return monoSurface ? CODE_SURFACE_DATASET : undefined; - }, [copyTag, monoSurface]); - - return ( - - {children} - - ); -} - -interface MarkdownParagraphViewProps { - paragraphStyle: ViewStyle; - containsImage?: boolean; - children: ReactNode; -} - -const MARKDOWN_PARAGRAPH_RESET: ViewStyle = {}; - -// Same shape as Android — paragraph is a View so block-level children (images) -// keep their natural layout. Web text selection already spans nested inline -// elements via CSS user-select, so no UITextView equivalent is needed. -export function MarkdownParagraphView({ paragraphStyle, children }: MarkdownParagraphViewProps) { - const style = useMemo(() => [paragraphStyle, MARKDOWN_PARAGRAPH_RESET], [paragraphStyle]); - return ( - - {children} - - ); -} diff --git a/packages/app/src/components/markdown/fence/index.tsx b/packages/app/src/components/markdown/fence/index.tsx deleted file mode 100644 index 846aa91355..0000000000 --- a/packages/app/src/components/markdown/fence/index.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import type { ComponentType } from "react"; -import { HighlightedCodeBlock } from "@/components/highlighted-code-block"; -import { getMarkdownFenceLanguage } from "./language"; -import { MermaidFence } from "./mermaid"; -import type { MarkdownFenceRendererProps } from "./types"; - -export interface MarkdownFenceBlockProps extends MarkdownFenceRendererProps { - info: string | null | undefined; -} - -const diagramFences: Partial>> = { - mermaid: MermaidFence, -}; - -export function MarkdownFenceBlock({ - code, - info, - phase, - inheritedStyles, - textStyle, -}: MarkdownFenceBlockProps) { - const language = getMarkdownFenceLanguage(info); - const DiagramFence = language ? diagramFences[language] : undefined; - if (DiagramFence) { - return ( - - ); - } - return ( - - ); -} diff --git a/packages/app/src/components/markdown/fence/language.test.ts b/packages/app/src/components/markdown/fence/language.test.ts deleted file mode 100644 index 172f72c310..0000000000 --- a/packages/app/src/components/markdown/fence/language.test.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { getMarkdownFenceLanguage } from "./language"; - -describe("getMarkdownFenceLanguage", () => { - it("normalizes the first info-string token", () => { - expect(getMarkdownFenceLanguage(" Sirena theme=dark ")).toBe("sirena"); - expect(getMarkdownFenceLanguage("TypeScript title=example")).toBe("typescript"); - }); - - it("returns null when no language is declared", () => { - expect(getMarkdownFenceLanguage("")).toBeNull(); - expect(getMarkdownFenceLanguage(null)).toBeNull(); - expect(getMarkdownFenceLanguage(undefined)).toBeNull(); - }); -}); diff --git a/packages/app/src/components/markdown/fence/language.ts b/packages/app/src/components/markdown/fence/language.ts deleted file mode 100644 index fd414a5ac2..0000000000 --- a/packages/app/src/components/markdown/fence/language.ts +++ /dev/null @@ -1,3 +0,0 @@ -export function getMarkdownFenceLanguage(info: string | null | undefined): string | null { - return info?.trim().split(/\s+/)[0]?.toLowerCase() || null; -} diff --git a/packages/app/src/components/markdown/fence/mermaid/build-runtime.mjs b/packages/app/src/components/markdown/fence/mermaid/build-runtime.mjs deleted file mode 100644 index 6ed821ceac..0000000000 --- a/packages/app/src/components/markdown/fence/mermaid/build-runtime.mjs +++ /dev/null @@ -1,81 +0,0 @@ -import esbuild from "esbuild"; -import fs from "node:fs/promises"; -import path from "node:path"; -import { fileURLToPath } from "node:url"; - -const __dirname = path.dirname(fileURLToPath(import.meta.url)); -const appRoot = path.resolve(__dirname, "../../../../.."); -const repoRoot = path.resolve(appRoot, "../.."); -const entry = path.join(__dirname, "runtime/entry.ts"); -const output = path.join(__dirname, "runtime/html.gen.ts"); - -const result = await esbuild.build({ - entryPoints: [entry], - bundle: true, - write: false, - format: "iife", - platform: "browser", - target: ["ios15", "chrome100"], - minify: true, - // The app's deployment floor is iOS 15.1 but structuredClone (used by - // mermaid) only exists from iOS 15.4; esbuild targets transform syntax, - // not runtime APIs. Mermaid only clones plain config objects, so a - // JSON-based fallback is sufficient. - banner: { - js: 'if(typeof globalThis.structuredClone!=="function"){globalThis.structuredClone=function(v){return v===undefined?v:JSON.parse(JSON.stringify(v));};}', - }, - logLevel: "info", -}); - -const rawJs = result.outputFiles[0]?.text; -if (!rawJs) { - throw new Error("Mermaid runtime bundle produced no JavaScript"); -} -// JSON.stringify below escapes the TypeScript representation, not the runtime -// HTML — an embedded closing-script sequence would still terminate the inline -// - -`; - -const contents = `// Generated by packages/app/src/components/markdown/fence/mermaid/build-runtime.mjs. -// Do not edit by hand. - -export const mermaidRuntimeHtml = ${JSON.stringify(html)}; -`; - -await fs.writeFile(output, contents); -console.log(`Wrote ${path.relative(repoRoot, output)} (${html.length} bytes)`); diff --git a/packages/app/src/components/markdown/fence/mermaid/fullscreen-viewer.web.tsx b/packages/app/src/components/markdown/fence/mermaid/fullscreen-viewer.web.tsx deleted file mode 100644 index 8b68ae61cb..0000000000 --- a/packages/app/src/components/markdown/fence/mermaid/fullscreen-viewer.web.tsx +++ /dev/null @@ -1,140 +0,0 @@ -import { useCallback, useEffect, useMemo, useRef } from "react"; -import { Modal, View } from "react-native"; -import { X } from "lucide-react-native"; -import { useTranslation } from "react-i18next"; -import { StyleSheet } from "react-native-unistyles"; -import { useSafeAreaInsets } from "react-native-safe-area-context"; -import { ZoomableViewport } from "@/components/zoomable-viewport"; -import { SPACING } from "@/styles/theme"; -import { WindowChromeRootRegion } from "@/utils/desktop-window"; -import { MermaidIframeRuntime, type MermaidRenderedMessage } from "./iframe-runtime.web"; -import type { DiagramColorScheme } from "./render-model"; -import { useMermaidRenderModel } from "./use-render-model"; - -interface MermaidFullscreenViewerProps { - source: string; - colorScheme: DiagramColorScheme; - onClose: () => void; -} - -// The diagram is already in the render cache, so the viewport knows its size before the -// fullscreen iframe has re-rendered it. -const FALLBACK_SIZE = { width: 240, height: 240 }; -const VIEWER_FIT = { padding: SPACING[6] }; - -export function MermaidFullscreenViewer({ - source, - colorScheme, - onClose, -}: MermaidFullscreenViewerProps) { - const { t } = useTranslation(); - const insets = useSafeAreaInsets(); - const { state, request, rendered, renderFailed } = useMermaidRenderModel({ - source, - phase: "complete", - colorScheme, - }); - const handleRendered = useCallback( - (message: MermaidRenderedMessage) => { - rendered({ - revision: message.revision, - source: message.source, - colorScheme: message.colorScheme, - dimensions: { height: message.height, width: message.width }, - }); - }, - [rendered], - ); - - useEffect(() => { - function handleKeydown(event: KeyboardEvent): void { - if (event.key === "Escape") onClose(); - } - window.addEventListener("keydown", handleKeydown); - return () => window.removeEventListener("keydown", handleKeydown); - }, [onClose]); - - // The Modal focus trap walks descendants and focuses the first one that takes it. Focus the - // content layer ourselves (this effect runs after the trap's) so keystrokes stay in this - // document instead of disappearing into a focused diagram iframe. - const contentLayerRef = useRef(null); - useEffect(() => { - contentLayerRef.current?.focus(); - }, []); - - const visible = state.visible; - const contentSize = useMemo( - () => (visible ? { width: visible.width, height: visible.height } : FALLBACK_SIZE), - [visible], - ); - const actions = useMemo( - () => [ - { - icon: X, - label: t("common.actions.close"), - onPress: onClose, - testID: "mermaid-fullscreen-close", - }, - ], - [onClose, t], - ); - const contentLayerStyle = useMemo( - () => [ - styles.contentLayer, - { - paddingTop: insets.top, - paddingRight: insets.right, - paddingBottom: insets.bottom, - paddingLeft: insets.left, - }, - ], - [insets.bottom, insets.left, insets.right, insets.top], - ); - - return ( - - - - - - {/* biome-ignore lint/a11y/noNoninteractiveTabindex: focus holder for the Escape shortcut */} -
- - - -
-
-
-
-
- ); -} - -const focusLayerDomStyle: React.CSSProperties = { - display: "flex", - flexDirection: "column", - height: "100%", - minHeight: 0, - outline: "none", -}; - -const styles = StyleSheet.create((theme) => ({ - root: { flex: 1, minHeight: 0, minWidth: 0 }, - backdrop: { position: "absolute", inset: 0, backgroundColor: theme.colors.surface0 }, - contentLayer: { position: "absolute", inset: 0 }, - viewport: { flex: 1 }, -})); diff --git a/packages/app/src/components/markdown/fence/mermaid/host.native.tsx b/packages/app/src/components/markdown/fence/mermaid/host.native.tsx deleted file mode 100644 index 044341f3aa..0000000000 --- a/packages/app/src/components/markdown/fence/mermaid/host.native.tsx +++ /dev/null @@ -1,379 +0,0 @@ -import { useCallback, useEffect, useMemo, useRef, useState } from "react"; -import { - Modal, - Pressable, - StyleSheet as RNStyleSheet, - ScrollView, - View, - type TextStyle, - type ViewStyle, -} from "react-native"; -import { WebView, type WebViewMessageEvent } from "react-native-webview"; -import { Code, Workflow, X } from "lucide-react-native"; -import { useTranslation } from "react-i18next"; -import { StyleSheet, withUnistyles } from "react-native-unistyles"; -import { HighlightedCodeBlock } from "@/components/highlighted-code-block"; -import type { Theme } from "@/styles/theme"; -import type { MarkdownFenceRendererProps } from "../types"; -import type { MermaidRenderRequest } from "./render-model"; -import { mermaidRuntimeHtml } from "./runtime/html.gen"; -import { - parseMermaidRuntimeMessage, - serializeMermaidRuntimeRenderMessage, - type MermaidRuntimeRenderMessage, -} from "./runtime/messages"; -import { MermaidRuntimeRequestDriver } from "./runtime/request-driver"; -import { useMermaidRenderModel } from "./use-render-model"; -import { getDiagramBoxStyle } from "./presentation"; - -interface MermaidFenceHostImplProps extends MarkdownFenceRendererProps { - colorScheme?: "light" | "dark"; -} - -const WEBVIEW_SOURCE = { html: mermaidRuntimeHtml }; -const WEBVIEW_ORIGIN_WHITELIST = ["*"]; -const MAX_PREVIEW_HEIGHT = 480; - -interface MermaidWebViewProps { - request: MermaidRenderRequest | null; - interactive: boolean; - onRendered: (message: { - revision: number; - source: string; - colorScheme: "light" | "dark"; - height: number; - width: number; - }) => void; - onRenderFailed: (revision: number) => void; - style?: ViewStyle; -} - -function MermaidWebView({ - request, - interactive, - onRendered, - onRenderFailed, - style, -}: MermaidWebViewProps) { - const webViewRef = useRef(null); - const driverRef = useRef(null); - driverRef.current ??= new MermaidRuntimeRequestDriver(); - - const sendRequest = useCallback( - (current: MermaidRenderRequest | null) => { - if (!current || !webViewRef.current) { - return; - } - const message: MermaidRuntimeRenderMessage = { - type: "render", - revision: current.revision, - source: current.source, - colorScheme: current.colorScheme, - interactive, - }; - const payload = serializeMermaidRuntimeRenderMessage(message); - webViewRef.current.injectJavaScript( - `window.__PASEO_MERMAID_RUNTIME_RECEIVE__ && window.__PASEO_MERMAID_RUNTIME_RECEIVE__(${payload}); true;`, - ); - }, - [interactive], - ); - - useEffect(() => { - sendRequest(driverRef.current?.update(request) ?? null); - }, [request, sendRequest]); - - const handleMessage = useCallback( - (event: WebViewMessageEvent) => { - let value: unknown; - try { - value = JSON.parse(event.nativeEvent.data); - } catch { - return; - } - const message = parseMermaidRuntimeMessage(value); - if (!message) { - return; - } - if (message.type === "bridgeReady") { - sendRequest(driverRef.current?.ready() ?? null); - return; - } - if (message.type === "renderError") { - onRenderFailed(message.revision); - sendRequest(driverRef.current?.settled(message.revision, false) ?? null); - return; - } - onRendered(message); - sendRequest(driverRef.current?.settled(message.revision, true) ?? null); - }, - [onRenderFailed, onRendered, sendRequest], - ); - const handleShouldStartLoad = useCallback( - (load: { url: string }) => load.url === "about:blank" || load.url.startsWith("data:"), - [], - ); - - return ( - - ); -} - -const webViewStyles = RNStyleSheet.create({ - webView: { backgroundColor: "transparent" }, -}); - -interface MermaidDiagramViewerProps { - code: string; - colorScheme: "light" | "dark"; - onClose: () => void; - inheritedStyles: TextStyle; - textStyle: TextStyle; -} - -function MermaidDiagramViewer({ - code, - colorScheme, - onClose, - inheritedStyles, - textStyle, -}: MermaidDiagramViewerProps) { - const { t } = useTranslation(); - const [showSource, setShowSource] = useState(false); - const toggleSource = useCallback(() => setShowSource((current) => !current), []); - const { request, rendered, renderFailed } = useMermaidRenderModel({ - source: code, - phase: "complete", - colorScheme, - }); - const handleRendered = useCallback( - (message: { - revision: number; - source: string; - colorScheme: "light" | "dark"; - height: number; - width: number; - }) => { - rendered({ - revision: message.revision, - source: message.source, - colorScheme: message.colorScheme, - dimensions: { height: message.height, width: message.width }, - }); - }, - [rendered], - ); - - return ( - - - - - {showSource ? ( - - - - ) : null} - - - - {showSource ? ( - - ) : ( - - )} - - - - - - - - ); -} - -const ThemedCloseIcon = withUnistyles(X); -const ThemedSourceIcon = withUnistyles(Code); -const ThemedDiagramIcon = withUnistyles(Workflow); -const closeIconColor = (theme: Theme) => ({ color: theme.colors.foreground }); - -const viewerStyles = StyleSheet.create((theme, runtime) => ({ - backdrop: { flex: 1, backgroundColor: theme.colors.surface0 }, - webView: { - flex: 1, - marginTop: runtime.insets.top, - marginBottom: runtime.insets.bottom, - }, - runtime: { flex: 1 }, - sourceOverlay: { - position: "absolute", - inset: 0, - backgroundColor: theme.colors.surface0, - }, - source: { - paddingTop: theme.spacing[12], - paddingHorizontal: theme.spacing[4], - paddingBottom: theme.spacing[4], - }, - actions: { - position: "absolute", - top: runtime.insets.top + theme.spacing[3], - right: theme.spacing[4], - flexDirection: "row", - gap: theme.spacing[2], - }, - actionButton: { - padding: theme.spacing[2], - borderRadius: theme.borderRadius.lg, - backgroundColor: theme.colors.surface2, - }, -})); - -function MermaidFenceHostImpl({ - code, - phase, - inheritedStyles, - textStyle, - colorScheme = "dark", -}: MermaidFenceHostImplProps) { - const { t } = useTranslation(); - const { state, request, rendered, renderFailed } = useMermaidRenderModel({ - source: code, - phase, - colorScheme, - }); - const [hasRuntimeContent, setHasRuntimeContent] = useState(false); - const handleRendered = useCallback( - (message: { - revision: number; - source: string; - colorScheme: "light" | "dark"; - height: number; - width: number; - }) => { - setHasRuntimeContent(true); - rendered({ - revision: message.revision, - source: message.source, - colorScheme: message.colorScheme, - dimensions: { height: message.height, width: message.width }, - }); - }, - [rendered], - ); - const [viewerOpen, setViewerOpen] = useState(false); - const openViewer = useCallback(() => setViewerOpen(true), []); - const closeViewer = useCallback(() => setViewerOpen(false), []); - const visible = state.visible; - const canShowDiagram = visible !== null && hasRuntimeContent; - const previewInnerStyle = useMemo( - () => - canShowDiagram && visible - ? { height: Math.min(visible.height, MAX_PREVIEW_HEIGHT) } - : previewStyles.measuringInner, - [canShowDiagram, visible], - ); - const diagramBoxStyle = getDiagramBoxStyle(textStyle); - - return ( - <> - {!canShowDiagram ? ( - - ) : null} - - - - - - {viewerOpen && canShowDiagram && visible ? ( - - ) : null} - - ); -} - -const previewStyles = RNStyleSheet.create({ - measuring: { - position: "absolute", - left: 0, - right: 0, - opacity: 0, - pointerEvents: "none", - }, - measuringInner: { height: 240 }, - preview: { overflow: "hidden" }, -}); - -const mapColorScheme = (theme: Theme) => ({ colorScheme: theme.colorScheme }); -const ThemedMermaidFenceHost = withUnistyles(MermaidFenceHostImpl); - -export function MermaidFenceHost(props: MarkdownFenceRendererProps) { - return ; -} diff --git a/packages/app/src/components/markdown/fence/mermaid/host.tsx b/packages/app/src/components/markdown/fence/mermaid/host.tsx deleted file mode 100644 index 50e9596740..0000000000 --- a/packages/app/src/components/markdown/fence/mermaid/host.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import { HighlightedCodeBlock } from "@/components/highlighted-code-block"; -import type { MarkdownFenceRendererProps } from "../types"; - -export function MermaidFenceHost({ code, inheritedStyles, textStyle }: MarkdownFenceRendererProps) { - return ( - - ); -} diff --git a/packages/app/src/components/markdown/fence/mermaid/host.web.tsx b/packages/app/src/components/markdown/fence/mermaid/host.web.tsx deleted file mode 100644 index e79922ec43..0000000000 --- a/packages/app/src/components/markdown/fence/mermaid/host.web.tsx +++ /dev/null @@ -1,193 +0,0 @@ -import { useCallback, useMemo, useState } from "react"; -import { Pressable, View, type TextStyle, type ViewStyle } from "react-native"; -import { Code, Maximize2, Workflow } from "lucide-react-native"; -import { useTranslation } from "react-i18next"; -import { StyleSheet, withUnistyles } from "react-native-unistyles"; -import { HighlightedCodeBlock } from "@/components/highlighted-code-block"; -import { ZoomableViewport } from "@/components/zoomable-viewport"; -import type { Theme } from "@/styles/theme"; -import type { MarkdownFenceRendererProps } from "../types"; -import { MermaidFullscreenViewer } from "./fullscreen-viewer.web"; -import { MermaidIframeRuntime, type MermaidRenderedMessage } from "./iframe-runtime.web"; -import { useMermaidRenderModel } from "./use-render-model"; -import { getDiagramBoxStyle } from "./presentation"; - -interface MermaidFenceHostImplProps extends MarkdownFenceRendererProps { - colorScheme?: "light" | "dark"; -} - -function MermaidFenceHostImpl({ - code, - phase, - inheritedStyles, - textStyle, - colorScheme = "dark", -}: MermaidFenceHostImplProps) { - const { t } = useTranslation(); - const { state, request, rendered, renderFailed } = useMermaidRenderModel({ - source: code, - phase, - colorScheme, - }); - const [hasRuntimeContent, setHasRuntimeContent] = useState(false); - const [showSource, setShowSource] = useState(false); - const [isFullscreen, setIsFullscreen] = useState(false); - const showSourcePress = useCallback(() => setShowSource(true), []); - const showDiagramPress = useCallback(() => setShowSource(false), []); - const openFullscreen = useCallback(() => setIsFullscreen(true), []); - const closeFullscreen = useCallback(() => setIsFullscreen(false), []); - const handleRendered = useCallback( - (message: MermaidRenderedMessage) => { - setHasRuntimeContent(true); - rendered({ - revision: message.revision, - source: message.source, - colorScheme: message.colorScheme, - dimensions: { height: message.height, width: message.width }, - }); - }, - [rendered], - ); - const visible = state.visible; - const canShowDiagram = visible !== null && hasRuntimeContent; - const diagramVisible = canShowDiagram && !showSource; - const runtimeHeight = Math.max(visible?.height ?? 240, 1); - const actions = useMemo( - () => [ - { - icon: Code, - label: t("message.diagram.viewSource"), - onPress: showSourcePress, - }, - { - icon: Maximize2, - label: t("message.diagram.fullscreen"), - onPress: openFullscreen, - testID: "mermaid-fullscreen", - }, - ], - [openFullscreen, showSourcePress, t], - ); - const sourceView = useMemo(() => { - const { marginTop, marginBottom, marginVertical, ...sourceTextStyle } = textStyle; - const margins: ViewStyle = { - marginTop: marginTop ?? marginVertical, - marginBottom: marginBottom ?? marginVertical, - }; - const text: TextStyle = sourceTextStyle; - return { container: [margins, sourceContainerStyle], text }; - }, [textStyle]); - const diagramStyle = useMemo( - () => [ - getDiagramBoxStyle(textStyle), - containerStyle, - { height: runtimeHeight, minHeight: MIN_DIAGRAM_BOX_HEIGHT }, - ], - [runtimeHeight, textStyle], - ); - const diagramSize = useMemo( - () => (visible ? { width: visible.width, height: visible.height } : MEASURING_SIZE), - [visible], - ); - const sourceVisible = !diagramVisible; - const sourceContainer = showSource ? sourceView.container : sourceContainerStyle; - const sourceTextStyle = showSource ? sourceView.text : textStyle; - const viewportStyle = diagramVisible ? diagramStyle : measuringStyle; - - return ( - <> - {sourceVisible ? ( - - - {showSource && canShowDiagram ? ( - - {({ hovered }) => ( - - )} - - ) : null} - - ) : null} - - - - {isFullscreen && visible ? ( - - ) : null} - - ); -} - -const MEASURING_SIZE = { width: 240, height: 240 }; -/** - * The toolbar overlays the top of the box (8px offset + 32px compact buttons) and the box clips - * with `overflow: hidden`, so a shorter box leaves the buttons half-clipped and unclickable. - */ -const MIN_DIAGRAM_BOX_HEIGHT = 56; -const sourceContainerStyle: ViewStyle = { position: "relative" }; -/** - * The viewport's own root is `flex: 1`, so as a flex item it has `flex-basis: 0%` and that basis - * replaces the height below. In the markdown column there is no free space to grow into, so the - * box collapses and the diagram is scaled down to fit a couple of pixels. Size it from the height. - */ -const containerStyle: ViewStyle = { - flexBasis: "auto", - flexGrow: 0, - flexShrink: 0, - overflow: "hidden", - position: "relative", -}; -const measuringStyle: ViewStyle = { - position: "absolute", - left: 0, - right: 0, - top: 0, - height: 240, - opacity: 0, - pointerEvents: "none", -}; -const controlStyles = StyleSheet.create((theme) => ({ - sourceButton: { - position: "absolute", - top: theme.spacing[2], - right: theme.spacing[2], - padding: theme.spacing[1], - }, - icon: { color: theme.colors.foregroundMuted }, - iconHovered: { color: theme.colors.foreground }, -})); -const mapColorScheme = (theme: Theme) => ({ colorScheme: theme.colorScheme }); -const ThemedMermaidFenceHost = withUnistyles(MermaidFenceHostImpl); - -export function MermaidFenceHost(props: MarkdownFenceRendererProps) { - return ; -} diff --git a/packages/app/src/components/markdown/fence/mermaid/iframe-runtime.web.tsx b/packages/app/src/components/markdown/fence/mermaid/iframe-runtime.web.tsx deleted file mode 100644 index a5ef30f218..0000000000 --- a/packages/app/src/components/markdown/fence/mermaid/iframe-runtime.web.tsx +++ /dev/null @@ -1,92 +0,0 @@ -import { useCallback, useEffect, useRef } from "react"; -import type { DiagramColorScheme, MermaidRenderRequest } from "./render-model"; -import { mermaidRuntimeHtml } from "./runtime/html.gen"; -import { parseMermaidRuntimeMessage, type MermaidRuntimeRenderMessage } from "./runtime/messages"; -import { MermaidRuntimeRequestDriver } from "./runtime/request-driver"; - -export interface MermaidRenderedMessage { - revision: number; - source: string; - colorScheme: DiagramColorScheme; - height: number; - width: number; -} - -interface MermaidIframeRuntimeProps { - request: MermaidRenderRequest | null; - onRendered: (message: MermaidRenderedMessage) => void; - onRenderFailed: (revision: number) => void; -} - -/** Sandboxed Mermaid renderer. Sizing and gestures belong to the surrounding viewport. */ -export function MermaidIframeRuntime({ - request, - onRendered, - onRenderFailed, -}: MermaidIframeRuntimeProps) { - const iframeRef = useRef(null); - const driverRef = useRef(null); - driverRef.current ??= new MermaidRuntimeRequestDriver(); - - const sendRequest = useCallback((current: MermaidRenderRequest | null) => { - const target = iframeRef.current?.contentWindow; - if (!current || !target) return; - const message: MermaidRuntimeRenderMessage = { - type: "render", - revision: current.revision, - source: current.source, - colorScheme: current.colorScheme, - interactive: false, - }; - target.postMessage(message, "*"); - }, []); - - useEffect(() => { - sendRequest(driverRef.current?.update(request) ?? null); - }, [request, sendRequest]); - - useEffect(() => { - function receiveMessage(event: MessageEvent): void { - if (event.source !== iframeRef.current?.contentWindow) return; - const message = parseMermaidRuntimeMessage(event.data); - if (!message) return; - if (message.type === "bridgeReady") { - sendRequest(driverRef.current?.ready() ?? null); - return; - } - if (message.type === "renderError") { - onRenderFailed(message.revision); - sendRequest(driverRef.current?.settled(message.revision, false) ?? null); - return; - } - onRendered(message); - sendRequest(driverRef.current?.settled(message.revision, true) ?? null); - } - window.addEventListener("message", receiveMessage); - return () => window.removeEventListener("message", receiveMessage); - }, [onRenderFailed, onRendered, sendRequest]); - - // `inert` (not just tabIndex) because the Modal focus trap focuses descendants - // programmatically; a focused iframe swallows every keystroke, including Escape. - return ( - `},\"putIntoIFrame\"),n$e=v((t,e,r,n,i)=>{let a=t.append(\"div\");a.attr(\"id\",r),n&&a.attr(\"style\",n);let s=a.append(\"svg\").attr(\"id\",e).attr(\"width\",\"100%\").attr(\"xmlns\",gNt);return i&&s.attr(\"xmlns:xlink\",i),s.append(\"g\"),t},\"appendDivSvgG\");function VK(t,e){return t.append(\"iframe\").attr(\"id\",e).attr(\"style\",\"width: 100%; height: 100%;\").attr(\"sandbox\",\"\")}v(VK,\"sandboxedIframe\");var INt=v((t,e,r,n)=>{t.getElementById(e)?.remove(),t.getElementById(r)?.remove(),t.getElementById(n)?.remove()},\"removeExistingElements\"),MNt=v(async function(t,e,r){lM();let n=qK(e);e=n.code;let i=It();ne.debug(i),e.length>(i?.maxTextSize??hNt)&&(e=dNt);let a=`#${t}`,s=\"i\"+t,o=\"#\"+s,l=\"d\"+t,u=\"#\"+l,h=v(()=>{let _=tt(f?o:u).node();_&&\"remove\"in _&&_.remove()},\"removeTempElements\"),d=tt(document.body),f=i.securityLevel===fNt,p=i.securityLevel===pNt,g=i.fontFamily;if(r!==void 0){if(r&&(r.innerHTML=\"\"),f){let R=VK(tt(r),s);d=tt(R.nodes()[0].contentDocument.body),d.node().style.margin=\"0\"}else d=tt(r);n$e(d,t,l,`font-family: ${g}`,mNt)}else{if(INt(document,t,l,s),f){let R=VK(tt(document.body),s);d=tt(R.nodes()[0].contentDocument.body),d.node().style.margin=\"0\"}else d=tt(\"body\");n$e(d,t,l)}let m,y;try{m=await GK.fromText(e,{title:n.title})}catch(R){if(i.suppressErrorRendering)throw h(),R;m=await GK.fromText(\"error\"),y=R}let x=d.select(u).node(),b=m.type,T=x.firstChild,k=T.firstChild,C=m.renderer.getClasses?.(e,m),w=_Nt(i,b,C,a),S=document.createElement(\"style\");S.innerHTML=w,T.insertBefore(S,k);try{await m.renderer.draw(e,t,\"11.16.0\",m)}catch(R){throw i.suppressErrorRendering?h():$Mt.draw(e,t,\"11.16.0\"),R}let A=d.select(`${u} svg`),L=m.db.getAccTitle?.(),M=m.db.getAccDescription?.();K$e(b,A,L,M),d.select(`[id=\"${t}\"]`).selectAll(\"foreignobject > *\").attr(\"xmlns\",yNt);let P=d.select(u).node().innerHTML;if(ne.debug(\"config.arrowMarkerAbsolute\",i.arrowMarkerAbsolute),P=RNt(P,f,yl(i.arrowMarkerAbsolute)),f){let R=d.select(u+\" svg\").node();P=LNt(P,R)}else p||(P=to.sanitize(P,{ADD_TAGS:kNt,ADD_ATTR:SNt,HTML_INTEGRATION_POINTS:{foreignobject:!0}}));if(sNt(),y)throw y;return h(),{diagramType:b,svg:P,bindFunctions:m.db.bindFunctions}},\"render\");function X$e(t={}){let e=Jn({},t);e?.fontFamily&&!e.themeVariables?.fontFamily&&(e.themeVariables||(e.themeVariables={}),e.themeVariables.fontFamily=e.fontFamily),NJ(e),e?.theme&&e.theme in qc?e.themeVariables=qc[e.theme].getThemeVariables(e.themeVariables):e&&(e.themeVariables=qc.default.getThemeVariables(e.themeVariables));let r=typeof e==\"object\"?DJ(e):AD();rC(r.logLevel),lM()}v(X$e,\"initialize\");var j$e=v((t,e={})=>{let{code:r}=WK(t);return GK.fromText(r,e)},\"getDiagramFromText\");function K$e(t,e,r,n){W$e(e,t),q$e(e,r,n,e.attr(\"id\"))}v(K$e,\"addA11yInfo\");var Im=Object.freeze({render:MNt,parse:Y$e,getDiagramFromText:j$e,initialize:X$e,getConfig:It,setConfig:_D,getSiteConfig:AD,updateSiteConfig:OJ,reset:v(()=>{CC()},\"reset\"),globalReset:v(()=>{CC(Gp)},\"globalReset\"),defaultConfig:Gp});rC(It().logLevel);CC(It());var DNt=v((t,e,r)=>{ne.warn(t),fA(t)?(r&&r(t.str,t.hash),e.push({...t,message:t.str,error:t})):(r&&r(t),t instanceof Error&&e.push({str:t.message,message:t.message,hash:t.name,error:t}))},\"handleError\"),Z$e=v(async function(t={querySelector:\".mermaid\"}){try{await NNt(t)}catch(e){if(fA(e)&&ne.error(e.str),wd.parseError&&wd.parseError(e),!t.suppressErrors)throw ne.error(\"Use the suppressErrors option to suppress these errors\"),e}},\"run\"),NNt=v(async function({postRenderCallback:t,querySelector:e,nodes:r}={querySelector:\".mermaid\"}){let n=Im.getConfig();ne.debug(`${t?\"\":\"No \"}Callback function found`);let i;if(r)i=r;else if(e)i=document.querySelectorAll(e);else throw new Error(\"Nodes and querySelector are both undefined\");ne.debug(`Found ${i.length} diagrams`),n?.startOnLoad!==void 0&&(ne.debug(\"Start On Load: \"+n?.startOnLoad),Im.updateSiteConfig({startOnLoad:n?.startOnLoad}));let a=new Zt.InitIDGenerator(n.deterministicIds,n.deterministicIDSeed),s,o=[];for(let l of Array.from(i)){if(ne.info(\"Rendering diagram: \"+l.id),l.getAttribute(\"data-processed\"))continue;l.setAttribute(\"data-processed\",\"true\");let u=`mermaid-${a.next()}`;s=l.innerHTML,s=CA(Zt.entityDecode(s)).trim().replace(//gi,\"
\");let h=Zt.detectInit(s);h&&ne.debug(\"Detected early reinit: \",h);try{let{svg:d,bindFunctions:f}=await tFe(u,s,l);l.innerHTML=d,t&&await t(u),f&&f(l)}catch(d){DNt(d,o,wd.parseError)}}if(o.length>0)throw o[0]},\"runThrowsErrors\"),Q$e=v(function(t){Im.initialize(t)},\"initialize\"),ONt=v(async function(t,e,r){ne.warn(\"mermaid.init is deprecated. Please use run instead.\"),t&&Q$e(t);let n={postRenderCallback:r,querySelector:\".mermaid\"};typeof e==\"string\"?n.querySelector=e:e&&(e instanceof HTMLElement?n.nodes=[e]:n.nodes=e),await Z$e(n)},\"init\"),PNt=v(async(t,{lazyLoad:e=!0}={})=>{lM(),c5(...t),e===!1&&await iNt()},\"registerExternalDiagrams\"),J$e=v(function(){if(wd.startOnLoad){let{startOnLoad:t}=Im.getConfig();t&&wd.run().catch(e=>ne.error(\"Mermaid failed to initialize\",e))}},\"contentLoaded\");typeof document<\"u\"&&window.addEventListener(\"load\",J$e,!1);var BNt=v(function(t){wd.parseError=t},\"setParseErrorHandler\"),oM=[],zK=!1,eFe=v(async()=>{if(!zK){for(zK=!0;oM.length>0;){let t=oM.shift();if(t)try{await t()}catch(e){ne.error(\"Error executing queue\",e)}}zK=!1}},\"executeQueue\"),$Nt=v(async(t,e)=>new Promise((r,n)=>{let i=v(()=>new Promise((a,s)=>{Im.parse(t,e).then(o=>{a(o),r(o)},o=>{ne.error(\"Error parsing\",o),wd.parseError?.(o),s(o),n(o)})}),\"performCall\");oM.push(i),eFe().catch(n)}),\"parse\"),tFe=v((t,e,r)=>new Promise((n,i)=>{let a=v(()=>new Promise((s,o)=>{Im.render(t,e,r).then(l=>{s(l),n(l)},l=>{ne.error(\"Error parsing\",l),wd.parseError?.(l),o(l),i(l)})}),\"performCall\");oM.push(a),eFe().catch(i)}),\"render\"),FNt=v(()=>Object.keys(Bd).map(t=>({id:t})),\"getRegisteredDiagramsMetadata\"),wd={startOnLoad:!0,mermaidAPI:Im,parse:$Nt,render:tFe,init:ONt,run:Z$e,registerExternalDiagrams:PNt,registerLayoutLoaders:_G,initialize:Q$e,parseError:void 0,contentLoaded:J$e,setParseErrorHandler:BNt,detectType:wC,registerIconPacks:qy,getRegisteredDiagramsMetadata:FNt},UK=wd;function zNt(t){return typeof t==\"object\"&&t!==null}function GNt(t){return t===\"light\"||t===\"dark\"}function rFe(t){return!zNt(t)||t.type!==\"render\"||typeof t.revision!=\"number\"||!Number.isInteger(t.revision)||typeof t.source!=\"string\"||!GNt(t.colorScheme)||typeof t.interactive!=\"boolean\"?null:{type:\"render\",revision:t.revision,source:t.source,colorScheme:t.colorScheme,interactive:t.interactive}}function YK(t){window.ReactNativeWebView?.postMessage?.(JSON.stringify(t)),window.parent!==window&&window.parent.postMessage(t,\"*\")}function VNt(t){UK.initialize({startOnLoad:!1,securityLevel:\"strict\",suppressErrorRendering:!0,theme:t===\"dark\"?\"dark\":\"default\",secure:[\"secure\",\"securityLevel\",\"startOnLoad\",\"maxTextSize\",\"suppressErrorRendering\",\"maxEdges\",\"theme\",\"themeVariables\",\"themeCSS\"],flowchart:{htmlLabels:!1},class:{htmlLabels:!1}})}function WNt(t){document.querySelector('meta[name=\"viewport\"]')?.setAttribute(\"content\",t?\"width=device-width, initial-scale=1, maximum-scale=8\":\"width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no\")}var XK=0,cM=null,uM=!1,HK=!1;function qNt(t){t.style.maxWidth=\"100%\",t.style.maxHeight=\"100%\",t.style.width=\"100%\",t.style.height=\"100%\"}function UNt(t,e){let r=e?.viewBox.baseVal;if(r&&r.width>0&&r.height>0)return{height:Math.ceil(r.height),width:Math.ceil(r.width)};let n=e?.getBoundingClientRect();return{height:Math.ceil(n?.height??t.scrollHeight),width:Math.ceil(n?.width??t.scrollWidth)}}async function HNt(t){try{VNt(t.colorScheme);let{svg:e}=await UK.render(`paseo-mermaid-${t.revision}`,t.source);if(t.revision!==XK)return;WNt(t.interactive);let r=document.getElementById(\"diagram\");if(!r)return;r.innerHTML=e;let n=r.querySelector(\"svg\");n&&qNt(n);let i=UNt(r,n);YK({type:\"rendered\",revision:t.revision,source:t.source,colorScheme:t.colorScheme,height:i.height,width:i.width})}catch{t.revision===XK&&YK({type:\"renderError\",revision:t.revision})}}async function YNt(){if(!uM){uM=!0;try{for(;cM;){let t=cM;cM=null,await HNt(t)}}finally{uM=!1}}}function nFe(t){let e=rFe(t);e&&(XK=e.revision,cM=e,!(uM||HK)&&(HK=!0,window.setTimeout(()=>{HK=!1,YNt()},0)))}window.__PASEO_MERMAID_RUNTIME_RECEIVE__=nFe;window.addEventListener(\"message\",t=>{t.source===window.parent&&nFe(t.data)});YK({type:\"bridgeReady\"});})();\n/*! Bundled license information:\n\ndompurify/dist/purify.es.mjs:\n (*! @license DOMPurify 3.4.12 | (c) Cure53 and other contributors | Released under the Apache license 2.0 and Mozilla Public License 2.0 | github.com/cure53/DOMPurify/blob/3.4.12/LICENSE *)\n\nmermaid/dist/chunks/mermaid.core/chunk-ZIRB5QZD.mjs:\n (*! Bundled license information:\n \n js-yaml/dist/js-yaml.mjs:\n (*! js-yaml 4.1.1 https://github.com/nodeca/js-yaml @license MIT *)\n *)\n\nlodash-es/lodash.js:\n (**\n * @license\n * Lodash (Custom Build) \n * Build: `lodash modularize exports=\"es\" --repo lodash/lodash#4.18.1 -o ./`\n * Copyright OpenJS Foundation and other contributors \n * Released under MIT license \n * Based on Underscore.js 1.8.3 \n * Copyright Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors\n *)\n\ncytoscape/dist/cytoscape.esm.mjs:\n (*!\n Embeddable Minimum Strictly-Compliant Promises/A+ 1.1.1 Thenable\n Copyright (c) 2013-2014 Ralf S. Engelschall (http://engelschall.com)\n Licensed under The MIT License (http://opensource.org/licenses/MIT)\n *)\n (*!\n Event object based on jQuery events, MIT license\n \n https://jquery.org/license/\n https://tldrlegal.com/license/mit-license\n https://github.com/jquery/jquery/blob/master/src/event.js\n *)\n (*! Bezier curve function generator. Copyright Gaetan Renaudeau. MIT License: http://en.wikipedia.org/wiki/MIT_License *)\n (*! Runge-Kutta spring physics function generator. Adapted from Framer.js, copyright Koen Bok. MIT License: http://en.wikipedia.org/wiki/MIT_License *)\n\n@mermaid-js/parser/dist/chunks/mermaid-parser.core/chunk-KEIR6QF5.mjs:\n (*! Bundled license information:\n \n lodash-es/lodash.js:\n (**\n * @license\n * Lodash (Custom Build) \n * Build: `lodash modularize exports=\"es\" -o ./`\n * Copyright OpenJS Foundation and other contributors \n * Released under MIT license \n * Based on Underscore.js 1.8.3 \n * Copyright Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors\n *)\n *)\n\nmermaid/dist/mermaid.core.mjs:\n (*! Check if previously processed *)\n (*!\n * Wait for document loaded before starting the execution\n *)\n*/\n\n \n"; diff --git a/packages/app/src/components/markdown/fence/mermaid/runtime/messages.ts b/packages/app/src/components/markdown/fence/mermaid/runtime/messages.ts deleted file mode 100644 index 99b23f9d6d..0000000000 --- a/packages/app/src/components/markdown/fence/mermaid/runtime/messages.ts +++ /dev/null @@ -1,91 +0,0 @@ -import type { DiagramColorScheme, DiagramDimensions } from "../render-model"; - -export interface MermaidRuntimeRenderMessage { - type: "render"; - revision: number; - source: string; - colorScheme: DiagramColorScheme; - interactive: boolean; -} - -export type MermaidRuntimeMessage = - | { type: "bridgeReady" } - | ({ - type: "rendered"; - revision: number; - source: string; - colorScheme: DiagramColorScheme; - } & DiagramDimensions) - | { type: "renderError"; revision: number }; - -function isRecord(value: unknown): value is Record { - return typeof value === "object" && value !== null; -} - -function isColorScheme(value: unknown): value is DiagramColorScheme { - return value === "light" || value === "dark"; -} - -export function parseMermaidRuntimeRenderMessage( - value: unknown, -): MermaidRuntimeRenderMessage | null { - if ( - !isRecord(value) || - value.type !== "render" || - typeof value.revision !== "number" || - !Number.isInteger(value.revision) || - typeof value.source !== "string" || - !isColorScheme(value.colorScheme) || - typeof value.interactive !== "boolean" - ) { - return null; - } - return { - type: "render", - revision: value.revision, - source: value.source, - colorScheme: value.colorScheme, - interactive: value.interactive, - }; -} - -export function parseMermaidRuntimeMessage(value: unknown): MermaidRuntimeMessage | null { - if (!isRecord(value) || typeof value.type !== "string") { - return null; - } - if (value.type === "bridgeReady") { - return { type: "bridgeReady" }; - } - if ( - value.type === "renderError" && - typeof value.revision === "number" && - Number.isInteger(value.revision) - ) { - return { type: "renderError", revision: value.revision }; - } - if ( - value.type === "rendered" && - typeof value.revision === "number" && - Number.isInteger(value.revision) && - typeof value.source === "string" && - isColorScheme(value.colorScheme) && - typeof value.height === "number" && - Number.isFinite(value.height) && - typeof value.width === "number" && - Number.isFinite(value.width) - ) { - return { - type: "rendered", - revision: value.revision, - source: value.source, - colorScheme: value.colorScheme, - height: value.height, - width: value.width, - }; - } - return null; -} - -export function serializeMermaidRuntimeRenderMessage(message: MermaidRuntimeRenderMessage): string { - return JSON.stringify(message).replace(/<\/script/gi, "<\\/script"); -} diff --git a/packages/app/src/components/markdown/fence/mermaid/runtime/request-driver.test.ts b/packages/app/src/components/markdown/fence/mermaid/runtime/request-driver.test.ts deleted file mode 100644 index f2e93e2437..0000000000 --- a/packages/app/src/components/markdown/fence/mermaid/runtime/request-driver.test.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { describe, expect, it } from "vitest"; -import type { MermaidRenderRequest } from "../render-model"; -import { MermaidRuntimeRequestDriver } from "./request-driver"; - -function request(revision: number, source: string): MermaidRenderRequest { - return { revision, source, colorScheme: "dark" }; -} - -describe("MermaidRuntimeRequestDriver", () => { - it("tries queued streaming prefixes in order until the runtime renders one", () => { - const driver = new MermaidRuntimeRequestDriver(); - const first = request(1, "flow"); - const valid = request(2, "flowchart TD\nA --> B\n"); - const transient = request(3, "flowchart TD\nA --> B\nB --"); - - expect(driver.update(first)).toBeNull(); - expect(driver.update(valid)).toBeNull(); - expect(driver.update(transient)).toBeNull(); - expect(driver.ready()).toBe(valid); - expect(driver.settled(valid.revision, true)).toBe(transient); - }); - - it("keeps completed lines but collapses incomplete token revisions before readiness", () => { - const driver = new MermaidRuntimeRequestDriver(); - const partial = request(1, "flowchart TD\nA --"); - const completed = request(2, "flowchart TD\nA --> B\n"); - const nextPartial = request(3, "flowchart TD\nA --> B\nB --"); - const latestPartial = request(4, "flowchart TD\nA --> B\nB -->"); - - driver.update(partial); - driver.update(completed); - driver.update(nextPartial); - driver.update(latestPartial); - - expect(driver.ready()).toBe(completed); - expect(driver.settled(completed.revision, true)).toBe(latestPartial); - }); - - it("collapses later updates to the newest request after the first render", () => { - const driver = new MermaidRuntimeRequestDriver(); - const first = request(1, "flowchart TD\nA --> B"); - const second = request(2, "flowchart TD\nA --> B\nB --> C"); - const latest = request(3, "flowchart TD\nA --> B\nB --> D"); - - driver.update(first); - expect(driver.ready()).toBe(first); - expect(driver.settled(first.revision, true)).toBeNull(); - expect(driver.update(second)).toBe(second); - expect(driver.update(latest)).toBeNull(); - expect(driver.settled(second.revision, false)).toBe(latest); - }); -}); diff --git a/packages/app/src/components/markdown/fence/mermaid/runtime/request-driver.ts b/packages/app/src/components/markdown/fence/mermaid/runtime/request-driver.ts deleted file mode 100644 index 8e30587017..0000000000 --- a/packages/app/src/components/markdown/fence/mermaid/runtime/request-driver.ts +++ /dev/null @@ -1,63 +0,0 @@ -import type { MermaidRenderRequest } from "../render-model"; - -function isCompletedLineCandidate(source: string): boolean { - return source.endsWith("\n") && source.trim().includes("\n"); -} - -export class MermaidRuntimeRequestDriver { - private readyState = false; - private renderedOnce = false; - private inFlight: MermaidRenderRequest | null = null; - private queued: MermaidRenderRequest[] = []; - - update(request: MermaidRenderRequest | null): MermaidRenderRequest | null { - if (!request || this.hasRevision(request.revision)) { - return null; - } - if (this.renderedOnce) { - this.queued = [request]; - } else { - const lastIndex = this.queued.length - 1; - const last = this.queued[lastIndex]; - if (last && !isCompletedLineCandidate(last.source)) { - this.queued[lastIndex] = request; - } else { - this.queued.push(request); - } - } - return this.takeNext(); - } - - ready(): MermaidRenderRequest | null { - this.readyState = true; - return this.takeNext(); - } - - settled(revision: number, rendered: boolean): MermaidRenderRequest | null { - if (this.inFlight?.revision !== revision) { - return null; - } - this.inFlight = null; - if (rendered) { - this.renderedOnce = true; - const latest = this.queued.at(-1); - this.queued = latest ? [latest] : []; - } - return this.takeNext(); - } - - private hasRevision(revision: number): boolean { - return ( - this.inFlight?.revision === revision || - this.queued.some((request) => request.revision === revision) - ); - } - - private takeNext(): MermaidRenderRequest | null { - if (!this.readyState || this.inFlight || this.queued.length === 0) { - return null; - } - this.inFlight = this.queued.shift() ?? null; - return this.inFlight; - } -} diff --git a/packages/app/src/components/markdown/fence/mermaid/runtime/runtime-html.test.ts b/packages/app/src/components/markdown/fence/mermaid/runtime/runtime-html.test.ts deleted file mode 100644 index a11ffd5092..0000000000 --- a/packages/app/src/components/markdown/fence/mermaid/runtime/runtime-html.test.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { mermaidRuntimeHtml } from "./html.gen"; - -describe("Mermaid runtime document", () => { - it("ships a closed network policy", () => { - expect(mermaidRuntimeHtml).toContain("default-src 'none'"); - expect(mermaidRuntimeHtml).toContain("connect-src 'none'"); - expect(mermaidRuntimeHtml).toContain("img-src data: blob:"); - expect(mermaidRuntimeHtml).toContain("font-src 'none'"); - expect(mermaidRuntimeHtml).toContain("frame-src 'none'"); - }); -}); diff --git a/packages/app/src/components/markdown/fence/mermaid/runtime/runtime.browser.test.ts b/packages/app/src/components/markdown/fence/mermaid/runtime/runtime.browser.test.ts deleted file mode 100644 index 7ffbbc79db..0000000000 --- a/packages/app/src/components/markdown/fence/mermaid/runtime/runtime.browser.test.ts +++ /dev/null @@ -1,146 +0,0 @@ -import { afterEach, describe, expect, it } from "vitest"; -import { mermaidRuntimeHtml } from "./html.gen"; -import { parseMermaidRuntimeMessage, type MermaidRuntimeMessage } from "./messages"; - -const mountedFrames: HTMLIFrameElement[] = []; - -function waitForRuntimeMessage( - frame: HTMLIFrameElement, - predicate: (message: MermaidRuntimeMessage) => boolean, -): Promise { - return new Promise((resolve, reject) => { - const timeout = window.setTimeout(() => { - window.removeEventListener("message", receive); - reject(new Error("Timed out waiting for Mermaid runtime")); - }, 10_000); - function receive(event: MessageEvent): void { - if (event.source !== frame.contentWindow) { - return; - } - const message = parseMermaidRuntimeMessage(event.data); - if (!message || !predicate(message)) { - return; - } - window.clearTimeout(timeout); - window.removeEventListener("message", receive); - resolve(message); - } - window.addEventListener("message", receive); - }); -} - -async function mountRuntime(size?: { width: number; height: number }): Promise { - const frame = document.createElement("iframe"); - frame.sandbox.add("allow-scripts"); - if (size) { - frame.style.width = `${size.width}px`; - frame.style.height = `${size.height}px`; - } - const ready = waitForRuntimeMessage(frame, (message) => message.type === "bridgeReady"); - frame.srcdoc = mermaidRuntimeHtml; - document.body.append(frame); - mountedFrames.push(frame); - await ready; - return frame; -} - -function renderedSize(message: MermaidRuntimeMessage): { height: number; width: number } { - if (message.type !== "rendered") { - throw new Error(`Expected a rendered diagram, got ${message.type}`); - } - return { height: message.height, width: message.width }; -} - -function render( - frame: HTMLIFrameElement, - input: { revision: number; source: string }, -): Promise { - const response = waitForRuntimeMessage( - frame, - (message) => - message.type !== "bridgeReady" && - "revision" in message && - message.revision === input.revision, - ); - frame.contentWindow?.postMessage( - { - type: "render", - revision: input.revision, - source: input.source, - colorScheme: "dark", - interactive: false, - }, - "*", - ); - return response; -} - -afterEach(() => { - for (const frame of mountedFrames.splice(0)) { - frame.remove(); - } -}); - -describe("Mermaid sandbox runtime", () => { - it("renders successive valid streaming prefixes and reports an invalid prefix", async () => { - const frame = await mountRuntime(); - const firstSource = "flowchart TD\nA --> B"; - const secondSource = `${firstSource}\nB --> C`; - - const first = await render(frame, { revision: 1, source: firstSource }); - const invalid = await render(frame, { revision: 2, source: "not a mermaid diagram" }); - const second = await render(frame, { revision: 3, source: secondSource }); - - expect(first).toMatchObject({ type: "rendered", revision: 1, source: firstSource }); - expect(invalid).toEqual({ type: "renderError", revision: 2 }); - expect(second).toMatchObject({ type: "rendered", revision: 3, source: secondSource }); - }); - - /** - * The host sizes this frame from the reported size, so a size that depends on the frame feeds - * back: every re-render measures inside a frame the previous measurement already shrank by the - * container's padding, and a streaming diagram ratchets down to a few pixels. - */ - it("reports the same size whatever frame the host gives it", async () => { - const source = "flowchart TD\nA[Start] --> B[Middle]\nB --> C[Ship]"; - const narrowFrame = await mountRuntime({ height: 60, width: 60 }); - const wideFrame = await mountRuntime({ height: 600, width: 900 }); - - const narrow = await render(narrowFrame, { revision: 1, source }); - const wide = await render(wideFrame, { revision: 1, source }); - - expect(renderedSize(narrow)).toEqual(renderedSize(wide)); - }); - - it("coalesces queued input and never reports an obsolete result", async () => { - const frame = await mountRuntime(); - const obsoleteSource = `flowchart TD\n${Array.from({ length: 250 }, (_, index) => `A${index} --> A${index + 1}`).join("\n")}`; - const currentSource = "flowchart TD\nCurrent --> Result"; - const obsoleteResponses: MermaidRuntimeMessage[] = []; - function collect(event: MessageEvent): void { - if (event.source !== frame.contentWindow) { - return; - } - const message = parseMermaidRuntimeMessage(event.data); - if (message && message.type !== "bridgeReady" && message.revision === 10) { - obsoleteResponses.push(message); - } - } - window.addEventListener("message", collect); - frame.contentWindow?.postMessage( - { - type: "render", - revision: 10, - source: obsoleteSource, - colorScheme: "dark", - interactive: false, - }, - "*", - ); - const current = await render(frame, { revision: 11, source: currentSource }); - window.removeEventListener("message", collect); - - expect(current).toMatchObject({ type: "rendered", revision: 11, source: currentSource }); - expect(obsoleteResponses).toEqual([]); - }); -}); diff --git a/packages/app/src/components/markdown/fence/mermaid/source-policy.test.ts b/packages/app/src/components/markdown/fence/mermaid/source-policy.test.ts deleted file mode 100644 index 48b844e1e3..0000000000 --- a/packages/app/src/components/markdown/fence/mermaid/source-policy.test.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { containsUnsafeMermaidSource } from "./source-policy"; - -describe("containsUnsafeMermaidSource", () => { - it("rejects resource-bearing constructs", () => { - expect(containsUnsafeMermaidSource('flowchart TD\n A@{ img: "https://x/y.png" }')).toBe(true); - expect(containsUnsafeMermaidSource("flowchart TD\n A@{ icon: 'pack:name' }")).toBe(true); - expect( - containsUnsafeMermaidSource('%%{init: {"themeCSS": "a { color: red }"}}%%\ngraph TD'), - ).toBe(true); - expect(containsUnsafeMermaidSource("graph TD\n A[url(http://x)]")).toBe(true); - expect(containsUnsafeMermaidSource("graph TD\n A[@import 'x']")).toBe(true); - expect(containsUnsafeMermaidSource('graph TD\n A[""]')).toBe(true); - expect(containsUnsafeMermaidSource('graph TD\n A["styled"]')).toBe(true); - expect(containsUnsafeMermaidSource('graph TD\n A["<img src=x>"]')).toBe(true); - expect(containsUnsafeMermaidSource('graph TD\n A[""]')).toBe(true); - }); - - it("rejects all shape-data constructs, including yaml key evasions", () => { - expect(containsUnsafeMermaidSource('flowchart TD\n A@{ "img": "https://x/y.png" }')).toBe( - true, - ); - expect(containsUnsafeMermaidSource("flowchart TD\n A@{ 'img': 'https://x/y.png' }")).toBe( - true, - ); - expect( - containsUnsafeMermaidSource('flowchart TD\n A@{ "\\u0069mg": "https://x/y.png" }'), - ).toBe(true); - expect( - containsUnsafeMermaidSource('flowchart TD\n A@{ "\\u{69}mg": "https://x/y.png" }'), - ).toBe(true); - expect(containsUnsafeMermaidSource('flowchart TD\n A@{ "\\x69mg": "https://x/y.png" }')).toBe( - true, - ); - expect( - containsUnsafeMermaidSource('flowchart TD\n A@{ "\\U00000069mg": "https://attacker/x" }'), - ).toBe(true); - expect(containsUnsafeMermaidSource('flowchart TD\n A@{ "icon": "pack:name" }')).toBe(true); - expect( - containsUnsafeMermaidSource( - 'flowchart TD\n A@{ ? img # comment\n : "https://attacker/x" }', - ), - ).toBe(true); - expect( - containsUnsafeMermaidSource( - 'flowchart TD\n A@{ dummy: &k img\n ? *k # comment\n : "https://attacker/x" }', - ), - ).toBe(true); - }); - - it("fails closed for malformed or out-of-range escapes without throwing", () => { - expect(() => - containsUnsafeMermaidSource('graph TD\n A["\\u{110000} disguised"]'), - ).not.toThrow(); - expect(containsUnsafeMermaidSource('graph TD\n A["\\u{110000} disguised"]')).toBe(true); - expect(() => - containsUnsafeMermaidSource('graph TD\n A["\\u{FFFFFF} disguised"]'), - ).not.toThrow(); - expect(containsUnsafeMermaidSource('graph TD\n A["\\u{FFFFFF} disguised"]')).toBe(true); - }); - - it("allows ordinary diagrams including formatting-only labels", () => { - expect(containsUnsafeMermaidSource("flowchart TD\n A[Start] --> B{Choice}")).toBe(false); - expect(containsUnsafeMermaidSource('graph TD\n A["line one
line two"]')).toBe(false); - expect(containsUnsafeMermaidSource('graph TD\n A["line one
line two"]')).toBe(false); - expect(containsUnsafeMermaidSource('graph TD\n A["formatted"]')).toBe(false); - expect(containsUnsafeMermaidSource("sequenceDiagram\n Alice->>Bob: a < b and x > y")).toBe( - false, - ); - expect(containsUnsafeMermaidSource("flowchart TD\n A@{ shape: rect }")).toBe(true); - }); -}); diff --git a/packages/app/src/components/markdown/fence/mermaid/source-policy.ts b/packages/app/src/components/markdown/fence/mermaid/source-policy.ts deleted file mode 100644 index df6ad0312b..0000000000 --- a/packages/app/src/components/markdown/fence/mermaid/source-policy.ts +++ /dev/null @@ -1,52 +0,0 @@ -// Mermaid can fetch external resources while *rendering* — image shapes -// (`A@{ img: "url" }`) construct an Image and await decode before any output -// sanitization runs, and CSS can pull url()/@import — so a prompt-injected -// diagram could exfiltrate data in a request URL. securityLevel "strict" does -// not prevent this (mermaid-js/mermaid#7645). Mermaid 11.16 parses the shape -// metadata object with yaml.JSON_SCHEMA semantics, including aliases and -// explicit keys; matching those keys safely would require a real parser. We -// therefore reject any `@{ ... }` shape-data construct up front and fall back -// to the source code block. Formatting-only `
` and `` tags are common -// in generated labels and carry no resource-bearing attributes; all other -// tags (and entity-encoded text that could smuggle one) are rejected. -const UNSAFE_MERMAID_SOURCE = - /@\s*\{|url\s*\(|@import\b|themeCSS|&#|<(?!\/?(?:br|i)\s*\/?>)[a-z!/]/i; - -// Mermaid labels can contain escaped text. Decode the escape forms we care -// about before running the denylist so disguised HTML still gets caught. -// Invalid escape sequences fail closed and fall back to the source block. -function normalizeMermaidSource(code: string): string | null { - try { - return code - .replace(/\\u\{([0-9a-fA-F]{1,6})\}/g, (_, hex: string) => - decodeCodePointEscape(Number.parseInt(hex, 16)), - ) - .replace(/\\u([0-9a-fA-F]{4})/g, (_, hex: string) => - String.fromCharCode(Number.parseInt(hex, 16)), - ) - .replace(/\\x([0-9a-fA-F]{2})/g, (_, hex: string) => - String.fromCharCode(Number.parseInt(hex, 16)), - ) - .replace(/["'`\\]/g, ""); - } catch { - return null; - } -} - -function decodeCodePointEscape(value: number): string { - if (!Number.isInteger(value) || value < 0 || value > 0x10ffff) { - throw new RangeError("Invalid Unicode code point"); - } - return String.fromCodePoint(value); -} - -export function containsUnsafeMermaidSource(code: string): boolean { - if (UNSAFE_MERMAID_SOURCE.test(code)) { - return true; - } - const normalized = normalizeMermaidSource(code); - if (normalized === null) { - return true; - } - return UNSAFE_MERMAID_SOURCE.test(normalized); -} diff --git a/packages/app/src/components/markdown/fence/mermaid/use-render-model.ts b/packages/app/src/components/markdown/fence/mermaid/use-render-model.ts deleted file mode 100644 index cb35549b10..0000000000 --- a/packages/app/src/components/markdown/fence/mermaid/use-render-model.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { useCallback, useEffect, useMemo, useReducer } from "react"; -import { - createMermaidRenderModel, - getMermaidRenderRequest, - reduceMermaidRenderModel, - type DiagramColorScheme, - type DiagramDimensions, - type RenderedDiagram, -} from "./render-model"; -import { containsUnsafeMermaidSource } from "./source-policy"; -import type { MarkdownPhase } from "../types"; - -const renderCache = new Map(); -const RENDER_CACHE_LIMIT = 50; - -function cacheKey(source: string, colorScheme: DiagramColorScheme): string { - return `${colorScheme}\u0000${source}`; -} - -function readCachedRender(source: string, colorScheme: DiagramColorScheme): RenderedDiagram | null { - return renderCache.get(cacheKey(source, colorScheme)) ?? null; -} - -function cacheRender(rendered: RenderedDiagram): void { - if (renderCache.size >= RENDER_CACHE_LIMIT) { - const oldest = renderCache.keys().next().value; - if (oldest !== undefined) { - renderCache.delete(oldest); - } - } - renderCache.set(cacheKey(rendered.source, rendered.colorScheme), rendered); -} - -export function useMermaidRenderModel({ - source, - phase, - colorScheme, -}: { - source: string; - phase: MarkdownPhase; - colorScheme: DiagramColorScheme; -}) { - const renderInput = useMemo( - () => ({ - source, - phase, - colorScheme, - rejected: containsUnsafeMermaidSource(source), - cached: readCachedRender(source, colorScheme), - }), - [colorScheme, phase, source], - ); - const [state, dispatch] = useReducer( - reduceMermaidRenderModel, - renderInput, - createMermaidRenderModel, - ); - - useEffect(() => { - dispatch({ type: "inputChanged", input: renderInput }); - }, [renderInput]); - - const rendered = useCallback( - (response: { - revision: number; - source: string; - colorScheme: DiagramColorScheme; - dimensions: DiagramDimensions; - }) => { - const cached: RenderedDiagram = { - source: response.source, - colorScheme: response.colorScheme, - ...response.dimensions, - }; - cacheRender(cached); - dispatch({ - type: "rendered", - revision: response.revision, - source: response.source, - colorScheme: response.colorScheme, - dimensions: response.dimensions, - }); - }, - [], - ); - const renderFailed = useCallback((revision: number) => { - dispatch({ type: "renderFailed", revision }); - }, []); - - return { - state, - request: getMermaidRenderRequest(state), - rendered, - renderFailed, - }; -} diff --git a/packages/app/src/components/markdown/fence/types.ts b/packages/app/src/components/markdown/fence/types.ts deleted file mode 100644 index b499bc893b..0000000000 --- a/packages/app/src/components/markdown/fence/types.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type { TextStyle } from "react-native"; - -export type MarkdownPhase = "streaming" | "complete"; - -export interface MarkdownFenceRendererProps { - code: string; - phase: MarkdownPhase; - inheritedStyles: TextStyle; - textStyle: TextStyle; -} diff --git a/packages/app/src/components/markdown/html-ish.test.ts b/packages/app/src/components/markdown/html-ish.test.ts deleted file mode 100644 index f8606d2714..0000000000 --- a/packages/app/src/components/markdown/html-ish.test.ts +++ /dev/null @@ -1,529 +0,0 @@ -import { readFileSync } from "node:fs"; -import { describe, expect, it } from "vitest"; -import { normalizeHtmlishMarkdown, splitHtmlishMarkdown } from "./html-ish"; - -describe("splitHtmlishMarkdown", () => { - it("renders heading text beside a linked picture without literal heading tags", () => { - const source = - '

RetriggerConfidence Score: 5/5

'; - - expect(splitHtmlishMarkdown(source)).toEqual([ - { - kind: "inlineImage", - alt: "Retrigger", - src: "https://example.com/l.svg", - href: "https://example.com/r", - flowsWithText: true, - }, - { kind: "markdown", text: "Confidence Score: 5/5" }, - ]); - }); - - it.each(["h1", "h2", "h3", "h4", "h5", "h6"])( - "keeps a mixed %s heading together after preceding markdown", - (heading) => { - const source = `Intro\n\n<${heading}>IconCaption\n\nOutro`; - - expect(splitHtmlishMarkdown(source)).toEqual([ - { kind: "markdown", text: "Intro\n\n" }, - { - kind: "inlineImage", - alt: "Icon", - src: "https://example.com/icon.svg", - flowsWithText: true, - }, - { kind: "markdown", text: "Caption\n\nOutro" }, - ]); - }, - ); - - it("preserves formatted text before and after images in a details heading", () => { - const source = - '
More

BeforeIconAfter

'; - - expect(splitHtmlishMarkdown(source)).toEqual([ - { - kind: "details", - summary: "More", - body: "**Before***After*", - bodyParts: [ - { kind: "markdown", text: "**Before**" }, - { kind: "inlineImage", alt: "Icon", src: "https://example.com/icon.svg" }, - { kind: "markdown", text: "*After*" }, - ], - }, - ]); - }); - - it("keeps mixed HTML headings literal in code examples", () => { - const heading = '

Caption

'; - const source = `Use \`${heading}\`\n\n\`\`\`html\n${heading}\n\`\`\``; - - expect(splitHtmlishMarkdown(source)).toEqual([{ kind: "markdown", text: source }]); - }); - - it("preserves unknown wrappers around text-only headings", () => { - expect(splitHtmlishMarkdown("

Heading

")).toEqual([ - { kind: "markdown", text: "
Heading
" }, - ]); - }); - - const inlineImageBody = [ - 'Priority Spoofed browser User-Agent allows access control bypass', - "", - "The middleware now trusts any browser-like User-Agent for unauthenticated requests.", - "", - "```ts", - 'const isBrowser = userAgent.includes("Mozilla");', - "```", - ].join("\n"); - - const multiDetailsBody = [ - "### Bot Review", - "", - "
", - "

Important Files Changed

", - "", - "- `packages/server/src/server/session.ts`", - "", - "
", - "", - "
", - "

Security Findings

", - "", - "No blocking findings.", - "", - "
", - "", - "", - 'Reviews (8): Last reviewed commit: “revert: undo parser” | Re-trigger Greptile', - ].join("\n"); - - it("classifies linked HTML images as generic inline images, not block markdown images", () => { - const [image, text] = splitHtmlishMarkdown(inlineImageBody); - - expect(image).toEqual({ - kind: "inlineImage", - alt: "Priority", - src: "https://example.com/assets/priority.svg?v=9", - flowsWithText: true, - }); - expect(text).toEqual({ - kind: "markdown", - text: [ - " Spoofed browser User-Agent allows access control bypass", - "", - "The middleware now trusts any browser-like User-Agent for unauthenticated requests.", - "", - "```ts", - 'const isBrowser = userAgent.includes("Mozilla");', - "```", - ].join("\n"), - }); - }); - - it("flags both images as flowsWithText when two inline images precede title text on the same line", () => { - const twoImageBody = [ - 'Priority Security **Title text here**', - "", - "Body paragraph.", - ].join("\n"); - - const parts = splitHtmlishMarkdown(twoImageBody); - const [first, second, third] = parts; - - expect(first).toEqual({ - kind: "inlineImage", - alt: "Priority", - src: "https://example.com/priority.svg", - flowsWithText: true, - }); - expect(second).toEqual({ kind: "markdown", text: " " }); - expect(third).toEqual({ - kind: "inlineImage", - alt: "Security", - src: "https://example.com/security.svg", - flowsWithText: true, - }); - }); - - it("keeps safe width and height attributes on inline images", () => { - expect( - splitHtmlishMarkdown( - 'Small', - ), - ).toEqual([ - { - kind: "inlineImage", - alt: "Small", - src: "https://example.com/small.svg", - width: 18, - height: 12, - }, - ]); - }); - - it("keeps safe non-empty image links", () => { - expect( - splitHtmlishMarkdown( - 'Small', - ), - ).toEqual([ - { - kind: "inlineImage", - alt: "Small", - src: "https://example.com/small.svg", - href: "https://example.com/details", - }, - ]); - }); - - it("renders a linked picture inside a heading without exposing its HTML", () => { - const source = [ - '

', - '', - '', - 'Retrigger', - "

", - "", - "Confidence Score: 5/5", - "", - "
SummaryNo outstanding findings.
", - ].join("\n"); - - expect(splitHtmlishMarkdown(source)).toEqual([ - { - kind: "inlineImage", - alt: "Retrigger", - src: "https://example.com/default.svg", - href: "https://example.com/retrigger?id=73851569", - width: 85, - height: 24, - }, - { kind: "markdown", text: "\n\nConfidence Score: 5/5\n\n" }, - { kind: "details", summary: "Summary", body: "No outstanding findings." }, - ]); - }); - - it.each([ - ["", ""], - ["

", "

"], - ['', ""], - ])("uses the picture fallback without activating unsafe links (%s)", (wrapper, closing) => { - const picture = - 'Image'; - - expect(splitHtmlishMarkdown(`${wrapper}${picture}${closing}`)).toEqual([ - { kind: "inlineImage", src: "https://example.com/default.svg", alt: "Image" }, - ]); - }); - - it("keeps picture examples literal inside inline and fenced code", () => { - const picture = - ''; - const source = `Use \`${picture}\`\n\n\`\`\`html\n${picture}\n\`\`\``; - expect(splitHtmlishMarkdown(source)).toEqual([{ kind: "markdown", text: source }]); - }); - - it.each(["p", "div"])("renders a linked picture inside %s", (wrapper) => { - const source = `<${wrapper}>Image`; - expect(splitHtmlishMarkdown(source)).toEqual([ - { - kind: "inlineImage", - src: "https://example.com/default.svg", - alt: "Image", - href: "https://example.com/page", - }, - ]); - }); - - it.each(["h2", "p", "div"])("keeps text after an image-only %s on a separate row", (wrapper) => { - const source = `<${wrapper}>ImageNext section`; - expect(splitHtmlishMarkdown(source)).toEqual([ - { kind: "inlineImage", src: "https://example.com/image.svg", alt: "Image" }, - { kind: "markdown", text: "Next section" }, - ]); - }); - - it("handles deeply nested image wrappers without exhausting the stack", () => { - const source = `${"
".repeat(10000)}Image${"
".repeat(10000)}`; - expect(splitHtmlishMarkdown(source)).toEqual([ - { - kind: "inlineImage", - src: "https://example.com/image.svg", - alt: "Image", - href: "https://example.com/page", - }, - ]); - }); - - it("preserves inline image parts inside details bodies", () => { - expect( - splitHtmlishMarkdown( - '
ImagesIcon Inline text
', - ), - ).toEqual([ - { - kind: "details", - summary: "Images", - body: "Inline text", - bodyParts: [ - { - kind: "inlineImage", - alt: "Icon", - src: "https://example.com/icon.svg", - href: "https://example.com/page", - flowsWithText: true, - }, - { kind: "markdown", text: " Inline text" }, - ], - }, - ]); - }); - - it("does not flag standalone inline images as flowing with text", () => { - expect( - splitHtmlishMarkdown('Shot\n\nCaption below'), - ).toEqual([ - { kind: "inlineImage", alt: "Shot", src: "https://example.com/shot.png" }, - { kind: "markdown", text: "\n\nCaption below" }, - ]); - }); - - it("does not flag mid-line inline images as flowing with text", () => { - const [, image] = splitHtmlishMarkdown( - 'Before Icon after', - ); - - expect(image).toEqual({ - kind: "inlineImage", - alt: "Icon", - src: "https://example.com/icon.png", - }); - }); - - it("leaves ordinary markdown images on the markdown path", () => { - const source = "![Ordinary](https://example.com/full-size.png)"; - - expect(splitHtmlishMarkdown(source)).toEqual([{ kind: "markdown", text: source }]); - }); - - it("leaves unsafe HTML image sources inert", () => { - const source = 'Bad'; - - expect(splitHtmlishMarkdown(source)).toEqual([{ kind: "markdown", text: source }]); - }); - - it("removes raw image anchor and image tags from rendered markdown text", () => { - const text = splitHtmlishMarkdown(inlineImageBody) - .map((part) => (part.kind === "markdown" ? part.text : "")) - .join(""); - - expect(text).not.toContain(""); - }); - - it("unwraps sub text and strips HTML comments", () => { - const parts = splitHtmlishMarkdown(multiDetailsBody); - const tail = parts.at(-1); - - expect(tail).toEqual({ - kind: "markdown", - text: "\n\nReviews (8): Last reviewed commit: “revert: undo parser” | [Re-trigger Greptile](https://app.greptile.com)", - }); - }); - - it("does not leak stray closing details tags across multiple details blocks", () => { - const renderedText = splitHtmlishMarkdown(multiDetailsBody) - .map((part) => { - if (part.kind === "markdown") return part.text; - if (part.kind === "details") return `${part.summary}\n${part.body}`; - return part.alt; - }) - .join("\n"); - - expect(renderedText).not.toContain(""); - expect(renderedText).not.toContain(" 3<4")).toBe( - "1 { - const source = "
OpenStill open"; - - expect(splitHtmlishMarkdown(source)).toEqual([{ kind: "markdown", text: source }]); - }); - - it("falls back to inert markdown when summary is missing", () => { - const source = "
Hidden
"; - - expect(splitHtmlishMarkdown(source)).toEqual([{ kind: "markdown", text: source }]); - }); - - it("does not execute or render unknown HTML as structured content", () => { - const source = - '
Safe
'; - - expect(splitHtmlishMarkdown(source)).toEqual([ - { kind: "markdown", text: "" }, - { kind: "details", summary: "Safe", body: '' }, - ]); - }); -}); - -function withoutArrayToSorted(callback: () => T): T { - const descriptor = Object.getOwnPropertyDescriptor(Array.prototype, "toSorted"); - Reflect.deleteProperty(Array.prototype, "toSorted"); - try { - return callback(); - } finally { - if (descriptor) { - Reflect.defineProperty(Array.prototype, "toSorted", descriptor); - } - } -} diff --git a/packages/app/src/components/markdown/html-ish.ts b/packages/app/src/components/markdown/html-ish.ts deleted file mode 100644 index 94eb7cab5b..0000000000 --- a/packages/app/src/components/markdown/html-ish.ts +++ /dev/null @@ -1,926 +0,0 @@ -import { Parser } from "htmlparser2"; - -export interface MarkdownTextPart { - kind: "markdown"; - text: string; -} - -export interface MarkdownDetailsPart { - kind: "details"; - summary: string; - body: string; - bodyParts?: MarkdownDisplayPart[]; -} - -export interface MarkdownInlineImagePart { - kind: "inlineImage"; - alt: string; - src: string; - href?: string; - width?: number; - height?: number; - /** The image starts a line and text continues on that same line. */ - flowsWithText?: boolean; -} - -export type MarkdownDisplayPart = MarkdownTextPart | MarkdownDetailsPart | MarkdownInlineImagePart; - -const FENCE_LINE_RE = /^ {0,3}([`~]{3,})[^\n\r]*(?:\r?\n|$)/gm; -const BACKTICK_RUN_RE = /`+/g; -const SAFE_IMAGE_SRC_RE = /^(https?:\/\/|data:image\/(?:png|gif|jpe?g);base64,)/i; -const SAFE_LINK_HREF_RE = /^(https?:\/\/|#(?:$|[\w-]))/i; -// CommonMark open and closing tags (spec "Raw HTML"); comments are matched separately. -const HTML_ATTRIBUTE = String.raw`\s+[A-Za-z_:][\w.:-]*(?:\s*=\s*(?:[^\s"'=<>\x60]+|'[^']*'|"[^"]*"))?`; -const HTML_TAG_RE = new RegExp( - String.raw`<(?:[A-Za-z][A-Za-z0-9-]*(?:${HTML_ATTRIBUTE})*\s*\/?|\/[A-Za-z][A-Za-z0-9-]*\s*)>`, - "y", -); -const VOID_HTML_TAGS = new Set(["br", "img", "source"]); -const MARKDOWN_TAG_WRAPPERS: Readonly> = { - b: ["**", "**"], - del: ["~~", "~~"], - em: ["*", "*"], - i: ["*", "*"], - s: ["~~", "~~"], - strike: ["~~", "~~"], - strong: ["**", "**"], -}; - -interface ProtectedMarkdownRange { - start: number; - end: number; -} - -interface MarkdownImageDimensions { - width?: number; - height?: number; -} - -interface HtmlTextToken { - kind: "text"; - value: string; -} - -interface HtmlCommentToken { - kind: "comment"; -} - -interface HtmlTagToken { - kind: "tag"; - name: string; - closing: boolean; - selfClosing: boolean; - attributes: Record; - raw: string; -} - -type HtmlToken = HtmlTextToken | HtmlCommentToken | HtmlTagToken; - -interface InlineImageParseResult { - part: MarkdownInlineImagePart; - end: number; -} - -interface MarkdownDelimiterMatch { - index: number; - end: number; -} - -interface HtmlishTokenParser { - write(chunk: string): void; - skip(length: number): void; - end(): void; -} - -export function splitHtmlishMarkdown(source: string): MarkdownDisplayPart[] { - return splitHtmlishTokens(tokenizeHtmlishMarkdown(source)); -} - -function splitHtmlishTokens(tokens: HtmlToken[]): MarkdownDisplayPart[] { - const parts: MarkdownDisplayPart[] = []; - let cursor = 0; - - while (cursor < tokens.length) { - const token = tokens[cursor]; - if (isOpenTag(token, "details")) { - const closeIndex = findMatchingClose(tokens, cursor, "details"); - if (closeIndex !== null) { - const details = parseDetailsTokens(tokens.slice(cursor + 1, closeIndex)); - if (details) { - parts.push(details); - cursor = closeIndex + 1; - continue; - } - } - } - - const inlineImage = parseInlineImageAt(tokens, cursor); - if (inlineImage) { - parts.push( - flowsWithFollowingText(tokens, cursor, inlineImage.end) - ? { ...inlineImage.part, flowsWithText: true } - : inlineImage.part, - ); - cursor = inlineImage.end; - continue; - } - - if (isHeadingTag(token)) { - const closeIndex = findMixedHeadingClose(tokens, cursor); - if (closeIndex !== null) { - // Headings render as their contents. Split those contents together so an - // embedded image cannot strand the opening heading tag in a text slice. - for (const part of splitHtmlishTokens(tokens.slice(cursor + 1, closeIndex))) { - appendDisplayPart(parts, part); - } - cursor = closeIndex + 1; - continue; - } - } - - const end = findNextDisplayPartIndex(tokens, cursor + 1) ?? tokens.length; - appendMarkdownPart(parts, renderInlineTokens(tokens.slice(cursor, end))); - cursor = end; - } - - return parts; -} - -function parseInlineImageAt(tokens: HtmlToken[], start: number): InlineImageParseResult | null { - const token = tokens[start]; - if (token?.kind !== "tag" || token.closing) { - return null; - } - - if (token.name === "img") { - const image = imageTokenToInlineImage(token, undefined); - return image ? { part: image, end: start + 1 } : null; - } - - if (token.name !== "a" && token.name !== "picture" && !isImageBlockWrapper(token)) { - return null; - } - - return parseWrappedImageAt(tokens, start); -} - -function parseWrappedImageAt(tokens: HtmlToken[], start: number): InlineImageParseResult | null { - // Walk an image-only wrapper chain once. Recursing over copied child slices - // makes deeply nested review HTML quadratic and can exhaust the JS stack. - const wrappers: string[] = []; - let image: MarkdownInlineImagePart | null = null; - let href: string | undefined; - let hasLink = false; - - for (let index = start; index < tokens.length; index += 1) { - const child = tokens[index]; - if (child.kind === "comment" || isWhitespaceText(child)) { - continue; - } - if (child.kind !== "tag") { - return null; - } - if (child.closing) { - if (!image || wrappers.pop() !== child.name) { - return null; - } - if (wrappers.length === 0) { - return { part: image, end: index + 1 }; - } - continue; - } - // Pictures use their img fallback, not responsive source selection. - if (child.name === "source" && wrappers.at(-1) === "picture") { - continue; - } - if (image) { - return null; - } - if (child.name === "img") { - image = imageTokenToInlineImage(child, href); - if (!image) { - return null; - } - continue; - } - if (child.name !== "a" && child.name !== "picture" && !isImageBlockWrapper(child)) { - return null; - } - if (child.name === "a" && !hasLink) { - href = safeHref(child.attributes.href); - hasLink = true; - } - wrappers.push(child.name); - } - return null; -} - -function isImageBlockWrapper(token: HtmlToken | undefined): boolean { - return isHeadingTag(token) || isOpenTag(token, "p") || isOpenTag(token, "div"); -} - -function flowsWithFollowingText(tokens: HtmlToken[], start: number, end: number): boolean { - if (isImageBlockWrapper(tokens[start])) { - return false; - } - - const previous = tokens[start - 1]; - // At line start if nothing precedes this image, or the preceding token ends with only - // whitespace since the last newline (covers a bare space between two images on the same line). - const atLineStart = - previous === undefined || - (previous.kind === "text" && /(?:^|[\n\r])[ \t]*$/.test(previous.value)); - if (!atLineStart) { - return false; - } - - // Scan forward past same-line whitespace-only text tokens and inline images to find - // the first substantive text token, without crossing a newline. - let cursor = end; - while (cursor < tokens.length) { - const token = tokens[cursor]; - if (token === undefined) { - break; - } - if (token.kind === "text") { - const sameLine = token.value.split(/\r?\n/, 1)[0] ?? ""; - if (sameLine.trim().length > 0) { - return true; - } - // Whitespace-only on this line — keep scanning only if no newline was crossed. - if (token.value.includes("\n") || token.value.includes("\r")) { - return false; - } - cursor += 1; - continue; - } - // An inline image tag — skip over it (the image itself and its possible wrapping close tag). - if (token.kind === "tag") { - if (isImageBlockWrapper(token)) { - return false; - } - const imageResult = parseInlineImageAt(tokens, cursor); - if (imageResult) { - cursor = imageResult.end; - continue; - } - } - break; - } - return false; -} - -function findNextDisplayPartIndex(tokens: HtmlToken[], start: number): number | null { - for (let index = start; index < tokens.length; index += 1) { - if ( - isOpenTag(tokens[index], "details") || - findMixedHeadingClose(tokens, index) !== null || - parseInlineImageAt(tokens, index) - ) { - return index; - } - } - return null; -} - -function findMixedHeadingClose(tokens: HtmlToken[], start: number): number | null { - const token = tokens[start]; - if (!isHeadingTag(token)) { - return null; - } - const closeIndex = findMatchingClose(tokens, start, token.name); - if (closeIndex === null) { - return null; - } - for (let index = start + 1; index < closeIndex; index += 1) { - if (parseInlineImageAt(tokens, index)) { - return closeIndex; - } - } - return null; -} - -function appendDisplayPart(parts: MarkdownDisplayPart[], part: MarkdownDisplayPart): void { - if (part.kind === "markdown") { - appendMarkdownPart(parts, part.text); - } else { - parts.push(part); - } -} - -function appendMarkdownPart(parts: MarkdownDisplayPart[], text: string): void { - if (!text) { - return; - } - const previous = parts.at(-1); - if (previous?.kind === "markdown") { - previous.text += text; - return; - } - parts.push({ kind: "markdown", text }); -} - -function parseDetailsTokens(tokens: HtmlToken[]): MarkdownDetailsPart | null { - const summaryOpenIndex = findNextOpenTag(tokens, 0, "summary"); - if (summaryOpenIndex === null) { - return null; - } - - const summaryCloseIndex = findMatchingClose(tokens, summaryOpenIndex, "summary"); - if (summaryCloseIndex === null) { - return null; - } - - const summaryTokens = tokens.slice(summaryOpenIndex + 1, summaryCloseIndex); - const bodyTokens = [...tokens.slice(0, summaryOpenIndex), ...tokens.slice(summaryCloseIndex + 1)]; - const summary = renderSummaryTokens(summaryTokens).trim(); - if (!summary) { - return null; - } - - const bodyParts = splitHtmlishTokens(bodyTokens); - const body = renderBodyText(bodyParts); - - return { - kind: "details", - summary, - body: body.trim(), - ...(bodyParts.some((part) => part.kind !== "markdown") - ? { bodyParts: trimBodyParts(bodyParts) } - : {}), - }; -} - -function renderBodyText(parts: MarkdownDisplayPart[]): string { - return parts.map((part) => (part.kind === "markdown" ? part.text : "")).join(""); -} - -function trimBodyParts(parts: MarkdownDisplayPart[]): MarkdownDisplayPart[] { - const trimmed = [...parts]; - const first = trimmed[0]; - if (first?.kind === "markdown") { - first.text = first.text.trimStart(); - } - const last = trimmed.at(-1); - if (last?.kind === "markdown") { - last.text = last.text.trimEnd(); - } - return trimmed.filter((part) => part.kind !== "markdown" || part.text.length > 0); -} - -function renderSummaryTokens(tokens: HtmlToken[]): string { - return stripSingleHeadingWrapper(renderInlineTokens(tokens)); -} - -function stripSingleHeadingWrapper(text: string): string { - const tokens = tokenizeHtmlishMarkdown(text.trim()); - if (tokens.length < 3) { - return text; - } - - const first = tokens[0]; - const last = tokens.at(-1); - if (!isHeadingTag(first) || !isClosingTag(last, first.name)) { - return text; - } - - return renderInlineTokens(tokens.slice(1, -1)); -} - -function renderInlineTokens(tokens: HtmlToken[]): string { - let output = ""; - for (let index = 0; index < tokens.length; index += 1) { - const token = tokens[index]; - if (!token) { - continue; - } - if (token.kind === "text") { - output += token.value; - continue; - } - if (token.kind === "comment" || token.closing) { - continue; - } - - if (token.name === "br") { - output += "\n"; - continue; - } - - if (token.name === "img") { - output += renderImageToken(token); - continue; - } - - const closeIndex = token.selfClosing ? null : findMatchingClose(tokens, index, token.name); - if (closeIndex === null) { - output += renderUnknownTag(token); - continue; - } - - const children = tokens.slice(index + 1, closeIndex); - const close = tokens[closeIndex]; - const closeRaw = isClosingTag(close, token.name) ? close.raw : ``; - output += renderHtmlTag(token, children, closeRaw); - index = closeIndex; - } - - return output; -} - -function renderHtmlTag(token: HtmlTagToken, children: HtmlToken[], closeRaw: string): string { - if (token.name === "a") { - return renderLinkToken(token, children); - } - if (token.name === "sub" || isHeadingTagName(token.name)) { - return renderInlineTokens(children); - } - if (token.name === "code" && children.every((child) => child.kind === "text")) { - return `\`${renderInlineTokens(children)}\``; - } - const wrapper = MARKDOWN_TAG_WRAPPERS[token.name]; - if (wrapper) { - return `${wrapper[0]}${renderInlineTokens(children)}${wrapper[1]}`; - } - if (token.name === "table") { - return renderTableTokens(children); - } - if (token.name === "p" || token.name === "div") { - return `\n\n${renderInlineTokens(children).trim()}\n\n`; - } - return `${token.raw}${renderInlineTokens(children)}${closeRaw}`; -} - -function renderTableTokens(tokens: HtmlToken[]): string { - const rows: string[] = []; - - for (let index = 0; index < tokens.length; index += 1) { - if (!isOpenTag(tokens[index], "tr")) { - continue; - } - const closeIndex = findMatchingClose(tokens, index, "tr"); - if (closeIndex === null) { - continue; - } - const cells = renderTableCells(tokens.slice(index + 1, closeIndex)); - if (cells.length === 1) { - rows.push(`- ${cells[0]}`); - } else if (cells.length > 1) { - const label = - cells[0].startsWith("**") && cells[0].endsWith("**") ? cells[0] : `**${cells[0]}**`; - rows.push(`- ${label}: ${cells.slice(1).join(" ")}`); - } - index = closeIndex; - } - - return rows.length > 0 ? `\n${rows.join("\n")}\n` : ""; -} - -function renderTableCells(tokens: HtmlToken[]): string[] { - const cells: string[] = []; - - for (let index = 0; index < tokens.length; index += 1) { - const token = tokens[index]; - if (!isOpenTag(token, "td") && !isOpenTag(token, "th")) { - continue; - } - const closeIndex = findMatchingClose(tokens, index, token.name); - if (closeIndex === null) { - continue; - } - const cell = renderInlineTokens(tokens.slice(index + 1, closeIndex)).trim(); - if (cell) { - cells.push(cell); - } - index = closeIndex; - } - - return cells; -} - -function renderImageToken(token: HtmlTagToken): string { - const image = imageTokenToInlineImage(token, undefined); - if (!image) { - return token.raw; - } - - return `![${escapeMarkdownImageAlt(image.alt)}](${image.src})`; -} - -function renderLinkToken(token: HtmlTagToken, children: HtmlToken[]): string { - const imageOnly = getSingleImageChild(children); - if (imageOnly) { - return renderImageToken(imageOnly); - } - - const label = renderInlineTokens(children); - const href = token.attributes.href ?? ""; - if (!label || !SAFE_LINK_HREF_RE.test(href) || href === "#") { - return label; - } - - return `[${label}](${href})`; -} - -function imageTokenToInlineImage( - token: HtmlTagToken, - href: string | undefined, -): MarkdownInlineImagePart | null { - const src = token.attributes.src ?? ""; - if (!SAFE_IMAGE_SRC_RE.test(src)) { - return null; - } - - return { - kind: "inlineImage", - alt: token.attributes.alt ?? "", - src, - ...(href ? { href } : {}), - ...parseImageDimensions(token.attributes), - }; -} - -function parseImageDimensions(attributes: Record): MarkdownImageDimensions { - return { - ...parseImageDimension("width", attributes.width), - ...parseImageDimension("height", attributes.height), - }; -} - -function parseImageDimension(key: "width" | "height", value: string | undefined) { - if (!value || !/^\d+(?:\.\d+)?$/.test(value)) { - return {}; - } - - const parsed = Number(value); - return Number.isFinite(parsed) && parsed > 0 ? { [key]: parsed } : {}; -} - -function safeHref(href: string | undefined): string | undefined { - if (!href || href === "#" || !SAFE_LINK_HREF_RE.test(href)) { - return undefined; - } - return href; -} - -function getSingleImageChild(tokens: HtmlToken[]): HtmlTagToken | null { - const visible = tokens.filter((token) => token.kind !== "comment" && !isWhitespaceText(token)); - return visible.length === 1 && isOpenTag(visible[0], "img") ? visible[0] : null; -} - -function renderUnknownTag(token: HtmlTagToken): string { - if (token.name === "script" || token.name === "style") { - return ""; - } - return token.raw; -} - -function escapeMarkdownImageAlt(value: string): string { - return value.replace(/]/g, "\\]"); -} - -function findNextOpenTag(tokens: HtmlToken[], start: number, name: string): number | null { - for (let index = start; index < tokens.length; index += 1) { - if (isOpenTag(tokens[index], name)) { - return index; - } - } - return null; -} - -function findMatchingClose(tokens: HtmlToken[], openIndex: number, name: string): number | null { - let depth = 1; - for (let index = openIndex + 1; index < tokens.length; index += 1) { - const token = tokens[index]; - if (isOpenTag(token, name) && !token.selfClosing) { - depth += 1; - continue; - } - if (isClosingTag(token, name)) { - depth -= 1; - if (depth === 0) { - return index; - } - } - } - return null; -} - -function isOpenTag(token: HtmlToken | undefined, name: string): token is HtmlTagToken { - return token?.kind === "tag" && token.name === name && !token.closing; -} - -function isClosingTag(token: HtmlToken | undefined, name: string): token is HtmlTagToken { - return token?.kind === "tag" && token.name === name && token.closing; -} - -function isHeadingTag(token: HtmlToken | undefined): token is HtmlTagToken { - return token?.kind === "tag" && isHeadingTagName(token.name) && !token.closing; -} - -function isHeadingTagName(name: string): boolean { - return ( - name === "h1" || - name === "h2" || - name === "h3" || - name === "h4" || - name === "h5" || - name === "h6" - ); -} - -function isWhitespaceText(token: HtmlToken): boolean { - return token.kind === "text" && token.value.trim() === ""; -} - -function tokenizeHtmlishMarkdown(source: string): HtmlToken[] { - const protectedRanges = getProtectedMarkdownRanges(source); - const tokens: HtmlToken[] = []; - const parser = createHtmlishTokenParser(source, tokens); - let cursor = 0; - - for (const range of protectedRanges) { - parser.write(source.slice(cursor, range.start)); - tokens.push({ kind: "text", value: source.slice(range.start, range.end) }); - parser.skip(range.end - range.start); - cursor = range.end; - } - - parser.write(source.slice(cursor)); - parser.end(); - return tokens; -} - -function createHtmlishTokenParser(source: string, tokens: HtmlToken[]): HtmlishTokenParser { - let stripNextLeadingLineBreak = false; - let skippedLength = 0; - let writtenLength = 0; - let chunkStart = 0; - let parser!: Parser; - // Unknown tags render as text, so keep them as written: the parser lowercases names - // and rewrites attributes. A tag split by a protected range has no single slice. - const readRawTag = (): string | null => - parser.startIndex < chunkStart - ? null - : source.slice(parser.startIndex + skippedLength, parser.endIndex + 1 + skippedLength); - parser = new Parser( - { - onopentag(name, attributes, isImplied) { - if (isImplied) { - return; - } - const selfClosing = VOID_HTML_TAGS.has(name); - tokens.push({ - kind: "tag", - name, - closing: false, - selfClosing, - attributes, - raw: readRawTag() ?? renderStartTag(name, attributes), - }); - }, - onclosetag(name, isImplied) { - if (isImplied || VOID_HTML_TAGS.has(name)) { - return; - } - tokens.push({ - kind: "tag", - name, - closing: true, - selfClosing: false, - attributes: {}, - raw: readRawTag() ?? ``, - }); - }, - ontext(value) { - if (stripNextLeadingLineBreak) { - stripNextLeadingLineBreak = false; - value = stripLeadingLineBreak(value); - } - if (!value) { - return; - } - tokens.push({ kind: "text", value }); - }, - oncomment() { - stripNextLeadingLineBreak = isLineStart(source, parser.startIndex + skippedLength); - tokens.push({ kind: "comment" }); - }, - }, - { - decodeEntities: false, - lowerCaseAttributeNames: true, - lowerCaseTags: true, - recognizeSelfClosing: true, - }, - ); - return { - write(chunk) { - chunkStart = writtenLength; - writtenLength += chunk.length; - parser.write(chunk); - }, - skip(length) { - skippedLength += length; - }, - end() { - parser.end(); - }, - }; -} - -function isLineStart(source: string, index: number): boolean { - return index === 0 || source[index - 1] === "\n" || source[index - 1] === "\r"; -} - -function stripLeadingLineBreak(value: string): string { - if (value.startsWith("\r\n")) { - return value.slice(2); - } - if (value.startsWith("\n") || value.startsWith("\r")) { - return value.slice(1); - } - return value; -} - -function renderStartTag(name: string, attributes: Record): string { - const renderedAttributes = Object.entries(attributes) - .map(([key, value]) => (value === "" ? ` ${key}` : ` ${key}="${escapeAttribute(value)}"`)) - .join(""); - return `<${name}${renderedAttributes}>`; -} - -function escapeAttribute(value: string): string { - return value.replaceAll("&", "&").replaceAll('"', """); -} - -function getProtectedMarkdownRanges(source: string): ProtectedMarkdownRange[] { - const fencedRanges = getFencedCodeRanges(source); - const codeRanges = mergeProtectedRanges([ - ...fencedRanges, - ...getInlineCodeRanges(source, fencedRanges), - ]); - return mergeProtectedRanges([...codeRanges, ...getLiteralAngleBracketRanges(source, codeRanges)]); -} - -/** - * htmlparser2 reads any `<` followed by a letter as a tag and keeps collecting attributes up - * to the next `>`, so `1", index + 2); - commentClose = close === -1 ? source.length : close; - } - htmlEnd = commentClose === source.length ? null : commentClose + 3; - } else { - HTML_TAG_RE.lastIndex = index; - htmlEnd = HTML_TAG_RE.test(source) ? HTML_TAG_RE.lastIndex : null; - } - - if (htmlEnd === null) { - ranges.push({ start: index, end: index + 1 }); - htmlEnd = index + 1; - } - index = source.indexOf("<", htmlEnd); - } - - return ranges; -} - -function getFencedCodeRanges(source: string): ProtectedMarkdownRange[] { - const ranges: ProtectedMarkdownRange[] = []; - FENCE_LINE_RE.lastIndex = 0; - - while (true) { - const open = FENCE_LINE_RE.exec(source); - if (!open) { - return ranges; - } - - const marker = open[1]; - if (!marker) { - continue; - } - - const close = findClosingFence(source, FENCE_LINE_RE.lastIndex, marker); - if (!close) { - ranges.push({ start: open.index, end: source.length }); - return ranges; - } - - ranges.push({ start: open.index, end: close.end }); - FENCE_LINE_RE.lastIndex = close.end; - } -} - -function findClosingFence( - source: string, - start: number, - marker: string, -): MarkdownDelimiterMatch | null { - const closeRe = new RegExp( - `^ {0,3}[${marker[0]}]{${marker.length},}[^\\n\\r]*(?:\\r?\\n|$)`, - "gm", - ); - closeRe.lastIndex = start; - const close = closeRe.exec(source); - return close ? { index: close.index, end: closeRe.lastIndex } : null; -} - -function getInlineCodeRanges( - source: string, - fencedRanges: ProtectedMarkdownRange[], -): ProtectedMarkdownRange[] { - const ranges: ProtectedMarkdownRange[] = []; - BACKTICK_RUN_RE.lastIndex = 0; - - while (true) { - const open = BACKTICK_RUN_RE.exec(source); - if (!open) { - return ranges; - } - if (isProtectedIndex(open.index, fencedRanges)) { - continue; - } - - const marker = open[0]; - const afterOpen = BACKTICK_RUN_RE.lastIndex; - const close = findClosingBacktickRun(source, afterOpen, marker, fencedRanges); - if (!close) { - // Unmatched backtick run — skip past it so the loop doesn't restart from 0. - // findClosingBacktickRun exhausts the global regex, which resets lastIndex - // to 0 when exec() returns null (ECMAScript §22.2.7.2). - BACKTICK_RUN_RE.lastIndex = afterOpen; - continue; - } - - ranges.push({ start: open.index, end: close.end }); - BACKTICK_RUN_RE.lastIndex = close.end; - } -} - -function findClosingBacktickRun( - source: string, - start: number, - marker: string, - fencedRanges: ProtectedMarkdownRange[], -): MarkdownDelimiterMatch | null { - BACKTICK_RUN_RE.lastIndex = start; - - while (true) { - const close = BACKTICK_RUN_RE.exec(source); - if (!close) { - return null; - } - if (close[0] === marker && !isProtectedIndex(close.index, fencedRanges)) { - return { index: close.index, end: BACKTICK_RUN_RE.lastIndex }; - } - } -} - -function mergeProtectedRanges(ranges: ProtectedMarkdownRange[]): ProtectedMarkdownRange[] { - const sorted = [...ranges].sort((a, b) => a.start - b.start); - const merged: ProtectedMarkdownRange[] = []; - - for (const range of sorted) { - const previous = merged.at(-1); - if (!previous || range.start > previous.end) { - merged.push({ ...range }); - continue; - } - previous.end = Math.max(previous.end, range.end); - } - - return merged; -} - -function isProtectedIndex(index: number, ranges: ProtectedMarkdownRange[]): boolean { - return ranges.some((range) => index >= range.start && index < range.end); -} - -export function normalizeHtmlishMarkdown(source: string): string { - return renderInlineTokens(tokenizeHtmlishMarkdown(source)); -} diff --git a/packages/app/src/file-pane/markdown-preview/image-source.test.ts b/packages/app/src/components/markdown/image-source.test.ts similarity index 100% rename from packages/app/src/file-pane/markdown-preview/image-source.test.ts rename to packages/app/src/components/markdown/image-source.test.ts diff --git a/packages/app/src/file-pane/markdown-preview/image-source.ts b/packages/app/src/components/markdown/image-source.ts similarity index 100% rename from packages/app/src/file-pane/markdown-preview/image-source.ts rename to packages/app/src/components/markdown/image-source.ts diff --git a/packages/app/src/components/markdown/images.ts b/packages/app/src/components/markdown/images.ts new file mode 100644 index 0000000000..703aa2dbed --- /dev/null +++ b/packages/app/src/components/markdown/images.ts @@ -0,0 +1,95 @@ +import { useMemo, useEffect } from "react"; +import { useFetchQueries } from "@/data/query"; +import { createAssistantImageFileAcquisition } from "@/assistant-image/file-acquisition"; +import { + persistAttachmentFromBytes, + resolveAttachmentPreviewUrl, + releaseAttachmentPreviewUrl, +} from "@/attachments/service"; +import { createAssistantImageAcquisitionCache } from "@/assistant-image/acquisition-cache"; +import { retainAttachmentForGarbageCollection } from "@/attachments/gc-retention"; +import { resolveAssistantImageSource } from "@/utils/assistant-image-source"; +import type { DaemonClient } from "@getpaseo/client/internal/daemon-client"; +import type { AttachmentMetadata } from "@/attachments/types"; +import { resolveMarkdownPreviewImageSource } from "./image-source"; +import { prepareMarkdownSource } from "./source"; + +const urls = createAssistantImageAcquisitionCache<{ uri: string; attachment: AttachmentMetadata }>({ + capacity: 500, + onRetain: ({ uri, attachment }) => { + const release = retainAttachmentForGarbageCollection(attachment.id); + return () => { + release(); + void releaseAttachmentPreviewUrl({ attachment, url: uri }); + }; + }, +}); + +export interface MarkdownImageContext { + client?: Pick | null; + workspaceRoot?: string; + serverId?: string; + filePath?: string; +} + +export function useMarkdownSource(text: string, context: MarkdownImageContext) { + const prepared = useMemo(() => prepareMarkdownSource(text), [text]); + const sources = [...new Set(prepared.images.map((image) => image.source))]; + const acquisitions = sources.map((source) => { + const resolved = context.filePath + ? resolveMarkdownPreviewImageSource({ + source, + markdownPath: context.filePath, + workspaceRoot: context.workspaceRoot, + }) + : source; + return createAssistantImageFileAcquisition({ + port: context.client + ? { + readFile: (cwd, path) => context.client!.readFile(cwd, path), + persist: persistAttachmentFromBytes, + } + : null, + resolution: resolved + ? resolveAssistantImageSource({ source: resolved, workspaceRoot: context.workspaceRoot }) + : null, + serverId: context.serverId, + occurrenceKey: context.filePath ?? context.workspaceRoot ?? "markdown", + unavailableMessage: "Image unavailable", + }); + }); + const results = useFetchQueries( + acquisitions.map((acquisition, index) => ({ + queryKey: ["markdownImage", acquisition?.key ?? sources[index]], + enabled: acquisition !== null, + immutableWhen: () => true, + dataShape: "value", + gcTime: 0, + queryFn: async () => { + if (!acquisition) throw new Error("No image acquisition"); + return urls.acquire(acquisition.key, async () => { + const attachment = await acquisition.locate(); + const uri = await resolveAttachmentPreviewUrl(attachment); + return { attachment, uri }; + }); + }, + })), + ); + useEffect(() => { + const releases = results.flatMap((result, index) => { + const acquisition = acquisitions[index]; + if (!result.data || !acquisition) return []; + const value = result.data; + return [urls.acquireRetained(acquisition.key, async () => value).release]; + }); + return () => releases.forEach((release) => release()); + }, [results, acquisitions]); + const resolved = new Map(); + results.forEach((result, index) => { + if (result.data) resolved.set(sources[index]!, result.data.uri); + }); + return { + markdown: prepared.render(resolved), + imageSources: Object.fromEntries([...resolved].map(([source, url]) => [url, source])), + }; +} diff --git a/packages/app/src/components/markdown/inline-image-size.ts b/packages/app/src/components/markdown/inline-image-size.ts deleted file mode 100644 index 8f83f80fcc..0000000000 --- a/packages/app/src/components/markdown/inline-image-size.ts +++ /dev/null @@ -1,62 +0,0 @@ -export interface InlineImageDimensions { - width: number; - height: number; -} - -export interface InlineImageExplicitDimensions { - width?: number; - height?: number; -} - -const INLINE_IMAGE_MAX_WIDTH = 240; -const INLINE_IMAGE_MAX_HEIGHT = 160; - -export function resolveInlineImageSize(input: { - explicit: InlineImageExplicitDimensions; - natural: InlineImageDimensions | null; -}): InlineImageDimensions { - const dimensions = resolveInlineImageDimensions(input); - const scale = Math.min( - 1, - INLINE_IMAGE_MAX_WIDTH / dimensions.width, - INLINE_IMAGE_MAX_HEIGHT / dimensions.height, - ); - - return { - width: Math.round(dimensions.width * scale), - height: Math.round(dimensions.height * scale), - }; -} - -function resolveInlineImageDimensions(input: { - explicit: InlineImageExplicitDimensions; - natural: InlineImageDimensions | null; -}): InlineImageDimensions { - if (input.explicit.width && input.explicit.height) { - return { width: input.explicit.width, height: input.explicit.height }; - } - - if (input.explicit.width && input.natural) { - return { - width: input.explicit.width, - height: input.explicit.width * (input.natural.height / input.natural.width), - }; - } - - if (input.explicit.height && input.natural) { - return { - width: input.explicit.height * (input.natural.width / input.natural.height), - height: input.explicit.height, - }; - } - - if (input.explicit.width) { - return { width: input.explicit.width, height: input.explicit.width }; - } - - if (input.explicit.height) { - return { width: input.explicit.height, height: input.explicit.height }; - } - - return input.natural ?? { width: INLINE_IMAGE_MAX_WIDTH, height: INLINE_IMAGE_MAX_HEIGHT }; -} diff --git a/packages/app/src/components/markdown/link-children.ts b/packages/app/src/components/markdown/link-children.ts deleted file mode 100644 index 9a81042c86..0000000000 --- a/packages/app/src/components/markdown/link-children.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { Children, cloneElement, isValidElement, type ReactNode } from "react"; -import type { StyleProp, TextStyle } from "react-native"; - -export function colorMarkdownLinkChildren( - children: ReactNode, - color: TextStyle["color"], -): ReactNode { - return Children.map(children, (child) => { - if (!isValidElement<{ style?: StyleProp }>(child)) return child; - return cloneElement(child, { style: [child.props.style, { color }] }); - }); -} - -const MARKDOWN_LINK_HOVER_STYLE: TextStyle = { textDecorationLine: "underline" }; - -export function markdownLinkTextStyle( - style: StyleProp, - hovered: boolean, -): StyleProp { - return [style, hovered && MARKDOWN_LINK_HOVER_STYLE]; -} diff --git a/packages/app/src/components/markdown/link-text.tsx b/packages/app/src/components/markdown/link-text.tsx deleted file mode 100644 index 18b1b6c93b..0000000000 --- a/packages/app/src/components/markdown/link-text.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import { useMemo, useState, type ReactNode } from "react"; -import { Pressable, Text, type StyleProp, type TextStyle } from "react-native"; -import { useStableEvent } from "@/hooks/use-stable-event"; -import { markdownLinkTextStyle } from "./link-children"; - -interface MarkdownLinkTextProps { - style: StyleProp; - dataSet?: Record; - onPress(): void; - onHoverIn?(): void; - children?: ReactNode; -} - -export function MarkdownLinkText({ - style, - dataSet, - onPress, - onHoverIn, - children, -}: MarkdownLinkTextProps) { - const [hovered, setHovered] = useState(false); - const handleHoverIn = useStableEvent(() => { - setHovered(true); - onHoverIn?.(); - }); - const handleHoverOut = useStableEvent(() => setHovered(false)); - const textStyle = useMemo(() => markdownLinkTextStyle(style, hovered), [hovered, style]); - - return ( - - - {children} - - - ); -} diff --git a/packages/app/src/components/markdown/part-groups.test.ts b/packages/app/src/components/markdown/part-groups.test.ts deleted file mode 100644 index 3b2b54e044..0000000000 --- a/packages/app/src/components/markdown/part-groups.test.ts +++ /dev/null @@ -1,92 +0,0 @@ -import { describe, expect, it } from "vitest"; -import type { MarkdownDisplayPart } from "./html-ish"; -import { groupMarkdownParts } from "./part-groups"; - -describe("groupMarkdownParts", () => { - const flowImage: MarkdownDisplayPart = { - kind: "inlineImage", - alt: "Priority", - src: "https://example.com/priority.svg", - flowsWithText: true, - }; - - it("flows an image with the lead paragraph and keeps later paragraphs full width", () => { - expect( - groupMarkdownParts([ - flowImage, - { kind: "markdown", text: " **Title line**\n\nSecond paragraph." }, - ]), - ).toEqual([ - { - kind: "imageText", - images: [flowImage], - lead: "**Title line**", - rest: "Second paragraph.", - }, - ]); - }); - - it("flows an image with a single-paragraph text", () => { - expect(groupMarkdownParts([flowImage, { kind: "markdown", text: " Only line" }])).toEqual([ - { kind: "imageText", images: [flowImage], lead: "Only line", rest: "" }, - ]); - }); - - it("groups two consecutive flowing images with the lead paragraph", () => { - const secondImage: MarkdownDisplayPart = { - kind: "inlineImage", - alt: "Severity", - src: "https://example.com/severity.svg", - flowsWithText: true, - }; - - expect( - groupMarkdownParts([ - flowImage, - { kind: "markdown", text: " " }, - secondImage, - { kind: "markdown", text: " **Title text here**\n\nBody paragraph." }, - ]), - ).toEqual([ - { - kind: "imageText", - images: [flowImage, secondImage], - lead: "**Title text here**", - rest: "Body paragraph.", - }, - ]); - }); - - it("keeps standalone images as plain parts", () => { - const image: MarkdownDisplayPart = { - kind: "inlineImage", - alt: "Shot", - src: "https://example.com/shot.png", - }; - - expect(groupMarkdownParts([image, { kind: "markdown", text: "\n\nCaption" }])).toEqual([ - { kind: "part", part: image }, - { kind: "part", part: { kind: "markdown", text: "\n\nCaption" } }, - ]); - }); - - it("keeps a flowing image alone when no markdown follows", () => { - expect(groupMarkdownParts([flowImage])).toEqual([{ kind: "part", part: flowImage }]); - }); - - it("keeps a flowing image alone when the following markdown starts with a blank line", () => { - expect(groupMarkdownParts([flowImage, { kind: "markdown", text: "\n\nBelow" }])).toEqual([ - { kind: "part", part: flowImage }, - { kind: "part", part: { kind: "markdown", text: "\n\nBelow" } }, - ]); - }); - - it("passes other part kinds through unchanged", () => { - const details: MarkdownDisplayPart = { kind: "details", summary: "More", body: "Body" }; - - expect(groupMarkdownParts([{ kind: "markdown", text: "Intro" }, details])).toEqual([ - { kind: "part", part: { kind: "markdown", text: "Intro" } }, - { kind: "part", part: details }, - ]); - }); -}); diff --git a/packages/app/src/components/markdown/part-groups.ts b/packages/app/src/components/markdown/part-groups.ts deleted file mode 100644 index 016152fb00..0000000000 --- a/packages/app/src/components/markdown/part-groups.ts +++ /dev/null @@ -1,89 +0,0 @@ -import type { MarkdownDisplayPart, MarkdownInlineImagePart } from "./html-ish"; - -export type MarkdownPartGroup = - | { kind: "part"; part: MarkdownDisplayPart } - | { kind: "imageText"; images: MarkdownInlineImagePart[]; lead: string; rest: string }; - -/** - * Pairs a run of inline images that flow with text (small status images at the - * start of a line, GitHub-style) with the lead paragraph of the following - * markdown part, so the renderer can lay them out side by side instead of - * stacking the images as their own blocks. - * - * Between consecutive flowing images the parser may emit whitespace-only - * markdown parts — these are consumed and discarded when building the group. - */ -export function groupMarkdownParts(parts: readonly MarkdownDisplayPart[]): MarkdownPartGroup[] { - const groups: MarkdownPartGroup[] = []; - let index = 0; - - while (index < parts.length) { - const part = parts[index]; - - if (part.kind === "inlineImage" && part.flowsWithText) { - // Collect the maximal run of flowing images, skipping whitespace-only markdown between them. - const images: MarkdownInlineImagePart[] = [part]; - let lookahead = index + 1; - - while (lookahead < parts.length) { - const candidate = parts[lookahead]; - if (candidate === undefined) { - break; - } - if (isWhitespaceOnlyMarkdown(candidate)) { - lookahead += 1; - continue; - } - if (candidate.kind === "inlineImage" && candidate.flowsWithText) { - images.push(candidate); - lookahead += 1; - continue; - } - break; - } - - const trailing = parts[lookahead]; - if (trailing?.kind === "markdown") { - const { lead, rest } = splitLeadParagraph(trailing.text); - if (lead) { - groups.push({ kind: "imageText", images, lead, rest }); - index = lookahead + 1; - continue; - } - } - - // No usable lead — emit the images (and any whitespace parts) as plain parts. - for (let i = index; i < lookahead; i += 1) { - const p = parts[i]; - if (p !== undefined) { - groups.push({ kind: "part", part: p }); - } - } - index = lookahead; - continue; - } - - groups.push({ kind: "part", part }); - index += 1; - } - - return groups; -} - -function isWhitespaceOnlyMarkdown(part: MarkdownDisplayPart): boolean { - return part.kind === "markdown" && part.text.trim() === ""; -} - -function splitLeadParagraph(text: string): { lead: string; rest: string } { - if (/^[ \t]*\r?\n/.test(text)) { - return { lead: "", rest: text }; - } - const boundary = /\r?\n[ \t]*\r?\n/.exec(text); - if (!boundary) { - return { lead: text.trim(), rest: "" }; - } - return { - lead: text.slice(0, boundary.index).trim(), - rest: text.slice(boundary.index + boundary[0].length), - }; -} diff --git a/packages/app/src/components/markdown/renderer.test.ts b/packages/app/src/components/markdown/renderer.test.ts index 2dcab5c327..a28a959ae0 100644 --- a/packages/app/src/components/markdown/renderer.test.ts +++ b/packages/app/src/components/markdown/renderer.test.ts @@ -1,99 +1,38 @@ -/** - * @vitest-environment jsdom - */ -import * as React from "react"; -import { createElement, type ReactNode } from "react"; -import { fireEvent, render } from "@testing-library/react"; -import { Text, type StyleProp, type TextStyle } from "react-native"; -import { describe, expect, it, vi } from "vitest"; -import { resolveInlineImageSize } from "./inline-image-size"; -import { colorMarkdownLinkChildren } from "./link-children"; -import { MarkdownLinkText } from "./link-text"; +import { describe, expect, it } from "vitest"; +import { prepareMarkdownSource } from "./source"; -vi.stubGlobal("React", React); - -vi.mock("react-native", () => ({ - Pressable: ({ - accessibilityRole, - children, - onHoverIn, - onHoverOut, - onPress, - }: { - accessibilityRole?: string; - children?: ReactNode; - onHoverIn?(): void; - onHoverOut?(): void; - onPress?(): void; - }) => - createElement( - "div", - { - role: accessibilityRole, - onClick: onPress, - onMouseEnter: onHoverIn, - onMouseLeave: onHoverOut, - }, - children, - ), - Text: ({ children, style }: { children?: ReactNode; style?: StyleProp }) => - createElement("span", { style: flattenStyle(style) }, children), -})); - -function flattenStyle(style: StyleProp): TextStyle { - return Object.assign({}, ...(Array.isArray(style) ? style.filter(Boolean) : [style])); -} - -describe("resolveInlineImageSize", () => { - it("respects a one-sided explicit width using natural aspect ratio", () => { - expect( - resolveInlineImageSize({ explicit: { width: 18 }, natural: { width: 90, height: 45 } }), - ).toEqual({ - width: 18, - height: 9, - }); +describe("shared Markdown source adaptation", () => { + it("keeps paragraphs, fences and quotes together for the library", () => { + const source = "First paragraph.\n\nSecond paragraph.\n\n```ts\nconst n = 1;\n```\n\n> Quote"; + expect(prepareMarkdownSource(source).render()).toBe(source); }); - - it("respects a one-sided explicit height using natural aspect ratio", () => { - expect( - resolveInlineImageSize({ explicit: { height: 18 }, natural: { width: 90, height: 45 } }), - ).toEqual({ - width: 36, - height: 18, - }); + it("resolves workspace images without rewriting code or remote image URLs", () => { + const source = + "![Local](assets/a.png)\n\n![Remote](https://example.com/a.png)\n\n```md\n![Code](assets/a.png)\n```"; + const prepared = prepareMarkdownSource(source); + expect(prepared.images.map((image) => image.source)).toEqual([ + "assets/a.png", + "https://example.com/a.png", + ]); + expect(prepared.render(new Map([["assets/a.png", "blob:cached"]]))).toBe( + "![Local]()\n\n![Remote](https://example.com/a.png)\n\n```md\n![Code](assets/a.png)\n```", + ); }); - - it("reserves a capped 3:2 placeholder when no dimensions are known", () => { - expect(resolveInlineImageSize({ explicit: {}, natural: null })).toEqual({ - width: 240, - height: 160, - }); + it("resolves reference images and images nested in lists and quotes", () => { + const prepared = prepareMarkdownSource("> - ![Image][picture]\n\n[picture]: ../a.png"); + expect(prepared.render(new Map([["../a.png", "file:///cache/a.png"]]))).toBe( + "> - ![Image]()\n\n[picture]: ../a.png", + ); + }); + it("escapes spoiler delimiters only in prose", () => { + expect(prepareMarkdownSource("Show ||words|| and `||code||`.").render()).toBe( + "Show \\|\\|words\\|\\| and `||code||`.", + ); }); }); -describe("shared Markdown links", () => { - it("renders accent text and underlines it while hovered", () => { - const onPress = vi.fn(); - const children = colorMarkdownLinkChildren( - createElement(Text, { style: { color: "white" } }, "Paseo"), - "rgb(0, 122, 255)", - ); - const view = render( - createElement(MarkdownLinkText, { style: { color: "rgb(0, 122, 255)" }, onPress }, children), - ); - const link = view.getByRole("link"); - const linkText = link.firstElementChild as HTMLElement; - - expect((view.getByText("Paseo") as HTMLElement).style.color).toBe("rgb(0, 122, 255)"); - expect(linkText.style.textDecorationLine).toBe(""); - - fireEvent.mouseEnter(link); - expect(linkText.style.textDecorationLine).toBe("underline"); - - fireEvent.mouseLeave(link); - expect(linkText.style.textDecorationLine).toBe(""); - - fireEvent.click(link); - expect(onPress).toHaveBeenCalledOnce(); - }); +it("preserves named and numeric entities as literal rendered text", () => { + expect(prepareMarkdownSource("AT&T 😀 # heading and `&`").render()).toBe( + "AT\\&T 😀 \\# heading and `&`", + ); }); diff --git a/packages/app/src/components/markdown/renderer.tsx b/packages/app/src/components/markdown/renderer.tsx index 83380ea919..e5fb52fbb1 100644 --- a/packages/app/src/components/markdown/renderer.tsx +++ b/packages/app/src/components/markdown/renderer.tsx @@ -1,790 +1,195 @@ -import React, { - useCallback, - useEffect, - useMemo, - useState, - type ComponentType, - type ReactNode, -} from "react"; +import { View } from "react-native"; +import { markdownCopyImageSourcesDataSet } from "@/assistant-selection-copy/markup"; +import { memo, useMemo, useState } from "react"; import { - Image, - Pressable, - Text, - View, - type TextProps, - type TextStyle, - type ViewStyle, -} from "react-native"; -import { ChevronDown, ChevronRight } from "lucide-react-native"; -import Markdown, { - MarkdownIt, - type ASTNode, - type RenderRules, -} from "react-native-markdown-display"; -import { StyleSheet, withUnistyles } from "react-native-unistyles"; + EnrichedMarkdownText, + type MarkdownStyle, + type CodeBlockRendererProps, +} from "react-native-enriched-markdown"; +import { withUnistyles } from "react-native-unistyles"; +import { + useAssistantFileLinkActions, + useAssistantFileLinkEnvironment, +} from "@/assistant-file-links"; +import { useStableEvent } from "@/hooks/use-stable-event"; import { HighlightedCodeBlock } from "@/components/highlighted-code-block"; -import { MarkdownFenceBlock } from "@/components/markdown/fence"; -import { MarkdownParagraphView, MarkdownTextSpan } from "@/components/markdown-text"; -import { MarkdownTableCellText } from "@/components/markdown-text-selection"; -import { getMarkdownListMarker, getMarkdownListSpacing } from "@/utils/markdown-list"; -import { markdownNodeContainsType } from "@/utils/markdown-ast"; -import { createMarkdownParser } from "@/utils/markdown-parser"; -import { createCompactMarkdownStyles, createMarkdownStyles } from "@/styles/markdown-styles"; +import { AttachmentLightbox, type ImageLightboxSource } from "@/components/attachment-lightbox"; import type { Theme } from "@/styles/theme"; -import { openExternalUrl } from "@/utils/open-external-url"; -import { isNative } from "@/constants/platform"; -import { - splitHtmlishMarkdown, - type MarkdownDisplayPart, - type MarkdownInlineImagePart, -} from "./html-ish"; -import { resolveInlineImageSize, type InlineImageDimensions } from "./inline-image-size"; -import { - getAssistantImageMetadata, - setAssistantImageMetadata, -} from "@/utils/assistant-image-metadata"; -import { groupMarkdownParts, type MarkdownPartGroup } from "./part-groups"; -import { colorMarkdownLinkChildren } from "./link-children"; -import { MarkdownLinkText } from "./link-text"; +import { isWeb } from "@/constants/platform"; +import { useMarkdownSource, type MarkdownImageContext } from "./images"; -export type MarkdownStyles = Record; - -interface MarkdownWithStableRendererProps { - children: ReactNode; - style: ReturnType | ReturnType; - rules?: RenderRules; - markdownit?: ReturnType; - onLinkPress?: (url: string) => boolean; - allowedImageHandlers?: readonly string[]; - topLevelMaxExceededItem?: ReactNode; -} - -const MarkdownWithStableRenderer = Markdown as ComponentType; -const ThemedMarkdown = withUnistyles(MarkdownWithStableRenderer); - -function markdownStyleMapping(theme: Theme): Partial { - return { style: createMarkdownStyles(theme) }; -} - -function compactMarkdownStyleMapping(theme: Theme): Partial { - return { style: createCompactMarkdownStyles(theme) }; -} - -// Serves PR comment bodies and the markdown file preview; agent chat passes its -// own parser. The preview has to show the bytes on disk, so no typographer. -const defaultMarkdownParser = createMarkdownParser({ linkify: true }); -const EMPTY_TEXT_STYLE: TextStyle = {}; -const MARKDOWN_LIST_ITEM_CONTENT_FLEX: ViewStyle = { flex: 1, flexShrink: 1, minWidth: 0 }; -export interface MarkdownRendererProps { - text: string; - compact?: boolean; - rules?: RenderRules; - markdownit?: ReturnType; - onLinkPress?: (url: string) => boolean; - allowedImageHandlers?: readonly string[]; - topLevelMaxExceededItem?: ReactNode; - enableHtmlish?: boolean; -} - -export function MarkdownRenderer({ - text, - compact = false, - rules, - markdownit = defaultMarkdownParser, - onLinkPress, - allowedImageHandlers, - topLevelMaxExceededItem, - enableHtmlish = true, -}: MarkdownRendererProps) { - const markdownRules = useMemo(() => rules ?? createSharedMarkdownRules(), [rules]); - const parts = useMemo( - () => (enableHtmlish ? splitHtmlishMarkdown(text) : [{ kind: "markdown" as const, text }]), - [enableHtmlish, text], - ); - const rendererProps = useMemo( - () => ({ - compact, - rules: markdownRules, - markdownit, - onLinkPress, - allowedImageHandlers, - topLevelMaxExceededItem, - }), - [ - allowedImageHandlers, - compact, - markdownRules, - markdownit, - onLinkPress, - topLevelMaxExceededItem, - ], - ); - - return ; -} - -type MarkdownPartRendererProps = Omit & { - rules: RenderRules; +const ThemedMarkdown = withUnistyles(EnrichedMarkdownText); +const MD4C_FLAGS = { latexMath: false, hardSoftBreaks: true, admonitions: true }; +const FILE_PATTERN = + /(?:https?:\/\/[^\s<>]+|(?:\.{0,2}\/|~\/)?[\w@.-]+(?:\/[\w@.-]+)+(?::\d+(?::\d+)?)?|[\w@-]+\.[a-zA-Z][\w-]*(?::\d+(?::\d+)?)?)/g; +const LINK_RECOGNITION = { + text: FILE_PATTERN, + inlineCode: new RegExp(`^(?:${FILE_PATTERN.source})$`), }; -function MarkdownPartList({ - parts, - rendererProps, -}: { - parts: MarkdownDisplayPart[]; - rendererProps: MarkdownPartRendererProps; -}) { - const keyedGroups = useMemo(() => keyMarkdownGroups(groupMarkdownParts(parts)), [parts]); - return ( - <> - {keyedGroups.map(({ key, group }) => - group.kind === "part" ? ( - - ) : ( - - ), - )} - - ); -} - -function keyMarkdownGroups( - groups: MarkdownPartGroup[], -): { key: string; group: MarkdownPartGroup }[] { - return groups.map((group, index) => ({ key: `${group.kind}:${index}`, group })); -} - -function MarkdownPart({ - part, - rendererProps, -}: { - part: MarkdownDisplayPart; - rendererProps: MarkdownPartRendererProps; -}) { - if (part.kind === "details") { - return ; - } - - if (part.kind === "inlineImage") { - return ; - } - - if (part.text.length === 0) { - return null; - } - - return ; -} - -function MarkdownFragment({ - text, - compact, - rules, - markdownit, - onLinkPress, - allowedImageHandlers, - topLevelMaxExceededItem, -}: MarkdownRendererProps & { rules: RenderRules }) { - const uniProps = compact ? compactMarkdownStyleMapping : markdownStyleMapping; - return ( - - {text} - - ); -} - -function useNaturalImageDimensions(part: MarkdownInlineImagePart): { - natural: InlineImageDimensions | null; - failed: boolean; - setFailed: (failed: boolean) => void; -} { - const cached = useMemo(() => getAssistantImageMetadata({ source: part.src }), [part.src]); - const [resolved, setResolved] = useState<{ - source: string; - dimensions: InlineImageDimensions; - } | null>(null); - const natural = resolved?.source === part.src ? resolved.dimensions : cached; - const [failed, setFailed] = useState(false); - - useEffect(() => { - if ((part.width && part.height) || cached) { - return; - } - - let cancelled = false; - Image.getSize( - part.src, - (width, height) => { - setAssistantImageMetadata({ source: part.src }, { width, height }); - if (!cancelled) { - setResolved({ source: part.src, dimensions: { width, height } }); - } - }, - () => { - if (!cancelled) { - setFailed(true); - } - }, - ); - - return () => { - cancelled = true; - }; - }, [cached, part.height, part.src, part.width]); - - return { natural, failed, setFailed }; -} - -function MarkdownInlineImage({ - part, - onLinkPress, -}: { - part: Extract; - onLinkPress?: (url: string) => boolean; -}) { - const { natural: naturalDimensions } = useNaturalImageDimensions(part); - const explicitDimensions = useMemo( - () => ({ width: part.width, height: part.height }), - [part.height, part.width], - ); - const handlePress = useCallback(() => { - if (!part.href) return; - if (onLinkPress?.(part.href) === false) return; - void openExternalUrl(part.href); - }, [onLinkPress, part.href]); - const source = useMemo(() => ({ uri: part.src }), [part.src]); - const imageSize = useMemo( - () => resolveInlineImageSize({ explicit: explicitDimensions, natural: naturalDimensions }), - [explicitDimensions, naturalDimensions], - ); - const imageStyle = useMemo(() => [detailsStyles.inlineImage, imageSize], [imageSize]); - - const image = ( - - ); - - if (!part.href) { - return {image}; - } - - return ( - - {image} - - ); -} - -const FLOW_IMAGE_MAX_HEIGHT = 18; - -function MarkdownFlowImage({ - part, - onLinkPress, -}: { - part: MarkdownInlineImagePart; - onLinkPress?: (url: string) => boolean; -}) { - const { natural, failed, setFailed } = useNaturalImageDimensions(part); - const handlePress = useCallback(() => { - if (!part.href) return; - if (onLinkPress?.(part.href) === false) return; - void openExternalUrl(part.href); - }, [onLinkPress, part.href]); - const handleError = useCallback(() => setFailed(true), [setFailed]); - const source = useMemo(() => ({ uri: part.src }), [part.src]); - const imageSize = useMemo(() => { - const size = resolveInlineImageSize({ - explicit: { width: part.width, height: part.height }, - natural, - }); - const scale = Math.min(1, FLOW_IMAGE_MAX_HEIGHT / size.height); - return { width: Math.round(size.width * scale), height: Math.round(size.height * scale) }; - }, [natural, part.height, part.width]); - const imageStyle = useMemo(() => [detailsStyles.flowImage, imageSize], [imageSize]); - - if (failed) { - if (!part.alt) { - return null; - } - return ( - - {part.alt} - - ); - } - - const image = ( - - ); - - if (!part.href) { - return image; - } - - return ( - - {image} - - ); -} - -function MarkdownImageTextGroup({ - group, - rendererProps, -}: { - group: Extract; - rendererProps: MarkdownPartRendererProps; -}) { - return ( - <> - - {group.images.map((image) => ( - - ))} - - - - - {group.rest ? : null} - - ); -} - -function MarkdownDetails({ - part, - rendererProps, -}: { - part: Extract; - rendererProps: MarkdownPartRendererProps; -}) { - const [open, setOpen] = useState(false); - const toggle = useCallback(() => setOpen((current) => !current), []); - const bodyParts = useMemo( - () => part.bodyParts ?? [{ kind: "markdown" as const, text: part.body }], - [part.body, part.bodyParts], - ); - return ( - - - {open ? ( - - ) : ( - - )} - {part.summary} - - {open ? ( - - - - ) : null} - - ); -} - -interface MarkdownInheritedTextProps { - inheritedStyles: TextStyle; - textStyle: TextStyle; - style?: TextStyle; - monoSurface?: boolean; - onPress?: TextProps["onPress"]; - accessibilityRole?: TextProps["accessibilityRole"]; - children: ReactNode; -} - -export function MarkdownInheritedText({ - inheritedStyles, - textStyle, - style: overrideStyle, - monoSurface, - onPress, - accessibilityRole, - children, -}: MarkdownInheritedTextProps) { - const style = useMemo( - () => [inheritedStyles, textStyle, overrideStyle], - [inheritedStyles, textStyle, overrideStyle], - ); - return ( - - {children} - - ); -} - -interface MarkdownListItemContentProps { - contentStyle: ViewStyle; - children: ReactNode; -} - -function MarkdownListItemContent({ contentStyle, children }: MarkdownListItemContentProps) { - const style = useMemo(() => [contentStyle, MARKDOWN_LIST_ITEM_CONTENT_FLEX], [contentStyle]); - return {children}; -} - -interface MarkdownListViewProps { - baseStyle: ViewStyle; - spacing: { marginTop: number; marginBottom: number }; - children: ReactNode; -} - -function MarkdownListView({ baseStyle, spacing, children }: MarkdownListViewProps) { - const style = useMemo(() => [baseStyle, spacing], [baseStyle, spacing]); - return {children}; -} - -interface SharedMarkdownLinkProps { - href: string; - inheritedStyles: TextStyle; - linkStyle: TextStyle; - onLinkPress?: (url: string) => boolean; - children: ReactNode; -} - -function SharedMarkdownLink({ - href, - inheritedStyles, - linkStyle, - onLinkPress, - children, -}: SharedMarkdownLinkProps) { - const handlePress = useCallback(() => { - if (!href) return; - if (onLinkPress?.(href) === false) return; - void openExternalUrl(href); - }, [href, onLinkPress]); - const style = useMemo(() => [inheritedStyles, linkStyle], [inheritedStyles, linkStyle]); - - if (!isNative) { - return ( - - {children} - - ); - } - - return ( - - {children} - - ); -} - -function getMarkdownLinkHref(node: ASTNode): string { - const href = node.attributes?.href; - return typeof href === "string" ? href : ""; -} - -export function createSharedMarkdownRules(): RenderRules { +function markdownStyle(theme: Theme, compact = false): { markdownStyle: MarkdownStyle } { + const fontSize = compact ? theme.fontSize.base : theme.fontSize.content; + const paragraph = { + fontSize, + fontFamily: theme.fontFamily.ui, + lineHeight: Math.round(fontSize * 1.4), + color: theme.colors.foreground, + marginTop: 0, + marginBottom: theme.spacing[3], + }; + const heading = (scale: number) => ({ + ...paragraph, + fontSize: Math.round(fontSize * scale), + lineHeight: Math.round(fontSize * scale * 1.3), + fontWeight: "bold", + marginTop: theme.spacing[3], + }); return { - text: ( - node: ASTNode, - _children: ReactNode[], - _parent: ASTNode[], - styles: MarkdownStyles, - inheritedStyles: TextStyle = {}, - ) => ( - - {node.content} - - ), - textgroup: ( - node: ASTNode, - children: ReactNode[], - _parent: ASTNode[], - styles: MarkdownStyles, - inheritedStyles: TextStyle = {}, - ) => ( - - {children} - - ), - strong: ( - node: ASTNode, - children: ReactNode[], - _parent: ASTNode[], - styles: MarkdownStyles, - inheritedStyles: TextStyle = {}, - ) => ( - - {children} - - ), - em: ( - node: ASTNode, - children: ReactNode[], - _parent: ASTNode[], - styles: MarkdownStyles, - inheritedStyles: TextStyle = {}, - ) => ( - - {children} - - ), - s: ( - node: ASTNode, - children: ReactNode[], - _parent: ASTNode[], - styles: MarkdownStyles, - inheritedStyles: TextStyle = {}, - ) => ( - - {children} - - ), - hardbreak: ( - node: ASTNode, - _children: ReactNode[], - _parent: ASTNode[], - styles: MarkdownStyles, - ) => ( - - {"\n"} - - ), - softbreak: ( - node: ASTNode, - _children: ReactNode[], - _parent: ASTNode[], - styles: MarkdownStyles, - ) => ( - - {"\n"} - - ), - code_block: ( - node: ASTNode, - _children: ReactNode[], - _parent: ASTNode[], - styles: MarkdownStyles, - inheritedStyles: TextStyle = {}, - ) => ( - - ), - fence: ( - node: ASTNode, - _children: ReactNode[], - _parent: ASTNode[], - styles: MarkdownStyles, - inheritedStyles: TextStyle = {}, - ) => ( - - ), - code_inline: ( - node: ASTNode, - _children: ReactNode[], - _parent: ASTNode[], - styles: MarkdownStyles, - inheritedStyles: TextStyle = {}, - ) => ( - - {node.content ?? ""} - - ), - bullet_list: ( - node: ASTNode, - children: ReactNode[], - parent: ASTNode[], - styles: MarkdownStyles, - ) => ( - - {children} - - ), - ordered_list: ( - node: ASTNode, - children: ReactNode[], - parent: ASTNode[], - styles: MarkdownStyles, - ) => ( - - {children} - - ), - list_item: ( - node: ASTNode, - children: ReactNode[], - parent: ASTNode[], - styles: MarkdownStyles, - ) => { - const { isOrdered, marker } = getMarkdownListMarker(node, parent); - const iconStyle = isOrdered ? styles.ordered_list_icon : styles.bullet_list_icon; - const contentStyle = isOrdered ? styles.ordered_list_content : styles.bullet_list_content; - - return ( - - {marker} - {children} - - ); + markdownStyle: { + paragraph, + h1: heading(1.6), + h2: heading(1.4), + h3: heading(1.2), + h4: heading(1.1), + h5: heading(1), + h6: heading(1), + list: { + ...paragraph, + bulletColor: theme.colors.foreground, + markerColor: theme.colors.foregroundMuted, + gapWidth: 7, + itemSpacing: 4, + marginLeft: 20, + }, + blockquote: { + ...paragraph, + color: theme.colors.foregroundMuted, + backgroundColor: theme.colors.surface2, + borderColor: theme.colors.border, + borderWidth: 3, + padding: 12, + gapWidth: 8, + }, + link: { color: theme.colors.accent, underline: false }, + linkVariants: { + "^(?!https?://).+": { + color: theme.colors.accent, + pill: { borderRadius: 4, paddingHorizontal: 3 }, + }, + }, + code: { + fontFamily: theme.fontFamily.mono === "ui-monospace" && !isWeb ? "" : theme.fontFamily.mono, + fontSize: theme.fontSize.code, + color: theme.colors.foreground, + backgroundColor: theme.colors.surface2, + borderColor: "transparent", + }, + codeBlock: { + ...paragraph, + fontFamily: theme.fontFamily.mono === "ui-monospace" && !isWeb ? "" : theme.fontFamily.mono, + fontSize: theme.fontSize.code, + backgroundColor: theme.colors.surface2, + borderColor: theme.colors.border, + borderRadius: 8, + padding: 12, + }, + table: { + ...paragraph, + borderColor: theme.colors.border, + headerBackgroundColor: theme.colors.surface2, + }, + image: { height: 320, resizeMode: "contain", borderRadius: 8 }, + thematicBreak: { color: theme.colors.border, height: 1, marginTop: 12, marginBottom: 12 }, }, - th: (node: ASTNode, children: ReactNode[], _parent: ASTNode[], styles: MarkdownStyles) => ( - - {children} - - ), - td: (node: ASTNode, children: ReactNode[], _parent: ASTNode[], styles: MarkdownStyles) => ( - - {children} - - ), - paragraph: ( - node: ASTNode, - children: ReactNode[], - _parent: ASTNode[], - styles: MarkdownStyles, - ) => ( - - {children} - - ), - link: ( - node: ASTNode, - children: ReactNode[], - _parent: ASTNode[], - styles: MarkdownStyles, - onLinkPress?: (url: string) => boolean, - ) => ( - - {colorMarkdownLinkChildren(children, styles.link.color)} - - ), }; } - -const detailsStyles = StyleSheet.create((theme) => ({ - container: { - borderWidth: 1, - borderColor: theme.colors.border, - borderRadius: theme.borderRadius.md, - marginBottom: theme.spacing[2], - overflow: "hidden", - }, - summaryRow: { - flexDirection: "row", - alignItems: "center", - gap: theme.spacing[1], - paddingHorizontal: theme.spacing[2], - paddingVertical: theme.spacing[2], - backgroundColor: theme.colors.surface2, - }, - summaryIcon: { - color: theme.colors.foregroundMuted, - }, - summaryText: { - flex: 1, - minWidth: 0, +const regularStyle = (theme: Theme) => markdownStyle(theme); +const compactStyle = (theme: Theme) => markdownStyle(theme, true); +const ThemedCodeBlock = withUnistyles(HighlightedCodeBlock); +const codeStyle = (theme: Theme) => ({ + inheritedStyles: { color: theme.colors.foreground, - fontSize: theme.fontSize.base, - fontWeight: theme.fontWeight.normal, - lineHeight: 18, + fontFamily: theme.fontFamily.mono, + fontSize: theme.fontSize.code, }, - body: { - paddingHorizontal: theme.spacing[2], - paddingTop: theme.spacing[2], - }, - inlineImageWrap: { - alignSelf: "flex-start", - marginBottom: theme.spacing[1], - }, - inlineImage: {}, - imageTextRow: { - flexDirection: "row", - alignItems: "flex-start", - gap: theme.spacing[2], - marginBottom: theme.spacing[3], - }, - imageTextRowContent: { - flex: 1, - minWidth: 0, - }, - flowImage: { - marginTop: 2, - }, - flowImageFallback: { - marginTop: 2, - paddingHorizontal: theme.spacing[1], - borderRadius: theme.borderRadius.sm, - backgroundColor: theme.colors.surface2, - }, - flowImageFallbackText: { - fontSize: 10, - lineHeight: 14, - color: theme.colors.foregroundMuted, - }, -})); + textStyle: { backgroundColor: theme.colors.surface2, padding: 12, borderRadius: 8 }, +}); +function WebCodeBlock({ code, language }: CodeBlockRendererProps) { + return ; +} +const WEB_CONTAINER = { userSelect: "text" as const }; +const WEB_RENDERERS = { codeBlock: WebCodeBlock }; + +export interface MarkdownRendererProps extends MarkdownImageContext { + text: string; + compact?: boolean; + onLinkPress?: (url: string) => unknown; +} + +/** All Markdown surfaces share the same engine and source adaptation. */ +export const MarkdownRenderer = memo(function MarkdownRenderer({ + text, + compact = false, + onLinkPress, + ...imageContext +}: MarkdownRendererProps) { + const environment = useAssistantFileLinkEnvironment(); + const actions = useAssistantFileLinkActions(); + const client = + imageContext.client ?? + (environment.client?.readFile + ? { readFile: environment.client.readFile.bind(environment.client) } + : undefined); + const { markdown, imageSources } = useMarkdownSource(text, { + workspaceRoot: environment.workspaceRoot, + serverId: environment.serverId, + ...imageContext, + client, + }); + const [image, setImage] = useState(null); + const pressLink = useStableEvent(({ url }: { url: string }) => { + const source = { href: url, text: url }; + if (onLinkPress && !actions.canResolveFile(source)) onLinkPress(url); + else actions.open(source, "preferred"); + }); + const pressImage = useStableEvent(({ url }: { url: string }) => + setImage({ type: "uri", uri: url }), + ); + const closeImage = useStableEvent(() => setImage(null)); + const pills = useMemo( + () => + Object.fromEntries( + [...markdown.matchAll(FILE_PATTERN)] + .filter(([url]) => !/^https?:/i.test(url)) + .map(([url]) => [url, { label: url }]), + ), + [markdown], + ); + const content = ( + <> + + {image ? : null} + + ); + return isWeb && Object.keys(imageSources).length > 0 ? ( + {content} + ) : ( + content + ); +}); diff --git a/packages/app/src/components/markdown/source.ts b/packages/app/src/components/markdown/source.ts new file mode 100644 index 0000000000..893c2ada01 --- /dev/null +++ b/packages/app/src/components/markdown/source.ts @@ -0,0 +1,78 @@ +import { decodeString } from "micromark-util-decode-string"; +import { fromMarkdown } from "mdast-util-from-markdown"; +import type { Nodes } from "mdast"; + +export interface MarkdownImage { + source: string; + alt: string; + title: string | null; + start: number; + end: number; +} + +/** Preserve source spelling; adapt workspace images, spoiler delimiters, and entity text. */ +export function prepareMarkdownSource(source: string) { + const images: MarkdownImage[] = []; + const edits: { start: number; end: number; text: string }[] = []; + if (!source.includes("![") && !source.includes("||") && !source.includes("&")) { + return { images, render: () => source }; + } + const tree = fromMarkdown(source); + const definitions = new Map(); + function definitionsIn(node: Nodes) { + if (node.type === "definition") definitions.set(node.identifier, node); + if ("children" in node) node.children.forEach(definitionsIn); + } + definitionsIn(tree); + function visit(node: Nodes) { + const start = node.position?.start.offset; + const end = node.position?.end.offset; + if (start !== undefined && end !== undefined) { + if (node.type === "image" || node.type === "imageReference") { + const target = node.type === "image" ? node : definitions.get(node.identifier); + if (target) + images.push({ + source: target.url, + alt: node.alt ?? "", + title: target.title ?? null, + start, + end, + }); + return; + } + if (node.type === "text") { + const text = source + .slice(start, end) + .replace(/(? + decodeString(entity).replace(/[!-/:-@[-`{-~]/g, "\\$&"), + ) + .replace(/(? = new Map()) { + const replacements = [...edits]; + for (const image of images) { + if (/^(https?:|data:|blob:)/i.test(image.source)) continue; + const url = urls.get(image.source); + replacements.push({ + start: image.start, + end: image.end, + text: url + ? `![${image.alt.replace(/[[\]\\]/g, "\\$&")}](<${url}>${image.title ? ` ${JSON.stringify(image.title)}` : ""})` + : image.alt, + }); + } + let result = source; + for (const edit of replacements.sort((a, b) => b.start - a.start)) { + result = result.slice(0, edit.start) + edit.text + result.slice(edit.end); + } + return result; + }, + }; +} diff --git a/packages/app/src/components/message.tsx b/packages/app/src/components/message.tsx index 722f6e7b31..0735b81e89 100644 --- a/packages/app/src/components/message.tsx +++ b/packages/app/src/components/message.tsx @@ -1,54 +1,73 @@ -import { MarkdownProse } from "@/components/markdown-prose"; -import type { AgentMessage } from "@getpaseo/protocol/agent-message"; -import { ASSISTANT_IMAGE_DEFAULT_ASPECT_RATIO } from "@/utils/assistant-image-metadata"; -import { LoadingSpinner } from "@/components/ui/loading-spinner"; +import { useStableEvent } from "@/hooks/use-stable-event"; +import { getAgentAttachmentPillContent } from "@/attachments/attachment-pill-content"; +import { AssistantForkMenu, type AssistantForkTarget } from "@/components/assistant-fork-menu"; +import { AttachmentLightbox, type ImageLightboxSource } from "@/components/attachment-lightbox"; +import { + AttachmentFrame, + AttachmentLabel, + AttachmentThumbnail, +} from "@/components/attachment-pill"; +import { MarkdownRenderer } from "@/components/markdown/renderer"; +import { useRetainedPanelActive } from "@/components/retained-panel"; +import { RewindMenu, type RewindMode } from "@/components/rewind/rewind-menu"; +import { useRewindAgentMutation } from "@/components/rewind/use-rewind-agent-mutation"; import { TaskListRow } from "@/components/task-list-row"; -import { - View, - Text, - Image, - Pressable, - type GestureResponderEvent, - type LayoutChangeEvent, - StyleProp, - ViewStyle, - type TextStyle, -} from "react-native"; -import { useTranslation } from "react-i18next"; -import { MarkdownParagraphView, MarkdownTextSpan } from "@/components/markdown-text"; -import { MarkdownTableCellText } from "@/components/markdown-text-selection"; -import * as React from "react"; -import { - useState, - useEffect, - useRef, - memo, - useMemo, - useCallback, - createContext, - useContext, -} from "react"; -import type { ComponentType, ReactNode } from "react"; -import type MarkdownIt from "markdown-it"; -import { type ASTNode, type RenderRules } from "react-native-markdown-display"; +import { LoadingSpinner } from "@/components/ui/loading-spinner"; +import { useIsCompactFormFactor } from "@/constants/layout"; +import { isNative, isWeb } from "@/constants/platform"; +import { useRevealedText, type MarkdownPhase } from "@/hooks/use-revealed-text"; +import { ICON_SIZE, type Theme } from "@/styles/theme"; +import { inlineUnistylesStyle } from "@/styles/unistyles-inline-style"; +import { buildToolCallPresentation } from "@/tool-calls/presentation"; +import type { TaskActivity, TodoEntry, UserMessageImageAttachment } from "@/types/stream"; +import { setAssistantMessageHeight } from "@/utils/assistant-message-height-estimate"; +import { isRenderProfileEnabled } from "@/utils/render-profiler"; +import { writeMarkdownToRichClipboard } from "@/utils/rich-clipboard"; +import { getDefaultMarkdownClipboardEnvironment } from "@/utils/rich-clipboard-default-environment"; +import { formatDuration, formatMessageTimestamp } from "@/utils/time"; +import { resolveToolCallIcon } from "@/utils/tool-call-icon"; +import type { DaemonClient } from "@getpaseo/client/internal/daemon-client"; +import type { AgentMessage } from "@getpaseo/protocol/agent-message"; +import type { AgentCapabilityFlags, ToolCallDetail } from "@getpaseo/protocol/agent-types"; +import type { AgentAttachment } from "@getpaseo/protocol/messages"; import MaskedView from "@react-native-masked-view/masked-view"; import { - Info, - XCircle, - ChevronRight, Check, CheckSquare, + ChevronRight, CircleDot, Copy, - Plus, - TriangleAlertIcon, - Scissors, - MicVocal, FileSymlink, + Info, + MicVocal, + Plus, + Scissors, + TriangleAlertIcon, + XCircle, } from "lucide-react-native"; -import { StyleSheet, withUnistyles } from "react-native-unistyles"; -import { ICON_SIZE, type Theme } from "@/styles/theme"; -import { useIsCompactFormFactor } from "@/constants/layout"; +import type { ComponentType, ReactNode } from "react"; +import * as React from "react"; +import { + createContext, + memo, + useCallback, + useContext, + useEffect, + useMemo, + useRef, + useState, +} from "react"; +import { useTranslation } from "react-i18next"; +import { + Pressable, + StyleProp, + Text, + View, + ViewStyle, + type GestureResponderEvent, + type LayoutChangeEvent, + type TextStyle, +} from "react-native"; import Animated, { Easing, cancelAnimation, @@ -57,67 +76,14 @@ import Animated, { withRepeat, withTiming, } from "react-native-reanimated"; -import Svg, { Defs, LinearGradient as SvgLinearGradient, Rect, Stop } from "react-native-svg"; -import { inlineUnistylesStyle } from "@/styles/unistyles-inline-style"; -import { MarkdownRenderer, type MarkdownStyles } from "@/components/markdown/renderer"; -import type { TaskActivity, TodoEntry, UserMessageImageAttachment } from "@/types/stream"; -import type { AgentAttachment } from "@getpaseo/protocol/messages"; -import type { ToolCallDetail } from "@getpaseo/protocol/agent-types"; -import { buildToolCallPresentation } from "@/tool-calls/presentation"; -import { resolveToolCallIcon } from "@/utils/tool-call-icon"; -import { getMarkdownListMarker, getMarkdownListSpacing } from "@/utils/markdown-list"; -import { markdownNodeContainsType } from "@/utils/markdown-ast"; -import { useStableEvent } from "@/hooks/use-stable-event"; -import { HighlightedCodeBlock } from "@/components/highlighted-code-block"; -import { MarkdownFenceBlock } from "@/components/markdown/fence"; -import type { MarkdownPhase } from "@/components/markdown/fence/types"; -import { splitMarkdownBlocks } from "@/utils/split-markdown-blocks"; -import { useRevealedText } from "@/hooks/use-revealed-text"; -import { colorMarkdownLinkChildren } from "@/components/markdown/link-children"; -import { createAssistantMarkdownParser } from "@/utils/assistant-markdown-parser"; -import { formatDuration, formatMessageTimestamp } from "@/utils/time"; -import { getTurnDurationLabel } from "./assistant-turn-footer-label"; -import { writeMarkdownToRichClipboard } from "@/utils/rich-clipboard"; -import { getDefaultMarkdownClipboardEnvironment } from "@/utils/rich-clipboard-default-environment"; -import { setAssistantMarkdownBlockHeight } from "@/utils/assistant-message-height-estimate"; -import { isRenderProfileEnabled } from "@/utils/render-profiler"; -import { getAgentAttachmentPillContent } from "@/attachments/attachment-pill-content"; -import { PlanCard } from "./plan-card"; -import { useToolCallSheet } from "./tool-call-sheet"; -import { ToolCallDetailsContent } from "./tool-call-details"; -import { - AssistantInlineCodePathLink, - type AssistantFileLinkSource, - AssistantMarkdownCodeLink, - AssistantMarkdownLink, - type InlinePathTarget, - useAssistantFileLinkActions, - useAssistantLinkPress, -} from "@/assistant-file-links"; -import { getCompactionMarkerLabel } from "./message-compaction-label"; -import { useAssistantImage } from "@/assistant-image/use-assistant-image"; -import { localFileSourceToPath } from "@/attachments/utils"; -import { - AttachmentFrame, - AttachmentLabel, - AttachmentThumbnail, -} from "@/components/attachment-pill"; -import { AttachmentLightbox, type ImageLightboxSource } from "@/components/attachment-lightbox"; -import type { DaemonClient } from "@getpaseo/client/internal/daemon-client"; -import { isWeb, isNative } from "@/constants/platform"; -import type { AgentCapabilityFlags } from "@getpaseo/protocol/agent-types"; -import { RewindMenu, type RewindMode } from "@/components/rewind/rewind-menu"; -import { useRewindAgentMutation } from "@/components/rewind/use-rewind-agent-mutation"; -import { AssistantForkMenu, type AssistantForkTarget } from "@/components/assistant-fork-menu"; -import { useRetainedPanelActive } from "@/components/retained-panel"; -import { - markdownCopyDataSet, - markdownCopyImageDataSet, - markdownCopyOrderedListDataSet, - markdownCopyTableCellDataSet, - type MarkdownCopyInlineTag, -} from "@/assistant-selection-copy/markup"; +import Svg, { Defs, Rect, Stop, LinearGradient as SvgLinearGradient } from "react-native-svg"; +import { StyleSheet, withUnistyles } from "react-native-unistyles"; import { capAssistantMessageForRender, getUtf8ByteLength } from "./assistant-message-render-limit"; +import { getTurnDurationLabel } from "./assistant-turn-footer-label"; +import { getCompactionMarkerLabel } from "./message-compaction-label"; +import { PlanCard } from "./plan-card"; +import { ToolCallDetailsContent } from "./tool-call-details"; +import { useToolCallSheet } from "./tool-call-sheet"; export type { InlinePathTarget } from "@/assistant-file-links"; export type { AssistantForkTarget }; @@ -160,20 +126,10 @@ function useDisableOuterSpacing(disableOuterSpacing: boolean | undefined) { const WEB_TOOLCALL_SHIMMER_KEYFRAME_ID = "paseo-toolcall-shimmer-keyframes"; const WEB_TOOLCALL_SHIMMER_ANIMATION_NAME = "paseo-toolcall-shimmer"; -const MARKDOWN_ALLOWED_IMAGE_HANDLERS = [ - "data:image/png;base64", - "data:image/gif;base64", - "data:image/jpeg;base64", - "https://", - "http://", -] as const; -const MARKDOWN_TOP_LEVEL_MAX_EXCEEDED_ITEM = ...; - const ThemedMicVocal = withUnistyles(MicVocal); const ThemedFileSymlinkIcon = withUnistyles(FileSymlink); const ThemedTriangleAlertIcon = withUnistyles(TriangleAlertIcon); const ThemedChevronRightIcon = withUnistyles(ChevronRight); -const ThemedLoadingSpinner = withUnistyles(LoadingSpinner); const ThemedNotificationInfo = withUnistyles(Info); const ThemedNotificationWarning = withUnistyles(TriangleAlertIcon); const ThemedNotificationError = withUnistyles(XCircle); @@ -753,8 +709,6 @@ export const LiveElapsed = memo(function LiveElapsed({ }); interface AssistantMessageProps { - markdownKind?: "prose" | "quote" | "block"; - markdownProseBlocks?: readonly string[]; renderFullContent?: boolean; occurrenceKey: string; message: string; @@ -821,200 +775,6 @@ export const assistantMessageStylesheet = StyleSheet.create((theme) => ({ }, })); -function AssistantMarkdownImage({ - source, - occurrenceKey, - alt, - hasLeadingContent, - client, - workspaceRoot, - serverId, -}: { - source: string; - occurrenceKey: string; - alt?: string; - hasLeadingContent: boolean; - client?: DaemonClient | null; - workspaceRoot?: string; - serverId?: string; -}) { - const { t } = useTranslation(); - const [viewerOpen, setViewerOpen] = useState(false); - const openViewer = useCallback(() => setViewerOpen(true), []); - const closeViewer = useCallback(() => setViewerOpen(false), []); - const containerStyle = useMemo>( - () => ({ - marginTop: hasLeadingContent ? 16 : 0, - marginBottom: 0, - }), - [hasLeadingContent], - ); - const image = useAssistantImage({ - source, - occurrenceKey, - client, - workspaceRoot, - serverId, - }); - const binding = image.status === "failed" ? null : image.binding; - const aspectRatio = image.status === "failed" ? null : image.aspectRatio; - const imageUri = binding?.uri ?? ""; - const imageSource = useMemo(() => ({ uri: imageUri }), [imageUri]); - const frameStyle = useMemo>( - () => [assistantMessageStylesheet.imageFrame, containerStyle], - [containerStyle], - ); - const imageSizeStyle = useMemo(() => { - if (image.status === "failed") return { height: 160 }; - return { aspectRatio: aspectRatio ?? ASSISTANT_IMAGE_DEFAULT_ASPECT_RATIO }; - }, [aspectRatio, image.status]); - const surfaceStyle = useMemo>( - () => [assistantMessageStylesheet.imageSurface, imageSizeStyle], - [imageSizeStyle], - ); - const lightboxSource = useMemo(() => { - if (!viewerOpen || !imageUri) return null; - return { - type: "uri", - uri: imageUri, - contentSize: aspectRatio ? { width: aspectRatio, height: 1 } : undefined, - }; - }, [aspectRatio, imageUri, viewerOpen]); - - const stateFrameStyle = useMemo>( - () => [ - assistantMessageStylesheet.imageFrame, - containerStyle, - imageSizeStyle, - assistantMessageStylesheet.imageState, - ], - [containerStyle, imageSizeStyle], - ); - const copyDataSet = useMemo(() => markdownCopyImageDataSet(source, alt), [source, alt]); - - if (image.status === "failed") { - return ( - - {image.message} - - ); - } - - if (!binding) { - return ( - - - - ); - } - - return ( - - - - - {image.status === "loading" ? ( - - - - ) : null} - - - - - ); -} - -function getInlineCodeAutoLinkUrl(markdownParser: MarkdownIt, content: string): string | null { - const trimmed = content.trim(); - if (!trimmed) { - return null; - } - - const matches: - | { - index: number; - lastIndex: number; - url: string; - }[] - | null = markdownParser.linkify.match(trimmed); - if (!matches || matches.length !== 1) { - return null; - } - - const [match] = matches; - if (!match || match.index !== 0 || match.lastIndex !== trimmed.length) { - return null; - } - - return match.url; -} - -function getInlineCodeAutoLinkSource(input: { - href: string; - content: string; -}): AssistantFileLinkSource { - return { - href: input.href, - text: input.content, - markup: "linkify", - sourceInfo: "auto", - }; -} - -interface AssistantMarkdownAstNode extends ASTNode { - sourceInfo?: string; -} - -function getMarkdownLinkSource(node: AssistantMarkdownAstNode): AssistantFileLinkSource { - return { - href: typeof node.attributes?.href === "string" ? node.attributes.href : "", - text: getMarkdownNodeText(node), - title: typeof node.attributes?.title === "string" ? node.attributes.title : undefined, - markup: node.markup, - sourceInfo: node.sourceInfo, - sourceType: node.sourceType === "inline-code" ? "inline-code" : undefined, - }; -} - -function getMarkdownNodeText(node: ASTNode): string { - if (!node.children.length) { - return node.content ?? ""; - } - - return node.children.map(getMarkdownNodeText).join(""); -} - -function nodeHasParentType(parent: unknown, type: string): boolean { - if (Array.isArray(parent)) { - return parent.some((entry) => entry?.type === type); - } - - return ( - typeof parent === "object" && - parent !== null && - "type" in parent && - (parent as Record<"type", unknown>)["type"] === type - ); -} - const turnCopyButtonStylesheet = StyleSheet.create((theme) => ({ container: { alignSelf: "flex-start", @@ -1367,138 +1127,7 @@ function NativeShimmerPeakSvg({ gradientId }: { gradientId: string }) { ); } -interface AssistantMessageBlockContainerProps { - block: string; - marginBottom: number; - children: ReactNode; -} - -function AssistantMessageBlockContainer({ - block, - marginBottom, - children, -}: AssistantMessageBlockContainerProps) { - const style = useMemo(() => (marginBottom > 0 ? { marginBottom } : undefined), [marginBottom]); - const handleLayout = useCallback( - (event: LayoutChangeEvent) => { - const { width, height } = event.nativeEvent.layout; - setAssistantMarkdownBlockHeight({ block, width, height }); - }, - [block], - ); - return ( - - {children} - - ); -} - -interface MemoizedMarkdownBlockProps { - text: string; - rules: RenderRules; - parser: MarkdownIt; - onLinkPress: (url: string) => boolean; -} - -const MemoizedMarkdownBlock = React.memo(function MemoizedMarkdownBlock({ - text, - rules, - parser, - onLinkPress, -}: MemoizedMarkdownBlockProps) { - return ( - - ); -}); - -interface MarkdownInheritedTextProps { - inheritedStyles: TextStyle; - textStyle: TextStyle; - style?: StyleProp; - monoSurface?: boolean; - copyTag?: MarkdownCopyInlineTag; - children: ReactNode; -} - -function MarkdownInheritedText({ - inheritedStyles, - textStyle, - style: overrideStyle, - monoSurface, - copyTag, - children, -}: MarkdownInheritedTextProps) { - const style = useMemo( - () => [inheritedStyles, textStyle, overrideStyle], - [inheritedStyles, textStyle, overrideStyle], - ); - // When this span renders link label text on iOS, pick up the link's press - // handler from context and hand it to MarkdownTextSpan, which forwards it to - // the leaf string children react-native-uitextview makes tappable. Null - // outside a link (and on every other platform, where no provider mounts), so - // ordinary text is unaffected. See assistant-file-links/link-press-context. - const linkPress = useAssistantLinkPress(); - return ( - - {children} - - ); -} - -interface MarkdownListItemContentProps { - contentStyle: ViewStyle; - children: ReactNode; -} - -const MARKDOWN_LIST_ITEM_CONTENT_FLEX: ViewStyle = { flex: 1, flexShrink: 1, minWidth: 0 }; - -function MarkdownListItemContent({ contentStyle, children }: MarkdownListItemContentProps) { - const style = useMemo(() => [contentStyle, MARKDOWN_LIST_ITEM_CONTENT_FLEX], [contentStyle]); - return {children}; -} - -interface MarkdownListViewProps { - baseStyle: ViewStyle; - copyTag: "ol" | "ul"; - orderedStart?: unknown; - spacing: { marginTop: number; marginBottom: number }; - children: ReactNode; -} - -function MarkdownListView({ - baseStyle, - copyTag, - orderedStart, - spacing, - children, -}: MarkdownListViewProps) { - const style = useMemo(() => [baseStyle, spacing], [baseStyle, spacing]); - const copyDataSet = - copyTag === "ol" ? markdownCopyOrderedListDataSet(orderedStart) : markdownCopyDataSet.ul; - return ( - - {children} - - ); -} - export const AssistantMessage = memo(function AssistantMessage({ - markdownKind = "block", - markdownProseBlocks, renderFullContent = false, occurrenceKey, message, @@ -1510,11 +1139,6 @@ export const AssistantMessage = memo(function AssistantMessage({ phase, }: AssistantMessageProps) { const { t } = useTranslation(); - const markdownParser = useMemo(createAssistantMarkdownParser, []); - const streamingMarkdownParser = useMemo( - () => createAssistantMarkdownParser({ streaming: true }), - [], - ); const renderedMessage = useMemo( () => renderFullContent ? { text: message, capped: false } : capAssistantMessageForRender(message), @@ -1527,452 +1151,6 @@ export const AssistantMessage = memo(function AssistantMessage({ [message, phase, renderedMessage.capped], ); - const fileLinkActions = useAssistantFileLinkActions(); - const handleMarkdownLinkPress = useStableEvent((url: string) => { - fileLinkActions.open({ href: url }, "preferred"); - // react-native-markdown-display opens the link itself when this returns true. - // We already handled it above, so return false to avoid duplicate opens. - return false; - }); - - const markdownRules = useMemo(() => { - return { - heading1: ( - node: ASTNode, - children: ReactNode[], - _parent: ASTNode[], - styles: MarkdownStyles, - ) => ( - - {children} - - ), - heading2: ( - node: ASTNode, - children: ReactNode[], - _parent: ASTNode[], - styles: MarkdownStyles, - ) => ( - - {children} - - ), - heading3: ( - node: ASTNode, - children: ReactNode[], - _parent: ASTNode[], - styles: MarkdownStyles, - ) => ( - - {children} - - ), - heading4: ( - node: ASTNode, - children: ReactNode[], - _parent: ASTNode[], - styles: MarkdownStyles, - ) => ( - - {children} - - ), - heading5: ( - node: ASTNode, - children: ReactNode[], - _parent: ASTNode[], - styles: MarkdownStyles, - ) => ( - - {children} - - ), - heading6: ( - node: ASTNode, - children: ReactNode[], - _parent: ASTNode[], - styles: MarkdownStyles, - ) => ( - - {children} - - ), - blockquote: ( - node: ASTNode, - children: ReactNode[], - _parent: ASTNode[], - styles: MarkdownStyles, - ) => ( - - {children} - - ), - hr: (node: ASTNode, _children: ReactNode[], _parent: ASTNode[], styles: MarkdownStyles) => ( - - ), - table: (node: ASTNode, children: ReactNode[], _parent: ASTNode[], styles: MarkdownStyles) => ( - - {children} - - ), - thead: (node: ASTNode, children: ReactNode[], _parent: ASTNode[], styles: MarkdownStyles) => ( - - {children} - - ), - tbody: (node: ASTNode, children: ReactNode[], _parent: ASTNode[], styles: MarkdownStyles) => ( - - {children} - - ), - tr: (node: ASTNode, children: ReactNode[], _parent: ASTNode[], styles: MarkdownStyles) => ( - - {children} - - ), - text: ( - node: ASTNode, - _children: ReactNode[], - _parent: ASTNode[], - styles: MarkdownStyles, - inheritedStyles: TextStyle = {}, - ) => ( - - {node.content} - - ), - textgroup: ( - node: ASTNode, - children: ReactNode[], - _parent: ASTNode[], - styles: MarkdownStyles, - inheritedStyles: TextStyle = {}, - ) => ( - - {children} - - ), - // strong/em/s have no custom rule in react-native-markdown-display's - // defaults beyond wrapping children in a plain RN . On iOS the - // paragraph/textgroup are native UITextViews (see markdown-text.ios.tsx), - // and a plain nested inside one is not hoisted into a - // UITextViewChild, so its content renders invisibly. Route these inline - // marks through MarkdownTextSpan (same path as text/textgroup) so the - // styled content composes and stays visible + selectable on iOS. - strong: ( - node: ASTNode, - children: ReactNode[], - _parent: ASTNode[], - styles: MarkdownStyles, - inheritedStyles: TextStyle = {}, - ) => ( - - {children} - - ), - em: ( - node: ASTNode, - children: ReactNode[], - _parent: ASTNode[], - styles: MarkdownStyles, - inheritedStyles: TextStyle = {}, - ) => ( - - {children} - - ), - s: ( - node: ASTNode, - children: ReactNode[], - _parent: ASTNode[], - styles: MarkdownStyles, - inheritedStyles: TextStyle = {}, - ) => ( - - {children} - - ), - // hardbreak/softbreak fall back to react-native-markdown-display's - // default, a plain RN {"\n"}. Inside the paragraph UITextView that - // plain is not hoisted into a UITextViewChild and is dropped (same - // root cause as strong/em/s) — so on iOS a hard line break vanished, and - // a softbreak between words jammed them together ("one\ntwo" -> "onetwo"). - // Emit the break through MarkdownTextSpan so it composes on iOS. Keep - // the resolved break styles: hardbreak is a full-width flex-row child on - // Android, and dropping that width joins the surrounding text spans. - hardbreak: ( - node: ASTNode, - _children: ReactNode[], - _parent: ASTNode[], - styles: MarkdownStyles, - ) => ( - - {"\n"} - - ), - softbreak: ( - node: ASTNode, - _children: ReactNode[], - _parent: ASTNode[], - styles: MarkdownStyles, - ) => ( - - {"\n"} - - ), - code_block: ( - node: ASTNode, - _children: ReactNode[], - _parent: ASTNode[], - styles: MarkdownStyles, - inheritedStyles: TextStyle = {}, - ) => ( - - ), - fence: ( - node: ASTNode, - _children: ReactNode[], - _parent: ASTNode[], - styles: MarkdownStyles, - inheritedStyles: TextStyle = {}, - ) => ( - - ), - code_inline: ( - node: ASTNode, - _children: ReactNode[], - parent: ASTNode[], - styles: MarkdownStyles, - inheritedStyles: TextStyle = {}, - ) => { - const content = node.content ?? ""; - const isLinkedInlineCode = nodeHasParentType(parent, "link"); - const inlineCodeSource: AssistantFileLinkSource = { - href: content, - text: content, - sourceType: "inline-code", - }; - const shouldResolveInlinePath = - !isLinkedInlineCode && fileLinkActions.canResolveFile(inlineCodeSource); - - if (shouldResolveInlinePath) { - return ( - - ); - } - - const inlineCodeLinkUrl = getInlineCodeAutoLinkUrl(markdownParser, content); - if (inlineCodeLinkUrl) { - const source = getInlineCodeAutoLinkSource({ - href: inlineCodeLinkUrl, - content, - }); - return ( - - {content} - - ); - } - - return ( - - {content} - - ); - }, - bullet_list: ( - node: ASTNode, - children: ReactNode[], - parent: ASTNode[], - styles: MarkdownStyles, - ) => ( - - {children} - - ), - ordered_list: ( - node: ASTNode, - children: ReactNode[], - parent: ASTNode[], - styles: MarkdownStyles, - ) => ( - - {children} - - ), - list_item: ( - node: ASTNode, - children: ReactNode[], - parent: ASTNode[], - styles: MarkdownStyles, - ) => { - const { isOrdered, marker } = getMarkdownListMarker(node, parent); - const iconStyle = isOrdered ? styles.ordered_list_icon : styles.bullet_list_icon; - const contentStyle = isOrdered ? styles.ordered_list_content : styles.bullet_list_content; - - return ( - - - {marker} - - - {children} - - - ); - }, - th: (node: ASTNode, children: ReactNode[], _parent: ASTNode[], styles: MarkdownStyles) => ( - - - {children} - - - ), - td: (node: ASTNode, children: ReactNode[], _parent: ASTNode[], styles: MarkdownStyles) => ( - - - {children} - - - ), - paragraph: ( - node: ASTNode, - children: ReactNode[], - _parent: ASTNode[], - styles: MarkdownStyles, - ) => ( - - {children} - - ), - link: (node: ASTNode, children: ReactNode[], _parent: ASTNode[], styles: MarkdownStyles) => ( - - {colorMarkdownLinkChildren(children, styles.link.color)} - - ), - image: ( - node: ASTNode, - _children: ReactNode[], - parent: ASTNode[], - _styles: MarkdownStyles, - ) => { - const paragraphNode = Array.isArray(parent) - ? parent.find((ancestor) => ancestor?.type === "paragraph") - : null; - const paragraphChildren = Array.isArray(paragraphNode?.children) - ? paragraphNode.children - : []; - const imageIndex = paragraphChildren.findIndex((child: ASTNode) => child?.key === node.key); - const hasLeadingContent = imageIndex > 0; - - return ( - - ); - }, - }; - }, [client, fileLinkActions, markdownParser, occurrenceKey, phase, serverId, workspaceRoot]); - - const blocks = useMemo( - () => (markdownKind === "block" ? splitMarkdownBlocks(revealedMessage) : []), - [revealedMessage, markdownKind], - ); - const keyedBlocks = useMemo( - () => blocks.map((block, index) => ({ key: `block:${index}`, block })), - [blocks], - ); - const assistantContainerStyle = useMemo( () => [ assistantMessageStylesheet.container, @@ -1995,33 +1173,22 @@ export const AssistantMessage = memo(function AssistantMessage({ [occurrenceKey, revealedMessage.length], ); + const measureMessage = useStableEvent((event: import("react-native").LayoutChangeEvent) => { + setAssistantMessageHeight({ markdown: revealedMessage, ...event.nativeEvent.layout }); + }); return ( - - + - {keyedBlocks.map(({ key, block }, index) => ( - - - - ))} - + client={client} + workspaceRoot={workspaceRoot} + serverId={serverId} + /> {fullMessageByteLength !== null ? ( ; - -function MarkdownInlineText({ - inheritedStyle, - ruleStyle, - children, -}: { - inheritedStyle: StyleProp; - ruleStyle: StyleProp; - children: ReactNode; -}) { - const style = useMemo(() => [inheritedStyle, ruleStyle], [inheritedStyle, ruleStyle]); - return {children}; -} - -function MarkdownListItemContent({ - contentStyle, - children, -}: { - contentStyle: StyleProp; - children: ReactNode; -}) { - const style = useMemo(() => [contentStyle, LIST_ITEM_CONTENT_INNER], [contentStyle]); - return {children}; -} - -function MarkdownParagraph({ - paragraphStyle, - isLastChild, - children, -}: { - paragraphStyle: StyleProp; - isLastChild: boolean; - children: ReactNode; -}) { - const style = useMemo>( - () => [paragraphStyle, isLastChild ? PARAGRAPH_LAST_CHILD : null], - [paragraphStyle, isLastChild], - ); - return {children}; -} - -function createPlanMarkdownRules() { - return { - text: ( - node: ASTNode, - _children: ReactNode[], - _parent: ASTNode[], - styles: MarkdownRuleStyles, - inheritedStyles: TextStyle = {}, - ) => ( - - {node.content} - - ), - textgroup: ( - node: ASTNode, - children: ReactNode[], - _parent: ASTNode[], - styles: MarkdownRuleStyles, - inheritedStyles: TextStyle = {}, - ) => ( - - {children} - - ), - code_block: ( - node: ASTNode, - _children: ReactNode[], - _parent: ASTNode[], - styles: MarkdownRuleStyles, - inheritedStyles: TextStyle = {}, - ) => ( - - {node.content} - - ), - fence: ( - node: ASTNode, - _children: ReactNode[], - _parent: ASTNode[], - styles: MarkdownRuleStyles, - inheritedStyles: TextStyle = {}, - ) => ( - - {node.content} - - ), - code_inline: ( - node: ASTNode, - _children: ReactNode[], - _parent: ASTNode[], - styles: MarkdownRuleStyles, - inheritedStyles: TextStyle = {}, - ) => ( - - {node.content} - - ), - bullet_list: ( - node: ASTNode, - children: ReactNode[], - _parent: ASTNode[], - styles: MarkdownRuleStyles, - ) => ( - - {children} - - ), - ordered_list: ( - node: ASTNode, - children: ReactNode[], - _parent: ASTNode[], - styles: MarkdownRuleStyles, - ) => ( - - {children} - - ), - list_item: ( - node: ASTNode, - children: ReactNode[], - parent: ASTNode[], - styles: MarkdownRuleStyles, - ) => { - const { isOrdered, marker } = getMarkdownListMarker(node, parent); - const iconStyle = isOrdered ? styles.ordered_list_icon : styles.bullet_list_icon; - const contentStyle = isOrdered ? styles.ordered_list_content : styles.bullet_list_content; - - return ( - - {marker} - {children} - - ); - }, - paragraph: ( - node: ASTNode, - children: ReactNode[], - parent: ASTNode[], - styles: MarkdownRuleStyles, - ) => { - const isLastChild = parent[0]?.children?.at(-1)?.key === node.key; - return ( - - {children} - - ); - }, - }; -} +import { ChevronRight } from "lucide-react-native"; +import { useCallback, useMemo, useState, type ReactNode } from "react"; +import { useTranslation } from "react-i18next"; +import { Pressable, Text, View } from "react-native"; +import { StyleSheet, withUnistyles } from "react-native-unistyles"; export type PlanOutcome = "pending" | "approved" | "rejected" | "canceled"; @@ -204,7 +21,6 @@ interface PlanCardProps { const ThemedChevron = withUnistyles(ChevronRight); const chevronColor = (theme: Theme) => ({ color: theme.colors.foregroundMuted }); -const markdownRules = createPlanMarkdownRules(); export function PlanCard(props: PlanCardProps) { // A resolution starts its own presentation state; subsequent taps stay local. @@ -262,7 +78,7 @@ function PlanCardContent({ {expanded ? ( {description ? {description} : null} - + ) : null} {footer ? {footer} : null} @@ -307,6 +123,3 @@ const styles = StyleSheet.create((theme) => ({ gap: theme.spacing[2], }, })); - -const LIST_ITEM_CONTENT_INNER = { flex: 1, flexShrink: 1, minWidth: 0 }; -const PARAGRAPH_LAST_CHILD = { marginBottom: 0 }; diff --git a/packages/app/src/file-pane/markdown-preview/index.tsx b/packages/app/src/file-pane/markdown-preview/index.tsx index a79705323f..8a45b5c296 100644 --- a/packages/app/src/file-pane/markdown-preview/index.tsx +++ b/packages/app/src/file-pane/markdown-preview/index.tsx @@ -1,16 +1,9 @@ -import { useMemo } from "react"; +import { MarkdownRenderer } from "@/components/markdown/renderer"; import type { DaemonClient } from "@getpaseo/client/internal/daemon-client"; -import type { ASTNode, RenderRules } from "react-native-markdown-display"; +import { useMemo } from "react"; import { Text, View } from "react-native"; import { StyleSheet } from "react-native-unistyles"; -import { - createSharedMarkdownRules, - MarkdownRenderer, - type MarkdownStyles, -} from "@/components/markdown/renderer"; import { parseMarkdownPreviewDocument } from "./document"; -import { resolveMarkdownPreviewImageSource } from "./image-source"; -import { FileMarkdownPreviewImage } from "./preview-image"; export function FileMarkdownPreview({ source, @@ -26,34 +19,6 @@ export function FileMarkdownPreview({ serverId?: string; }) { const document = useMemo(() => parseMarkdownPreviewDocument(source), [source]); - const rules = useMemo( - () => ({ - ...createSharedMarkdownRules(), - image: (node: ASTNode, _children, _parent: ASTNode[], _styles: MarkdownStyles) => { - const resolved = resolveMarkdownPreviewImageSource({ - source: String(node.attributes?.src ?? ""), - markdownPath: filePath, - workspaceRoot, - }); - if (!resolved) { - return null; - } - - return ( - - ); - }, - }), - [client, filePath, serverId, workspaceRoot], - ); return ( @@ -79,7 +44,13 @@ export function FileMarkdownPreview({ ))} ) : null} - + ); diff --git a/packages/app/src/file-pane/markdown-preview/preview-image.tsx b/packages/app/src/file-pane/markdown-preview/preview-image.tsx deleted file mode 100644 index ef27c14978..0000000000 --- a/packages/app/src/file-pane/markdown-preview/preview-image.tsx +++ /dev/null @@ -1,151 +0,0 @@ -import { useCallback, useMemo, useState } from "react"; -import type { DaemonClient } from "@getpaseo/client/internal/daemon-client"; -import { useTranslation } from "react-i18next"; -import { Image, Pressable, Text, View, type StyleProp, type ViewStyle } from "react-native"; -import { StyleSheet, withUnistyles } from "react-native-unistyles"; -import { useAssistantImage } from "@/assistant-image/use-assistant-image"; -import { AttachmentLightbox, type ImageLightboxSource } from "@/components/attachment-lightbox"; -import { LoadingSpinner } from "@/components/ui/loading-spinner"; -import type { Theme } from "@/styles/theme"; - -const ThemedLoadingSpinner = withUnistyles(LoadingSpinner); -const foregroundMutedColorMapping = (theme: Theme) => ({ - color: theme.colors.foregroundMuted, -}); - -const PREVIEW_IMAGE_MIN_HEIGHT = 160; - -export function FileMarkdownPreviewImage({ - source, - occurrenceKey, - alt, - client, - workspaceRoot, - serverId, -}: { - source: string; - occurrenceKey: string; - alt?: string; - client?: DaemonClient | null; - workspaceRoot?: string; - serverId?: string; -}) { - const { t } = useTranslation(); - const [viewerOpen, setViewerOpen] = useState(false); - const openViewer = useCallback(() => setViewerOpen(true), []); - const closeViewer = useCallback(() => setViewerOpen(false), []); - const image = useAssistantImage({ - source, - occurrenceKey, - client, - workspaceRoot, - serverId, - }); - const binding = image.status === "failed" ? null : image.binding; - const aspectRatio = image.status === "failed" ? null : image.aspectRatio; - const imageUri = binding?.uri ?? ""; - const imageSource = useMemo(() => ({ uri: imageUri }), [imageUri]); - const imageSizeStyle = useMemo(() => { - if (aspectRatio) { - return { aspectRatio }; - } - return { height: PREVIEW_IMAGE_MIN_HEIGHT }; - }, [aspectRatio]); - const surfaceStyle = useMemo>( - () => [styles.surface, imageSizeStyle], - [imageSizeStyle], - ); - const lightboxSource = useMemo(() => { - if (!viewerOpen || !imageUri) return null; - return { - type: "uri", - uri: imageUri, - contentSize: aspectRatio ? { width: aspectRatio, height: 1 } : undefined, - }; - }, [aspectRatio, imageUri, viewerOpen]); - - if (image.status === "failed") { - return ( - - {image.message} - - ); - } - - if (!binding) { - return ( - - - - ); - } - - return ( - - - - - {image.status === "loading" ? ( - - - - ) : null} - - - - - ); -} - -const styles = StyleSheet.create((theme) => ({ - frame: { - width: "100%", - marginBottom: theme.spacing[4], - }, - surface: { - width: "100%", - overflow: "hidden", - borderRadius: theme.borderRadius.md, - backgroundColor: theme.colors.surface2, - }, - image: { - width: "100%", - height: "100%", - }, - state: { - minHeight: PREVIEW_IMAGE_MIN_HEIGHT, - alignItems: "center", - justifyContent: "center", - paddingHorizontal: theme.spacing[4], - paddingVertical: theme.spacing[6], - backgroundColor: theme.colors.surface2, - borderRadius: theme.borderRadius.md, - }, - loadingOverlay: { - position: "absolute", - top: 0, - right: 0, - bottom: 0, - left: 0, - alignItems: "center", - justifyContent: "center", - }, - errorText: { - color: theme.colors.foregroundMuted, - fontSize: theme.fontSize.base, - textAlign: "center", - }, -})); diff --git a/packages/app/src/hooks/use-revealed-text.ts b/packages/app/src/hooks/use-revealed-text.ts index d3e1c905ab..6d7130927d 100644 --- a/packages/app/src/hooks/use-revealed-text.ts +++ b/packages/app/src/hooks/use-revealed-text.ts @@ -11,7 +11,7 @@ import { type TextRevealState, visibleRevealedText, } from "@/agent-stream/text-reveal"; -import type { MarkdownPhase } from "@/components/markdown/fence/types"; +export type MarkdownPhase = "streaming" | "complete"; /** * Binds the paced reveal in @/agent-stream/text-reveal to a frame clock. diff --git a/packages/app/src/screens/workspace/workspace-screen.tsx b/packages/app/src/screens/workspace/workspace-screen.tsx index bfe131d008..90a46d9e06 100644 --- a/packages/app/src/screens/workspace/workspace-screen.tsx +++ b/packages/app/src/screens/workspace/workspace-screen.tsx @@ -1,3 +1,4 @@ +import { AssistantFileLinkResolverProvider, type InlinePathTarget } from "@/assistant-file-links"; import { WorkspaceComposerEditingSessionsProvider, useComposerTabFullscreen, @@ -3626,6 +3627,10 @@ function WorkspaceScreenContent({ ); + const openMarkdownFile = useStableEvent((target: InlinePathTarget) => { + handleOpenPreferredAssistantFile({ location: target, parentTabId: activeTabId }); + }); + const renderedWorkspaceScreen = ( @@ -3665,7 +3670,15 @@ function WorkspaceScreenContent({ } return ( - {renderedWorkspaceScreen} + + {renderedWorkspaceScreen} + ); } diff --git a/packages/app/src/styles/markdown-styles.test.ts b/packages/app/src/styles/markdown-styles.test.ts deleted file mode 100644 index 061468d785..0000000000 --- a/packages/app/src/styles/markdown-styles.test.ts +++ /dev/null @@ -1,142 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { createCompactMarkdownStyles, createMarkdownStyles } from "./markdown-styles"; -import { darkTheme } from "./theme"; - -describe("createMarkdownStyles", () => { - it("uses the content size for conversation prose and list markers", () => { - const styles = createMarkdownStyles(darkTheme); - const proseLineHeight = Math.round(darkTheme.fontSize.content * 1.4); - - expect(styles.body).toMatchObject({ - fontSize: darkTheme.fontSize.content, - lineHeight: proseLineHeight, - }); - expect(styles.bullet_list_icon).toMatchObject({ - fontSize: darkTheme.fontSize.content, - lineHeight: proseLineHeight, - }); - expect(styles.ordered_list_icon).toMatchObject({ - fontSize: darkTheme.fontSize.content, - lineHeight: proseLineHeight, - }); - }); - - it("applies shrink-and-wrap constraints to long markdown text and links", () => { - const styles = createMarkdownStyles(darkTheme); - - expect(styles.body).toMatchObject({ - flexShrink: 1, - minWidth: 0, - width: "100%", - }); - - expect(styles.paragraph).toMatchObject({ - flexShrink: 1, - minWidth: 0, - width: "100%", - flexWrap: "wrap", - }); - - expect(styles.text).toMatchObject({ - flexShrink: 1, - minWidth: 0, - overflowWrap: "anywhere", - }); - - expect(styles.link).toMatchObject({ - flexShrink: 1, - minWidth: 0, - overflowWrap: "anywhere", - }); - - expect(styles.blocklink).toMatchObject({ - flexShrink: 1, - minWidth: 0, - overflowWrap: "anywhere", - }); - }); - - it("keeps assistant markdown text selectable on web", () => { - const styles = createMarkdownStyles(darkTheme); - - expect(styles.body).toMatchObject({ - userSelect: "text", - }); - expect(styles.text).toMatchObject({ - userSelect: "text", - }); - expect(styles.heading1).toMatchObject({ - userSelect: "text", - }); - expect(styles.link).toMatchObject({ - userSelect: "text", - }); - expect(styles.code_inline).toMatchObject({ - userSelect: "text", - }); - expect(styles.code_block).toMatchObject({ - userSelect: "text", - }); - expect(styles.fence).toMatchObject({ - userSelect: "text", - }); - expect(styles.bullet_list_icon).toMatchObject({ - userSelect: "text", - }); - expect(styles.ordered_list_icon).toMatchObject({ - userSelect: "text", - }); - }); - - it("uses the mono font-size token directly for inline and block code", () => { - const styles = createMarkdownStyles(darkTheme); - const compactStyles = createCompactMarkdownStyles(darkTheme); - - expect(styles.code_inline).toMatchObject({ - fontFamily: darkTheme.fontFamily.mono, - fontSize: darkTheme.fontSize.code, - }); - expect(styles.code_inline).not.toHaveProperty("lineHeight"); - expect(styles.code_block).toMatchObject({ - fontFamily: darkTheme.fontFamily.mono, - fontSize: darkTheme.fontSize.code, - }); - expect(styles.fence).toMatchObject({ - fontFamily: darkTheme.fontFamily.mono, - fontSize: darkTheme.fontSize.code, - }); - expect(compactStyles.code_inline).toMatchObject({ - fontFamily: darkTheme.fontFamily.mono, - fontSize: darkTheme.fontSize.code, - }); - expect(compactStyles.code_inline).not.toHaveProperty("lineHeight"); - }); - - it("scales Markdown headings from content size with safe line heights", () => { - const largeContentTheme = { - ...darkTheme, - fontSize: { ...darkTheme.fontSize, content: 21 }, - }; - const styles = createMarkdownStyles(largeContentTheme); - - expect(styles.heading1.lineHeight).toBeGreaterThan(styles.heading1.fontSize); - expect(styles.heading2.lineHeight).toBeGreaterThan(styles.heading2.fontSize); - expect(styles.heading3.lineHeight).toBeGreaterThan(styles.heading3.fontSize); - }); - - it("keeps blockquotes quiet with a square left edge", () => { - const styles = createMarkdownStyles(darkTheme); - - expect(styles.blockquote).toMatchObject({ - backgroundColor: darkTheme.colors.surface1, - color: `${darkTheme.colors.foreground}cc`, - borderLeftColor: darkTheme.colors.surface2, - paddingTop: darkTheme.spacing[3], - paddingBottom: 0, - borderTopLeftRadius: 0, - borderBottomLeftRadius: 0, - }); - expect(styles.paragraph.marginBottom).toBe(darkTheme.spacing[3]); - expect(styles.text).not.toHaveProperty("color"); - }); -}); diff --git a/packages/app/src/styles/markdown-styles.ts b/packages/app/src/styles/markdown-styles.ts deleted file mode 100644 index 5d10624136..0000000000 --- a/packages/app/src/styles/markdown-styles.ts +++ /dev/null @@ -1,422 +0,0 @@ -import { FONT_SIZE, type Theme } from "./theme"; -import { isWeb } from "@/constants/platform"; - -const webSelectableTextStyle = isWeb ? { userSelect: "text" as const } : {}; - -function contentHeadingSize(contentSize: number, tier: keyof typeof FONT_SIZE): number { - return Math.round(contentSize * (FONT_SIZE[tier] / FONT_SIZE.base)); -} - -function contentHeadingLineHeight(contentSize: number, tier: keyof typeof FONT_SIZE): number { - return Math.round(contentHeadingSize(contentSize, tier) * 1.3); -} - -/** - * Creates comprehensive markdown styles for react-native-markdown-display. - * - * Usage: - * const markdownStyles = useMemo(() => createMarkdownStyles(theme), [theme]); - * {content} - * - * Always pass `markdownit` from `@/utils/markdown-parser`. Omit it and - * react-native-markdown-display builds its own parser with `typographer: true`, - * which rewrites a literal `(c)` as ©. - */ -export function createMarkdownStyles(theme: Theme) { - return { - // ========================================================================= - // BASE STYLES - // ========================================================================= - - body: { - ...webSelectableTextStyle, - color: theme.colors.foreground, - fontSize: theme.fontSize.content, - // Prose line-height scales with the content size, not the - // code-size-coupled lineHeight.diff token used by code/diff surfaces. - lineHeight: Math.round(theme.fontSize.content * 1.4), - flexShrink: 1, - minWidth: 0, - width: "100%" as const, - }, - - text: { - ...webSelectableTextStyle, - flexShrink: 1, - minWidth: 0, - overflowWrap: "anywhere" as const, - }, - - paragraph: { - marginTop: 0, - marginBottom: theme.spacing[3], - flexWrap: "wrap" as const, - flexDirection: "row" as const, - alignItems: "flex-start" as const, - justifyContent: "flex-start" as const, - flexShrink: 1, - minWidth: 0, - width: "100%" as const, - }, - - // ========================================================================= - // HEADINGS - // ========================================================================= - - heading1: { - ...webSelectableTextStyle, - fontSize: contentHeadingSize(theme.fontSize.content, "4xl"), - fontWeight: theme.fontWeight.bold, - color: theme.colors.foreground, - marginTop: theme.spacing[6], - marginBottom: theme.spacing[3], - lineHeight: contentHeadingLineHeight(theme.fontSize.content, "4xl"), - borderBottomWidth: 1, - borderBottomColor: theme.colors.border, - paddingBottom: theme.spacing[2], - }, - - heading2: { - ...webSelectableTextStyle, - fontSize: contentHeadingSize(theme.fontSize.content, "3xl"), - fontWeight: theme.fontWeight.bold, - color: theme.colors.foreground, - marginTop: theme.spacing[6], - marginBottom: theme.spacing[3], - lineHeight: contentHeadingLineHeight(theme.fontSize.content, "3xl"), - borderBottomWidth: 1, - borderBottomColor: theme.colors.border, - paddingBottom: theme.spacing[2], - }, - - heading3: { - ...webSelectableTextStyle, - fontSize: contentHeadingSize(theme.fontSize.content, "2xl"), - fontWeight: theme.fontWeight.semibold, - color: theme.colors.foreground, - marginTop: theme.spacing[4], - marginBottom: theme.spacing[2], - lineHeight: contentHeadingLineHeight(theme.fontSize.content, "2xl"), - }, - - heading4: { - ...webSelectableTextStyle, - fontSize: contentHeadingSize(theme.fontSize.content, "xl"), - fontWeight: theme.fontWeight.semibold, - color: theme.colors.foreground, - marginTop: theme.spacing[4], - marginBottom: theme.spacing[2], - lineHeight: contentHeadingLineHeight(theme.fontSize.content, "xl"), - }, - - heading5: { - ...webSelectableTextStyle, - fontSize: contentHeadingSize(theme.fontSize.content, "lg"), - fontWeight: theme.fontWeight.semibold, - color: theme.colors.foreground, - marginTop: theme.spacing[3], - marginBottom: theme.spacing[1], - lineHeight: contentHeadingLineHeight(theme.fontSize.content, "lg"), - }, - - heading6: { - ...webSelectableTextStyle, - fontSize: contentHeadingSize(theme.fontSize.content, "lg"), - fontWeight: theme.fontWeight.semibold, - color: theme.colors.foregroundMuted, - marginTop: theme.spacing[3], - marginBottom: theme.spacing[1], - lineHeight: contentHeadingLineHeight(theme.fontSize.content, "lg"), - textTransform: "uppercase" as const, - letterSpacing: 0.5, - }, - - // ========================================================================= - // TEXT FORMATTING - // ========================================================================= - - strong: { - ...webSelectableTextStyle, - fontWeight: theme.fontWeight.medium, - }, - - em: { - ...webSelectableTextStyle, - fontStyle: "italic" as const, - }, - - s: { - ...webSelectableTextStyle, - textDecorationLine: "line-through" as const, - color: theme.colors.foregroundMuted, - }, - - link: { - ...webSelectableTextStyle, - color: theme.colors.accentBright, - textDecorationLine: "none" as const, - flexShrink: 1, - minWidth: 0, - overflowWrap: "anywhere" as const, - }, - - blocklink: { - ...webSelectableTextStyle, - color: theme.colors.accentBright, - textDecorationLine: "none" as const, - flexShrink: 1, - minWidth: 0, - overflowWrap: "anywhere" as const, - }, - - // ========================================================================= - // CODE - // ========================================================================= - - code_inline: { - ...webSelectableTextStyle, - backgroundColor: theme.colors.surface2, - color: theme.colors.foreground, - paddingHorizontal: theme.spacing[1], - paddingVertical: 2, - borderRadius: theme.borderRadius.md, - borderWidth: 0, - fontFamily: theme.fontFamily.mono, - fontSize: theme.fontSize.code, - }, - - code_block: { - ...webSelectableTextStyle, - backgroundColor: theme.colors.surface2, - color: theme.colors.foreground, - padding: theme.spacing[3], - borderRadius: theme.borderRadius.md, - fontFamily: theme.fontFamily.mono, - fontSize: theme.fontSize.code, - marginVertical: theme.spacing[2], - }, - - fence: { - ...webSelectableTextStyle, - backgroundColor: theme.colors.surface2, - color: theme.colors.foreground, - padding: theme.spacing[3], - borderRadius: theme.borderRadius.md, - borderWidth: 1, - borderColor: theme.colors.border, - fontFamily: theme.fontFamily.mono, - fontSize: theme.fontSize.code, - marginVertical: theme.spacing[3], - }, - - pre: { - marginVertical: theme.spacing[2], - }, - - // ========================================================================= - // TABLES - // ========================================================================= - - table: { - borderWidth: 1, - borderColor: theme.colors.border, - borderRadius: theme.borderRadius.md, - marginVertical: theme.spacing[3], - }, - - thead: { - backgroundColor: theme.colors.surface2, - }, - - tbody: {}, - - th: { - ...webSelectableTextStyle, - padding: theme.spacing[2], - borderBottomWidth: 1, - borderRightWidth: 1, - borderColor: theme.colors.border, - backgroundColor: theme.colors.surface2, - fontWeight: theme.fontWeight.semibold, - color: theme.colors.foreground, - fontSize: theme.fontSize.content, - textAlign: "left" as const, - }, - - tr: { - borderBottomWidth: 1, - borderColor: theme.colors.border, - flexDirection: "row" as const, - }, - - td: { - ...webSelectableTextStyle, - padding: theme.spacing[2], - borderRightWidth: 1, - borderColor: theme.colors.border, - color: theme.colors.foreground, - fontSize: theme.fontSize.content, - flex: 1, - }, - - // ========================================================================= - // LISTS - // ========================================================================= - - bullet_list: { - paddingLeft: 0, - width: "100%" as const, - }, - - ordered_list: { - paddingLeft: 0, - width: "100%" as const, - }, - - list_item: { - marginBottom: theme.spacing[1], - flexDirection: "row" as const, - alignItems: "flex-start" as const, - flexShrink: 1, - }, - - bullet_list_content: { - flex: 1, - flexShrink: 1, - }, - - ordered_list_content: { - flex: 1, - flexShrink: 1, - }, - - bullet_list_icon: { - ...webSelectableTextStyle, - color: theme.colors.foregroundMuted, - marginRight: 4, - fontSize: theme.fontSize.content, - lineHeight: Math.round(theme.fontSize.content * 1.4), - }, - - ordered_list_icon: { - ...webSelectableTextStyle, - color: theme.colors.foregroundMuted, - marginRight: 4, - fontSize: theme.fontSize.content, - fontWeight: theme.fontWeight.normal, - lineHeight: Math.round(theme.fontSize.content * 1.4), - minWidth: 12, - }, - - // ========================================================================= - // BLOCKQUOTE - // ========================================================================= - - blockquote: { - backgroundColor: theme.colors.surface1, - color: `${theme.colors.foreground}cc`, - borderLeftWidth: 4, - borderLeftColor: theme.colors.surface2, - paddingHorizontal: theme.spacing[4], - paddingTop: theme.spacing[3], - paddingBottom: 0, - marginVertical: theme.spacing[3], - borderRadius: theme.borderRadius.md, - borderTopLeftRadius: 0, - borderBottomLeftRadius: 0, - }, - - // ========================================================================= - // HORIZONTAL RULE - // ========================================================================= - - hr: { - backgroundColor: theme.colors.border, - height: 1, - marginVertical: 10, - }, - - // ========================================================================= - // IMAGES - // ========================================================================= - - image: { - borderRadius: theme.borderRadius.md, - marginVertical: theme.spacing[2], - }, - - // ========================================================================= - // BREAKS - // ========================================================================= - - hardbreak: { - height: theme.spacing[2], - }, - - softbreak: {}, - }; -} - -/** - * Creates a smaller variant of markdown styles for compact UI elements - * like thought bubbles, tooltips, or side panels. - */ -export function createCompactMarkdownStyles(theme: Theme) { - const baseStyles = createMarkdownStyles(theme); - - return { - ...baseStyles, - - body: { - ...baseStyles.body, - fontSize: theme.fontSize.content, - lineHeight: Math.round(theme.fontSize.content * 1.4), - }, - - heading1: { - ...baseStyles.heading1, - fontSize: contentHeadingSize(theme.fontSize.content, "2xl"), - marginTop: theme.spacing[4], - marginBottom: theme.spacing[2], - lineHeight: contentHeadingLineHeight(theme.fontSize.content, "2xl"), - }, - - heading2: { - ...baseStyles.heading2, - fontSize: contentHeadingSize(theme.fontSize.content, "xl"), - marginTop: theme.spacing[3], - marginBottom: theme.spacing[2], - lineHeight: contentHeadingLineHeight(theme.fontSize.content, "xl"), - }, - - heading3: { - ...baseStyles.heading3, - fontSize: contentHeadingSize(theme.fontSize.content, "lg"), - marginTop: theme.spacing[3], - marginBottom: theme.spacing[1], - lineHeight: contentHeadingLineHeight(theme.fontSize.content, "lg"), - }, - - paragraph: { - ...baseStyles.paragraph, - marginBottom: theme.spacing[2], - }, - - code_inline: { - ...baseStyles.code_inline, - fontSize: theme.fontSize.code, - }, - - code_block: { - ...baseStyles.code_block, - fontSize: theme.fontSize.code, - padding: theme.spacing[2], - }, - - fence: { - ...baseStyles.fence, - fontSize: theme.fontSize.code, - padding: theme.spacing[2], - }, - }; -} diff --git a/packages/app/src/types/react-native-markdown-display.d.ts b/packages/app/src/types/react-native-markdown-display.d.ts deleted file mode 100644 index 1b9f8044ac..0000000000 --- a/packages/app/src/types/react-native-markdown-display.d.ts +++ /dev/null @@ -1,11 +0,0 @@ -// The runtime sets `sourceInfo` on fence/code nodes (tokensToAST.js sets -// `sourceInfo: token.info`), but the shipped .d.ts omits the field. The -// re-export is what marks this file as a module so the `declare module` -// below augments the package instead of shadowing it. -export type { ASTNode } from "react-native-markdown-display"; - -declare module "react-native-markdown-display" { - interface ASTNode { - sourceInfo?: string; - } -} diff --git a/packages/app/src/types/stream.ts b/packages/app/src/types/stream.ts index d3cea1a391..9425cabbed 100644 --- a/packages/app/src/types/stream.ts +++ b/packages/app/src/types/stream.ts @@ -712,10 +712,6 @@ export interface AssistantMessageItem { text: string; timestamp: Date; /** Display-only fields, assigned after source-item plugin transforms. */ - blockGroupId?: string; - blockIndex?: number; - markdownKind?: "prose" | "quote" | "block"; - markdownProseBlocks?: readonly string[]; } export interface TimelinePosition { diff --git a/packages/app/src/utils/__tests__/split-markdown-blocks.test.ts b/packages/app/src/utils/__tests__/split-markdown-blocks.test.ts deleted file mode 100644 index 22eddf8152..0000000000 --- a/packages/app/src/utils/__tests__/split-markdown-blocks.test.ts +++ /dev/null @@ -1,127 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { splitMarkdownBlocks } from "../split-markdown-blocks"; - -describe("splitMarkdownBlocks", () => { - it("returns a single block for a single paragraph", () => { - expect(splitMarkdownBlocks("Hello world")).toEqual(["Hello world"]); - }); - - it("splits two paragraphs separated by a double newline", () => { - expect(splitMarkdownBlocks("First paragraph\n\nSecond paragraph")).toEqual([ - "First paragraph", - "Second paragraph", - ]); - }); - - it("keeps a fenced code block with internal double newlines as one block", () => { - expect(splitMarkdownBlocks("```ts\nconst a = 1;\n\nconst b = 2;\n```")).toEqual([ - "```ts\nconst a = 1;\n\nconst b = 2;\n```", - ]); - }); - - it("does not treat 4-space-indented backticks as a fence", () => { - expect(splitMarkdownBlocks("Before\n\n ```\n code\n ```\n\nAfter")).toEqual([ - "Before", - " ```\n code\n ```", - "After", - ]); - }); - - it("handles tilde fences", () => { - expect(splitMarkdownBlocks("Before\n\n~~~\ncode\n~~~\n\nAfter")).toEqual([ - "Before", - "~~~\ncode\n~~~", - "After", - ]); - }); - - it("splits mixed paragraph, code fence, and paragraph content into three blocks", () => { - expect( - splitMarkdownBlocks( - "Intro paragraph\n\n```ts\nconst a = 1;\n\nconst b = 2;\n```\n\nOutro paragraph", - ), - ).toEqual(["Intro paragraph", "```ts\nconst a = 1;\n\nconst b = 2;\n```", "Outro paragraph"]); - }); - - it("keeps everything from an unclosed fence start as one block for streaming content", () => { - expect(splitMarkdownBlocks("Before fence\n\n```ts\nconst a = 1;\n\nconst b = 2;")).toEqual([ - "Before fence", - "```ts\nconst a = 1;\n\nconst b = 2;", - ]); - }); - - it("returns an empty array for empty input", () => { - expect(splitMarkdownBlocks("")).toEqual([]); - }); - - it("splits a heading followed by a paragraph into two blocks", () => { - expect(splitMarkdownBlocks("# Heading\n\nParagraph text")).toEqual([ - "# Heading", - "Paragraph text", - ]); - }); - - it("keeps consecutive list items together when there is no double newline", () => { - expect(splitMarkdownBlocks("- First item\n- Second item\n- Third item")).toEqual([ - "- First item\n- Second item\n- Third item", - ]); - }); - - it("keeps a loose nested list in its outer list block", () => { - expect( - splitMarkdownBlocks( - "Before\n\n3. Outer three\n\n 7. Inner seven\n 8. Inner eight\n\nAfter", - ), - ).toEqual(["Before", "3. Outer three\n\n 7. Inner seven\n 8. Inner eight", "After"]); - }); - - it("keeps a link reference definition with the paragraph that uses it", () => { - expect(splitMarkdownBlocks("See the [docs][d].\n\n[d]: https://example.com")).toEqual([ - "See the [docs][d].\n\n[d]: https://example.com", - ]); - }); - - it("folds a leading definition-only block into the block below it", () => { - expect(splitMarkdownBlocks('[d]: https://example.com "Docs"\n\nSee the [docs][d].')).toEqual([ - '[d]: https://example.com "Docs"\n\nSee the [docs][d].', - ]); - }); - - it("folds several definition lines and several definition blocks into one block", () => { - expect( - splitMarkdownBlocks( - "See [one][a] and [two][b].\n\n[a]: https://example.com/a\n[b]: \n\n[c]: https://example.com/c 'Third'\n\nAfter", - ), - ).toEqual([ - "See [one][a] and [two][b].\n\n[a]: https://example.com/a\n[b]: \n\n[c]: https://example.com/c 'Third'", - "After", - ]); - }); - - it("recognizes every destination the renderer accepts, including escaped spaces", () => { - expect(splitMarkdownBlocks("See [docs].\n\n[docs]: docs\\ folder/readme")).toEqual([ - "See [docs].\n\n[docs]: docs\\ folder/readme", - ]); - expect(splitMarkdownBlocks("See [docs].\n\n[docs]: 'Title'")).toEqual([ - "See [docs].\n\n[docs]: 'Title'", - ]); - }); - - it("leaves a definition-only message as its own block", () => { - expect(splitMarkdownBlocks("[d]: https://example.com")).toEqual(["[d]: https://example.com"]); - }); - - it("does not fold a paragraph that merely starts with a bracketed link", () => { - expect(splitMarkdownBlocks("Intro\n\n[Link](https://example.com) and more prose")).toEqual([ - "Intro", - "[Link](https://example.com) and more prose", - ]); - }); - - it("treats triple newlines as a split point and filters empty blocks", () => { - expect(splitMarkdownBlocks("First paragraph\n\n\nSecond paragraph")).toEqual([ - "First paragraph", - "Second paragraph", - ]); - }); -}); diff --git a/packages/app/src/utils/assistant-image-metadata.test.ts b/packages/app/src/utils/assistant-image-metadata.test.ts deleted file mode 100644 index 3ebee656dd..0000000000 --- a/packages/app/src/utils/assistant-image-metadata.test.ts +++ /dev/null @@ -1,105 +0,0 @@ -import { beforeEach, describe, expect, it } from "vitest"; -import { DEFAULT_CONTENT_MAX_WIDTH } from "@/styles/theme"; -import { - clearAssistantImageMetadataCache, - estimateAssistantMessageHeightFromCache, - extractAssistantImageSources, - getAssistantImageMetadata, - setAssistantImageMetadata, -} from "./assistant-image-metadata"; - -describe("assistant image metadata", () => { - beforeEach(() => { - clearAssistantImageMetadataCache(); - }); - - it("extracts markdown image sources", () => { - expect( - extractAssistantImageSources( - 'Before\n\n![local](/tmp/paseo.png)\n\n![remote](https://example.com/test.png "Remote")', - ), - ).toEqual(["/tmp/paseo.png", "https://example.com/test.png"]); - }); - - it("keeps local image metadata scoped to its server and workspace", () => { - const image = { - source: "screenshot.png", - workspaceRoot: "/workspace/one", - serverId: "server-1", - }; - setAssistantImageMetadata(image, { width: 1200, height: 800 }); - - expect(getAssistantImageMetadata({ ...image, workspaceRoot: "/workspace/two" })).toBeNull(); - expect(getAssistantImageMetadata({ ...image, serverId: "server-2" })).toBeNull(); - expect(getAssistantImageMetadata({ source: image.source })).toBeNull(); - expect(getAssistantImageMetadata(image)).toEqual({ - width: 1200, - height: 800, - aspectRatio: 1.5, - }); - - setAssistantImageMetadata( - { ...image, workspaceRoot: "/workspace/two" }, - { width: 800, height: 1200 }, - ); - expect(getAssistantImageMetadata(image)?.aspectRatio).toBe(1.5); - expect( - getAssistantImageMetadata({ ...image, workspaceRoot: "/workspace/two" })?.aspectRatio, - ).toBe(2 / 3); - }); - - it("reuses direct image metadata without workspace context", () => { - const source = "https://example.com/shared.png"; - setAssistantImageMetadata( - { source, workspaceRoot: "/workspace/one", serverId: "server-1" }, - { width: 1200, height: 800 }, - ); - expect(getAssistantImageMetadata({ source })).toEqual({ - width: 1200, - height: 800, - aspectRatio: 1.5, - }); - }); - - it("reserves the default image aspect ratio before metadata arrives", () => { - expect( - estimateAssistantMessageHeightFromCache({ - markdown: "![Pending](https://example.com/pending.png)", - contentMaxWidth: 608, - }), - ).toBe(464); - }); - - it("estimates assistant message height from cached image metadata", () => { - setAssistantImageMetadata( - { - source: "https://example.com/landscape.png", - }, - { width: 1200, height: 800 }, - ); - - expect( - estimateAssistantMessageHeightFromCache({ - markdown: "Here is the screenshot\n\n![Screenshot](https://example.com/landscape.png)", - contentMaxWidth: DEFAULT_CONTENT_MAX_WIDTH, - }), - ).toBeGreaterThan(220); - }); - - it("estimates image-only data-image markdown without caching the full payload as text", () => { - const source = `data:image/png;base64,${"a".repeat(512)}`; - setAssistantImageMetadata({ source }, { width: 1200, height: 800 }); - - const imageOnlyHeight = estimateAssistantMessageHeightFromCache({ - markdown: `![Screenshot](${source})`, - contentMaxWidth: DEFAULT_CONTENT_MAX_WIDTH, - }); - const mixedHeight = estimateAssistantMessageHeightFromCache({ - markdown: `Text\n\n![Screenshot](${source})`, - contentMaxWidth: DEFAULT_CONTENT_MAX_WIDTH, - }); - - expect(imageOnlyHeight).toBeGreaterThan(220); - expect(mixedHeight).toBeGreaterThan(imageOnlyHeight ?? 0); - }); -}); diff --git a/packages/app/src/utils/assistant-image-metadata.ts b/packages/app/src/utils/assistant-image-metadata.ts deleted file mode 100644 index ac7bd217b2..0000000000 --- a/packages/app/src/utils/assistant-image-metadata.ts +++ /dev/null @@ -1,196 +0,0 @@ -import { resolveAssistantImageSource } from "@/utils/assistant-image-source"; -import { createImageSourceCacheKey } from "@/attachments/utils"; - -export interface AssistantImageContext { - serverId?: string; - workspaceRoot?: string; -} - -export interface AssistantImageMetadata { - width: number; - height: number; - aspectRatio: number; -} - -const assistantImageMetadataCache = new Map(); -const assistantImageParseCache = new Map(); -const ASSISTANT_IMAGE_METADATA_CACHE_LIMIT = 500; -const ASSISTANT_IMAGE_PARSE_CACHE_LIMIT = 500; - -const MARKDOWN_IMAGE_PATTERN = /!\[[^\]]*]\((<[^>]+>|[^)\n]+)\)/g; -export const ASSISTANT_IMAGE_DEFAULT_ASPECT_RATIO = 3 / 2; -const ASSISTANT_IMAGE_INSET = 8; -const ASSISTANT_IMAGE_MIN_HEIGHT = 160; -const ASSISTANT_IMAGE_BLOCK_GAP = 24; -const ASSISTANT_MESSAGE_BASE_HEIGHT = 96; -const ASSISTANT_MESSAGE_MIN_HEIGHT = 220; -const ASSISTANT_MESSAGE_IMAGE_ONLY_BASE_HEIGHT = 40; - -function touchCacheEntry(cache: Map, key: K, value: V, limit: number): void { - cache.delete(key); - cache.set(key, value); - if (cache.size <= limit) { - return; - } - const oldestKey = cache.keys().next().value; - if (oldestKey !== undefined) { - cache.delete(oldestKey); - } -} - -function normalizeAssistantImageSourceToken(value: string): string | null { - const trimmed = value.trim(); - if (!trimmed) { - return null; - } - - if (trimmed.startsWith("<") && trimmed.endsWith(">")) { - const inner = trimmed.slice(1, -1).trim(); - return inner || null; - } - - const titleMatch = /^(.*?)(?:\s+(['"]).*?\2)?$/.exec(trimmed); - const source = titleMatch?.[1]?.trim() ?? trimmed; - return source || null; -} - -function parseAssistantImageMarkdown(markdown: string): { - sources: string[]; - hasNonImageText: boolean; -} { - const sources: string[] = []; - for (const match of markdown.matchAll(MARKDOWN_IMAGE_PATTERN)) { - const normalized = normalizeAssistantImageSourceToken(match[1] ?? ""); - if (normalized) { - sources.push(normalized); - } - } - return { - sources, - hasNonImageText: markdown.replace(MARKDOWN_IMAGE_PATTERN, "").trim().length > 0, - }; -} - -function getAssistantImageMetadataKey( - input: AssistantImageContext & { source: string }, -): string | null { - const resolution = resolveAssistantImageSource({ - source: input.source, - workspaceRoot: input.workspaceRoot, - }); - if (!resolution) { - return null; - } - if (resolution.kind === "direct") { - return `direct:${createImageSourceCacheKey(resolution.uri)}`; - } - // A file path identifies an image only within its daemon and workspace. - // Callers without that context must use the default aspect ratio. - if (!input.serverId) { - return null; - } - return JSON.stringify(["file", input.serverId, resolution.cwd, resolution.path]); -} - -export function getAssistantImageMetadata( - input: AssistantImageContext & { source: string }, -): AssistantImageMetadata | null { - const key = getAssistantImageMetadataKey(input); - const metadata = key ? assistantImageMetadataCache.get(key) : undefined; - if (!key || !metadata) { - return null; - } - touchCacheEntry(assistantImageMetadataCache, key, metadata, ASSISTANT_IMAGE_METADATA_CACHE_LIMIT); - return metadata; -} - -export function setAssistantImageMetadata( - input: AssistantImageContext & { source: string }, - dimensions: { width: number; height: number }, -): AssistantImageMetadata | null { - const { width, height } = dimensions; - if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0) { - return null; - } - - const metadata: AssistantImageMetadata = { - width, - height, - aspectRatio: width / height, - }; - - const key = getAssistantImageMetadataKey(input); - if (key) { - touchCacheEntry( - assistantImageMetadataCache, - key, - metadata, - ASSISTANT_IMAGE_METADATA_CACHE_LIMIT, - ); - } - - return metadata; -} - -export function extractAssistantImageSources(markdown: string): string[] { - const shouldCacheParse = !/data:image\//i.test(markdown); - const cachedParse = shouldCacheParse ? assistantImageParseCache.get(markdown) : undefined; - if (cachedParse) { - touchCacheEntry( - assistantImageParseCache, - markdown, - cachedParse, - ASSISTANT_IMAGE_PARSE_CACHE_LIMIT, - ); - return cachedParse.sources; - } - - const parsed = parseAssistantImageMarkdown(markdown); - if (shouldCacheParse) { - touchCacheEntry(assistantImageParseCache, markdown, parsed, ASSISTANT_IMAGE_PARSE_CACHE_LIMIT); - } - return parsed.sources; -} - -export interface AssistantMessageHeightEstimateInput { - markdown: string; - contentMaxWidth: number; - imageContext?: AssistantImageContext; -} - -export function estimateAssistantMessageHeightFromCache({ - markdown, - contentMaxWidth, - imageContext, -}: AssistantMessageHeightEstimateInput): number | null { - const parsed = assistantImageParseCache.get(markdown) ?? parseAssistantImageMarkdown(markdown); - if (parsed.sources.length === 0) { - return null; - } - - const imageHeights = parsed.sources.map((source) => { - const aspectRatio = - getAssistantImageMetadata({ source, ...imageContext })?.aspectRatio ?? - ASSISTANT_IMAGE_DEFAULT_ASPECT_RATIO; - return Math.max( - ASSISTANT_IMAGE_MIN_HEIGHT, - Math.round((contentMaxWidth - ASSISTANT_IMAGE_INSET) / aspectRatio), - ); - }); - - const baseHeight = parsed.hasNonImageText - ? ASSISTANT_MESSAGE_BASE_HEIGHT - : ASSISTANT_MESSAGE_IMAGE_ONLY_BASE_HEIGHT; - - const estimatedHeight = - baseHeight + - imageHeights.reduce((sum, height) => sum + height, 0) + - ASSISTANT_IMAGE_BLOCK_GAP * imageHeights.length; - - return Math.max(ASSISTANT_MESSAGE_MIN_HEIGHT, estimatedHeight); -} - -export function clearAssistantImageMetadataCache(): void { - assistantImageMetadataCache.clear(); - assistantImageParseCache.clear(); -} diff --git a/packages/app/src/utils/assistant-markdown-parser.test.ts b/packages/app/src/utils/assistant-markdown-parser.test.ts index 05b1366e1c..f22307239a 100644 --- a/packages/app/src/utils/assistant-markdown-parser.test.ts +++ b/packages/app/src/utils/assistant-markdown-parser.test.ts @@ -2,141 +2,6 @@ import { describe, expect, it } from "vitest"; import { createAssistantMarkdownParser } from "./assistant-markdown-parser"; describe("createAssistantMarkdownParser", () => { - it("keeps bold text bold through every partial closing marker", () => { - const parser = createAssistantMarkdownParser({ streaming: true }); - - for (const source of ["**bold", "**bold*", "**bold**"]) { - expect(parser.renderInline(source)).toBe("bold"); - } - }); - - it("shows the growing link label and only links a complete destination", () => { - const parser = createAssistantMarkdownParser({ streaming: true }); - const source = "[docs](https://example.com/path)"; - for (let length = 1; length < source.length; length++) { - expect(parser.renderInline(source.slice(0, length))).toBe( - "docs".slice(0, Math.max(0, length - 1)), - ); - } - expect(parser.renderInline(source)).toBe('
docs'); - }); - - it.each([ - ["**", "strong"], - ["__", "strong"], - ["*", "em"], - ["_", "em"], - ["~~", "s"], - ["`", "code"], - ["``", "code"], - ])("keeps %s formatting stable as text and closing markers arrive", (marker, tag) => { - const parser = createAssistantMarkdownParser({ streaming: true }); - for (let length = 1; length <= 4; length++) { - expect(parser.renderInline(marker + "text".slice(0, length))).toBe( - `<${tag}>${"text".slice(0, length)}`, - ); - } - for (let length = 0; length <= marker.length; length++) { - expect(parser.renderInline(marker + "text" + marker.slice(0, length))).toBe( - `<${tag}>text`, - ); - } - }); - - it("keeps combined and nested emphasis stable through closing markers", () => { - const parser = createAssistantMarkdownParser({ streaming: true }); - for (const closing of ["", "*", "**", "***"]) { - expect(parser.renderInline("***both" + closing)).toBe("both"); - } - expect(parser.renderInline("**bold and *italic")).toBe( - "bold and italic", - ); - expect(parser.renderInline("*italic and **bold")).toBe( - "italic and bold", - ); - }); - - it.each(["*", "**", "***", "_", "__", "~~", "`", "``"])( - "hides an opening %s while waiting for its text", - (marker) => { - expect( - createAssistantMarkdownParser({ streaming: true }).renderInline("hello " + marker), - ).toBe("hello "); - }, - ); - - it.each([ - "[docs](https://example.com/a(b)c)", - '[docs](https://example.com "a title)")', - "[docs]()", - "[docs](file:///tmp/example.ts)", - ])("waits for the entire destination of %s", (source) => { - const parser = createAssistantMarkdownParser({ streaming: true }); - for (let length = 6; length < source.length; length++) { - expect(parser.renderInline(source.slice(0, length))).toBe("docs"); - } - expect(parser.renderInline(source)).toBe(createAssistantMarkdownParser().renderInline(source)); - }); - - it("preserves formatting in incomplete labels and around incomplete links", () => { - const parser = createAssistantMarkdownParser({ streaming: true }); - expect(parser.renderInline("[**bold")).toBe("bold"); - expect(parser.renderInline("[**bold**](https://exam")).toBe("bold"); - expect(parser.renderInline("**see [docs](https://exam")).toBe("see docs"); - }); - - it("does not auto-link a URL label before the destination is complete", () => { - const parser = createAssistantMarkdownParser({ streaming: true }); - expect(parser.renderInline("Read [example.com](https://exam")).toBe("Read example.com"); - expect(parser.renderInline("Read [example.com](https://example.org)")).toBe( - 'Read example.com', - ); - }); - - it.each([ - "\\*literal", - "\\[literal", - "some_identifier", - "some__identifier", - "`**literal [link](url`", - "``a ` b``", - "**already closed** after", - "[bad](javascript:alert(1))", - " { - expect(createAssistantMarkdownParser({ streaming: true }).renderInline(source)).toBe( - createAssistantMarkdownParser().renderInline(source), - ); - }); - - it.each([ - "```md\n**literal [link](url", - "~~~md\n**literal [link](url", - " **literal [link](url", - "**earlier\n\nplain tail", - "- **earlier\n- plain tail", - ])("leaves literal code and earlier blocks alone: %s", (source) => { - expect(createAssistantMarkdownParser({ streaming: true }).render(source)).toBe( - createAssistantMarkdownParser().render(source), - ); - }); - - it("completes inline formatting inside the final list item", () => { - expect(createAssistantMarkdownParser({ streaming: true }).render("- first\n- **bold")).toBe( - "
    \n
  • first
  • \n
  • bold
  • \n
\n", - ); - }); - - it("hides incomplete images until their source is complete", () => { - const parser = createAssistantMarkdownParser({ streaming: true }); - expect(parser.renderInline("before ![alt](https://exam")).toBe("before "); - expect(parser.renderInline("before ![alt](https://example.com/image.png)")).toBe( - 'before alt', - ); - }); - it("keeps ordinary parsing for completed messages", () => { const parser = createAssistantMarkdownParser(); expect(parser.renderInline("**unfinished")).toBe("**unfinished"); diff --git a/packages/app/src/utils/assistant-markdown-parser.ts b/packages/app/src/utils/assistant-markdown-parser.ts index e1e8f6f38a..1007fc5c93 100644 --- a/packages/app/src/utils/assistant-markdown-parser.ts +++ b/packages/app/src/utils/assistant-markdown-parser.ts @@ -1,19 +1,13 @@ import type MarkdownIt from "markdown-it"; import { createMarkdownParser } from "@/utils/markdown-parser"; -import { enableStreamingMarkdown } from "@/utils/streaming-markdown"; -export function createAssistantMarkdownParser({ streaming = false } = {}): MarkdownIt { +export function createAssistantMarkdownParser(): MarkdownIt { const parser = createMarkdownParser({ linkify: true }); const defaultValidateLink = parser.validateLink.bind(parser); - // Assistant messages are the only surface allowed to link into the - // filesystem. Every other parser keeps markdown-it's stricter default. + // Rich clipboard HTML preserves workspace file links. parser.validateLink = (url: string) => url.trim().toLowerCase().startsWith("file://") || defaultValidateLink(url); - if (streaming) { - enableStreamingMarkdown(parser); - } - return parser; } diff --git a/packages/app/src/utils/assistant-message-height-estimate.test.ts b/packages/app/src/utils/assistant-message-height-estimate.test.ts index d4e3a2ff94..545704a57d 100644 --- a/packages/app/src/utils/assistant-message-height-estimate.test.ts +++ b/packages/app/src/utils/assistant-message-height-estimate.test.ts @@ -3,69 +3,30 @@ import { DEFAULT_CONTENT_MAX_WIDTH } from "@/styles/theme"; import { clearAssistantMessageHeightEstimateCache, estimateAssistantMessageHeightFromCache, - setAssistantMarkdownBlockHeight, + setAssistantMessageHeight, } from "./assistant-message-height-estimate"; -import { - clearAssistantImageMetadataCache, - setAssistantImageMetadata, -} from "./assistant-image-metadata"; - describe("assistant message height estimate", () => { beforeEach(() => { clearAssistantMessageHeightEstimateCache(); - clearAssistantImageMetadataCache(); }); - it("estimates assistant message height from measured markdown block heights", () => { - setAssistantMarkdownBlockHeight({ - block: "First paragraph", - width: 804, - height: 18.2, + it("uses a whole-message measurement only at the measured width", () => { + setAssistantMessageHeight({ + markdown: "First\n\nSecond", + width: DEFAULT_CONTENT_MAX_WIDTH, + height: 97, }); - setAssistantMarkdownBlockHeight({ - block: "Second paragraph", - width: 804, - height: 41.1, - }); - expect( estimateAssistantMessageHeightFromCache({ - markdown: "First paragraph\n\nSecond paragraph", + markdown: "First\n\nSecond", contentMaxWidth: DEFAULT_CONTENT_MAX_WIDTH, }), ).toBe(97); - }); - - it("reads block heights measured at the configured content width", () => { - setAssistantMarkdownBlockHeight({ block: "Wide paragraph", width: 1584, height: 20 }); - expect( estimateAssistantMessageHeightFromCache({ - markdown: "Wide paragraph", + markdown: "First\n\nSecond", contentMaxWidth: 1600, }), - ).toBe(44); - expect( - estimateAssistantMessageHeightFromCache({ - markdown: "Wide paragraph", - contentMaxWidth: DEFAULT_CONTENT_MAX_WIDTH, - }), ).toBeNull(); }); - - it("falls back to image metadata when markdown blocks are not measured", () => { - setAssistantImageMetadata( - { - source: "https://example.com/landscape.png", - }, - { width: 1200, height: 800 }, - ); - - expect( - estimateAssistantMessageHeightFromCache({ - markdown: "Here is the screenshot\n\n![Screenshot](https://example.com/landscape.png)", - contentMaxWidth: DEFAULT_CONTENT_MAX_WIDTH, - }), - ).toBeGreaterThan(220); - }); }); diff --git a/packages/app/src/utils/assistant-message-height-estimate.ts b/packages/app/src/utils/assistant-message-height-estimate.ts index 02e153e0bb..a7d34b115b 100644 --- a/packages/app/src/utils/assistant-message-height-estimate.ts +++ b/packages/app/src/utils/assistant-message-height-estimate.ts @@ -1,20 +1,11 @@ -import { - type AssistantMessageHeightEstimateInput, - estimateAssistantMessageHeightFromCache as estimateAssistantImageMessageHeightFromCache, -} from "@/utils/assistant-image-metadata"; -import { splitMarkdownBlocks } from "@/utils/split-markdown-blocks"; +const ASSISTANT_MESSAGE_HEIGHT_CACHE_LIMIT = 1000; -const ASSISTANT_MARKDOWN_BLOCK_HEIGHT_CACHE_LIMIT = 1000; -const ASSISTANT_MARKDOWN_BLOCK_INSET = 16; -const ASSISTANT_MESSAGE_VERTICAL_PADDING = 24; -const ASSISTANT_MARKDOWN_BLOCK_GAP = 12; - -interface MarkdownBlockHeightInput { - block: string; +interface MessageHeightInput { + markdown: string; width: number; } -const assistantMarkdownBlockHeightCache = new Map(); +const assistantMessageHeightCache = new Map(); function touchCacheEntry(cache: Map, key: K, value: V, limit: number): void { cache.delete(key); @@ -28,98 +19,67 @@ function touchCacheEntry(cache: Map, key: K, value: V, limit: number } } -function hashMarkdownBlock(block: string): string { +function hashMessage(markdown: string): string { let hash = 2166136261; - for (let index = 0; index < block.length; index += 1) { - hash ^= block.charCodeAt(index); + for (let index = 0; index < markdown.length; index += 1) { + hash ^= markdown.charCodeAt(index); hash = Math.imul(hash, 16777619); } - return `${block.length}:${(hash >>> 0).toString(36)}`; + return `${markdown.length}:${(hash >>> 0).toString(36)}`; } -function normalizeMarkdownBlockWidth(width: number): number | null { +function normalizeMessageWidth(width: number): number | null { if (!Number.isFinite(width) || width <= 0) { return null; } return Math.round(width); } -function createMarkdownBlockHeightKey(input: MarkdownBlockHeightInput): string | null { - const normalizedWidth = normalizeMarkdownBlockWidth(input.width); +function createMessageHeightKey(input: MessageHeightInput): string | null { + const normalizedWidth = normalizeMessageWidth(input.width); if (normalizedWidth === null) { return null; } - if (input.block.length === 0) { + if (input.markdown.length === 0) { return null; } - return `${normalizedWidth}:${hashMarkdownBlock(input.block)}`; + return `${normalizedWidth}:${hashMessage(input.markdown)}`; } -export function setAssistantMarkdownBlockHeight(input: { - block: string; +export function setAssistantMessageHeight(input: { + markdown: string; width: number; height: number; }): number | null { if (!Number.isFinite(input.height) || input.height <= 0) { return null; } - const key = createMarkdownBlockHeightKey({ - block: input.block, + const key = createMessageHeightKey({ + markdown: input.markdown, width: input.width, }); if (!key) { return null; } const height = Math.ceil(input.height); - touchCacheEntry( - assistantMarkdownBlockHeightCache, - key, - height, - ASSISTANT_MARKDOWN_BLOCK_HEIGHT_CACHE_LIMIT, - ); + touchCacheEntry(assistantMessageHeightCache, key, height, ASSISTANT_MESSAGE_HEIGHT_CACHE_LIMIT); return height; } -function estimateAssistantMarkdownBlockHeightFromCache( - markdown: string, - contentMaxWidth: number, -): number | null { - const blocks = splitMarkdownBlocks(markdown); - if (blocks.length === 0) { - return null; - } - - let blockHeight = 0; - for (const block of blocks) { - const key = createMarkdownBlockHeightKey({ - block, - width: contentMaxWidth - ASSISTANT_MARKDOWN_BLOCK_INSET, - }); - const cachedHeight = key ? assistantMarkdownBlockHeightCache.get(key) : undefined; - if (cachedHeight === undefined) { - return null; - } - blockHeight += cachedHeight; - } - - return ( - ASSISTANT_MESSAGE_VERTICAL_PADDING + - blockHeight + - ASSISTANT_MARKDOWN_BLOCK_GAP * Math.max(0, blocks.length - 1) - ); -} - export function estimateAssistantMessageHeightFromCache({ markdown, contentMaxWidth, - imageContext, -}: AssistantMessageHeightEstimateInput): number | null { +}: { + markdown: string; + contentMaxWidth: number; +}): number | null { return ( - estimateAssistantMarkdownBlockHeightFromCache(markdown, contentMaxWidth) ?? - estimateAssistantImageMessageHeightFromCache({ markdown, contentMaxWidth, imageContext }) + assistantMessageHeightCache.get( + createMessageHeightKey({ markdown: markdown, width: contentMaxWidth }) ?? "", + ) ?? null ); } export function clearAssistantMessageHeightEstimateCache(): void { - assistantMarkdownBlockHeightCache.clear(); + assistantMessageHeightCache.clear(); } diff --git a/packages/app/src/utils/markdown-ast.test.ts b/packages/app/src/utils/markdown-ast.test.ts deleted file mode 100644 index e7b439d79f..0000000000 --- a/packages/app/src/utils/markdown-ast.test.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { markdownNodeContainsType } from "./markdown-ast"; - -describe("markdownNodeContainsType", () => { - it("matches the node itself", () => { - expect(markdownNodeContainsType({ type: "image", children: [] }, "image")).toBe(true); - }); - - it("matches descendants", () => { - const paragraph = { - type: "paragraph", - children: [ - { type: "text", children: [] }, - { - type: "link", - children: [{ type: "image", children: [] }], - }, - ], - }; - - expect(markdownNodeContainsType(paragraph, "image")).toBe(true); - }); - - it("returns false when the type is absent", () => { - const paragraph = { - type: "paragraph", - children: [ - { type: "text", children: [] }, - { type: "strong", children: [{ type: "text", children: [] }] }, - ], - }; - - expect(markdownNodeContainsType(paragraph, "image")).toBe(false); - }); -}); diff --git a/packages/app/src/utils/markdown-ast.ts b/packages/app/src/utils/markdown-ast.ts deleted file mode 100644 index bcd2526e41..0000000000 --- a/packages/app/src/utils/markdown-ast.ts +++ /dev/null @@ -1,12 +0,0 @@ -export interface MarkdownAstNodeWithChildren { - type: string; - children: MarkdownAstNodeWithChildren[]; -} - -export function markdownNodeContainsType(node: MarkdownAstNodeWithChildren, type: string): boolean { - if (node.type === type) { - return true; - } - - return node.children.some((child) => markdownNodeContainsType(child, type)); -} diff --git a/packages/app/src/utils/markdown-list.test.ts b/packages/app/src/utils/markdown-list.test.ts deleted file mode 100644 index a30a69a9eb..0000000000 --- a/packages/app/src/utils/markdown-list.test.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { getMarkdownListMarker, getMarkdownListSpacing } from "./markdown-list"; - -describe("getMarkdownListMarker", () => { - it("returns a bullet marker for unordered list items", () => { - expect(getMarkdownListMarker({ index: 0 }, [{ type: "bullet_list" }])).toEqual({ - isOrdered: false, - marker: "•", - }); - }); - - it("returns numbered markers for ordered list items", () => { - expect(getMarkdownListMarker({ index: 1, markup: "." }, [{ type: "ordered_list" }])).toEqual({ - isOrdered: true, - marker: "2.", - }); - }); - - it("respects ordered list start attribute", () => { - expect( - getMarkdownListMarker({ index: 2, markup: ")" }, [ - { type: "ordered_list", attributes: { start: "5" } }, - ]), - ).toEqual({ - isOrdered: true, - marker: "7)", - }); - }); - - it("prefers the nearest list ancestor in nested lists", () => { - expect( - getMarkdownListMarker({ index: 0, markup: "." }, [ - { type: "ordered_list" }, - { type: "bullet_list" }, - ]), - ).toEqual({ - isOrdered: true, - marker: "1.", - }); - }); -}); - -describe("getMarkdownListSpacing", () => { - it("keeps top-level list spacing as a section boundary", () => { - const paragraph = { type: "paragraph" }; - const list = { type: "bullet_list" }; - const body = { type: "body", children: [list, paragraph] }; - - expect(getMarkdownListSpacing(list, [body])).toEqual({ - marginTop: 4, - marginBottom: 16, - }); - }); - - it("does not add bottom spacing after a list at the end of a markdown block", () => { - const list = { type: "bullet_list" }; - const body = { type: "body", children: [list] }; - - expect(getMarkdownListSpacing(list, [body])).toEqual({ - marginTop: 4, - marginBottom: 0, - }); - }); - - it("uses a smaller gap between adjacent top-level lists", () => { - const list = { type: "bullet_list" }; - const body = { type: "body", children: [list, { type: "ordered_list" }] }; - - expect(getMarkdownListSpacing(list, [body])).toEqual({ - marginTop: 4, - marginBottom: 8, - }); - }); - - it("does not add section spacing after a nested list", () => { - expect( - getMarkdownListSpacing({ type: "bullet_list" }, [ - { type: "list_item" }, - { type: "bullet_list" }, - { type: "body" }, - ]), - ).toEqual({ - marginTop: 4, - marginBottom: 0, - }); - }); -}); diff --git a/packages/app/src/utils/markdown-list.ts b/packages/app/src/utils/markdown-list.ts deleted file mode 100644 index 3dd5150242..0000000000 --- a/packages/app/src/utils/markdown-list.ts +++ /dev/null @@ -1,147 +0,0 @@ -import { SPACING } from "@/styles/theme"; - -interface MarkdownNode { - type?: string; - index?: number; - markup?: string; - attributes?: { - start?: number | string; - }; - children?: MarkdownNode[]; -} - -const LIST_BULLET = "•"; -const DEFAULT_ORDERED_LIST_MARKUP = "."; -const MARKDOWN_LIST_MARGIN_TOP = SPACING[1]; -const MARKDOWN_LIST_MARGIN_BOTTOM_TO_PROSE = SPACING[4]; -const MARKDOWN_LIST_MARGIN_BOTTOM_TO_LIST = SPACING[2]; -const MARKDOWN_NESTED_LIST_MARGIN_BOTTOM = 0; -const MARKDOWN_TERMINAL_LIST_MARGIN_BOTTOM = 0; - -function toParentNodes(parent: unknown): MarkdownNode[] { - if (Array.isArray(parent)) { - return parent; - } - - if (parent && typeof parent === "object") { - return [parent as MarkdownNode]; - } - - return []; -} - -function getNearestListParent(parent: unknown): MarkdownNode | undefined { - return toParentNodes(parent).find( - (ancestor) => ancestor?.type === "ordered_list" || ancestor?.type === "bullet_list", - ); -} - -function getOrderedListItemIndex(node: MarkdownNode, listParent: MarkdownNode): number { - if (typeof node.index === "number" && Number.isFinite(node.index) && node.index >= 0) { - return node.index; - } - - if (Array.isArray(listParent.children)) { - const fallbackIndex = listParent.children.indexOf(node); - if (fallbackIndex >= 0) { - return fallbackIndex; - } - } - - return 0; -} - -function parseOrderedListStart(node: MarkdownNode): number { - const rawStart = node.attributes?.start; - if (typeof rawStart === "number" && Number.isFinite(rawStart)) { - return rawStart; - } - - if (typeof rawStart === "string") { - const parsed = Number.parseInt(rawStart, 10); - if (Number.isFinite(parsed)) { - return parsed; - } - } - - return 1; -} - -export function getMarkdownNextSiblingType( - node: MarkdownNode, - parent: unknown, -): string | undefined { - const ancestors = toParentNodes(parent); - for (let i = ancestors.length - 1; i >= 0; i--) { - const ancestor = ancestors[i]; - if (!Array.isArray(ancestor?.children)) continue; - const idx = ancestor.children.indexOf(node); - if (idx >= 0) { - return ancestor.children[idx + 1]?.type; - } - } - return undefined; -} - -function isListType(type: string | undefined): boolean { - return type === "bullet_list" || type === "ordered_list"; -} - -function hasListItemAncestor(parent: unknown): boolean { - return toParentNodes(parent).some((ancestor) => ancestor?.type === "list_item"); -} - -export function getMarkdownListSpacing( - node: MarkdownNode, - parent: unknown, -): { marginTop: number; marginBottom: number } { - if (hasListItemAncestor(parent)) { - return { - marginTop: MARKDOWN_LIST_MARGIN_TOP, - marginBottom: MARKDOWN_NESTED_LIST_MARGIN_BOTTOM, - }; - } - - const nextType = getMarkdownNextSiblingType(node, parent); - if (!nextType) { - return { - marginTop: MARKDOWN_LIST_MARGIN_TOP, - marginBottom: MARKDOWN_TERMINAL_LIST_MARGIN_BOTTOM, - }; - } - - return { - marginTop: MARKDOWN_LIST_MARGIN_TOP, - marginBottom: isListType(nextType) - ? MARKDOWN_LIST_MARGIN_BOTTOM_TO_LIST - : MARKDOWN_LIST_MARGIN_BOTTOM_TO_PROSE, - }; -} - -export function getMarkdownListMarker( - node: MarkdownNode, - parent: unknown, -): { - isOrdered: boolean; - marker: string; -} { - const listParent = getNearestListParent(parent); - if (!listParent || listParent.type !== "ordered_list") { - return { - isOrdered: false, - marker: LIST_BULLET, - }; - } - - const orderedIndex = getOrderedListItemIndex(node, listParent); - const orderedStart = parseOrderedListStart(listParent); - const orderedMarkup = - typeof node.markup === "string" && node.markup.length > 0 - ? node.markup - : DEFAULT_ORDERED_LIST_MARKUP; - - return { - isOrdered: true, - marker: `${orderedStart + orderedIndex}${orderedMarkup}`, - }; -} diff --git a/packages/app/src/utils/split-markdown-blocks.ts b/packages/app/src/utils/split-markdown-blocks.ts deleted file mode 100644 index 8282fc3ca9..0000000000 --- a/packages/app/src/utils/split-markdown-blocks.ts +++ /dev/null @@ -1,92 +0,0 @@ -import MarkdownIt from "markdown-it"; - -// Only block maps are needed here; inline parsing belongs to each rendered block. -const markdownBlockParser = new MarkdownIt(); -markdownBlockParser.core.ruler.disable("inline"); - -// The renderer decides what counts as a definition, so ask the same parser: a block -// that produces no tokens but registers references is nothing but definitions. -function isLinkReferenceDefinitionBlock(block: string): boolean { - const env: { references?: Record } = {}; - const tokens = markdownBlockParser.parse(block, env); - return tokens.length === 0 && Object.keys(env.references ?? {}).length > 0; -} - -/** - * Definitions render nothing and resolve nothing on their own, so a block made only of - * them would paint an empty row and break every reference that pointed at it. Fold it - * into the block it belongs to: the one above, or the one below when it leads. - */ -function foldLinkReferenceDefinitions(blocks: string[]): string[] { - const folded: string[] = []; - let leading: string[] = []; - for (const block of blocks) { - if (isLinkReferenceDefinitionBlock(block)) { - if (folded.length > 0) folded[folded.length - 1] += `\n\n${block}`; - else leading.push(block); - continue; - } - folded.push([...leading, block].join("\n\n")); - leading = []; - } - if (leading.length > 0) folded.push(leading.join("\n\n")); - return folded; -} - -export function splitMarkdownBlocks(text: string): string[] { - if (text.length === 0) { - return []; - } - - const blocks: string[] = []; - let currentLines: string[] = []; - let sawBlockSeparator = false; - const lines = text.split("\n"); - const structuralBlankLines = getStructuralBlankLines(text, lines); - - for (const [index, line] of lines.entries()) { - const isBlankLine = line.trim().length === 0; - - if (isBlankLine && structuralBlankLines.has(index)) { - currentLines.push(line); - continue; - } - - if (isBlankLine) { - if (currentLines.length > 0) { - sawBlockSeparator = true; - } - continue; - } - - if (sawBlockSeparator) { - blocks.push(currentLines.join("\n")); - currentLines = []; - sawBlockSeparator = false; - } - - currentLines.push(line); - } - - if (currentLines.length > 0) { - blocks.push(currentLines.join("\n")); - } - - return foldLinkReferenceDefinitions(blocks.filter((block) => block.length > 0)); -} - -function getStructuralBlankLines(text: string, lines: string[]): Set { - const blankLines = new Set(); - for (const token of markdownBlockParser.parse(text, {})) { - if (token.level !== 0 || !token.map) { - continue; - } - const [start, end] = token.map; - for (let index = start; index < end - 1; index += 1) { - if (lines[index]?.trim().length === 0) { - blankLines.add(index); - } - } - } - return blankLines; -} diff --git a/packages/app/src/utils/streaming-markdown/index.ts b/packages/app/src/utils/streaming-markdown/index.ts deleted file mode 100644 index b70b464d76..0000000000 --- a/packages/app/src/utils/streaming-markdown/index.ts +++ /dev/null @@ -1,153 +0,0 @@ -import type MarkdownIt from "markdown-it"; -import type StateInline from "markdown-it/lib/rules_inline/state_inline.mjs"; - -const STREAMING_TAIL = Symbol("streaming markdown tail"); - -/** Adds provisional inline formatting without changing the source or literal code blocks. */ -export function enableStreamingMarkdown(parser: MarkdownIt): void { - parser.core.ruler.at("inline", (state) => { - const tail = state.tokens.findLast((token) => token.nesting !== -1); - for (const token of state.tokens) { - if (token.type === "inline") { - const env = { ...state.env, [STREAMING_TAIL]: token === tail }; - token.children = []; - state.md.inline.parse(token.content, state.md, env, token.children); - } - } - }); - parser.inline.ruler.before("backticks", "streaming_code", completeCode); - parser.inline.ruler.before("strikethrough", "streaming_marker", hideOpeningMarker); - parser.inline.ruler.after("image", "streaming_link", completeLink); - parser.inline.ruler2.after("balance_pairs", "streaming_emphasis", completeEmphasis); - parser.core.ruler.after("linkify", "streaming_link_text", (state) => { - for (const block of state.tokens) { - for (const token of block.children ?? []) { - if (token.type === "streaming_link_text") token.type = "text"; - } - } - }); -} - -function isStreamingTail(state: StateInline): boolean { - return state.env[STREAMING_TAIL] === true && state.posMax === state.src.length; -} - -function hideOpeningMarker(state: StateInline, silent: boolean): boolean { - if (silent || !isStreamingTail(state)) return false; - if (!/^(?:\*{1,3}|_{1,3}|~{1,2})$/.test(state.src.slice(state.pos))) return false; - if (state.scanDelims(state.pos, state.src[state.pos] !== "_").can_close) return false; - state.pos = state.posMax; - return true; -} - -function completeCode(state: StateInline, silent: boolean): boolean { - if (silent || !isStreamingTail(state) || state.src[state.pos] !== "`") return false; - const runs = /`+/g; - runs.lastIndex = state.pos; - const opening = runs.exec(state.src)!; - let closing: RegExpExecArray | null; - while ((closing = runs.exec(state.src))) { - if (closing[0].length === opening[0].length) return false; - } - let content = state.src.slice(state.pos + opening[0].length); - // A shorter trailing run may be the first characters of the closing marker. - const partialClosing = content.match(/`+$/); - if (partialClosing && partialClosing[0].length < opening[0].length) { - content = content.slice(0, -partialClosing[0].length); - } - if (content) { - const token = state.push("code_inline", "code", 0); - token.markup = opening[0]; - token.content = content.replace(/\n/g, " ").replace(/^ (.+) $/, "$1"); - } - state.pos = state.posMax; - return true; -} - -function completeLink(state: StateInline, silent: boolean): boolean { - if (silent || !isStreamingTail(state)) return false; - const image = state.src[state.pos] === "!"; - const start = state.pos + (image ? 1 : 0); - if (state.src[start] !== "[") return false; - const labelEnd = state.md.helpers.parseLinkLabel(state, start, false); - if (labelEnd !== -1 && labelEnd + 1 < state.posMax) { - if (state.src[labelEnd + 1] !== "(" || hasClosedDestination(state.src, labelEnd + 1)) { - return false; - } - } - const end = state.posMax; - if (!image) { - if (state.pending) state.pushPending(); - const firstLabelToken = state.tokens.length; - state.pos = start + 1; - state.posMax = labelEnd === -1 ? end : labelEnd; - state.level++; - state.md.inline.tokenize(state); - state.level--; - // Keep URL-shaped labels out of the automatic linkifier until the real - // destination arrives. Restore ordinary text before tokens reach the view. - for (let index = firstLabelToken; index < state.tokens.length; index++) { - if (state.tokens[index].type === "text") state.tokens[index].type = "streaming_link_text"; - } - } - state.pos = state.posMax = end; - return true; -} - -function hasClosedDestination(source: string, start: number): boolean { - let depth = 0; - let quote = ""; - for (let index = start; index < source.length; index++) { - const char = source[index]; - if (char === "\\") { - index++; - } else if (quote) { - if (char === quote) quote = ""; - } else if (char === "<") { - quote = ">"; - } else if ((char === '"' || char === "'") && /\s/.test(source[index - 1])) { - quote = char; - } else if (char === "(") { - depth++; - } else if (char === ")" && --depth === 0) { - return true; - } - } - return false; -} - -function completeEmphasis(state: StateInline): boolean { - if (!isStreamingTail(state)) return false; - const open = []; - let closedThrough = -1; - // Reuse markdown-it's escape, word-boundary, nesting and delimiter matching - // decisions. Only unmatched openers outside already-closed spans reach EOF. - for (const [index, delimiter] of state.delimiters.entries()) { - if (delimiter.end !== -1) { - closedThrough = Math.max(closedThrough, delimiter.end); - } else if (index > closedThrough && delimiter.open) { - open.push(delimiter); - } - } - for (const delimiter of open.toReversed()) { - if (delimiter.marker === 126 && /(?setAttribute(ATTR_URL, url); + } ++ const auto title = impl->getAttributeText(&linkDetail->title); ++ if (!title.empty()) { ++ node->setAttribute(ATTR_TITLE, title); ++ } + } + impl->pushNode(node); + break; +diff --git a/node_modules/react-native-enriched-markdown/lib/module/native/EnrichedMarkdownText.js b/node_modules/react-native-enriched-markdown/lib/module/native/EnrichedMarkdownText.js +index 9fb1ad2..cdca0a7 100644 +--- a/node_modules/react-native-enriched-markdown/lib/module/native/EnrichedMarkdownText.js ++++ b/node_modules/react-native-enriched-markdown/lib/module/native/EnrichedMarkdownText.js +@@ -106,6 +106,7 @@ export const EnrichedMarkdownText = ({ + writingDirection = 'first-strong', + numberOfLines, + ellipsizeMode, ++ renderers: _renderers, + ...rest + }) => { + // An equal config keeps the previous object, so an inline literal does not +diff --git a/node_modules/react-native-enriched-markdown/lib/module/types/MarkdownRenderers.js b/node_modules/react-native-enriched-markdown/lib/module/types/MarkdownRenderers.js +new file mode 100644 +index 0000000..c04179d +--- /dev/null ++++ b/node_modules/react-native-enriched-markdown/lib/module/types/MarkdownRenderers.js +@@ -0,0 +1,4 @@ ++"use strict"; ++ ++export {}; ++//# sourceMappingURL=MarkdownRenderers.js.map +\ No newline at end of file +diff --git a/node_modules/react-native-enriched-markdown/lib/module/web/EnrichedMarkdownText.js b/node_modules/react-native-enriched-markdown/lib/module/web/EnrichedMarkdownText.js +index 2cf2708..cddd7de 100644 +--- a/node_modules/react-native-enriched-markdown/lib/module/web/EnrichedMarkdownText.js ++++ b/node_modules/react-native-enriched-markdown/lib/module/web/EnrichedMarkdownText.js +@@ -16,6 +16,7 @@ export const EnrichedMarkdownText = ({ + markdownStyle = {}, + md4cFlags = {}, + linkRecognition, ++ renderers, + onLinkPress, + onLinkLongPress, + onImagePress, +@@ -31,7 +32,7 @@ export const EnrichedMarkdownText = ({ + ...rest + }) => { + const normalizedStyle = useMemo(() => normalizeMarkdownStyle(markdownStyle), [markdownStyle]); +- const [ast, setAst] = useState(null); ++ const [parsed, setParsed] = useState(null); + const [katex, setKatex] = useState(null); + const [parseError, setParseError] = useState(false); + const { +@@ -68,7 +69,10 @@ export const EnrichedMarkdownText = ({ + markInlineImages(result); + setParseError(false); + setKatex(katexInstance); +- setAst(recognizeTextLinks(result, linkRecognitionRef.current?.text, linkRecognitionRef.current?.inlineCode)); ++ setParsed({ ++ markdown, ++ ast: recognizeTextLinks(result, linkRecognitionRef.current?.text, linkRecognitionRef.current?.inlineCode) ++ }); + } + }).catch(error => { + if (!cancelled) { +@@ -76,7 +80,7 @@ export const EnrichedMarkdownText = ({ + console.error('[EnrichedMarkdownText] Parse failed:', error); + } + setParseError(true); +- setAst(null); ++ setParsed(null); + setKatex(null); + } + }); +@@ -93,8 +97,9 @@ export const EnrichedMarkdownText = ({ + }), [onLinkPress, onLinkLongPress, onImagePress, onTaskListItemPress, onCodeBlockPress]); + const capabilities = useMemo(() => ({ + katex, +- enableTaskListItemToggle +- }), [katex, enableTaskListItemToggle]); ++ enableTaskListItemToggle, ++ renderers ++ }), [katex, enableTaskListItemToggle, renderers]); + const lastChildStyle = useMemo(() => allowTrailingMargin ? normalizedStyle : zeroTrailingMargins(normalizedStyle), [normalizedStyle, allowTrailingMargin]); + const styles = useMemo(() => buildStyles(normalizedStyle), [normalizedStyle]); + const lastChildStyles = useMemo(() => buildStyles(lastChildStyle), [lastChildStyle]); +@@ -123,10 +128,10 @@ export const EnrichedMarkdownText = ({ + }) + }); + } +- if (!ast) return null; +- const children = ast.children ?? []; ++ const children = parsed?.ast.children ?? []; + const lastIdx = children.length - 1; + return /*#__PURE__*/_jsx("div", { ++ "aria-busy": parsed?.markdown !== markdown, + className: ENRM_TEXT_CLASS, + style: wrapperStyle, + dir: dir, +diff --git a/node_modules/react-native-enriched-markdown/lib/module/web/renderers/BlockRenderers.js b/node_modules/react-native-enriched-markdown/lib/module/web/renderers/BlockRenderers.js +index dd70713..ca0385a 100644 +--- a/node_modules/react-native-enriched-markdown/lib/module/web/renderers/BlockRenderers.js ++++ b/node_modules/react-native-enriched-markdown/lib/module/web/renderers/BlockRenderers.js +@@ -101,12 +101,20 @@ function AdmonitionRenderer({ + }); + } + function CodeBlockRenderer({ ++ capabilities, + node, + styles, + renderChildren, + callbacks + }) { + const language = node.attributes?.language; ++ const CustomCodeBlock = capabilities.renderers?.codeBlock; ++ if (CustomCodeBlock) { ++ return /*#__PURE__*/_jsx(CustomCodeBlock, { ++ code: extractNodeText(node), ++ language: language ?? '' ++ }); ++ } + const label = language ? `Code block: ${language}` : 'Code block'; + if (callbacks.onCodeBlockPress == null) { + return /*#__PURE__*/_jsx("pre", { +diff --git a/node_modules/react-native-enriched-markdown/lib/module/web/renderers/InlineRenderers.js b/node_modules/react-native-enriched-markdown/lib/module/web/renderers/InlineRenderers.js +index c8311d4..408950d 100644 +--- a/node_modules/react-native-enriched-markdown/lib/module/web/renderers/InlineRenderers.js ++++ b/node_modules/react-native-enriched-markdown/lib/module/web/renderers/InlineRenderers.js +@@ -127,6 +127,7 @@ function LinkRenderer({ + }; + return /*#__PURE__*/_jsx("a", { + href: url, ++ title: node.attributes?.title, + style: linkStyleForUrl(style, url), + target: "_blank", + rel: "noopener noreferrer", +diff --git a/node_modules/react-native-enriched-markdown/lib/module/web/renderers/ListRenderers.js b/node_modules/react-native-enriched-markdown/lib/module/web/renderers/ListRenderers.js +index 7718d0a..ed95c24 100644 +--- a/node_modules/react-native-enriched-markdown/lib/module/web/renderers/ListRenderers.js ++++ b/node_modules/react-native-enriched-markdown/lib/module/web/renderers/ListRenderers.js +@@ -16,6 +16,7 @@ function ListRenderer({ + const hasTaskChild = !isNested && node.children?.some(child => child.attributes?.isTask === 'true'); + const resolvedStyle = isNested ? styles.listNested : hasTaskChild ? styles.listTask : styles.list; + return /*#__PURE__*/_jsx(ListTag, { ++ start: ListTag === 'ol' ? Number(node.attributes?.start ?? 1) : undefined, + style: resolvedStyle, + children: renderChildren(node) + }); +diff --git a/node_modules/react-native-enriched-markdown/lib/module/web/wasm/md4c.js b/node_modules/react-native-enriched-markdown/lib/module/web/wasm/md4c.js +index 978a8c7..950cccd 100644 +--- a/node_modules/react-native-enriched-markdown/lib/module/web/wasm/md4c.js ++++ b/node_modules/react-native-enriched-markdown/lib/module/web/wasm/md4c.js +@@ -3,10 +3,11 @@ + var createMd4cModule = (() => { + var _scriptName = globalThis.document?.currentScript?.src; + return async function (moduleArg = {}) { ++ var moduleRtn; + var Module = moduleArg; + var ENVIRONMENT_IS_WEB = true; + var ENVIRONMENT_IS_WORKER = false; +- var programArgs = []; ++ var arguments_ = []; + var thisProgram = "./this.program"; + var scriptDirectory = ""; + var readAsync; +@@ -30,8 +31,6 @@ var createMd4cModule = (() => { + var err = console.error.bind(console); + var wasmBinary; + var ABORT = false; +- class EmscriptenEH {} +- class EmscriptenSjLj extends EmscriptenEH {} + function binaryDecode(bin) { + for (var i = 0, l = bin.length, o = new Uint8Array(l), c; i < l; ++i) { + c = bin.charCodeAt(i); +@@ -39,13 +38,12 @@ var createMd4cModule = (() => { + } + return o; + } ++ var readyPromiseResolve, readyPromiseReject; ++ var HEAP8, HEAPU8, HEAP16, HEAPU16, HEAP32, HEAPU32, HEAPF32, HEAPF64; ++ var HEAP64, HEAPU64; + var runtimeInitialized = false; +- function getMemoryBuffer() { +- return wasmMemory.buffer; +- } + function updateMemoryViews() { +- if (HEAP8?.buffer?.resizable) return; +- var b = getMemoryBuffer(); ++ var b = wasmMemory.buffer; + HEAP8 = new Int8Array(b); + HEAP16 = new Int16Array(b); + HEAPU8 = new Uint8Array(b); +@@ -58,10 +56,11 @@ var createMd4cModule = (() => { + HEAPU64 = new BigUint64Array(b); + } + function preRun() { +- var preRun = Module["preRun"]; +- if (preRun) { +- if (typeof preRun == "function") preRun = [preRun]; +- onPreRuns.push(...preRun); ++ if (Module["preRun"]) { ++ if (typeof Module["preRun"] == "function") Module["preRun"] = [Module["preRun"]]; ++ while (Module["preRun"].length) { ++ addOnPreRun(Module["preRun"].shift()); ++ } + } + callRuntimeCallbacks(onPreRuns); + } +@@ -70,25 +69,27 @@ var createMd4cModule = (() => { + wasmExports["__wasm_call_ctors"](); + } + function postRun() { +- var postRun = Module["postRun"]; +- if (postRun) { +- if (typeof postRun == "function") postRun = [postRun]; +- onPostRuns.push(...postRun); ++ if (Module["postRun"]) { ++ if (typeof Module["postRun"] == "function") Module["postRun"] = [Module["postRun"]]; ++ while (Module["postRun"].length) { ++ addOnPostRun(Module["postRun"].shift()); ++ } + } + callRuntimeCallbacks(onPostRuns); + } + function abort(what) { + Module["onAbort"]?.(what); +- what = `Aborted(${what})`; ++ what = "Aborted(" + what + ")"; + err(what); + ABORT = true; + what += ". Build with -sASSERTIONS for more info."; + var e = new WebAssembly.RuntimeError(what); ++ readyPromiseReject?.(e); + throw e; + } + var wasmBinaryFile; + function findWasmBinary() { +- return binaryDecode('asm¬`````````````` ````||`~`|``Tenv __cxa_throwenv\r__assert_failenv _abort_js\nenvemscripten_resize_heap˜–\n\r\n\n\n   \r     \r \n  \n  p44€€@ A à€ ¢memory__wasm_call_ctors\rparseMarkdown__indirect_function_table_emscripten_stack_restore—_emscripten_stack_alloc˜emscripten_stack_get_current™ JA 3” \rq !"#$%&\'()s*B=DZfghq*‚‚„’†*‘އ*‹‰*“•–• (\nªº–æAàÚB7AèÚA6A÷ÚA:AìÚAØ\r(6AðÚAÜ\r-:AñÚA:AƒÛA:AøÚA±\r/;AúÚA³\r-:AûÚA:AÛA:A„ÛAŠ(6AˆÛAŽ-:A‰ÛA:A›ÛA :AÛA™ )7A˜ÛA¡ -:A™ÛA:A ÛB칻֮سå7A«ÛA:A¨ÛA:A·ÛA:A¬ÛAá\r(6A°ÛAå\r/;A²ÛA:A¸ÛAt"6A¼ÛB‹€€€€‚€€€7 AÆ(6 A¿)7 A: AÏÛA:AÄÛA¦ (6AÈÛAª -:AÉÛA:AÛÛA:AÐÛAÈ (6AÔÛAÌ -:AÕÛA:AÜÛAt"6AàÛBŽ€€€€‚€€€7 AÖ)7 AÐ)7 A: AëÚ,AH@AèÚ(AàÚ(k õ”!~#A0k"$@ E@@A€\\"AèÚ("AÿÿÿÿqAkA\nAëÚ,"AH""M@AàÚ(AàÚ !@  @ A€ ü\n AëÚ- Av ÀAH@AäÚ 6  AëÚ Aÿq:  jA:  AàÚ   kAäÚ( "A  A€x AàÚAàÚ(AëÚ,AN  A:,  AG:/  AG:.  AG:-  AG:+  AG:*  AG:)  AG:(  AG:\'A$t"A6 B7 B7 B7 B7 A j" 6 ! \\"A÷ÿÿÿO\r@@ A O@ Ar"Ajt!  Aÿÿÿÿk6  6  6   : A j! E\r E\r  ü\n  jA: Aj! A\'j!#AÐk"$@ A j"( , " AH"E@A<t"B7 Aì6 B7 B7 B7 B7$ B74 B€€€ü7,  6  A j6  (! AéOA4 Aèn" A4OA jA !A<t" B7 Aì6 B7 B7 B7 B7$ B74 B€€€ü7,  A j6 (!  6@ E\r (" Ak6 \r (( r Aj! ("\n! ( " \nG@@@ Ak(" E\r ("Ak6 \r (( r \n Ak" G\r (!  \n6 @A€  A€O" ( kAuM\r At"t! \n k"\n@ \nü\n   j6  \n j6  6 E\r k !@@@@ (" ("\nI@ (6 ("6 @  (Aj6 Aj!  ("k" Au"Aj" A€€€€O\rAÿÿÿÿ \n k"\nAu"  K \nAøÿÿÿO" A€€€€O\r At"\nt" j" (6 ("6  \nj!\n   (Aj6 ( ( ("k" Au  Atk! @   ü\n  \n6  Aj" 6  6 E\r k  6    Aj! @ ,AH@ (A: A6  A: A: A€z (" (  , AH6 -! -! -! -! -! -! -!\r -! -! B7( A6$ A6 A6 A6 A6 A6  A tAìžAà– r \rAtr Atr Atr Atr Atr Atr Atr6 (  , "AH"! ( ! !\n#A€k" $@ A j"(@A! ("E\rA›    A4jAAüü 6 6 )7 )7 )7 )7$ ( 6, \n60 AA ("Aq6œ A„j"AA€ü A;ã A:® A;à A:ß A: A;¿ A:ª A:¥ A:„ A€„q@ A:‚ A€€q@ A:â A€ q@ A:¨ A€€€q@ A:Á A€€€q@ A:¯ Aq@ A:Ä Aq@ A:¾ A€q@ A:² A€Âq@ A:€ Aq@A!@ A G A GqE@ jA: Ar"AýqA F@  jA: Aj"A€G\r A E\rA  jAk-"A\rF\r A\nF ! A6` A 6D 64 A€€  A€€OAt6X A„jAÿAÔü B7ä B7Ü@AA \n ("E@ AÌj!" A°jAr!# Aüj!$ AèjAr!% A°j!\rAè! A!@@ \r!@@@@ (" K@ B7ø B7ð B7èA! A°jAA \r A°jF"j \r \rF"!\r (¨!& (!@@ j-"A G A Gq\r A|qAj Aj A F! Aj" G\r ! A°jA,A j Aj " 6 A°jA$A j A j " 6A! A°jA A j Aj " A6@ (”"AL\r (œ! (! !\n@@  Alj"-A>F@ \n O\r O\r j-A>G\r Aj"!@ Aj" O\r@ j-"\nA G \nA Gq\r A|qAj Aj \nA F! Aj" G\r !   kAkA  G"\n6  6 !  \n ( "I\r  \n k"\n6 ! Aj" G\r ! ! 6ä@@ O\r j-A\nk  L\r (!@  Alj-A>F\r Aj" G\r ! " #  Aj !A!\nA!A! !@A k!A k! („"\' (üjAk!( (¬E! (!! ( !) (œ! (¤!@@ ("AG@ !   6@  ("M@ !  !@ O\r  ( j-!@  j- AÿqG\r Aj" G\r ! @  k )I\r@  O\r@@  j-A k Aj" G\r ! 6ä \rA6  6ä  F@   (k"A M6 \rA6  (! @@@@ AG\r AL\r  L\rA! A6¤  O\r  j"-A\nk @ Aj  Aäj+AL\r A6¤ AþÿÿÿqAG\r \rA6 A6 ! \rA6  @ AG\r  (”G\r \rA6  (" (œk"A O6  \rA6@@ AL"\r G\r ( AljAk-"A>G6¨  A6¨ @ ! ( AljAk-! AÿqA>F@ !  G@ !  (€\r („"A H\r (ü jAk-AG\r A6¬  @ Aq@ !    AL\r  (” G\r  ! Alj"Ak-A>F\r  G\r  (€\r  \'A H\r  (-AG\r  6A AF\r   A$k(k"A M6 Ak ! A6¬ A6¨@ (" O\r@ AG\r@ -"A-k  G\r@@ Aj" O\r@  j-F@  Aj"K\r  @@ j-A k Aj" G\r ! 6ä \rA6 AA A=F6   \nI\r -"E\rA­ ÀAPE\rA! Aj"\n O\r@@@  \nj-" AÿqF"\r A F\r A G\r j! \nAj"\n G\r !\n AI\r  AI\r A\nk @@@  L\r G\r@ Aj   Aàj Aèj,E\r -è"A>F\r ! Alj"-G\r (ð" ( K\r (ô" k j" !@ (à" O\r@ j-"A G A Gq\r A|qAj Aj A F! Aj" G\r 6ä   k6  6 !  6ä   k" 6  6 O\r@ j-A\nk M@ j"6ô A6  Aj"6ô  Ak6  ! @  O@ (AF@ !  \rA6   k6 A6 ! \n Aj   Aäj Aèj,! (!@@@@@ @ (ä!@ AG\r  G\r@@ O\r j-"A\rF\r -è! A\nG\r AÿqA>F\r  A>! -èA>G\r Aÿq"A.G A)Gq\r (ìAG\r (ô" (ðk j"!@ O\r@ j-"A G A Gq\r A|qAj Aj A F! Aj" G\r ! 6ä   k"6  6  ,è6@@@ O\r j-A\nk Aj6ô   M@  j6ô A6  Aj6ô  Ak6 E@ Aj j-\r (”! @ (˜" J@ (!  A Av j AL"6˜ ( All"E@ (("E\rAõ (0   6 (”! Aè  F! Aj! Aj6”  Alj" )ø7  )ð7  )è7 (”! (! (! ! \n A F  Fq\r (ä! ! O@ !  @ j"\n-"A#G@ ! AþF\r AàF\r !  Ak"\nF\rA!  Aj"j-A#F\r  @@  j- G\r Aj" G\r !  k"AI@ !  6 @@@  O\r@  j-A G\r Aj" G\r   O\r@@  j-"A\nk @ AàG\r \n-AàG\r !  Aj" G\r ! 6ä \rA6 A6 A6 \r \rA 6  \nF\rA!  Aj"j-A#G\r  \nF\rA!  Aj"j-A#G\r  \nF\rA!  Aj"j-A#G\r  \nF\rA!  Aj"j-A#G\r  \nF\rA!  Aj"j-A#G\r  \nF\rA! Aj!   k! ! AJ@ !  @  O"\n\r (Aq\r  j-A k"AK@ !  A tA“€€q\r !  @ \n\r@  j-"A G A Gq\r Aj" G\r !  6 6ä \rA6  6 @@@@@  O\r  j-AF\r Aj"\n O\r A/G\r \n j-A>F\r  AjM@A!  A! AA   A j>E\rA! ( "\n O\r (!@@  \nj-"A k"AK\rA tA€€qE\r \nAj"\n G\r  AAA A\nF A\rF! Aj$ 6¤@  (AG\r A6¤ ("A€qE\r (AG\r  (O\r ( j-"A-k Aj  Aäj+ F@ A6¤ A6 \rA6 AüG\r  (”G\r (€"E\r (AG\r Aäj! Aàj!A!A!@ Aj"(" M\r (" j-AüG\rA! Aj" O\r@  j-A k"\nAK\rA \ntA€€qE\r Aj" G\r ! @@@@  I@  ( j-A:Fj!  O\r ("\n j-A-G\r@@@  \nj-"A-G\r Aj" G\r Aj!  A! Aj!@  A:Fj" O\r@@  \nj-"A k"AK\rA tA€€qE\r  Aj"G\r  AüG\rA!A! Aj" O\r@  \nj-A k"AK\rA tA€€qE\r Aj" I\r !   O\r@  \nj-A\nk E\r  ! E\r  6  6A  A E\r  (à6 \rA\n6 \rA6 (”"  F  (AF! A€qE\r jAL\r ( Alj"Ak-"E\r A?K\rB ­†B€€€€ÀƒP\r@ (ä" ("\n Aj"  \nK"O\r (!@ j-"A G A Gq\r Aj" G\r ! Aj" \nO\r (" j-AÛG\r  Aj"j,"E\rAÓ APE\r  j-AÝG\r \n Aj"G@ j-A k"AK\r A tA“€€qE\r Ak % " -Ar: Ak $  6@ ("O\r (!@ j-A k"AK\rA tA€€qE\r Aj" G\r ! 6ä  6  Aj"6ô  6  6Aè!A! ! ! Aj.A! Aj A@k/\r -Aq@ Aj AÜj/\r AjA-\r Aj0\rA!#Ak" $ A6$ Aj"\rA6@ \r(€AJ@ Aøj!@@@@@@ \r(ø j" ("AÿqAk AsA\nvAq6 /:  (6 (AsA\nvAq6 /:  AÿÿK6 /: (6  A6 /At(€\\"6 /! 6 At(€6 Aj6 A$j6 @@ ("A€q@@ A€qE\r Aÿq A j \r(, \r("@ \r($"E\rA¿ \r(,   ("Aÿq"AK\rA tAŽ€qE\r \r \r(Ak6 (! A€qE\r Aÿq A j \r(, \r("@ \r($"E\rAš \r(,   @ ("AÿqAk \r(Œ \r(Alj" -Ar: \r \r(Aj6  B7` B7X B7P B7H B7@ B78 B70 B7(@ ("AÿqA\nG\r A€€„I\r ("A€ Av"nM\r Aq! Aj!A!\nA!@ "AO@ A|q!A!@  Atj"( j ( j (j (j ( ( ( (jjjk! Aj! Aj" G\r E\r @  Atj"( j (k! Aj! \nAj"\n G\r  n AvO\r \r($"A¸ \r(,  (  A€~qA r"6 A! A6ˆ B7€ B7x B7p B7h \r("@ \r(Œ AljAk-As! A!@@@@@@@ AÿqAk Av6h  A€€I\r \r("\n ("j-! ( !@@@  \r("O\r@  \nj- AÿqG\r Aj" G\r  M\r@  \nj-A G\r Aj" G\r ! @  M\r@  \njAk-A G\r Ak" K\r ! A! \r  \nj  kA Aèj AÈj9\r \r(! !\n@  M\r@  \nj-A k"AMAA tA€€q\r \nAj"\n G\r !\n \r  j \n kA  A(j9\r :ˆ (!A!  A6l Av6h (Ak6p  Av6h @@@@@@@  Aÿq"A FqE@  Aèj \r(, \r("\r (! AÿqAk \r($"E\rAš \r(,   (!@ AÿÿM@ E\r Aj!@ ( (F@ A j! Ak"\r  @  A lj"A k( Ak(G\r Ak"\r  Ak"E\r Aj! \rA  :"E\r  \rA Aj (:"E\r  (!@@ Av"Atj"E@A! \r($"E\rAä \r(,  k  @ (! ("! ! !@ !@ M\r \r(!@  j-A-F\r Aj" G\r ! A!  K@ \r( jAk-A:F! @ M\r \r(!@@  j-"\nA-G\r Aj" G\r !  Ar  \nA:F!  At( 96 Ak! Aj! AJ\r A A \r(, \r("@ \r($"E\rAš \r(,  k  \rA ( (  ;"\rA A \r(, \r("@ \r($"E\rA¿ \r(,  k  AI\r@A A \r(, \r("E@ Aj!A!  \r($"E\rAš \r(,  k  @ \rA  Atj"( (  ;"\r Aj" G\r A A \r(, \r("E\r \r($"E\rA¿ \r(,  k  k  \r Aj (<"\r A!  -"A Fq\r  Aèj \r(, \r("E\r \r($"@A¿ \r(,  ! \r (X@ (Hk (Lk (Pk (8E\r ((k (,k (0k \r (! -AkAM@ A l j!  At j!  \r(Œ \r(Alj" -Aþq A\nvAqr: \r \r(Aj6 Aj" \r(€H\r \rA6€A! Aj$ "\r -Aq@A!A!#A@j"\n$@ Aj"(`"E\r (pE\r (X AAR@@AA (, ("E@ (`E\r \nA0j!  ($"E\rAš (,   @ (X Alj"( E\r \nB7 \nB7 \nB7 \nB7 B7 B7 \n ( 6( \n (6,@ ( (A  \nAj9@A!  A \nA(j (, ("@ ($"E\rAš (,   ( (<"\rA \nA(j (, ("E@A!  ($"E\rA¿ (,  \n(@ \n(k \n( k \n(k \r Aj" (`I\r A!AA (, ("E\r ($"@A¿ (,  ! \nA@k$ "\r A!AA (0 ("E\r (("@A¿ (0  !  \n6ä \rA6 !  ! !  A6¨ ! @ (ä"Aj" ("O\r (!@@ j"-A\nk @ -A\nk @ -A\nk @  j-A\nk Aj! Aj!  I\r @ I@ (!@A!@ j-A\nk Aj" G\r ! A! A°jA(A j Aj "\n 6 " \r("AG\r@@ ("M\r (!@  jAk-"A G A Gq\r Ak" K\r  @  O\r (!@  jAk-A#G\r Ak" K\r   F\r ( jAk-"A F@ !  A F@ !  ! -AqE\r \n 6  !@ AkAI\r  ("M\r (!@  jAk-"A G A Gq\r \n Ak"6  K\r @ ( j-A\rFj! I@ ( j-A\nFj! @ &E\r E\r  j"AL\r ( Alj"Ak-A>F\r (ü Ak(j" (A€r6 @ \r  j" (”N\r Aj -\r AJ@ AjA Al" (j"(" -Aq ( j,A A1\r AjA (ü" -éAq" ( j,A A1\r ( j" -Aýq r: ( j 6 @ (”!@ -AqE\r ( Alj"Ak-A>G\r \r(AG\r \n(" ("k"AkA K\r (" j"-AÛG\r -A!G\r  jAk-AÝG\r Ak! Aj!A!@@@@ Ak @  j-"A k  AákAÿqAIAÿq -¹"A k  AákAÿqAIAÿqG\r Aj" G\r A  @  j-"A k  AákAÿqAIAÿq -Î "A k  AákAÿqAIAÿqG@ A\nk Aj" G\r A  A!@  j-"A k  AákAÿqAIAÿq -ú "A k  AákAÿqAIAÿqG@ A\nG\r  Aj" G\r A  A!@  j-"A k  AákAÿqAIAÿq -‡"A k  AákAÿqAIAÿqG@ A\nG\r -"A k  AákAÿqAIAÿqAÃG\r -"A k  AákAÿqAIAÿqAÁG\r -"A k  AákAÿqAIAÿqAÕG\r -"A k  AákAÿqAIAÿqAÔG\r -"A k  AákAÿqAIAÿqAÉG\r -"A k  AákAÿqAIAÿqAÏG\r -"A k  AákAÿqAIAÿqAÎG\rA  Aj" G\r A ! Ak" -Ar: ( (”AljAk" -AÇq r: \rA6 (”!  k!@A!@@A ( Alj"-A)kt"AqE@ A!qE\rA! Aj.  („6 Aj  ( ,A1\r AjA (" -Aq ( j,A A1\r  AjAA -"AqA AvAqA1\r Aj" (”H\r @ \r(E@ Aj.@A!  A!Aè! -AÀqE\r ( \n(I\r (ŒAj6Œ  (Œ@@ („"Aj" (ˆ"J@ A€ Av j AL"6ˆ (ü l"E@A! (("E\rAõ (0   (€"@   (ükj6€ 6ü („"Aj6„  6„ (ü"\rA!   j"A; (Œ! A6  AtAr6 A6Œ @ (E\r Aj.E\rA!  @@@@ \r("Ak\n A! Aj.\r („"Aj" (ˆ"J@ A€ Av j AL"6ˆ (ü l"E@ (("E\r\nAõ (0  \n (€"@   (ükj6€ („"Aj6„ 6ü  j  6„ (ü"E\r  j " \r(AÃÌj-"; (! A6   Atr6 6€ Aj \r2\r Aj.\rA!Aè!  (€A: (€ /; (€" (A€r6A! Aj \r2\r Aj.\rA! (€E@Aè!  \rA6 \r!  (€A\n: (€ /; A 6AA Aj \r2!  @  (F\r Aj.E\rA!  @ (€\r \r! @ Aj"(€"Aj"\n („"J@ A€ Av j AL"6„ (ø l"E@A ($"E\rAõ (, A  (ü"\n@   \n (økj6ü  6ø  (€"\nAj6€  \nj   \n6€ (ø"E\r  j " \r(AÃÌj-"\n; \r(! A6  \n Atr6  6üA  A E\rA!  Aj \r2! A! E\r  A!  (("E\rAš \n  (P"@ (T"A!@@ (P Atj("E\r (@" M@   (D (HljI\r k (T! Aj" I\r (P k (H@A!@ (@ Atj"-"Aq (k -  Aq@ ( k Aj" (HI\r (@k (l"@ (p"A!@@ (l Atj("E\r (\\" M@   (` (dljI\r k (p! Aj" I\r (l k (d@A!@ (\\ Alj(k Aj" (dI\r (\\k (8k (xk (ük (k A€j$ @A<t"B7 Aì6 B7 B7 B7 B7$ B74 B€€€ü7,  6  A j6  (@ (("E\r ((" ($"G A$j!A!\r@@@  \rAt"\nj("(AF@ ($" (("G\r \rAj!\r   k"Au!@@ ((AF\rA! AG@@ Aj!  Atj"((AF@  O\r Ak(!  Atj(! \r \r  "G\r A! A68 B70A!  A! A68 B70 A6L B7D  A0j6@  AÄj6<@@@@@@  At"j"(" (AG\r  Ak(A !  Aj"K@  Atj( rAG\r AF\r @ Aj" O\r -"A k  AákAÿqAIAÿqAÓG\r -"A k  AákAÿqAIAÿqAÃG\r -"A k  AákAÿqAIAÿqAÒG\r -"A k  AákAÿqAIAÿqAÉG\r -"A k  AákAÿqAIAÿqAÐG\r -"A k  AákAÿqAIAÿqAÔG\r  j-A>F\r @ Aj" O\r -"A k  AákAÿqAIAÿqAÓG\r -"A k  AákAÿqAIAÿqAÔG\r -"A k  AákAÿqAIAÿqAÙG\r -"A k  AákAÿqAIAÿqAÌG\r -"A k  AákAÿqAIAÿqAÅG\r  j-A>F\r A\nj" O\r -"A k AákAÿqAIAÿqAÔG\r -"A k AákAÿqAIAÿqAÅG\r -"A k AákAÿqAIAÿqAØG\r -"A k AákAÿqAIAÿqAÔG\r -"A k AákAÿqAIAÿqAÁG\r -"A k AákAÿqAIAÿqAÒG\r -"A k AákAÿqAIAÿqAÅG\r - "A k AákAÿqAIAÿqAÁG\r  j-A>F\r\r Aj" G\r !  6A Aj" O\r Ak! (!@@  j"-A\nk\n\n /A­Ús -A>srE\r Aj! Aj!  I\r !  Aj" O\r Ak! (!@@  j"-A\nk /A¿üF\r Aj! Aj!  I\r !  Aj" O\r Ak! (!@@@  j-"A\nk  A>F\r Aj! Aj!  I\r !  Aj" O\r Ak! (!@@  j"-A\nk /Aݺs -A>srE\r Aj! Aj!  I\r !  @@  O\rA! ( j-A\nk  6 ( !   6A  6A  6A  6A  6A  Aj6A Å@  ("O\r  (˜O\r @ (" j-"E\r A>F@ A>:  6  -Aüq: Aj! Aj  A?K\rB ­†B€€€€€ ƒP\r  Aj"K@  j-A k"AK\rA tA“€€qE\r  :  6  -Aüq: Aj  A! A6  A j"  I"\nO\rA! !@@ j-" A0kAÿqA K\r  A\nljA0k"6 Aj" \nG\r \n! M\r O\r@ j-" A)k  Aj"K@  j-A k"AK\rA tA“€€qE\r  :  6  -Aüq:  k jAj 6  6A!  ç  ("H@@A!@@@A (Œ Alj"Ak"-A)kt"AqE@ A!qE\rA! Ak(!A! Ak-Aq@ ( j,! A! A  A1\r A ,A1\r  AA Ak-"AqA AvAqA1E\rA!  (Ak"6  H\r A Ú#A0k"$@ (ü"E\r@@@ ("AÿqAk A€qE\r (" (O\r ( j-AÛG\r@ ("E\r Aj! AG\r !  @ \r@ A/G\r Aj" O\r \n j-A>F\r AL\r@ AßqAÁkAÿqAI\r AßF\r A:G\r  Aj"M@A!  @@@ \nj,"A0kAÿqA\nI\r A_qAÁkAÿqAI\r E@A!  A  APE\r A!  Aj"G\r  A!  A! @@ Ak A=G\r Aj!A!  A+!@@@@ A"k E\r Aá AP\r A\rF\r A)!  A*! Aj! I\r A! E\r \rF\rA  A)FA !  Aj" Atj"(! (" I\r   M@A A!  Aj6 „#Ak"$@  O\r@@@ j"  k A j4"AÿK@A!A!@@@  jAm"AtA j("\nAu j"AtA j(Aÿÿÿq K@ Ak!   \nAvAq j"AtA j(AÿÿÿqM\r Aj!  L\r  AH\r  A F\r A F\r A kAI\r -A\nk ( j" I\r Aj$  à@@@ AÿK@A­!@@@  jAm"AtAÀj("Au j"AtAÀj("Aÿÿÿq K@ Ak!  AvAq j"AtAÀj(AÿÿÿqM\r Aj!  N\r  AH\rAÐ8!  A6  A r AÁkAI6 A!A4!@@@  jAm"AtAÀ1j("Au j"AtAÀ1j("Aÿÿÿq M@ AvAq j"AtAÀ1j(Aÿÿÿq O@ AH\rAà8!  Aj!  Ak!  N\r A!A!@@@  jAm"AtAÐ6j("Au j"AtAÐ6j("Aÿÿÿq M@ AvAq j"AtAÐ6j(Aÿÿÿq O@ AH\rAð8!  Aj!  Ak!  N\r A6  6  ( ( " lAtj! At"@   ü\n  6 F\r ( Aÿÿÿq"AjF@  sAsAq j6  ( kj6 õ #A k"$ B7 B7 B7 B7 A ?! A ?! @@@  K@   M\r @ \nE@   AjC!A! AqE@   C! A!  Atj( Aj Atj(k"\r Aj" ( " I! Aj" (" I!\n  K\r  I\r  K\r  I\r A! A j$  iA!@ ("(" ("("I\rA!  K\r ( ( ( (A"\rA! (" ("I\r K!  ‰#Ak"$@@@  O\r j  k A j4! ( j! AÿK@A!@@@  jAm"AtA j("\nAu j" AtA j(Aÿÿÿq K@ Ak!   \nAvAq j"AtA j(AÿÿÿqM\r Aj!  L\r  AN\r  A F\r A F\r A kAO\r A6 A 6  ?!   @ Aj$  D ("(" ("("I@A  K@A ( ( ( (A ›”#A0k"$ A6x#Ak"$ AjAA€ü @@ E\r Ak! A€j!  AtjAk!@  Atj"(!@ !@ !@ Aj" ("O@ !  (! @  j"-j-@ !  -j-@ !  -j-@ !   j-j-@ !  Aj! Aj"!  K\r @  K@ (!@  j"-" j-\r Aj" G\r Aj" I\r  @AA@@@@@@ AÜF@A! Aj" (O\r@  j-"A!kAÿqAI\r@ AÿqA\nku Aj I\r Aj! @@ (x" (|"N@ AÀ Av j AL"6| (t All"\rA! ($" E\rAõ (,   Aj6x (t"\rA!  6t (x"Aj6x  Alj"AÜ ; B7  Aj"6  6 A!@@@@@@@@@@@@ A!k\\ \r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\n\r \r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r Aj"\r!\n@  \rM\r@  \nj- G\r \nAj"\n I\r !\n A!A!A!  (F\r N"AÿM\rA!A!@@@@  jAm" AtA j("Au j"AtA j(Aÿÿÿq K@ Ak!   AvAq j"AtA j(AÿÿÿqM\r Aj!  N\r  A! AN\r A!Aä!@@@  jAm" AtA°9j("Au j"AtA°9j(Aÿÿÿq K@ Ak!   AvAq j"AtA°9j(AÿÿÿqM\r Aj!  N\r A! AN   k! !@@  j-" AàG\r Aj" G\r !A  A F ! !  k"A K\r At j( "\r  AtjAk(O\rA!A!@ E@ ! !\n  !\n \r " I\r @@@@ \nO\r@  j-"\rA G@ \rAàF\rA! Aj" \nG\r A! \n  !\r \nO@A! !  @  \rj-AàG"@ ! \r!  \rAj"\r \nG\r ! \n! @  k"\rF@A!\n  Atj"("\r I@  jAk-A F!\n As  Gq\r \r \rF \nrE\r E\r Aj  @ F\rA! \rAK\r \rAt jA j"\r \r("\r \rK6 \r  Aj"M@A!   Atj"\n(! \n(!\n  ( ! \n@ Ak!  Ak(" ("O\r@  j-"A G A Gq\r Aj" G\r ! @@ (x" (|"N@ AÀ Av j AL"6| (t All"\rA! ($" E\rAõ (,   Aj6x (t"\rA!  6t (x"Aj6x  Alj"Aà; B7  6  6@@ (x" (|"N@ AÀ Av j AL"6| (t All"\rA! ($" E\rAõ (,   Aj6x (t"\rA!  6t (x"Aj6x  Alj"Aà; B7  6  6 (t (x"Alj"Ak" Ak6 A(k" Ak6  -Ar:  -Ar:A! ! ! ! (M\r@@@   jAv"Atj"( K@ E\r  AtjAk( I\r Ak!  (K\r ! !  Aj! @@ (x" (|"N@ AÀ Av j AL"6| (t All"\rA! ($" E\rAõ (,   Aj6x (t"\rA!  6t (x"Aj6x  Alj"A¦; B7  Aj"6  6  (x"AL\r (t" AljAk-A&G\r (|" L@ AÀ Av j AL"6|  All"E@A! ($" E\rAõ (,   6t (x! Aj6x  Alj"A»; B7  Aj6  6  ("\n - AÀq\r\n   k"  \n A j>\r (" AjM\r -A!G\r  Aj"j-A-G\r -A-G\r A! (è" K  \nAkOq\r @ Aj" \n \nI"\rM@  j" /A­Ús -A>srE\r\r@@ Aj! Aj" \rK\r " j" /A­Ús -A>sr\r  !  \nI  Aj" Kq@  Atj"(! (!  6è (! A! -AqE\r Aj" O\r  j-AüG\r@@ (x" (|"N@ AÀ Av j AL"6| (t All"\rA! ($" E\rAõ (,   Aj6x (t"\rA!  6t (x"Aj6x  Alj"Aü; B7  Aj"6  6  ("Aj K\r  Ak"\rj"/A÷îs -A÷sr\r @  \rF\r \rN"AÿK@A!A!@@@@  jAm" AtA j("\nAu j"AtA j(Aÿÿÿq K@ Ak!   \nAvAq j"AtA j(AÿÿÿqM\r Aj!  N\r  AN\r A!Aä!@@@  jAm" AtA°9j("\nAu j"AtA°9j(Aÿÿÿq K@ Ak!   \nAvAq j"AtA°9j(AÿÿÿqM\r Aj!  N\r  AH\r\r  A F\r A F\r A\rkA}K\r A!kAI\r A:kAI\r AÛkAI\r AÿG AúKqE\r @@ (x" (|"N@ AÀ Av j AL"6| (t All"\rA! ($" E\rAõ (,   Aj6x (t"\rA!  6t (x"Aj6x  Alj"A®; B7  Aj"6  \r6@@ (x" (|"N@ AÀ Av j AL"6| (t All"\rA! ($" E\rAõ (,   Aj6x (t"\rA!  6t (x"Aj6x  Alj" (6 (! AÄ; B7  6  Aj!@@ (" Aj K\r  Ak"\nj(AèèуG\r  M\r /A¯ÞF\r @ Aj K\r  Ak"\nj"\r(Aèèуs \r-Aósr\r  M\r /A¯ÞF\r Aj K\r  Ak"\nj"/Aæès -Aðsr\r  M\r /A¯ÞG\r @@ (x" (|"N@ AÀ Av j AL"6| (t All"\rA! ($" E\rAõ (,   Aj6x (t"\rA!  6t (x"Aj6x  Alj"Aº; B7  6  \n6@@ (x" (|"N@ AÀ Av j AL"6| (t All"\rA! ($" E\rAõ (,   Aj6x (t"\rA!  6t (x"Aj6x  Alj" (6 (! AÄ; B7  6 Aj!  (Aj K\r @ Ak-"AßqAÁkAÿqAO@ Aj O\r A0kAÿqA\nO\r  Aj O\r\n  Aj"j-"A0kAÿqA\nO@ A_qAÁkAÿqAK\r\n @@ (x" (|"N@ AÀ Av j AL"6| (t All"\rA! ($" E\rAõ (,   Aj6x (t"\rA!  6t (x"Aj6x  Alj"AÀ; B7  6  6@@ (x" (|"N@ AÀ Av j AL"6| (t All"\rA! ($" E\rAõ (,   Aj6x (t"\rA!  6t (x"Aj6x  Alj" (6 (! AÄ; B7  6 Aj!  @@ (x" (|"N@ AÀ Av j AL"6| (t All"\rA! ($" E\rAõ (,   Aj6x (t"\rA!  6t (x"Aj6x  Alj"AÝ; B7  Aj"6  6 \r @@ (x" (|"N@ AÀ Av j AL"6| (t All"\rA! ($" E\rAõ (,   Aj6x (t"\rA!  6t (x"Aj6x  Alj" 6A!A  Aj" O\r  j-AÛG\r@@ (x" (|"N@ AÀ Av j AL"6| (t All"\rA! ($" E\rAõ (,   Aj6x (t"\rA!  6t (x"Aj6x  Alj" 6A!!A !  :  : B7   j"6@@ (x" (|"N@ AÀ Av j AL"6| (t All"\rA! ($" E\rAõ (,   Aj6x (t"\rA!  6t (x"Aj6x  Alj"AÄ; B7  6  6@@ (x" (|"N@ AÀ Av j AL"6| (t All"\rA! ($" E\rAõ (,   Aj6x (t"\rA!  6t (x"Aj6x  Alj"AÄ; B7  6  6 \n @@ E@  -\rA qAvq\r  E\r @@ (x" (|"N@ AÀ Av j AL"6| (t All"\rA! ($" E\rAõ (,   Aj6x (t"\rA!  6t (x"Aj6x  Alj"A:  : B7  Aj"6  6 \n @@@@@@@@@@@AA@@AA@@@@@@@@@@@@@@ >\r"""""""" " """"""""""""""""""" """""""""""""""""""""""""" AÞF\r AþG\r!A! Aj"\n!@  \nM"\r@  j-AþG" \r Aj" I\r !  k"AG\r ( "A€€qE\r \rA! A!  \nj ( \nkA4"AÿK\r A F\r A F\r A kAM\rA  A! Aj" I\r -AqE\r Aj"!@  M\r@@  j-AÞG\r Aj" I\r ! !  kAG\r%A! A!  j ( kA4"AÿM@ A F\r A F\r A kAM\rA  @@@  jAm"AtA j("\rAu j"\nAtA j(Aÿÿÿq K@ \nAk!   \rAvAq j"AtA j(AÿÿÿqM\r Aj!  N\r A  \nAN\rA  A !  (F\r N"AÿM\rA!A!@@@  jAm" AtA j("\rAu j"\nAtA j(Aÿÿÿq K@ \nAk!   \rAvAq j"AtA j(AÿÿÿqM\r Aj!  N\r !  \nAN\r !  -A qE\r Aj" O\r@  j" -A=F@  Aj"K\r  !  kAG\r"A!A!AA ( kA4"AÿM@A! A F\r A F\r A kAI  @@@  jAm" AtA j("\rAu j"\nAtA j(Aÿÿÿq K@ \nAk!   \rAvAq j"AtA j(AÿÿÿqM\r Aj!  N\r A!\n \nAN !  -A€qE\r Aj" O\r@  j" -A+F@  Aj"K\r  !  kAG\r!A!A!AA ( kA4"AÿM@A! A F\r A F\r A kAI  @@@  jAm" AtA j("\rAu j"\nAtA j(Aÿÿÿq K@ \nAk!   \rAvAq j"AtA j(AÿÿÿqM\r Aj!  N\r A!\n \nAN !  @@@  jAm"AtA j("Au j"\rAtA j(Aÿÿÿq K@ \rAk!   AvAq j"AtA j(AÿÿÿqM\r Aj!  N\r A  \rAN\rA  A !  (F\r\r N"AÿM\r A!A!@@@  jAm" AtA j("Au j"\rAtA j(Aÿÿÿq K@ \rAk!   AvAq j"AtA j(AÿÿÿqM\r Aj!  N\r !  \rAN\r\r !  ! AK\r ( ! ! A€qE\rA!  (M\r N"AÿM\rA!\nA!@@@@  \njAm"\rAtA j(" Au \rj"AtA j(Aÿÿÿq K@ Ak!   AvAq \rj"\nAtA j(AÿÿÿqM\r \nAj!\n  \nN\r  AN\r\n A!\nAä!@@@  \njAm"\rAtA°9j(" Au \rj"AtA°9j(Aÿÿÿq K@ Ak!   AvAq \rj"\nAtA°9j(AÿÿÿqM\r \nAj!\n  \nN\r A! AN  @  j-A$F@  Aj"K\r  A! !  kAK\rA!  (M\r N"AÿM\rA!A! @@@@  jAm"\nAtA j("Au \nj"\rAtA j(Aÿÿÿq K@ \rAk!   AvAq \nj"AtA j(AÿÿÿqM\r Aj!  L\r  \rAN\r A!Aä! @@@  jAm"\nAtA°9j("Au \nj"\rAtA°9j(Aÿÿÿq K@ \rAk!   AvAq \nj"AtA°9j(AÿÿÿqM\r Aj!  L\r A!\r \rAN  @ Aj" O\r@  j-A k"AK\rA tA€€qE\r Aj" I\r ! A F@ !  kAI\r @@ (x" (|"N@ AÀ Av j AL"6| (t All"\rA! ($" E\r Aõ (,  Aj6x (t"\rA!  6t (x"Aj6x  Alj"A:  : B7  6  6  @@ (x" (|"N@ AÀ Av j AL"6| (t All"\rA! ($" E\rAõ (,   Aj6x (t"\rA!  6t (x"Aj6x  Alj"A€ ; B7  Aj"6  6  A F\r A F\r A\rkA}K\r AÿG AúKq A!kAI A:kAIr AÛkAIrr ! @ E\rA! A!@  j" ( k" A4"AÿM@ A F\r A F\r A kAO\r  @@@  jAm"AtA j("\rAu j"\nAtA j(Aÿÿÿq K@ \nAk!   \rAvAq j"AtA j(AÿÿÿqM\r Aj!  N\r  \nAN\r A!Aä!  A4"AÿM@ AÿG AúKq A!kAI A:kAIr AÛkAIrr  @@@  jAm" AtA°9j("\rAu j"\nAtA°9j(Aÿÿÿq K@ \nAk!   \rAvAq j"AtA°9j(AÿÿÿqM\r Aj!  N\r A!\n \nAN ! !  Aq "E\r @@ (x" (|"N@ AÀ Av j AL"6| (t All"\rA! ($" E\rAõ (,   Aj6x (t"\rA!  6t (x"Aj6x  Alj" : A$: B7  6  6  A F\r A F\r A\rkA}K\r AÿG AúKq A!kAI A:kAIr AÛkAIrr ! @ E\rA! A!@  j" ( k" A4"AÿM@ A F\r A F\r A kAO\r  @@@  jAm"AtA j("\rAu j"\nAtA j(Aÿÿÿq K@ \nAk!   \rAvAq j"AtA j(AÿÿÿqM\r Aj!  N\r  \nAN\r A!Aä!  A4"AÿM@ AÿG AúKq A!kAI A:kAIr AÛkAIrr  @@@  jAm" AtA°9j("\rAu j"\nAtA°9j(Aÿÿÿq K@ \nAk!   \rAvAq j"AtA°9j(AÿÿÿqM\r Aj!  N\r A!\n \nAN ! !  Aq "E\r @@ (x" (|"N@ AÀ Av j AL"6| (t All"\rA! ($" E\rAõ (,   Aj6x (t"\rA!  6t (x"Aj6x  Alj" : Aþ: B7  6  6  A F\r A F\r A kAM\r !  A! ! AqE\r @@ (x" (|" N@ AÀ Av j AL"6| (t All" \rA! ($" E\rAõ (,   Aj6x (t" \rA!  6t (x"Aj6x Alj" : Aþ: B7  \n6  6  A! !  kAG\r @@  (F\r N"AÿK@A!A!@@@  jAm" AtA j("\rAu j"\nAtA j(Aÿÿÿq K@ \nAk!   \rAvAq j"AtA j(AÿÿÿqM\r Aj!  N\r !  \nAN\r !  A F\r A F\r A kAM\r !  A! ! AqE\r @@ (x" (|" N@ AÀ Av j AL"6| (t All" \rA! ($" E\rAõ (,   Aj6x (t" \rA!  6t (x"Aj6x Alj" : A+: B7  6  6  A! !  kAG\r @@  (F\r N"AÿK@A!A!@@@  jAm" AtA j("\rAu j"\nAtA j(Aÿÿÿq K@ \nAk!   \rAvAq j"AtA j(AÿÿÿqM\r Aj!  N\r !  \nAN\r !  A F\r A F\r A kAM\r !  A! ! AqE\r @@ (x" (|" N@ AÀ Av j AL"6| (t All" \rA! ($" E\rAõ (,   Aj6x (t" \rA!  6t (x"Aj6x Alj" : A=: B7  6  6 A F\r A F\r A kAM\r !  A! ! AqE\r @@ (x" (|" N@ AÀ Av j AL"6| (t All" \rA! ($" E\rAõ (,   Aj6x (t" \rA!  6t (x"Aj6x Alj" : AÞ: B7  6  6 @ Aj" O\r -A?G\rA!@ (ì" M@ !  !  \nAkO\r  Aj" \n \nI"\rM@  j/A¿üF\r@@ Aj! Aj" \rK\r " j/A¿üG\r  !  \nI  Aj"Kq  Atj"(! (!  6ì ( ! @ M\r -A!G\r  j-AßqAÁkAÿqAK\r@ Aj" O\r@  j-AßqAÁkAÿqAK\r Aj" G\r ! A! (ð" K  \nAkOq\r Aj" \n \nI"\rM@  j-A>F\r@@ Aj! Aj" \rK\r " j-A>G\r  !  \nI  Aj" Kq  Atj"(! (!  6ð ( ! @ A j" O\r )B¼ÂìšÄ¨ªÁ… 1Bۅ„BR\rA!\r (ô" K  \nAkOq\r@ Aj" \n \nI" M@  j"/Aݺs -A>srE\r@@ Aj! Aj" K\r  "j"/Aݺs -A>sr\r  !  \nI  \rAj"\rKq@  \rAtj"(! (!  6ô ( !\n ! Aj" \nO\r@ Aj" \nO\r  j-" A0kAÿqA\nIA AßqAÁkAÿqAI E\r@  j-"A0kAÿqA\nI\r AßqAÁkAÿqAI\r A+k"AK\r AF\r Aj" \nF\r@  kA K\r  j-" A:F@  \nO\r@  j-" A k"AMAA tA€€q\r A>G@ A I\r AG\r  j-A-F\rAð! Aj !@@ (x" (|"N@ AÀ Av j AL"6| (t All"\rA! ($" E\r\rAõ (,  \r Aj6x (t"\rA! 6t (x"Aj6x  Alj" Ar: A<: B7  6  6@@ (x" (|"N@ AÀ Av j AL"6| (t All"\rA! ($" E\r\rAõ (,  \r Aj6x (t"\rA! 6t (x"Aj6x  Alj" Ar: A>: B7  6  Ak6 (t (x"Alj"A k Ak6 Ak Ak6 ! !   6 @@@ (x" (|"N@ AÀ Av j AL"6| (t All"E\r 6t (x"Aj6x  Aj6x (t"E\r  Alj"A¼(; B7  6  6@ (x" (|"N@ AÀ Av j AL"6| (t All"E\r 6t (x"Aj6x  Aj6x (t"E\r ( !  Alj"A¾0; B7  6  6 (t (x"Alj"A k Ak6 Ak Ak6A! !  (M\r@@   jAv"Atj"( K@ E@ !   AtjAk(K@ ! Ak!  ( O@ ! \n Aj!  M\r A!  ($"E\rAõ (,  A!  Aj!  A F\r A F\r A\rkA}K\r AÿG AúKq A!kAI A:kAIr AÛkAIrr "! As AßFq! @  \nF\rA!A!@  \nj" ( \nk"A4"AÿM@ A F\r A F\r A kAO\r  @@@  jAm"AtA j(" Au j"AtA j(Aÿÿÿq K@ Ak!   AvAq j"AtA j(AÿÿÿqM\r Aj!  N\r  AN\r A!Aä!AAA  A4"AÿM@ AÿG AúKq A!kAI A:kAIr AÛkAIrr  @@@  jAm"AtA°9j(" Au j"AtA°9j(Aÿÿÿq K@ Ak!   AvAq j"AtA°9j(AÿÿÿqM\r Aj!  N\r A! AN "Aq As qAq"!A  ! ! \n!  rE\r  kAp!@@ (x" (|"N@ AÀ Av j AL"6| (t All"\rA! ($" E\rAõ (,   Aj6x (t"\rA!  6t (x"Aj6x  Alj" AtA# AG  LqAA  NA AJr" AFrA@k:  : B7  \n6  6 \n \r"M\r@@@ (x" (|"N@ AÀ Av j AL"6| (t All"\rA! ($" E\rAõ (,   Aj6x (t"\rA!  6t (x"Aj6x  Alj"AÄ; B7  \r6  \r6 \rAj"\r \nI\r ! \n! @@ (x" (|"N@ AÀ Av j AL"6| (t All"\rA! ($" E\rAõ (,   Aj6x (t"\rA!  6t (x" Aj6x (!  Alj"Aÿ ; B7  6  6A! Aj$@ " \r A (xAÏAJ@ (à"AN@ Ak!\r A€j! AØj! A j!@ (t" Al"j" (! @ -"AÄF\r ( !A! A! AN@   Alj" ( Alj! Al! (! -A q@ A!: Ak"6A!!  j!  I@ ( I\r  I@ ( I\r @  I@ A!G\r  AÛG\r ( A€€ÀqE\r (" (O\r ( j-AÞG\r Aj"6 (t j"(" M\r  ( j  kK"E\r ( "E@ (pAj"6p  6  (Aj6 6 (6 -A”r:  -A˜r: (! (! (  @@@@ -\rA qE\r -AÛG\r E\r (" ("kAG\r -AÛG\r E\r ( ("kAG\r -AÝG\r  AkG\r ( ("kAG\r  (AjG\r@@ Aj" N"@ !  (t! !@@  Alj"\n-"AÄF\r AüF\r \n( kAäK\r \n-AqE\r \n( ! Aj" H\r A!\n \n  \n(" F\r  kAäK\r  I@ (!@@  j-A\nk Aj" G\r A!@   Atj"("L@ (t! @ Alj( " N\r  6  ("L@ (t! @ Alj( " N\r  6 Aj"AG\r \n@@ \n( (I@@  L\r  kAk! (t! A!  AsjAq"@@ AljAÄ; Aj! Aj" G\r AI\r@ Alj"AÄ;L AÄ;8 AÄ;$ AÄ; Aj" G\r \n \n-Ar: \n(6  @ \r  kAk! (t! A!  AsjAq"@@ AljAÄ; Aj! Aj" G\r AI\r@ Alj"AÄ;L AÄ;8 AÄ;$ AÄ; Aj" G\r  \n(6 (6  (6 (t" j" 6 j" 6  -Ar: -Ar: (! (!  Aj L (  (6  (6 (t" j" 6 j" 6  -Ar: -Ar: (! (! (  \r (!  (" ("G\r ( AjK@ -AÀq\r    ( AjM  -AÀq\r   (  AjM "AH\r E\r  (6 (t Alj(!  @  ("O\r (" j-A(G\r Aj!\nA! \r!@@@   jAv"Atj"( K@ E\r  AtjAk( I\r Ak!   (M\r Aj!  O\r A! @@ Aj"  Atj(" I@@  j-A k"AK\rA tA€€qE\r Aj" G\r !  6@  O\r  j-A\nk Aj" O\r  Atj(!  6 @  O\r  j-A)G\rA! A6 B7  6  6 Aj! A    Atj"(" Aj Aj 5E\r@   k ( Aj A j A,j A(j A$j6@ ( " j! (," F!   j"Atj(! (!   ("6(  6$A!A! @  I@@  j-"A k"AK\rA tA€€qE\r Aj" G\r Aj" O\r   Atj("j-! AÿqA)G\r  Aj" 6 ($" (("M@A! A6 B7A   k! @ A6  6   j"6A  @ j"@  Atj!A! !@   Atj("  K" KA!  "kAq"@@  ( j-: Aj! Aj! Aj" G\r  kA|M@@  ( j-:  ( j-:  ( j-:  ( j-: Aj! Aj" G\r   O\r A\n: Aj!  Aj"Atj(! ($" E\rAä (,   A6  6   k6 (! A ! (x" \nL@  6  (t! \n!@  Alj"(K@@ -AqAG\r  ( "Alj( I\r \r k  Aj" H\r  6  \nL\r  kAk! (t! A!  AsjAq"@@ \nAljAÄ; \nAj!\n Aj" G\r AI\r@ \nAlj"AÄ; AÄ;L AÄ;8 AÄ;$ \nAj"\n G\r  -AÀq\r   ( ( AjM"AH\r E\r -Ar:  -Ar: Aj" (tj (6 (t j ( 6  (tj (6( Aj! (t! (@  Alj (Ð6 6Ð  Alj (6 (! (! -!  Aj L   AÛF"!   !   !   ! ! / AŒqE\r@@ "Aj! -$"AÄG" \r  I\r À! !@@ "Ak! \r  K\r -%" AqE\r E\r ( (G\rA‡ APE\r (  (tkAmG\r Ak( (G\r AÄ:$  Aïq:% AkAÄ: Ak" -Aïq: "AN\r B7à A6¸ @A! A6Ô (x"AL\r (t! A!@@ Alj"-"Aq\r -AüG\r ( (kAG\r A6  Ar:@ (ØAH@ 6Ø  (ÜAlj 6 6Ü Aj"6Ô Aj" G\r A!  A! A (xL  A! A0j$ Þ#Aðk"$ B7h B7` B7X B7P B7H B7@ B78 B70 A6( B7 B7 B7 B7A! @    Aj AÐj9\r  A Aj A0j9\r  6( (,! @  Aj (" E@A!  ($"E\rAÒ (,    Aj (" E@A!  ($"E\rAö (,  (`@ (Pk (Tk (Xk (@@ (0k (4k (8k Aðj$ Ý@ ( " ("I@ (!  ( Av jA "At"l"E@A ($"E\rAõ (, A  6 ( Ajl"E@A ($"E\rAõ (, A  6  6 ( ! ( Atj 6  ( Atj 6  ( Aj6 A Ü Aj!@@@ Aj" O\r j-A#G\r j-A rAøG\r !@@@@ j-"A0kAÿqA\nI\r AÁkAÿqAI\r  kAK\r AákAÿqAI\r   kAK\r Aj" G\r !  F\r  kAI\r  @ Aj" O"\r j-A#G\r !@@  kAK\r j-A0kAÿqA K\r Aj" G\r !  F\r  kAI\r   M\r j-AßqAÁkAÿqAK\r  \r@@ j-"AßqAÁkAÿqAO@   kA0K\r A0kAÿqA\nI\r    kA0M\r   Aj" G\r  " kAkA.K\r  M\r j-A;G\rA!  Aj6  ³#Ak"$@@  O\r (!@  j-A k"AK\rA tA€€qE\r Aj" G\r !   O\r (!@  jAk-A k"AK\rA tA€€qE\r Ak" K\r !  6  6  6@  Aj (, ("@ ($"E\rAš (,   AjA<"\r  Aj (, ("E@A!  ($"E\rA¿ (,  Aj$  à!#A0k"\r$  H@ A¸j! A€j! \rAr! \rAr! (!@@ (t" Al" j" -"Aq@ AqE\r@ E\r ,"E\r  [\r ( !  ,"\nAÿq"E\r  \n[E\r (" I\r@@@@@@@@@@@@@@@@@ A!k^  (! Aq@ AN@  Alj" -AÀr: 6  6  AH\r  Alj"( 6¸  6 6@ (ä" AN@  Alj 6  6à 6ä A6  Aj" (xN\r  Alj"-A;G\r (  (" \rHE\r 6  6 Ar:  -Ar: 6  AqE\r \r A€A˜ \nA*Fj" A j6 AÀq"AÀF@A!   \r Aj6 A€F\rA!  Aj6A! A°A´ ( kAFj!\n@ AqE\rA! \n("AH\r@   Atj"\n("L@@  Alj( " N\r \n 6  \n("L@@  Alj( " N\r \n 6 Aj" AG\r  Alj" 6 6  -Ar: -Ar: \r AqE\r \n(6 \n 6 @ AqE\rA! (Ä"AH\r@   Atj"\n("L@@  Alj( " N\r \n 6  \n("L@@  Alj( " N\r \n 6 Aj" AG\r  Alj" 6 6  -Ar: -Ar: AqE\r (Ä6 6Ä @ AqE\r (¼"AH\rA!  Alj" ( (k ( kG\r@   Atj"\n("L@@  Alj( " N\r \n 6  \n("L@@  Alj( " N\r \n 6 Aj" AG\r @ Aj" N\r  kAk!A!  AsjAq"\n@@  AljAÄ; Aj! Aj" \nG\r AI\r@  Alj" AÄ;L AÄ;8 AÄ;$ AÄ; Aj" G\r 6 6 -Ar: -Ar: A6¼ AqE\r\n (¼6 6¼ \n  Aj"Al"j"(! (! \r 6 \r ("6, \r 6( \r 6$ \nAÀF@   \rAAAA¨ \rA(jOAL\r\n \r(!  O\r N"AÿM\rA!A!@@@  jAm" AtA j("Au j"AtA j(Aÿÿÿq K@ Ak!   AvAq j"AtA j(AÿÿÿqM\r Aj!  N\r  AN\r  ( kAG\r@ AqE\rA! (À"AH\r@   Atj"\n("L@@  Alj( " N\r \n 6  \n("L@@  Alj( " N\r \n 6 Aj" AG\r  Alj" 6 6  -Ar: -Ar: AqE\r (À6 6À  ( kAG\r@ AqE\rA! (È"AH\r@   Atj"\n("L@@  Alj( " N\r \n 6  \n("L@@  Alj( " N\r \n 6 Aj" AG\r  Alj" 6 6  -Ar: -Ar:  AqE\r (È6 6È  ( kAG\r@ AqE\rA! (Ì"AH\r@   Atj"\n("L@@  Alj( " N\r \n 6  \n("L@@  Alj( " N\r \n 6 Aj" AG\r  Alj" 6 6  -Ar: -Ar:  AqE\r (Ì6 6Ì  A!A!  A F\r A F\r A kAI\r ( jAk,"@Aó AP\r \r((" I\r Ak!@@@ -AÄF\r ( K\r  (O\r -" Aq\r Ak" O\r  AqE\r   \rA,jAA.AAè \rA$jOAH\r \r(,!@ \nAÀF\r@  O\r (" j-A/G\r Aj  \rA,jAA/A‰A \rA$jOAH\r \r(," O\r  j-A/G\r \r Aj"6, @  O\r ( j-A?G\r Aj  \rA,jAA&AƒA¥ \rA$jOAH\r \r(,!  O\r ( j-A#G\r Aj  \rA,jAAAAë \rA$jOAH\r \r(,! @  O\rA!A!@ ( j" ( kA4"AÿM@ A F\r A F\r A kAO\r  @@@  jAm" AtA j("Au j"AtA j(Aÿÿÿq K@ Ak!   AvAq j"AtA j(AÿÿÿqM\r Aj!  N\r  AN\r ,"@A± A P\r \r($"  (xAlj"O\r@@@ -AÄF\r ( M\r ( K\r -"Aq\r Aj" I\r  AqE\r 6 6  \n:  6  6 (t" j" 6  j" 6  -Ar:  -Ar:  \r Atr 6 A q! Aj! AàqAàG@ \r Atj Aj6 Aj! As AGqE@ \r Atj Aj6 Aj! Aq! Aq!A!A!A! A!@@ \r Atj"(("AH\r  Alj! @ ( (M\r ! ! @ (("AH\r  Alj! @ ( (M\r ! ! Aj! Aj" G\r @ E\r \r Atj((" AH\r  Alj! @ ( (M\r ! ! E\r (! (! (! !\n@@@@@@  Alj"-"A!k\n AÛk A€A˜ A*Fj -"AtAuA qj!\n@ AvAk \nAj!\n  A°A´ ( (kAFj!\n  \nAj!\n @  k"  k" K@  (" (k k j"Al"j" (6  )7  )7   k" 6  6 (t j \n(6 \n 6   O\r  Alj j" (6  )7  )7  k j"6  6 A!\n@   \nAtj"("L@@ AN@  (t Alj( "6  N\r \nAj"\nAG\r (t" Alj" 6  j" 6  -Ar:  -Ar:  AqE\r A€A˜ \nA*Fj AtAuA qj!@@@ AvAk Aj!  Aj! (6  6 -"AqE\r Aq@ (t ( Alj(!  (! Aj" H\r \rA0j$ –#Ak"$@ (E\r  3! (  (pAtj("E@A!  @  ("I\r   ( (ljO\r  (F@ ( (  AE\r A!   6  6  6  Aj6 Aj!@ ("@@   Av"Atj"D"AH@   E\r Aj!  Asj "\r A! E@A!  (! Aj$  ý#A k"$ Aß: A¦Ô;A! ( "A€ q@ A$:A! A€€€q@ Aj rA=: Aj! A€€€q@ Aj rA+: Aj! A€€q@ Aj jAü: Aj! A€€q@ Aj jAÞ: Aj! A€„q@ Aj jAþ: Aj! A! Aj jA:    AjAJ ( "AŒq@ Aq@ AÀ:A! Aq@  rA:: Aj! A€q  rA.: Aj  rA:     AjJ A€jAÿAÐü A j$ ‰ (TE@A Ak! Ak!AA ("\n j-A!F j!A!@@   jAv"Atj" ( K@ E\r  AtjAk( I\r Ak!   (M\r Aj!  @  (M" E@@  kj"@ !@  Atj("  K" KA!  "kAq"\n@@  ( j-: Aj! Aj! Aj" \nG\r  kA|M@@  ( j-:  ( j-:  ( j-:  ( j-: Aj! Aj" G\r   O\r A : Aj! Aj" Atj(! A! ($"E\rAä (, A  k   \nj!  k ! A  Aj60A! Aj !\nA!@@ \n("E\r Aj i"AH\r   kK\r \nAj!\n  j" I\r A=! AH\r A \r  e E@A!  A! (0!\n@ \n(" E\r Aj i" j" K\r Aj a \nAj!\n  K\r A \r  A€Àse \r   \rH!   AHq\r A=! +0 \r   f"AN\r \n -!\n Aj! \r E\rA!@  Atj("\n@  Atj \n cA! Aj"A\nG\r A\nO@A! \n @  Atj(\rA! Aj"A\nG\r A!   :\'A! ! !   k"  H" AÿÿÿÿsJ\rA=! \r  j"  \rH" \nK\r A   e  a A0   A€€se A0  Ae a A   A€Àse ( Ak" O\r B€”ëÜT\r Ak" > @ "I@ Ak"(E\r (¬ k"6¬ ! AJ\r AH@ AjA nAj! AæF!@A A k" A O!\n@  M@AA (!  A€”ëÜ \nv!\rA \ntAs!A! !@  (" \nv j6  q \rl! Aj" I\r AA (! E\r  6 Aj! (¬ \nj"6¬   j" " Atj   kAu J! AH\r A!@  M\r  kAuA l!A\n! ("A\nI\r@ Aj!  A\nl"O\r A AæGk AçF AGqk"  kAuA lA kH@ A„`Aìc AHj A€Èj"A m"\rAtj!\nA\n!  \rA lk"AL@@ A\nl! Aj"AG\r @ \n("  n" lk"\rE \nAj" Fq\r@ AqE@D@C! A€”ëÜG\r \nO\r \nAk-AqE\r D@C! Dà?Dð?Dø?  FDø? \r Av"F \r I!@ \r -A-G\r š! š! \n  \rk"6    a\r \n  j"6 A€”ëÜO@@ \nA6 \nAk"\nK@ Ak" A6 \n \n(Aj"6 Aÿ“ëÜK\r  kAuA l!A\n! ("A\nI\r@ Aj!  A\nl"O\r \nAj"   K! @ " M"E@ Ak"(E\r @ AçG@ Aq!\r  AsA A " J A{Jq"\n j! AA~ \n j! Aq"\r\rAw!@ \r Ak("\nE\rA\n!A! \nA\np\r@ "\rAj! \n A\nl"pE\r \rAs!  kAuA l! A_qAÆF@A!\r  jA k"A AJ"  J!  A!\r  j jA k"A AJ"  J! A!\n AýÿÿÿAþÿÿÿ \rr"J\r AGjAj!@ A_q"AÆF@  AÿÿÿÿsJ\r A AJ!    Au"s k­ d"kAL@@ Ak"A0:  kAH\r Ak" : AkA-A+ AH:  k" AÿÿÿÿsJ\r  j" AÿÿÿÿsJ\r A   j" e  a A0   A€€se@@@ AÆF@ AjA r!  K"! @ 5 d!@  G@  AjM\r@ Ak"A0:  AjK\r   G\r Ak"A0:   ka Aj" M\r @ A‰Aa  M\r AL\r@ 5 d" AjK@@ Ak"A0:  AjK\r A A Na A k! Aj" O\r A J! ! \r  @ AH\r  Aj  K!\n AjA r! !@  5 d"F@ Ak"A0: @  G@  AjM\r@ Ak"A0:  AjK\r  Aa Aj! \rrE\r A‰Aa   k"  Ha k! Aj" \nO\r AN\r A0 AjAAe   ka  ! A0 A jA Ae A   A€Àse    J!\n   AtAuA qj!@ A K\r -!Dð?!@A4 Atk"A€N@Dà! AÿI@ Aÿk!  Dð!Aý  AýOAþk!  AxJ\rD`! A¸pK@ AÉj!  D!Aðh  AðhMA’j!  Aÿj­B4†¿¢! A-F@  š ¡ š!     ¡!  (¬" Au"s k­ d"F@ Ak"A0: (¬! Ar! A q! Ak"\r Aj: AkA-A+ AH: AqE ALq! Aj!@ " ü"AÀÐj- r:  ·¡D0@¢!@ Aj" AjkAG\r Da q\r A.: Aj! Db\r A!\n Aûÿÿÿ   \rk" jkJ\r A  Aj  Ajk" Ak H  " jj" e  a A0   A€€se Aj a A0  kAAe \r a A   A€Àse    J!\n A°j$ \n ¹~  (AjAxq"Aj6 )! )!#A k"$ Bÿÿÿÿÿÿ?ƒ!~ B0ˆBÿÿƒ"§"AøkAýM@ B† B<ˆ„! A€øk­!@ Bÿÿÿÿÿÿÿÿƒ"B€€€€€€€Z@ B|!  B€€€€€€€€R\r Bƒ |! B  BÿÿÿÿÿÿÿV"! ­ |  @  „P\r BÿÿR\r B† B<ˆ„B€€€€€€€„!Bÿ  Aþ‡K@B!Bÿ  A€øAø P"\n" k" AðJ@B!B   B€€€€€€À„ \n!A!\n G@ Aj! ! !@A€ k"AÀq@  A@j­†!B!  E\r  ­"† AÀ k­ˆ„!  †! 7 7 ) )„BR!\n @ AÀq@  A@j­ˆ!B!  E\r AÀ k­†  ­"ˆ„!  ˆ!  7  7 )B† )"B<ˆ„!@ \n­ Bÿÿÿÿÿÿÿÿƒ„"B€€€€€€€Z@ B|!  B€€€€€€€€R\r Bƒ |! B€€€€€€€…  BÿÿÿÿÿÿÿV"! ­ ! A j$ B€€€€€€€€€ƒ B4†„ „¿9 ¦ (T"(! (" ( ("k"  I"@   Q  ( j"6  ( k"6    K"@   Q  ( j"6  ( k6 A: (,"6 6  ¢ E@A @  AÿM\r@AÔÚ((E@ A€qA€¿F\rAèÛA6  AÿM@ A?qA€r: AvAÀr:A  A€@qA€ÀG A€°OqE@ A?qA€r: A vAàr: AvA?qA€r:A  A€€kAÿÿ?M@ A?qA€r: AvAðr: AvA?qA€r: A vA?qA€r:A  AèÛA6 AA  :A Ñ( #Ak"\n$@@@@@@@@@@ AôM@A¤Ü("A A jAøq A I"Av"v"Aq@@ AsAq j"At"AÌÜj" (Ô\\"("F@A¤Ü A~ wq6   6 6 Aj!  Ar6  j" (Ar6 A¬Ü("M\r @@ tA t"A krqh"At"AÌÜj" (Ô\\"("F@A¤Ü A~ wq"6   6  6 Ar6 j"  k"Ar6 j 6 @ AxqAÌÜj!A¸Ü(! A Avt"qE@A¤Ü  r6   ( !  6  6  6  6 Aj!A¸Ü 6A¬Ü 6 A¨Ü(" E\r hAt(Ô^"(Axq k! !@@ ("E@ ("E\r (Axq k"   K"!  ! !  (!  ( "G@ (" 6 6 \n (" Aj ("E\r Aj !@ ! "Aj! ("\r Aj! ("\r A6 A! A¿K\r A j"Axq!A¨Ü(" E\rA! AôÿÿM@ A& Avg"kvAq AtkA>j! A k!@@@ At(Ô^"E@A!  A! A AvkA AGt!@@ (Axq k" O\r ! "\rA! !  ("   AvAqj("F ! At! "\r rE@A!A t"A kr q"E\r hAt(Ô^! E\r @ (Axq k" I!   !  ! ("  ( "\r E\r A¬Ü( kO\r (!  ( "G@ (" 6 6  (" Aj ("E\r Aj !@ ! "Aj! ("\r Aj! ("\r A6  A¬Ü("M@A¸Ü(!@ k"AO@  j" Ar6 j 6  Ar6   Ar6 j" (Ar6A!A! A¬Ü 6A¸Ü 6 Aj! A°Ü("I@A°Ü  k"6A¼ÜA¼Ü(" j"6  Ar6 Ar6 Aj! A! A/j"Aüß(@A„à(  AˆàB7A€àB€ €€€€7Aüß \nA jApqAتժs6AàA6AàßA6A€ "j"A k"q" M\rAÜß("@AÔß(" j" M\r  I\r @Aàß-AqE@@@@@A¼Ü("@Aäß!@ (" M@   (jI\r ("\r An"AF\r !A€à("Ak" q@  k  jA kqj!  O\rAÜß("@AÔß(" j" M\r I\r n" G\r   k q"n" ( (jF\r ! AF\r A0j M@ !  A„à("  kjA kq"nAF\r  j! !  AG\r AàßAàß(Ar6 n!An! AF\r AF\r M\r k" A(jM\r AÔßAÔß( j"6AØß( I@AØß 6 @A¼Ü("@Aäß!@  (" ("jF\r ("\r  A´Ü("A ME@A´Ü 6 A!Aèß 6Aäß 6AÄÜA6AÈÜAüß(6AðßA6@ At" AÌÜj"6Ô\\  6Ø\\ Aj"A G\r A°Ü A(k"Ax kAq"k"6A¼Ü  j"6  Ar6 jA(6AÀÜAŒà(6   O\r  I\r ( Aq\r  j6A¼Ü Ax kAq"j"6A°ÜA°Ü( j" k"6  Ar6  jA(6AÀÜAŒà(6  A!  A!  A´Ü( K@A´Ü 6  j!Aäß!@@  ("G@ ("\r  - AqE\r Aäß!@@ (" M@   (j"I\r (!  A°Ü A(k"Ax kAq"k"6A¼Ü  j"6  Ar6 jA(6AÀÜAŒà(6  A\' kAqjA/k" AjI"A6 Aìß)7 Aäß)7Aìß Aj6Aèß 6Aäß 6AðßA6 Aj!@ A6 Aj! Aj!  I\r  F\r  (A~q6   k"Ar6  6 AÿM@ AøqAÌÜj!A¤Ü("A Avt"qE@A¤Ü  r6  ( ! 6  6 A !A  A! AÿÿÿM@ A& Avg"kvAq AtrA>s!  6 B7 AtAÔÞj!@@A¨Ü("A t"qE@A¨Ü  r6  6  6  A AvkA AGt! (!@ "(Axq F\r Av! At!  Aqj"("\r  6  6 A! ! !A  (" 6  6  6A!A!A j 6  j 6 A°Ü(" M\rA°Ü k"6A¼ÜA¼Ü(" j"6  Ar6 Ar6 Aj!  AèÛA06A!  6 ( j6 Ax kAqj" Ar6 Ax kAqj"  j"k!@A¼Ü( F@A¼Ü 6A°ÜA°Ü( j"6  Ar6  A¸Ü( F@A¸Ü 6A¬ÜA¬Ü( j"6  Ar6  j 6  ("AqAF@ Axq! ( !@ AÿM@ (" F@A¤ÜA¤Ü(A~ Avwq6  6  6  (!@  G@ (" 6  6  @ (" Aj ("E\r Aj !@ ! "Aj! ("\r Aj! ("\r A6  A! E\r@ ("At"(Ô^ F@ AÔÞj 6 \rA¨ÜA¨Ü(A~ wq6  @  (F@  6   6 E\r  6 ("@  6  6 ("E\r  6  6  j"(!  j!  A~q6  Ar6  j 6 AÿM@ AøqAÌÜj!A¤Ü("A Avt"qE@A¤Ü  r6   ( !  6  6  6  6  A! AÿÿÿM@ A& Avg"kvAq AtrA>s!  6 B7 AtAÔÞj!@@A¨Ü("A t"qE@A¨Ü r6  6  6  A AvkA AGt! (!@ "(Axq F\r Av! At!  Aqj"("\r  6  6  6  6  (" 6  6 A6  6  6 Aj!  @ E\r@ ("At"(Ô^ F@ AÔÞj 6 \rA¨Ü A~ wq" 6  @  (F@  6   6 E\r 6 ("@ 6  6 ("E\r 6  6 @ AM@   j"Ar6 j" (Ar6   Ar6  j" Ar6  j 6 AÿM@ AøqAÌÜj!A¤Ü("A Avt"qE@A¤Ü  r6  ( ! 6  6  6  6  A! AÿÿÿM@ A& Avg"kvAq AtrA>s!  6 B7 AtAÔÞj!@@ A t"qE@A¨Ü  r6  6  6  A AvkA AGt! (!@ "(Axq F\r Av! At!  Aqj"("\r  6  6  6  6  (" 6  6 A6  6  6 Aj!  @ E\r@ ("At"(Ô^ F@ AÔÞj 6 \rA¨Ü A~ wq6  @  (F@ 6  6 E\r 6 ("@ 6  6 ("E\r 6  6 @ AM@   j"Ar6 j" (Ar6   Ar6  j" Ar6  j 6 @ AxqAÌÜj!A¸Ü(!A Avt" qE@A¤Ü  r6   ( !  6  6 6 6 A¸Ü 6A¬Ü 6 Aj! \nAj$ ‚ @ E\r Ak" Ak("Axq"j!@ Aq\r AqE\r  ("k"A´Ü(I\r j!@@@A¸Ü( G@ ( ! AÿM@  ("G\rA¤ÜA¤Ü(A~ Avwq6  (!  G@ (" 6  6  (" Aj ("E\r Aj !@ ! "Aj! ("\r Aj! ("\r A6  ("AqAG\rA¬Ü 6  A~q6  Ar6  6  6  6  A! E\r@ ("At"(Ô^ F@ AÔÞj 6 \rA¨ÜA¨Ü(A~ wq6  @  (F@  6   6 E\r  6 ("@  6  6 ("E\r  6  6  O\r ("AqE\r@@@@ AqE@A¼Ü( F@A¼Ü 6A°ÜA°Ü( j"6  Ar6 A¸Ü(G\rA¬ÜA6A¸ÜA6 A¸Ü(" F@A¸Ü 6A¬ÜA¬Ü( j"6  Ar6 j 6 Axq j! ( ! AÿM@ (" F@A¤ÜA¤Ü(A~ Avwq6   6  6  (!  G@ (" 6  6  (" Aj ("E\r Aj !@ ! "Aj! ("\r Aj! ("\r A6   A~q6  Ar6 j 6  A! E\r@ ("At"(Ô^ F@ AÔÞj 6 \rA¨ÜA¨Ü(A~ wq6  @  (F@  6   6 E\r  6 ("@  6  6 ("E\r  6  6  Ar6 j 6  G\rA¬Ü 6 AÿM@ AøqAÌÜj!A¤Ü("A Avt"qE@A¤Ü r6   ( !  6 6  6  6 A! AÿÿÿM@ A& Avg"kvAq AtrA>s!  6 B7 AtAÔÞj!@A¨Ü("A t"qE@A¨Ü  r6  6A!A  A AvkA AGt! (!@ "(Axq F\r Av! At!  Aqj"("\r  6A! !A ! !   (" 6  6A!A!A !  j 6  6 j 6AÄÜAÄÜ(Ak"A 6 ‰  E@ j A@O@AèÛA06A A A jAxq A I! Ak"("Axq!@ AqE@ A€I\r Aj M@ !  kA„à(AtM\r A   j!@  O@  k"AI\r   AqrAr6  j" Ar6  (Ar6  m  A¼Ü( F@A°Ü( j" M\r   AqrAr6  j"  k"Ar6A°Ü 6A¼Ü 6  A¸Ü( F@A¬Ü( j" I\r@  k"AO@   AqrAr6  j" Ar6  j" 6  (A~q6   Aq rAr6  j" (Ar6A!A! A¸Ü 6A¬Ü 6  ("Aq\r Axq j"\n I\r \n k! ( !@ AÿM@ (" F@A¤ÜA¤Ü(A~ Avwq6   6  6  (! @  G@ (" 6  6  @ (" Aj ("E\r Aj !@ ! "Aj! ("\r Aj! ("\r A6  A! E\r@ ("At"(Ô^ F@ AÔÞj 6 \rA¨ÜA¨Ü(A~ wq6  @  (F@ 6  6 E\r  6 ("@  6  6 ("E\r  6  6 AM@  Aq \nrAr6  \nj" (Ar6    AqrAr6  j" Ar6  \nj" (Ar6  m !  "@ Aj j"E@A  A|Ax Ak("Aq Axqj"   KQ k  «  j!@@ ("Aq\r AqE\r (" j!@@@ k"A¸Ü(G@ ( ! AÿM@  ("G\rA¤ÜA¤Ü(A~ Avwq6  (! G@ (" 6  6  (" Aj ("E\r Aj !@ ! "Aj! ("\r Aj! ("\r A6  ("AqAG\rA¬Ü 6  A~q6 Ar6  6  6  6  A! E\r@ ("At"(Ô^ F@ AÔÞj 6 \rA¨ÜA¨Ü(A~ wq6  @ (F@  6   6 E\r  6 ("@  6  6 ("E\r  6  6 @@@@ ("AqE@A¼Ü( F@A¼Ü 6A°ÜA°Ü( j"6 Ar6 A¸Ü(G\rA¬ÜA6A¸ÜA6 A¸Ü(" F@A¸Ü 6A¬ÜA¬Ü( j"6 Ar6 j 6 Axq j! ( ! AÿM@ (" F@A¤ÜA¤Ü(A~ Avwq6   6  6  (!  G@ (" 6  6  (" Aj ("E\r Aj !@ ! "Aj! ("\r Aj! ("\r A6   A~q6 Ar6 j 6  A! E\r@ ("At"(Ô^ F@ AÔÞj 6 \rA¨ÜA¨Ü(A~ wq6  @  (F@  6   6 E\r  6 ("@  6  6 ("E\r  6  6 Ar6 j 6 G\rA¬Ü 6 AÿM@ AøqAÌÜj!A¤Ü("A Avt"qE@A¤Ü  r6   ( !  6  6 6 6 A! AÿÿÿM@ A& Avg"kvAq AtrA>s! 6 B7 AtAÔÞj!@@A¨Ü("A t"qE@A¨Ü  r6  6 6  A AvkA AGt! (!@ "(Axq F\r Av! At!  Aqj"("\r  6 6 6 6 (" 6  6 A6 6 6 W~@ ­B|BøÿÿÿƒAØÚ("­|"BÿÿÿÿX@ §"?AtM\r \r AèÛA06A AØÚ 6 ß  #Ak"$  6 @ AÓM@AÐÐAÒ A jp(!  A|O@]  AÒn"AÒl"k6AÒAÐÓ Ajp"( j! AÒkAu!@A! !@@@ ! A/F@AÓ!@ n" I\r  lF\r A\nj"n" I\r  lF\r A j"n" I\r  lF\r Aj"n" I\r  lF\r Aj"n" I\r  lF\r Aj"n" I\r  lF\r Aj"n" I\r  lF\r Aj"n" I\r  lF\r A$j"n" I\r  lF\r A(j"n" I\r  lF\r A*j"n" I\r  lF\r A.j"n" I\r  lF\r A4j"n" I\r  lF\r A:j"n" I\r  lF\r AA AM!@@ j"\rA”à("E\r \n  "E@u ]  \\w ¬@  (AÿÿÿÿqAkA\n , "AH"" (  "kM@ E\r ( AH! @  j  ü\n  j!@ , AH@ 6  Aÿq:  jA:   j k  A  x …Aöÿÿÿ k O@A÷ÿÿÿ! ( , AH! AòÿÿÿM@A  j" At"  K"ArAj A I! t!@ E\r E\r  ü\n @ E\r E\r  j  ü\n   j"k!\n@  F\r \nE\r  j j  j j \nü\n A\nG@ k 6 A€€€€xr6  j \nj"6  jA: u Å@@@ , "AN@A\n! A\nF\r AjAÿq:  (" (AÿÿÿÿqAk"G\r !@ AöÿÿÿG@A÷ÿÿÿ! ( , AH! AòÿÿÿM@A Aj" At"  K"ArAj A I! t!@ E\r E\r   ü\n @  F\r  k"E\r  j  j ü\n A\nG@ k 6 A€€€€xr6  u 6 ! Aj6 (! j"A: : « A÷ÿÿÿI@ (AÿÿÿÿqAkA\n , "AH" I@A ArAj A I"t! (!@ (  "Aj"E\r E\r   AH ü\n AH@ k 6 6 A€€€€xr6  u h (Aÿÿÿÿq" K@ 6 (!@ E\r E\r   ü\n  jA: Ak  kAj ("A   x Q A\nM@ : @ E\r E\r  ü\n jA: A\n A\nk - Aÿq"A  x #A k"$#Ak"$ Aj" A j" Aj"k"A L@A= A ArgkAÑ lA v"  At(°TIkAjH\r  A¿„=M@ AÎM@ AãM@ A M@  A0r: Aj   At/àT; Aj  AçM@  AÿÿqAän"A0r:   AälkAÿÿqAt/àT; Aj   ~  AŸM@  AÎn"A0j: Aj  AÎlk~     AÿÁ×/M@ Aÿ¬âM@  AÀ„=n"A0j: Aj  AÀ„=lk   €  Aÿ“ëÜM@  A€Â×/n"A0j: Aj  A€Â×/lk€   A€Â×/n"At/àT; Aj  A€Â×/lk€ !A 6  6 A j" (6  ( 6 Aj$@ ( " Aj"k"A÷ÿÿÿI@@ A\nM@ :  Ar"Ajt! Aÿÿÿÿk6 6 6 !  k!@  F\r E\r  ü\n jA:  u A j$ 2 Aän"At/àT;  AälkAt/àT; Aj * AÎn"At/àT; Aj  AÎlk~ * AÀ„=n"At/àT; Aj  AÀ„=lk  AÐjjAÐj  t E@ ( (F F@A ("-!@ ("-"E\r G\r@ -! -"E\r Aj! Aj! F\r F Ú#AÐk"$@A ( (F\r#A@j"$  ("Ak("j!@ Ak("(AÜÖ(F@A  !   "L@ B7 A6 AØÖ6  6  6 B7 B7$ B7, A6< B€€€€€€€74  Aj  AA (( (\r B7 A6 A¨Ö6  6 AØÖ6 B7 B7$ B7, B73 A6< A:;  Aj AA (( A!@@ (( (A ($AFA ( AFA (,AF!  (AG@ (,\r ( AG\r ($AG\r (! A@k$A "E\r ("E\r AjAA8ü A:K A6  6  6 A6D  Aj A (( (,"AF@  ($6 AF ! AÐj$  A6 Aç6 AŽ 6] v ($"E@ 6 6 A6$ (86 @@ ( (8G\r ( G\r (AG\r 6 A:6 A6 Aj6$  ( ((F@   … 5 ( ((F@   … ("    (( { ("Aq! -7AF@ Au" E\r ( j(  Au E\r  ((68A!A ! ("   j A Aq (( r ( ((F@   … ( ! Aj"   ˆ@ AI\r  Atj! Aj!@   ˆ -6\r Aj" I\r š A:5@  (G\r A:4@ ("E@ A6$ 6 6 AG\r (0AF\r   F@ ("AF@ 6 ! (0AG\r AF\r  ($Aj6$ A:6 Â@ ( ƒ@  (G\r (AF\r  6 @ ( ƒ@@ ( G@  (G\r AG\r A6   6 (,AF\r Aj" ( Atj!@@@ @  M\r A;4    A Œ -6\r -5AG\r -4AF@ (AF\rA!A! -AqE\r  A! -Aq\rA  AA Aq 6, \r  A6,  Aj! ( ! Aj"     AI\r  Atj! Aj!@ ("AqE@ ($AG\r @ -6\r      Aj" I\r  AqE@@ -6\r ($AF\r      Aj" I\r  @ -6\r ($AF@ (AF\r      Aj" I\r   6  ((Aj6( ($AG\r (AG\r A:6 K ("Au! ("   Aq ( j(  j A Aq  (( I ("Au! ("  Aq ( j(  j A Aq  (( Š@ ( ƒ@  (G\r (AF\r  6 ( ƒ@@ ( G@  (G\r AG\r A6   6 @ (,AF\r A;4 ("   A  (( -5AF@ A6, -4E\r  A6,  6  ((Aj6( ($AG\r (AG\r A:6 ("     (( ¦@ ( ƒ@  (G\r (AF\r  6 ( ƒE\r@ ( G@  (G\r AG\r A6   6  6  ((Aj6(@ ($AG\r (AG\r A:6 A6, › ( ƒ@    Š -5! ( ! A:5 -4! A:4 Aj"     Œ  -4"\nr!  -5" r!@ AI\r Atj! Aj!@ -6\r@ \nAq@ (AF\r -Aq\r  AqE\r -AqE\r A;4      Œ -5" rAq! -4"\n rAq! Aj" I\r  Aq:5  Aq:4 9 ( ƒ@    Š ("      ((  ( ƒ@    Š A§ 7 AàÙ6 ("Ak" (Ak"6 AH@ A kk ” k  (  $ # kApq"$ # ‘P(A€ Þ{"type":"Document"},"attributes":{summarytbodyTableBodyLatexMathDisplay\\u%04x-+ 0X0x-0X+0X 0X-0x+0x 0xTableRowdivnavmenuTextUnorderedListOrderedListstartSuperscriptSubscripttfootcountbodyRowCountheadRowCountbasefontDocumentimportantdefaulttightrightHighlightleftframesetfieldsetdt\\taddressdetailsEmphasisnoframes%s:%d: %strvectordirhrfootercenterSpoilerheaderfenceChar\\roptgroupcolgroup/Users/ernest/react-native-enriched-markdown/packages/core/cpp/wasm/ASTSerializer.cpp/emsdk/emscripten/system/lib/libcxxabi/src/private_typeinfo.cpptipcautionoptionfigcaptionAdmonitionsectionmainalignnan\\nformmenuitemListItemparamulurlcolhtmlTableHeaderCellTableCellleveldlisTasklinkLinkCodeBlocktrackSoftBreakLineBreakThematicBreaklibad_array_new_lengthParagraphStrikethroughsearchdialogStrongbasic_stringnodeTypeToStringwarningHeadinginf\\ftrueblockquoteBlockquotenotefalsebasefigureadmonitionTypeUnderlineLatexMathInlineBlankLineiframetitlearticletableTableImageCodeasidetdlegendtaskCheckedddtheadTableHead\\b"\'=<>`-_.-+_\\\\({[,"children":[NANINFcatching a class without an object?)}].!?,;mailto:,"content":h6h5h4h3h2h1http://Too many link reference definition instantiations.Unsupported abi_version.Aborted from text() callback.Aborted from enter_span() callback.Aborted from leave_span() callback.Aborted from enter_block() callback.Aborted from leave_block() callback.malloc() failed.realloc() failed.@:..!#$%&\'*+/=?^_`{|}~-_.:-+-.-_+-_*(null)Suppressing too sparse table (see https://github.com/mity/md4c/issues/345)._=().+-_~%false && "unhandled NodeType in nodeTypeToString"\\"{"type":"[]!xX \nñ<‡í bOFß*$®ŒšŸòT2q¾Îé<Ь¸ùlAè ~€ ¸+Œ ô)NSt3__220__shared_ptr_emplaceIN8Markdown15MarkdownASTNodeENS_9allocatorIS2_EEEE"ƒA€ ¹Nú‡RA  f ° à \rˆ P\rˆ \rð\rˆ ˆ  @`€ˆ ˆ  Àˆ ˆ ˆ ˆ ˆ A ?/A° Äè£\n-Aà dƒµ¯A 5KG[uzÞ9AÐ 50a\nÉ|‘\'A MÝÚ×ÔÑÎO’y¹Að A€ 8$èA  ‚–AÀ \r~XAà ¶¦ZA€ \rP¨A  Tw$AÀ E,5Â9N\nküA ®A  —  € @\n €/ _ 0A@Z€µÀ@րØ@ހ@.€2@6€9@G€J@v€xy@}€‚„†‡‰Š‹Ž‘“”–—˜œŸ @¤€¦§©¬®¯±²³µ·¸¼ÄÅÇÈÊË@Û€Þ@î€ñòôö÷ø@€ "@2€:;=>ACDEF@N€Eprv†ˆ@Š€ŒŽ‘@¡€£@«€ÂÏÐÑÕÖØ@î€ðñôõ÷ùúý@ÿ€@€@/€`@€€Š@¾€ÀÁ@Í€Ð@.€1@V€ @Å€ÇÍø@ý€€‚ƒ„…†‡ˆ‰@º€½@¿€@”€› @þ€@€@€(@/€8@?€H@M€Y[]_h@o€¸¹º»¾È@Ë€ØÙÚÛèéêëìøùúû&!*!+!2!`!@o!€ƒ!¶$@Ï$€,@/,€`,b,c,d,g,@k,€m,n,o,p,r,u,~,,€,@â,€ë,í,ò,@¦@l¦€€¦@š¦€"§@.§€2§@n§€y§{§}§~§@†§€‹§§§’§–§@¨§€ª§«§¬§­§®§°§±§²§³§´§@§€Ä§Å§Æ§Ç§É§Ë§Ì§@Ú§€Ü§Ý§â§õ§l«m«p«@¿«€!ÿ@:ÿ€@\'€°@Ó€p@z€|@Š€Œ@’€”•€ @² €P\r@e\r€ @¿€@n@_n€ n@¸n€@ßHßJßMßQßhß@n߀rß@~߀é@!é€az¼àöøþ/37:HKwÿz~sSƒ…TˆVWŒÝY[’`cih™oru¡¥€¨ƒ­ˆ°Š‹´¶’¹½ÆÆÉÉÌÌÜßïóóõ•¿ùž#3e,<šf,B€‰ŒGO¹qswó¬­¯ÌÍαÁÃËÃײ¸ÆÀÙïºÁ¸µøòû{}P_0Oa‹¿ÏÂÎÑ/a†-%-\'---ðõ24>ABBJcK¦ŠÐúýÿ•a¡ÿ \'07@EQSUW`g°±pq¹ruÐÑvwàáz{åxy|}ÉkåN!p!!„!Ð$é$0,_,a,k}}h,l,QqPRs,v,?@,ã,ì,î,ó,A¦m¦¦›¦#§/§3§o§z§|§y§‡§Œ§e‘§“§—§©§f\\aljž‡S«µ§Ã§”§‚Žȧʧdͧۧ›w|ö§K«L« ïAÿZÿ(OØû—¡£±³¹»¼À ò p\r…\rÀß`nn»nÓnAßIßKßNßRßißoßsßß"éCéAÀ1 Óß0Ið‡–—˜™šžP€@‡€ˆ@€@—€˜@Ÿ€ @§€¨@¯€²³´¶¼ÂÃÄÆÌÖäæòóôöüûûûûûûûûûû•ßA 3 ¥ssi¼nj e‚h1tw\ny\na¾ssʹ¹¹ ¹\'¹ ¹\'¹`¹g¹`¹g¹p¹±¹¬¹±B±¹t¹·¹®¹·B·¹¹BÁÅB|¹É¹Î¹ÉBɹfffiflststtvtetk~vtmssAÐ6 À°RTV·ÇÒÓ×âãç÷ûû¹ÅÅÅÅB±B¹·B¹¹¹¹BÅÅÅBÉB¹ffiffl@.À 5P A¤9 ¬!@/€:@@€[@`€{@~€¡@©€«@¬€®@±€´¶@¸€»¿×÷Â@Å€Ò@߀å@ë€íï@ÿ€u~„@…€‡ö‚Z@_€‰@Š€@€¾ÀÃÆó@ô€@€@€j@m€ÔÞéý@þ€@\r€ö@ù€þ@ÿ€0@>€^ˆd @e €p ò @ó €ú @û €ý v\nð\n@ñ\n€p ó @ú €w  „ O\ry\rô\r?OZ@[€@€@€468:@=€…¾@Å€Ç@Ì€Î@Ú€J@O€ž@Ÿ€û`@h€@™€m@n€›@œ€ë@í€5@6€Ô@Ö€Ø@Û€@\n€@D@E€Þ@ÿ€@€ @¦€¨@­€N@O€Z@j€t@€ü@ÿ€;@?€~@€À@Ç€Ó½¿@Á€Í@Ï€Ý@߀í@ï€ý@þ€ @\' €0 @^ €z @~ €Š @Ž €  @Ä €!@!€!@!€!@ !€!!@!€!@#!€%!\'!)!.!:!@;!€@!@D!€J!@M!€O!Š!@‹!€!@)$€@$@J$€œ$@é$€%@u\'€”\'@s+€v+@ÿ+€å,@ê,€ù,@ü,€þ,@ÿ,€p-.@..€0.@].€`.@c.€€.@™.€›.@ó.€/@Õ/€ð/@ÿ/€0@0€0@ 0€0060@70€=0@?0€›0@œ0€ 0û01@‘1€–1@Ÿ1€À1@å1€ï12@2€*2@G2€P2`2@2€Š2@°2€À2@ÿ3€ÀM@ÿM€¤@Ƥ€þ¤@ÿ¤€\r¦@¦€s¦~¦ò¦@÷¦€§@§€ §@!§€‰§@Ч€(¨@+¨€6¨@9¨€t¨@w¨€Î¨@Ϩ€ø¨@ú¨€ü¨.©@/©€_©Á©@Í©€Þ©@ß©€\\ª@_ª€wª@yª€Þª@ߪ€ðª@ñª€[«j«@k«€ë«)û²û@Òû€>ý@Oý€ý@‘ý€Èý@Ïý€üý@ÿý€þ@þ€0þ@Rþ€Tþ@fþ€hþ@kþ€ÿ@ÿ€ÿ@ ÿ€;ÿ@@ÿ€[ÿ@eÿ€àÿ@æÿ€èÿ@îÿ€üÿ@ýÿ€@€7@?€y@‰€Œ@Ž€@œ€ Ð@ü€ŸÐoWw@x€ ? P\n@X\n€\nÈ\nð\n@ö\n€9 @? €™ @œ €n\rŽ\r@\r€­É@Ê€Ð@Ø€U@Y€†@‰€G@M€»@¼€¾@Á€@@C€t@u€Å@È€ÍÛÝ@߀8@=€©Ô@Õ€×@Ø€K@O€Z@[€]ÆÁ@×€A@C€`@l€¹<@?€;D@F€â?@F€š@œ€ž@¢€@ €áA@E€p@q€÷@ø€C@O€Õ@ñ€ÿp$@t$€ñ/@ò/€nj@oj€õj7k@?k€Dk@Ek€mm@om€—n@šn€âoœ¼Ÿ¼Ì@ïÌ€úÌ@üÌ€Í@³Î€ºÎ@Ð΀ÒÎ@Ô΀ÝÎ@ý΀PÏ@ÃÏ€Ð@õЀÑ@&Ñ€)Ñ@dÑ€jÑ@lÑ€ƒÑ@„Ñ€ŒÑ@©Ñ€®Ñ@AÒ€EÒSÒ@ZÒ€]Ò@^Ò€`Ò@Ò€Ó@VÓ€ÁÖÛÖûÖ×5×O×o׉ש×Ãר@ÿÙ€7Ú@:Ú€mÚ@tÚ€vÚ@ƒÚ€…Ú@‹Ú€Û@Û€Oáÿâÿå^é@_逬ì°ì.íðî@ñî€ð@+ð€0ð@“ð€ ð@®ð€±ð@¿ð€Áð@Ïð€Ñð@õð€\rñ@®ñ€æñ@ò€ò@;ò€@ò@Hò€Pò@Qò€`ò@eò€ó@Ùö€Üö@ìö€ðö@üö€÷@Û÷€à÷@ë÷€ð÷@ ø€ø@Gø€Pø@Yø€`ø@‡ø€ø@­ø€°ø@»ø€Àø@Áø€Ðø@Øø€ù@Wú€`ú@mú€pú@|ú€€ú@Æú€ÈúÌú@Ýú€ßú@ëú€ïú@úú€û@’û€”û@ïû€úû AãÌ  AðÌ A  \n\n\n   AÁÍ ! \r\r  AûÍ  A‡Î  AµÎ AÁÎ  AïÎ AûÎ  A²Ï  AãÏ AïÏ  AÐ A©Ð € 0123456789ABCDEF \r%)+/5;=CGIOSYaegkmqƒ‰‹•—£§­³µ¿ÁÅÇÓ \r%)+/5;=CGIOSYaegkmqyƒ‰‹•—£§©­³µ»¿ÁÅÇѐ+Ø)NSt3__214__shared_countE, *Ð)NSt3__219__shared_weak_countEA´Ô œ\ndè\' †@B€–˜áõʚ;00010203040506070809101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899¸+4+<-N10__cxxabiv116__shim_type_infoE¸+d+(+N10__cxxabiv117__class_type_infoEX+ !"#$%&Ø+\'!"#()*¸+ä+X+N10__cxxabiv120__si_class_type_infoE4,+!"#,-.¸+@,X+N10__cxxabiv121__vmi_class_type_infoE°,/0+„,St9exception¸+ ,|,St9bad_alloc¸+¼,”,St20bad_array_new_lengthì,12¸+ø,|,St11logic_error-32¸+(-ì,St12length_error+D-St9type_infoAÔÚ  . 0€'); ++ return binaryDecode('asm«````````````` `````||`~`|``Tenv __cxa_throwenv\r__assert_failenvemscripten_resize_heapenv _abort_js ¾¼   \r\r    \r\n     \n\n\n\np99€€@ Aã€ ¢memory__wasm_call_ctors\rparseMarkdown__indirect_function_table_emscripten_stack_restore½_emscripten_stack_alloc¾emscripten_stack_get_current¿ PA 8º \r{ !"#$%&\'()*+,~-E@G_mno{-cc¦¶³¨-µ²©-´¯¬-¸-¹-·»¼»» (\n¼è¼ AÈÜB7AÐÜA6AßÜA:AÔÜAµ(6AØÜA¹-:AÙÜA:AëÜA:AàÜAŽ/;AâÜA-:AãÜA:A÷ÜA:AìÜAç(6AðÜAë-:AñÜA:AƒÝA :AøÜA¯ )7A€ÝA· -:AÝA:AˆÝB칻֮سå7A“ÝA:AÝA:AŸÝA:A”ÝA¾(6A˜ÝAÂ/;AšÝA:A ÝA"6A¤ÝB‹€€€€‚€€€7 A£(6 Aœ)7 A: A·ÝA:A¬ÝA¼ (6A°ÝAÀ -:A±ÝA:AÃÝA:A¸ÝAÞ (6A¼ÝAâ -:A½ÝA:AÄÝA"6AÈÝBŽ€€€€‚€€€7 A³)7 A­)7 A:AìÞAôÝ6AÄÞA€€€6AÀÞAã€6A¤ÞA*6AÈÞAÀÜ(6 AÓÜ,AH@AÐÜ(AÈÜ(r ó‹!~#A0k"$@ E@AÈÜ„!AÈ܃!@ AO@AÈÜ…"A€A‰#Ak"$AÈ܃AÈÜAŠ A: Aj Aj‹ Aj$  AÈÜ A k A AA€† AÈÜAÈÜ(AÓÜ,AN  A:,  AG:/  AG:.  AG:-  AG:+  AG:*  AG:)  AG:(  AG:\'A4"A60 B7( B7 B7 B7 B7 B7 A j" 6 ! a"AøÿÿÿO\r@@ A O@ Ar"Aj!  Aÿÿÿÿk6  6  6   : A j! E\r E\r  ü\n  jA: Aj! A\'j!#AÐk"$@ A j"( , " AH"E@A<"B7 Aà6 B7 B7 B7 B7$ B74 B€€€ü7,  6  A j6  (! AéOA4 Aèn" A4OA jA !A<"\nB7 \nAà6 \nB7 \nB7 \nB7 \nB7$ \nB74 \nB€€€ü7,  \nA j6 (!  \n6@ E\r  ("\nAk6 \n\r  (( | Aj! ("! ( "\n G@@@ \nAk("E\r  ("Ak6 \r  (( |  \nAk"\nG\r (!  6 @A€  A€O"\n ( kAuM\r \nAt"!\n  k"@ \n  ü\n   \nj6  \n j6  \n6 E\r r !@@@@ ("\n ("I@ \n (6 \n ("6 @  (Aj6 \nAj!\n  \n ("k"\nAu"Aj"A€€€€O\rAÿÿÿÿ  k"Au"   K AøÿÿÿO"A€€€€O\r At"" \nj" (6  ("6  j!    (Aj6 ( ( ("k"\nAu  Atk! \n@   \nü\n  6  Aj"\n6  6 E\r r  \n6    Aj!\n@ ,AH@ (A: A6  A: A: \nA€” A:$@ ,3AH@ ((A: A6,  A:3 A:( (" (  , AH6 -! -! -! -! -! -! -!\n -! -! B7( A6$ A6 A6 A6 A6 A6  A tA̞AÀ– r \nAtr Atr Atr Atr Atr Atr Atr6 (  , "AH"! ( ! !\n#A€k" $@ A j"(@A! ("E\rA‹    A4jAAüü 6 6 )7 )7 )7 )7$ ( 6, \n60 AA ("Aq6œ A„j"AA€ü A;ã A:® A;à A:ß A: A;¿ A:ª A:¥ A:„ A€„q@ A:‚ A€€q@ A:â A€ q@ A:¨ A€€€q@ A:Á A€€€q@ A:¯ Aq@ A:Ä Aq@ A:¾ A€q@ A:² A€Âq@ A:€ Aq@A!@ A G A GqE@ jA: Ar"AýqA F@  jA: Aj"A€G\r A E\rA  jAk-"A\rF\r A\nF ! A6` A 6D 64 A€€  A€€OAt6X A„jAÿAÔü B7ä B7Ü@AA \n ("E@ AÌj!% A°jAr!& Aüj!\' AèjAr!( A°j!AÜ!A!@@ !@@ (" K@ B7ø B7ð B7èA! A°jAA  A°jF""j   F"#! (¨!) (!@@ j-"A G A Gq\r A|qAj Aj A F! Aj" G\r ! A°jA,A "j Aj #" 6 A°jA$A "j A j #" 6A! A°jA A "j Aj #"$A6@ (”"AL\r (œ! (! !\n@@  Alj"-A>F@ \n O\r O\r j-A>G\r Aj"!@ Aj" O\r@ j-"\nA G \nA Gq\r A|qAj Aj \nA F! Aj" G\r !   kAkA  G"\n6  6 !  \n ( "I\r  \n k"\n6 ! Aj" G\r ! ! 6ä@@ O\r j-A\nk  N\r (!@  Alj-A>F\r Aj" G\r ! % & " Aj #!A!\nA!A! !@A k!A k! („" (üjAk! (¬E! (! ( !\r (œ!! (¤!@@ ("AG@ !   6@ ! ("M@ !  @ " O\r  ( j-!@  j- AÿqG\r Aj" G\r ! @  k \rI\r@  O\r@@  j-A k Aj" G\r ! 6ä A6 A6¨  6ä  F@   (k"A M6 A6 !  (! @@@@ AG\r AL\r  N\rA! A6¤  O\r  j" -A\nk @ Aj  Aäj.AL\r A6¤ AþÿÿÿqAG\r A6 A6  A6 !  @ AG\r  (”G\r A6  (" (œk"A O6 A6¨  A6@@ AL"\r  G\r ( AljAk-"A>G6¨  A6¨ \r ( AljAk-! AÿqA>F\r  G\r (€@ !  („"A H@ !  (ü jAk-AG@ !  A6¬ !  @ Aq@ !    AL\r  (” G\r   Alj"Ak-A>F\r   G\r  (€\r  A H\r  -AG\r  6A AF\r   A$k(k"A M6 Ak ! A6¬ A6¨@ (" !O\r@ AG\r@ -"A-k !  G\r@@@ Aj" O\r j- F\r @@ j-A k Aj" G\r ! 6ä A6 AA A=F6 !   \nI\r -"" E\rA ÀATE\rA! Aj"\n O\r@@@  \nj-" F"\r A F\r A G\r j! \nAj"\n G\r !\n AI\r  AI\r A\nk @@@  N\r  G\r@ Aj  Aàj Aèj/E\r -è"A>F\r  Alj"-G\r (ð" ( K\r (ô" k j"!@ (à" O\r@ j-"A G A Gq\r A|qAj Aj A F! Aj" G\r 6ä   k6  6 !  6ä   k"6  6 O\r@ j-A\nk  !M@  j"6ô A6  Aj"6ô  Ak6  ! @ !O@ (AF@ !  A6  !k6 A6 !  Aj  Aäj Aèj/! (!@@ @ (ä!@ AG\r  G\r@@ O\r j-"A\rF\r -è! A\nG\r AÿqA>F\r   A>! -èA>F\r   Aÿq"A.G A)Gq\r (ìAF\r   (ô" (ðk j"!@ O\r@ j-"A G A Gq\r A|qAj Aj A F! Aj" G\r ! 6ä   k"6  6  ,è6@@@ O\r j-A\nk Aj6ô   !M@  j6ô A6  Aj6ô  Ak6 E@ Aj  j0\r (”! @ (˜" J@ (!  A Av j AL"6˜ ( Als"E@ (("E\rAå (0   6 (”! AÜ   F! Aj! Aj6”  Alj" )ø7  )ð7  )è7 (”! (! (! ! A F  Fq\r (ä!  ! O@ !  @ j"\n-"A#G@ ! AþF\r AàF\r !  Ak"\nF\rA  Aj"j-A#G\r  \nF\rA  Aj"j-A#G\r  \nF\rA  Aj"j-A#G\r  \nF\rA  Aj"j-A#G\r  \nF\rA  Aj"j-A#G\r  \nF\rA  Aj"j-A#G\r  \nF\r Aj!A  @@  j- G\r Aj" G\r !  k"AI@ !  6 @@@  M\r@  j-A G\r Aj" G\r   M\r@@  j-"A\nk @ AàG\r \n-AàG\r !  Aj" G\r ! 6ä A6 A6 $A6 \n A 6 ! " k "AJ@ !  @  M"\n\r (Aq\r  j-A k"AK@ !  A tA“€€q\r !  @ \n\r@  j-"A G A Gq\r Aj" G\r !  6 6ä A6  6  @@@@@  O\r  j-AF\r Aj" O\r A/G\r  j-A>F\r AjM@A!  A! \rAA  A jAE\rA! ( " O\r \r(!@@  j-"A k" AK\rA tA€€qE\r Aj" G\r  AAA A\nF A\rF! Aj$ "6¤@  (AG\r A6¤ ("A€qE\r (AG\r  (O\r ( j-"A-k Aj  Aäj. F@ A6¤ $A6 A6 AüG\r  (”G\r (€"E\r (AG\r Aäj!\n Aàj!A!A!\r@ Aj"(" M\r (" j-AüG\rA! Aj" O\r@  j-A k" AK\rA tA€€qE\r Aj" G\r ! @@@@  I@  ( j-A:Fj!  O\r (" j-A-G\r@@@  j-"A-G\r Aj" G\r \rAj!\r  A! \rAj!\r@  A:Fj" O\r@@  j-"A k"AK\rA tA€€qE\r  Aj"G\r  AüG\rA!A! Aj" O\r@  j-A k"AK\rA tA€€qE\r Aj" I\r !   O\r@  j-A\nk E\r  ! E\r \n 6  \r6A  A E\r  (à6 A\n6  A6 (”"   F  (AF! A€qE\r  jAL\r ( Alj"Ak-"E\r A?K\rB ­†B€€€€ÀƒP\r@ (ä" ("\n Aj"  \nK"O\r (!@ j-"A G A Gq\r Aj" G\r ! Aj" \nO\r (" j-AÛG\r  Aj"j,"E\rAà ATE\r  j-AÝG\r \n Aj"G@ j-A k"AK\rA tA“€€qE\r Ak ( " -Ar: Ak \'  6@ ("O\r (!@ j-A k"AK\rA tA€€qE\r Aj" G\r ! 6ä  6  Aj"6ô  6  6AÜ!A! ! ! Aj1A! Aj A@k2\r -Aq@ Aj AÜj2\r AjA0\r Aj3\rA!#Ak"\r$ \rA6$ Aj" A6@ (€AJ@ \rAøj!@@@@@@ (ø j"("AÿqAk \r AsA\nvAq6 \r /:  \r (6 \r (AsA\nvAq6 \r /:  \r AÿÿK6 \r /: \r (6  \rA6 \r /At(€a"6 /! \r 6 \r At(€6 \r \rAj6 \r \rA$j6 @@ ("A€q@@ A€qE\r Aÿq \rA j (, ("@ ($"E\rA¯ (,   ("Aÿq"AK\rA tAŽ€qE\r (Ak6 (! A€qE\r Aÿq \rA j (, ("@ ($"E\rAŠ (,   @ ("AÿqAk (Œ (Alj" -Ar: (Aj6  \rB7` \rB7X \rB7P \rB7H \rB7@ \rB78 \rB70 \rB7(@ ("AÿqA\nG\r A€€„I\r ("A€ Av"nM\r Aq! Aj!A!\nA!@ "AO@ A|q!A!@  Atj"( j ( j (j (j ( ( ( (jjjk! Aj! Aj" G\r E\r @  Atj"( j (k! Aj! \nAj"\n G\r  n AvO\r  ($"A¨ (,  (  A€~qA r"6 A! \rA6ˆ \rB7€ \rB7x \rB7p \rB7h ("@ (Œ AljAk-AqE! A!@@@@@@@ AÿqAk \r Av6h  A€€I\r ("\n ("j-! ( !@@@  ("O\r@  \nj- AÿqG\r Aj" G\r  M\r@  \nj-A G\r Aj" G\r ! @  M\r@  \njAk-A G\r Ak" K\r ! A!  \nj  kA \rAèj \rAÈj<\r (! !\n@  M\r@  \nj-A k"AMAA tA€€q\r \nAj"\n G\r !\n  j \n kA  \rA(j<\r \r :ˆ (!A!  \rA6l \r Av6h \r (Ak6p  \r Av6h @@@@@@@  Aÿq"A FqE@  \rAèj (, ("\r (! AÿqAk ($"E\rAŠ (,   (!@ AÿÿM@ E\r Aj!@ ( (F@ A j! Ak"\r  @  A lj"A k( Ak(G\r Ak"\r  Ak"E\r Aj! A  ="E\r  A Aj (="E\r  (!@@ Av"Atq"E@A! ($"E\rAÔ (,  r  @ (! ("! ! !@ !@ M\r (!@  j-A-F\r Aj" G\r ! A!  I@ ( jAk-A:F! @ M\r (!@@  j-"\nA-G\r Aj" G\r !  Ar  \nA:F!  At(°:6 Ak! Aj! AJ\r A A (, ("@ ($"E\rAŠ (,  r  A ( (  >"\rA A (, ("@ ($"E\rA¯ (,  r  AI\r@A A (, ("E@ Aj!A!  ($"E\rAŠ (,  r  @ A  Atj"( (  >"\r Aj" G\r A A (, ("E\r ($"E\rA¯ (,  r  r  Aj (?"\r A!  (Aÿq"A Fq\r  \rAèj (, ("E\r ($"@A¯ (,  ! \r \r(X@ \r(Hr \r(Lr \r(Pr \r(8E\r \r((r \r(,r \r(0r \r (! -AkAM@ A l j!  At j!  (Œ (Alj" -Aþq A\nvAqr: (Aj6 Aj" (€H\r A6€A! \rAj$ "\r -Aq@A!A!#A@j"$@ Aj"(`"E\r (pE\r (X AAW@@AA (, ("E@ (`E\r A0j!\n  ($"E\rAŠ (,   @ (X Alj"( E\r B7 B7 B7 B7 \nB7 \nB7  ( 6(  (6,@  ( (A \n Aj<@A!  A A(j (, ("@ ($"E\rAŠ (,    ( (?"\rA A(j (, ("E@A!  ($"E\rA¯ (,  (@ (r ( r (r \r Aj" (`I\r A!AA (, ("E\r ($"@A¯ (,  ! A@k$ \r A!AA (0 ("E\r (("@A¯ (0  !  \n6ä A6 ! @ (ä"Aj" ("O\r (!@@ j"-A\nk @ -A\nk @ -A\nk @  j-A\nk Aj! Aj!  I\r @ I@ (!@A!@ j-A\nk Aj" G\r ! A! A°jA(A "j Aj #"\n 6 " ("AG\r@@ ("M\r (!@  jAk-"A G A Gq\r Ak" K\r  @  O\r (!@  jAk-A#G\r Ak" K\r   F\r ( jAk-"A F@ !  A F@ !  ! -AqE\r \n 6  !@ AkAI\r  ("M\r (!@  jAk-"A G A Gq\r \n Ak"6  I\r @ ( j-A\rFj! I@ ( j-A\nFj! @ )E\r E\r  j"AL\r ( Alj"Ak-A>F\r (ü Ak(j" (A€r6 @ \r  j" (”N\r Aj 0\r AJ@ AjA Al" (j"(" -Aq ( j,A A4\r AjA (ü" -éAq" ( j,A A4\r ( j" -Aýq r: ( j 6 @ (”!@ -AqE\r ( Alj"Ak-A>G\r (AG\r \n(" ("k"AkA K\r (" j"-AÛG\r -A!G\r  jAk-AÝG\r Ak! Aj!A!@@@@ Ak @  j-"A k  AákAÿqAIAÿq -–"A k  AákAÿqAIAÿqG\r Aj" G\r A  @  j-"A k  AákAÿqAIAÿq -\r"A k  AákAÿqAIAÿqG@ A\nk Aj" G\r A  A!@  j-"A k  AákAÿqAIAÿq -\n"A k  AákAÿqAIAÿqG@ A\nG\r  Aj" G\r A  A!@  j-"A k  AákAÿqAIAÿq -ä"A k  AákAÿqAIAÿqG@ A\nG\r -"A k  AákAÿqAIAÿqAÃG\r -"A k  AákAÿqAIAÿqAÁG\r -"A k  AákAÿqAIAÿqAÕG\r -"A k  AákAÿqAIAÿqAÔG\r -"A k  AákAÿqAIAÿqAÉG\r -"A k  AákAÿqAIAÿqAÏG\r -"A k  AákAÿqAIAÿqAÎG\rA  Aj" G\r A ! Ak" -Ar: ( (”AljAk" -AÇq r: A6 (”!  k!@A!@@A ( Alj"-A)kt"AqE@ A!qE\rA! Aj1  („6 Aj  ( ,A4\r AjA (" -Aq ( j,A A4\r  AjAA -"AqA AvAqA4\r Aj" (”H\r @ (E@ Aj1@A!  A!AÜ! -AÀqE\r ( \n(I\r (ŒAj6Œ  (Œ@@ („"Aj" (ˆ"J@ A€ Av j AL"6ˆ (ü s"E@A! (("E\rAå (0   (€"@   (ükj6€ 6ü („"Aj6„  6„ (ü"\rA!   j"A; (Œ! A6  AtAr6 A6Œ @ $(E\r Aj1E\rA!  @@@@ ("Ak\n A! Aj1\r („"Aj" (ˆ"J@ A€ Av j AL"6ˆ (ü s"E@ (("E\r\nAå (0  \n (€"@   (ükj6€ („"Aj6„ 6ü  j  6„ (ü"E\r  j " (AtAÐÍj(" /Atr6 (! A6   Atr6 6€ Aj 5\r Aj1\rA!AÜ!  (€A: (€ /; (€" (A€r6A! Aj 5\r Aj1\rA! (€E@AÜ!  A6 !  (€A\n: (€ /; A 6AA Aj 5!  @  (F\r Aj1E\rA!  @ (€\r !@ Aj"\n(€"Aj" \n(„"J@ \nA€ Av j AL"6„ \n(ø s"E@A \n($"E\rAå \n(, A  \n(ü"@ \n   \n(økj6ü \n 6ø \n \n(€"Aj6€  j  \n 6€ \n(ø"E\r  j " (AtAÐÍj(" /Atr6 (! A6   Atr6 \n 6üA  A E\rA!  Aj 5! A! E\r  A!  (("E\rAŠ \n  (P"@ (T"A!@@ (P Atj("E\r (@" M@   (D (HljI\r r (T! Aj" I\r (P r (H@A!@ (@ Atj"-"Aq (r -  Aq@ ( r Aj" (HI\r (@r (l"@ (p"A!@@ (l Atj("E\r (\\" M@   (` (dljI\r r (p! Aj" I\r (l r (d@A!@ (\\ Alj(r Aj" (dI\r (\\r (8r (xr (ür (r A€j$ @A<"B7 Aà6 B7 B7 B7 B7$ B74 B€€€ü7,  6  A j6  (@ (("E\r ((" ($"G A$j!A!@@@  At"j("(AF@ ($" (("G\r Aj!   k"Au!@@ ((AF\rA! AG@@ Aj!  Atj"((AF@  O\r Ak(!  Atj(! \r \r !  G\r A! A68 B70A!  A! A68 B70 A6L B7D  A0j6@  AÄj6<@@@@@@  At"j"("(AG\r  Ak(A !  Aj"K@  Atj( rAG\r AF\r -A/F\r Aj" G\r ! F\r Aj  k” I@@ Aj ((  ,3AH j-bÀš Aj" G\r  (," ,3" AH"O\r Av! Aj!\n@ (  Aq!@  j"-S@ Aj" I\r  -A>F\r -A/F\r@@ K@ !@@  j"-A=F\r -S\r -A>F\r -A/F\r Aj" I\r !  G\r Aj!  A6P B7H AÈj  k”  I@@ AÈj ((  ,3AH j-bÀš Aj" I\r @@  (, ,3" AH""O\r (  !@@  j"-SE\r Aj" G\r  -A=G\r@@@@@@ "Aj"M\r  j"-S\r B78 A6@@ -A"F\r -A\'F\r !@  j" -S\r -A>F\r -A/F\r Aj" G\r !  Aj"!@ M\r -!@  j- F\r Aj" I\r ! k"  k" I"AøÿÿÿO\r@@ A O@ Ar" Aj!  Aÿÿÿÿk64  6,  60   :7 A,j! E\r E\r   j ü\n jA: ,CAH@ (@ (8r  (46@  ),78   (, ,3" AHIj!  A6@ B78 ! M\r k"  k" I"AøÿÿÿO\r@@ A O@ Ar" Aj!  Aÿÿÿÿk64  6,  60   :7 A,j! E\r E\r   j ü\n jA: ,CAH@ (@ (8r  (46@  ),78  AÈj6\\ A,j \n AÈj AÜj@ (,"Aj" A8jF\r ,C! ,AN@ AN@ (@6 )87  (8 (<™  (8 A8j AH" (<  ˜ ,CAN\r (@ (8r   A¤AƁ"AüÛ6 AˆÜA  !  AÈj6, A8j \n AÈj A,j (8",AH@ A6 (  A: Aj A: ,SAH@ (P (Hr (,! -3!   Aÿq ÀAH""I\r A!A!@ ( ," AHAG\r ( Aj AH"(Aöґ«s -Aïsr\r A:S A:K A»/;H A½-:J Aj!\nA! AÈj"( , " AH""! (  " !@ "AI\r Ak"Aq@ !  (A•ÓÇÞl"Av sA•ÓÇÞl A•ÓÇÞls! ! Aj ! AI\r !@ (A•ÓÇÞl"Av sA•ÓÇÞl (A•ÓÇÞl"Av sA•ÓÇÞl A•ÓÇÞlsA•ÓÇÞls! Aj! Ak"AK\r @@@@ Ak -At s! -At s!  -sA•ÓÇÞl! @ \n("E\r \n( A\rv sA•ÓÇÞl"Av s" Akq i"AM\r   K\r  p " Atj("E\r ("E\r Ak! AK!\n@@@  ("G@@ \nE@  q!   K\r  p!  G\r  ( ," AH" G\r ( Aj  U\r !  ("\r A  ! ,SAH@ (P (Hr E\r ( ," AHE\rA<"B7 A!6 Aà6 B7 B7 B7 A6( B74 B€€€ü7, A:S AŽ/;H A-:J A:K  AÈj6, A8j Aj AÈj A,j@ (8" F\r Aj! Aj! ,! ,AN@ AN@  (6  )7   ( (™   (  AH" (  ˜ ,SAH@ (P (Hr A j! Aj ,AH@ ( (r @ ,3AH@ ((A: A6,  A:3 A:( @ E\r ( ( "F\r Ak(!  6  6 A$j Aj@ ( "E\r ("Ak6 \r (( | A!  A! E\r  ("Ak6 \r  (( |  A! ( ( F\r  ( " (kA I\r@ Ak("E\r  ("Ak6 \r  (( |  Ak6 Aàj$ à%#A k"$@@ E@A!  @@@@@@@@@@@@@@@@ Ak\n \r  A<"B7 A6 Aà6 B7 B7 B7 A6( B74 B€€€ü7,  6Œ  A j6ˆ  )ˆ7    A<"B7 A6 Aà6 B7 B7 B7 A6( B74 B€€€ü7,@ E\r (! A:• AA  AL" ANA0r:” A”ja"AøÿÿÿO\r@@ A O@ Ar"Aj!  Aÿÿÿÿk6€  6ø  6ü   :ƒ Aøj! E\r E\r  A”j ü\n  jA: AÔÜ6˜ A¤j AjAÔÜ A˜j@ (¤"Aj" AøjF\r ,ƒ! ,AN@ AN@  (€6  )ø7   (ø (ü™   (ø Aøj AH" (ü  ˜ ,ƒAN\r (€ (ør  6ô  A j6ð (Aj6  )ð7  Aj ("Ak6 \r (( |  A<"B7 A 6 Aà6 B7 B7 B7 A6( B74 B€€€ü7,  6ì  A j6è  )è7  Aj \r A<"B7 A 6 Aà6 B7 B7 B7 A6( B74 B€€€ü7, A j!@ E\r ("E\r ("E\r AøÿÿÿO\r@ A O@ Ar"Aj!  Aÿÿÿÿk6€  6ø  6ü   :ƒ Aøj! @   ü\n  jA: (ü -ƒ" ÀAH@ AÄÝ Aøj ,ƒAN\r (€ (ør  6ä  6à (Aj6  )à7  Aj ("Ak6 \r (( | A<"B7 A\r6 Aà6 B7 B7 B7 A6( B74 B€€€ü7,  6Ü  A j6Ø  )Ø7  A j A<"B7 A6 Aà6 B7 B7 B7 A6( B74 B€€€ü7, A j!@ E\r ("AF\r Aøj œ A¬Ý Aøj ,ƒAN\r (€ (ør  6Ô  6Ð (Aj6  )Ð7(  A(j ("Ak6 \r\n (( | \n A<"B7 A6 Aà6 B7 B7 B7 A6( B74 B€€€ü7, A j!@ E\r (E\r A”Ý AøjAû" , AH@ ( (r A Ý AøjAûA› -AßqAØF" , AN\r ( (r  6Ì  6È (Aj6  )È70  A0j ("Ak6 \r (( | A<"B7 A6 Aà6 B7 B7 B7 A6( B74 B€€€ü7, A j!@ E\r@ - "E\r A:¥  :¤ AøÜ Aøj A¤j" , AN\r ( (r ("E\r ("E\r AøÿÿÿO\r\n@ A O@ Ar"Aj!  Aÿÿÿÿk6€  6ø  6ü   :ƒ Aøj! @   ü\n  jA: (ü -ƒ" ÀAH@ AˆÝ Aøj ,ƒAN\r (€ (ør  6Ä  6À (Aj6  )À78  A8j ("Ak6 \r (( |  A<"B7 A6 Aà6 B7 B7 B7 A6( B74 B€€€ü7,  6¼  A j6¸  )¸7@  A@k  A<"B7 A6 Aà6 B7 B7 B7 A6( B74 B€€€ü7, A j!@ E\r Aøj (œ A¸Ý6˜ A¤j AjA¸Ý A˜j@ (¤"Aj" AøjF\r ,ƒ! ,AN@ AN@  (€6  )ø7   (ø (ü™   (ø Aøj AH" (ü  ˜ ,ƒAN\r (€ (ør  6´  6° (Aj6  )°7H  AÈj ("Ak6 \r (( |  A<"B7 A6 Aà6 B7 B7 B7 A6( B74 B€€€ü7, A j!@ E\r A:€ BãÞ±›ô­·ô7ø A:ƒ A¤j (œ  Aøj6” A˜j Aj" Aøj A”j@ (˜"Aj" A¤jF\r ,¯! ,AN@ AN@  (¬6  )¤7   (¤ (¨™   (¤ A¤j AH" (¨  ˜ ,¯AH@ (¬ (¤r ,ƒAH@ (€ (ør A"6ø BŒ€€€€‚€€€7ü Aù (6 Añ )7 A: A¤j (œ  Aøj6” A˜j  Aøj A”j@ (˜"Aj" A¤jF\r ,¯! ,AN@ AN@  (¬6  )¤7   (¤ (¨™   (¤ A¤j AH" (¨  ˜ ,¯AH@ (¬ (¤r ,ƒAH@ (€ (ør A"6ø BŒ€€€€‚€€€7ü Aì (6 Aä )7 A: A¤j (œ  Aøj6” A˜j  Aøj A”j@ (˜"Aj" A¤jF\r ,¯! ,AN@ AN@  (¬6  )¤7   (¤ (¨™   (¤ A¤j AH" (¨  ˜ ,¯AH@ (¬ (¤r ,ƒAN\r (€ (ør  6   6œ (Aj6  )œ7P  AÐj ("Ak6 \r (( |  A<"B7 A6 Aà6 B7 B7 B7 A6( B74 B€€€ü7,  6˜  A j6”  )”7X  AØj  A<"B7 A6 Aà6 B7 B7 B7 A6( B74 B€€€ü7,  6  A j6Œ  )Œ7`  Aàj  A<"B7 A6 Aà6 B7 B7 B7 A6( B74 B€€€ü7,  6ˆ  A j6„  )„7h  Aèj  A<"B7 Aà6 B7 B7 B7 A6( B74 B€€€ü7, AA AF6 @ E\rAš\n! (Ak"AM@ At(Ð! A:ƒ A:ý Aá\r(6ø Aå\r-:ü a"AøÿÿÿO\r@@ A O@ Ar"Aj!  Aÿÿÿÿk6¬  6¤  6¨   :¯ A¤j! E\r E\r   ü\n jA:  Aøj6” A˜j Aj Aøj A”j@ (˜"Aj" A¤jF\r ,¯! ,AN@ AN@ (¬6 )¤7  (¤ (¨™  (¤ A¤j AH" (¨  ˜ ,¯AH@ (¬ (¤r ,ƒAN\r (€ (ør  6€  A j6|  (Aj6  )|7p  Aðj  ("Ak6 \r  (( |  A:$ ,3AH@ ((A: A6,  A:3 A:( A j$    #Ak"$@ ( ," AHE\r ( " (F\rA<"B74 B€€€ü7, A6 Aà6 B7 B7 B7$  Aj")7  (6 A: A: Ak(!  6  A j6 A$j Aj@ ( "E\r  ("Ak6 \r  (( | ,AH@ (A: A6  A: A: Aj$ ª~A!@@@ (Ak ( ," AH""E\r  ( Aj "j! !@@ 1"B?V\rB †B€Ì€€ƒP\r Aj" G\r  kAF A!  ¹  #Ak"$ ("(" ("kAu!@  F\r@  Atj(E\r Aj" G\r ! @@ " M\r  Ak"Atj(\r A<"B7 A6 Aà6 B7 B7 B7 A6( B74 B€€€ü7,  6  A j6 A0j!  At"j!  At"j!@  kAu" (" ("kAuM@ (" k"Au I@  j!  G@@ (! ("@  (Aj6  6 (!  6@ E\r  ("Ak6 \r  (( | Aj! Aj" G\r (! !  G@@  (6  ("6 @  (Aj6 Aj! Aj" G\r    kj6   G@@ (! ("@  (Aj6  6 (!  6@ E\r  ("Ak6 \r  (( | Aj! Aj" G\r (!  G@@@ Ak("E\r  ("Ak6 \r  (( |  Ak"G\r  6  @ ! (" G@@@ Ak("E\r  ("Ak6 \r  (( |  Ak"G\r ( (!  6 r A6 B7A! @ A€€€€O\rAÿÿÿÿ Au"  K AøÿÿÿO"A€€€€O\r  At""6  6   j6  G@@  (6  ("6 @  (Aj6 Aj! Aj" G\r  6   ( Aj@ ( "E\r  ("Ak6 \r  (( | ("(! (!  G@@@ Ak("E\r  ("Ak6 \r  (( |  Ak"G\r  6 Aj$ A„  )A¤€"A€Ú6 A”Ú6 AèÚA  ~ AL@ (" ("kAu N@  k"Au"\n H@@   j" F@ !  ! !@  (6  ("6 @  (Aj6 Aj! Aj" G\r 6 \nAL@  At"j! !   k"K@ !@  (6  (6 B7 Aj! Aj" I\r 6  G@@ Ak")! B7 Ak(! Ak" 7@ E\r  ("Ak6 \r  (( |  G\r  F@ !@ (! ("@  (Aj6  6 (!  6@ E\r  ("Ak6 \r  (( | Aj! Aj" G\r   At"j! !   k"K@ !@  (6  (6 B7 Aj! Aj" I\r 6  G@@ Ak")! B7 Ak(! Ak" 7@ E\r  ("Ak6 \r  (( |  G\r  Atj! !@ (! ("@  (Aj6  6 (!  6@ E\r  ("Ak6 \r  (( | Aj! Aj" G\r  @  ("kAu j"A€€€€I@Aÿÿÿÿ  k"Au"   I AøÿÿÿO" A€€€€O\r AtA "  kj! At"Ak"Aq   (6  ("6 @  (Aj6 Aj! Aj !  j! @@  (6  ("6 @  (Aj6  (6  ( "6 @  (Aj6 Aj! Aj" G\r ( k"@   ü\n 6   ("k"k! @   ü\n  j6 6 ( Atj6 @ r    Ý#A k"$ ($" (("G@@ ( Aj" G\r @ (AG\r ($" (("F\r !@@@@@ (("  k"AH\r  "6  6   j6 A6 B7  Aj6  Aj6@ Aj!@@ (("  Aj!    Aj"G\r  Aj" G\r    ($"@ " (("G@@@ Ak("E\r  ("Ak6 \r  (( |  Ak"G\r ($! 6( (, r (6$ (6( (6, A6 B7 ("E\r " ( "G@@@ Ak("E\r  ("Ak6 \r  (( |  Ak"G\r (!  6 ( r ("E\r " ("G@@@ Ak("E\r  ("Ak6 \r  (( |  Ak"G\r (!  6 ( r A j$ ¶ ~#A k"$@@@ ($" (("F\r@ ("("A G A GqE@  Aj" G\r ((" ($"F\r A$j! @  \nAt"\rj("(" A\rkAO \nAjA!@ ($" (("F\r !@@@ ("(AG\r (( ($"kAG\r ((A!F\r Aj" G\r  A6 B7 A6 B7@@@ ("(AG\r (( ($"kAG\r ((A!G\r (" ( "G@A<"B7  6 Aà6 B7 B7 B7 B€€€€€€€À?7(  6  A j6  (68  64  60 A6 B7 Aj @ ("E\r  ("Ak6 \r  (( | (($! (" ("I@  (6  ("6 @  (Aj6  Aj6   ("k"Au"Aj"A€€€€O\rAÿÿÿÿ  k"Au"   I AøÿÿÿO"A€€€€O\r At"" j" (6  ("6  j!    (Aj6 ( ( k"Au  Atk! @   ü\n  6  Aj"6  6 @ r  6  Aj  Aj" G\r @ (" ( "F\rA<"B7  6 Aà6 B7 B7 B7 B€€€€€€€À?7(  6  A j6  (68  64  60 A6 B7 Aj  ("E\r  ("Ak6 \r  (( | ($ \rj" ! Aj" (("G@@ )! B7 (!  7@ E\r  ("Ak6 \r  (( | Aj! Aj" G\r ((!  G@@@ Ak("E\r  ("Ak6 \r  (( |  Ak"G\r 6( (" ("  kAu (" ! ( k! ("@ " ( "G@@@ Ak("E\r  ("Ak6 \r  (( |  Ak"G\r (!  6 ( r (! @ " ("G@@@ Ak("E\r  ("Ak6 \r  (( |  Ak"G\r (!  6 ( r Au! ($! ((!  \nj !\n \n  kAuI\r A j$   Þ#Ak"$ @ ("E\r ( " (F\r Ak(! ("@  (Aj6 Aj!  6  6 A$j Aj@ ( "E\r ("Ak6 \r (( |   @ ("E\r  ("Ak6 \r  (( | Aj$ ¢  } ( , " AH""! (  " !@ "AI\r Ak"Aq@ !  (A•ÓÇÞl"Av sA•ÓÇÞl A•ÓÇÞls! ! Aj ! AI\r !@ (A•ÓÇÞl"Av sA•ÓÇÞl (A•ÓÇÞl"Av sA•ÓÇÞl A•ÓÇÞlsA•ÓÇÞls! Aj! Ak"AK\r @@@@ Ak -At s! -At s!  -sA•ÓÇÞl! A\rv sA•ÓÇÞl"Av s! @ ("E\r (  Akq i"AM\r   K\r  p "Atj("E\r ("E\r Ak!\n AK! @@  ("G@@ E@  \nq!   K\r  p!  F\r  ( ," AH G\rA ( Aj AH UE\r ("\r A " 6 A6 Aj!@ (", AN@  (6  )7  (! (!\n#Ak" $@@@ \n‘@ " \n’  \nA÷ÿÿÿK\r Aj \nŒAj“ (   ("6  (   \n6   \nAj‰ Aj$   A6 B7 *!\r ( Aj³!@ @ \r ³” ]E\r @A  AkqAG AIr Atr"  \r•ü"  K"AF\r   AkqE\r w " ("M@  M\r ( ³ *•ü! @ AI\r iAK\r AA Akgkt AI  w "  K" O\r !@@ "@ A€€€€O\r At"! (!  6 @ ( r (!  6 @ A ü ("E\r Aj! (!@ i" AM@  Akq!   I\r  p! Atj 6 ("E\r Ak!\n AK! @ (!@ E@  \nq!   I\r  p! @  F@ !  Atj" (E@ 6 ! !   (6  ((6 ( 6 ("\r  (! A6 @ ( r A6   (" Ak"qE@  q!   K@ !   p! @ (" Atj"("E@  (6  6  Aj6 ("E\r (!@  Ak"qE@  q!   K\r  p!  Atj 6   (6  6  ( Aj6 A : 6 ¡#Ak"$  6 Aj Aj  Aj@ ("Aj" F\r , ! ,AN@ AN@  (6  )7   ( (™   (  AH" (  ˜ Aj$ } a"AøÿÿÿI@@@ A O@ Ar"Aj! Aÿÿÿÿk6 6 6  : ! E\r E\r   ü\n  jA:   o ("@@ (! ,AH@ ( (r ,AH@ ( (r r "\r (! A6 @ ( r  (" ("I@  (6  (6 B7 Aj6 @  ("k"Au"Aj"A€€€€I@Aÿÿÿÿ  k"Au"   I AøÿÿÿO" A€€€€O\r AtA " j" (6  (6 B7  Atk! @   ü\n  Atj6 Aj"6 6 @ r 6   Î#Ak"$ @ ("E\r ( ( "F\r Ak(! ("@ (Aj6  6  6 A$j Aj ( "E\r ("Ak6 \r (( | @ ("E\r  ("Ak6 \r  (( | Aj$ Ý#A0k"$ ("(" ("G@ !@@@ (@  Aj"G\r  A<"B7 A6 Aà6 B7 B7 B7 A6( B74 B€€€ü7,  6  A j6 A: A¢\n(6 A¦\n-: A: A: AôäÕ«6 A:  A j6$ A(j Aj A j A$j@ (("Aj" F\r ,AN@  (6  )7   A˜ , AN\r ( (r ,AH@ ( ( r (! (0"@ " (4"G@@@ Ak("E\r  ("Ak6 \r  (( |  Ak"G\r (0!  64 (8 r A68 B70  (60  (64  (68 A6 B7 ( Aj ("E\r  ("Ak6 \r  (( |  @ (  Aj" G\r ("(" ("G@@@ Ak("E\r  ("Ak6 \r  (( | Ak"G\r  6 A0j$ AßÜ,AH@AÜÜ(AÔÜ(r AëÜ,AH@AèÜ(AàÜ(r A÷Ü,AH@AôÜ(AìÜ(r AƒÝ,AH@A€Ý(AøÜ(r A“Ý,AH@AÝ(AˆÝ(r AŸÝ,AH@AœÝ(A”Ý(r A«Ý,AH@A¨Ý(A Ý(r A·Ý,AH@A´Ý(A¬Ý(r AÃÝ,AH@AÀÝ(A¸Ý(r AÏÝ,AH@AÌÝ(AÄÝ(r Aà6  Aà6 r ¬#Ak"$ A j"($"@ " (("G@@@ Ak("E\r ("Ak6 \r (( |  Ak"G\r ($!  6( (, r Aj ,AH@ (  (r Aj$  r µ  (!@@@@@@@@@@@@ ( Ak  Aj"M@  6A Ak! (!@@ ! !@ j"-"AF\r @ Aj" O\r -"A k  AákAÿqAIAÿqAÓG\r -"A k  AákAÿqAIAÿqAÃG\r -"A k  AákAÿqAIAÿqAÒG\r -"A k  AákAÿqAIAÿqAÉG\r -"A k  AákAÿqAIAÿqAÐG\r -"A k  AákAÿqAIAÿqAÔG\r  j-A>F\r @ Aj" O\r -"A k  AákAÿqAIAÿqAÓG\r -"A k  AákAÿqAIAÿqAÔG\r -"A k  AákAÿqAIAÿqAÙG\r -"A k  AákAÿqAIAÿqAÌG\r -"A k  AákAÿqAIAÿqAÅG\r  j-A>F\r A\nj" O\r -"A k AákAÿqAIAÿqAÔG\r -"A k AákAÿqAIAÿqAÅG\r -"A k AákAÿqAIAÿqAØG\r -"A k AákAÿqAIAÿqAÔG\r -"A k AákAÿqAIAÿqAÁG\r -"A k AákAÿqAIAÿqAÒG\r -"A k AákAÿqAIAÿqAÅG\r - "A k AákAÿqAIAÿqAÁG\r  j-A>F\r\r Aj" G\r !  6A Aj" O\r Ak! (!@@  j"-A\nk\n\n /A­Ús -A>srE\r Aj! Aj!  I\r !  Aj" O\r Ak! (!@@  j"-A\nk /A¿üF\r Aj! Aj!  I\r !  Aj" O\r Ak! (!@@@  j-"A\nk  A>F\r Aj! Aj!  I\r !  Aj" O\r Ak! (!@@  j"-A\nk /Aݺs -A>srE\r Aj! Aj!  I\r !  @@  O\rA! ( j-A\nk  6 ( !   6A  6A  6A  6A  6A  Aj6A Å@  ("O\r  (˜O\r @ (" j-"E\r A>F@ A>:  6  -Aüq: Aj! Aj  A?K\rB ­†B€€€€€ ƒP\r  Aj"K@  j-A k"AK\rA tA“€€qE\r  :  6  -Aüq: Aj  A! A6  A j"  I"\nO\rA! !@@ j-" A0kAÿqA K\r  A\nljA0k"6 Aj" \nG\r \n! M\r O\r@ j-" A)k  Aj"K@  j-A k"AK\rA tA“€€qE\r  :  6  -Aüq:  k jAj 6  6A!  ç  ("H@@A!@@@A (Œ Alj"Ak"-A)kt"AqE@ A!qE\rA! Ak(!A! Ak-Aq@ ( j,! A! A  A4\r A ,A4\r  AA Ak-"AqA AvAqA4E\rA!  (Ak"6  H\r A Þ#A0k"$@ (ü"E\r@@@ ("AÿqAk A€qE\r (" (O\r ( j-AÛG\r@ ("E\r Aj! AG\r !  @ \r@ A/G\r Aj" O\r \n j-A>F\r AL\r@ AßqAÁkAÿqAI\r AßF\r A:G\r  Aj"M@A!  @@@ \nj,"A0kAÿqA\nI\r A_qAÁkAÿqAI\r E@A!  A ATE\r A!  Aj"G\r  A!  A! @@ Ak A=G\r Aj!A!  A+!@@@@ A"k E\r AÑ AT\r A\rF\r A)!  A*! Aj! I\r A! E\r \rF\rA  A)FA !  Aj" Atj"(! (" I\r   M@A A!  Aj6 „#Ak"$@  O\r@@@ j"  k A j7"AÿK@A!A!@@@  jAm"AtA°j("\nAu j"AtA°j(Aÿÿÿq K@ Ak!   \nAvAq j"AtA°j(AÿÿÿqM\r Aj!  L\r  AH\r  A F\r A F\r A kAI\r -A\nk ( j" I\r Aj$  à@@@ AÿK@A­!@@@  jAm"AtAÐj("Au j"AtAÐj("Aÿÿÿq K@ Ak!  AvAq j"AtAÐj(AÿÿÿqM\r Aj!  N\r  AH\rAà9!  A6  A r AÁkAI6 A!A4!@@@  jAm"AtAÐ2j("Au j"AtAÐ2j("Aÿÿÿq M@ AvAq j"AtAÐ2j(Aÿÿÿq O@ AH\rAð9!  Aj!  Ak!  N\r A!A!@@@  jAm"AtAà7j("Au j"AtAà7j("Aÿÿÿq M@ AvAq j"AtAà7j(Aÿÿÿq O@ AH\rA€:!  Aj!  Ak!  N\r A6  6  ( ( " lAtj! At"@   ü\n  6 F\r ( Aÿÿÿq"AjF@  sAsAq j6  ( kj6 õ #A k"$ B7 B7 B7 B7 A B! A B! @@@  K@   M\r @ \nE@   AjF!A! AqE@   F! A!  Atj( Aj Atj(k"\r Aj" ( " I! Aj" (" I!\n  K\r  I\r  K\r  I\r A! A j$  iA!@ ("(" ("("I\rA!  K\r ( ( ( (D"\rA! (" ("I\r K!  ‰#Ak"$@@@  O\r j  k A j7! ( j! AÿK@A!@@@  jAm"AtA°j("\nAu j" AtA°j(Aÿÿÿq K@ Ak!   \nAvAq j"AtA°j(AÿÿÿqM\r Aj!  L\r  AN\r  A F\r A F\r A kAO\r A6 A 6  B!   C Aj$  D ("(" ("("I@A  K@A ( ( ( (D ž“#A0k"$ A6x#Ak"$ AjAA€ü @@@ E\r Ak! A€j!\r  AtjAk!@  Atj"(!@ !@ !@ Aj" ("O@ !  (! @ \r  j"-j-@ !  \r -j-@ !  \r -j-@ !  \r  j-j-@ !  Aj! Aj"!  K\r @  K@ (!@ \r  j"-" j-\r Aj" G\r Aj" I\r  @AA@@@@@@ AÜF@A! Aj" (O\r@  j-"A!kAÿqAI\r@ AÿqA\nku Aj I\r Aj! @@ (x" (|"N@ AÀ Av j AL"6| (t Als"\rA! ($"\rE\rAå (, \r  Aj6x (t"\r  6t (x"Aj6x  Alj"AÜ ; B7  Aj"6  6 A!@@@@@@@@@@@@ A!k\\ \r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\n\r \r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r Aj"!\n@  M\r@  \nj- G\r \nAj"\n I\r !\n A!A!A!  (F\r Q"AÿM\rA! A!@@@@  jAm" AtA°j("Au j"AtA°j(Aÿÿÿq K@ Ak!   AvAq j" AtA°j(AÿÿÿqM\r Aj!  N\r  A! AN\r A! Aä!@@@  jAm" AtAÀ:j("Au j"AtAÀ:j(Aÿÿÿq K@ Ak!   AvAq j" AtAÀ:j(AÿÿÿqM\r Aj!  N\r A! AN   k! !@@  j-" AàG\r Aj" G\r !A  A F ! !  k"A K\r At j( "  AtjAk(O\rA!A!@ E@ ! !\n  !\n  " I\r @@@@ \nO\r@  j-"A G@ AàF\rA! Aj" \nG\r A! \n  ! \nO@A! !  @  j-AàG" @ ! !  Aj" \nG\r ! \n! @  k"F@A!\n  Atj" (" I@  jAk-A F!\n As  Gq\r \r F \nrE\r E\r Aj  @ F\rA! AK\r At jA j" (" K6 \r  Aj"M@A!   Atj"\n(! \n(!\n  ( ! \n@ Ak!  Ak(" ("O\r@  j-"A G A Gq\r Aj" G\r ! @@ (x" (|"N@ AÀ Av j AL"6| (t Als"\rA! ($"\rE\rAå (, \r  Aj6x (t"\r  6t (x"Aj6x  Alj"Aà; B7  6  6@@ (x" (|"N@ AÀ Av j AL"6| (t Als"\rA! ($"\rE\rAå (, \r  Aj6x (t"\r  6t (x"Aj6x  Alj"Aà; B7  6  6 (t (x"Alj"Ak" Ak6 A(k" Ak6  -Ar:  -Ar:A! ! ! ! (M\r@@@@   jAv"Atj"( K@ E\r  AtjAk( I\r Ak!  (K\r ! ! !  Aj! A!  M\r ! !  @@ (x" (|"N@ AÀ Av j AL"6| (t Als"\rA! ($"\rE\rAå (, \r  Aj6x (t"\r  6t (x"Aj6x  Alj"A¦; B7  Aj"6  6  (x"AL\r (t" AljAk-A&G\r (|" L@ AÀ Av j AL"6|  Als"E@A! ($"\rE\rAå (, \r  6t (x! Aj6x  Alj"A»; B7  Aj6  6  ("\n - AÀq\r\n   k"  \n A jA\r (" AjM\r -A!G\r  Aj"j-A-G\r -A-G\r A! (è" K  \nAkOq\r @ Aj" \n \nI"M@  j" /A­Ús -A>srE\r\r@@ Aj! Aj" K\r " j" /A­Ús -A>sr\r  !  \nI Aj" Kq@  Atj"(! (!  6è (! A! -AqE\r Aj" O\r  j-AüG\r@@ (x" (|"N@ AÀ Av j AL"6| (t Als"\rA! ($"\rE\rAå (, \r  Aj6x (t"E\r  6t (x"Aj6x  Alj"Aü; B7  Aj"6  6  ("Aj K\r  Ak"j"/A÷îs -A÷sr\r @  F\r Q"AÿK@A!A!@@@@  jAm" AtA°j("\nAu j"AtA°j(Aÿÿÿq K@ Ak!   \nAvAq j"AtA°j(AÿÿÿqM\r Aj!  N\r  AN\r A!Aä!@@@  jAm" AtAÀ:j("\nAu j"AtAÀ:j(Aÿÿÿq K@ Ak!   \nAvAq j"AtAÀ:j(AÿÿÿqM\r Aj!  N\r  AH\r\r  A F\r A F\r A\rkA}K\r A!kAI\r A:kAI\r AÛkAI\r AÿG AúKqE\r @@ (x" (|"N@ AÀ Av j AL"6| (t Als"\rA! ($"\rE\rAå (, \r  Aj6x (t"E\r  6t (x"Aj6x  Alj"A®; B7  Aj"6  6@@ (x" (|"N@ AÀ Av j AL"6| (t Als"\rA! ($"\rE\rAå (, \r  Aj6x (t"E\r  6t (x"Aj6x  Alj" (6 (! AÄ; B7  6  Aj!@@ (" Aj K\r  Ak"\nj(AèèуG\r  M\r /A¯ÞF\r @ Aj K\r  Ak"\nj"(Aèèуs -Aósr\r  M\r /A¯ÞF\r Aj K\r  Ak"\nj"/Aæès -Aðsr\r  M\r /A¯ÞG\r @@ (x" (|"N@ AÀ Av j AL"6| (t Als"\rA! ($"\rE\rAå (, \r  Aj6x (t"E\r  6t (x"Aj6x  Alj"Aº; B7  6  \n6@@ (x" (|"N@ AÀ Av j AL"6| (t Als"\rA! ($"\rE\rAå (, \r  Aj6x (t"E\r  6t (x"Aj6x  Alj" (6 (! AÄ; B7  6 Aj!  (Aj K\r @ Ak-"AßqAÁkAÿqAO@ Aj O\r A0kAÿqA\nO\r  Aj O\r\n  Aj"j-"A0kAÿqA\nO@ A_qAÁkAÿqAK\r\n @@ (x" (|"N@ AÀ Av j AL"6| (t Als"\rA! ($"\rE\rAå (, \r  Aj6x (t"E\r  6t (x"Aj6x  Alj"AÀ; B7  6  6@@ (x" (|"N@ AÀ Av j AL"6| (t Als"\rA! ($"\rE\rAå (, \r  Aj6x (t"E\r  6t (x"Aj6x  Alj" (6 (! AÄ; B7  6 Aj!  @@ (x" (|"N@ AÀ Av j AL"6| (t Als"\rA! ($"\rE\rAå (, \r  Aj6x (t"E\r  6t (x"Aj6x  Alj"AÝ; B7  Aj"6  6 \r @@ (x" (|"N@ AÀ Av j AL"6| (t Als"\rA! ($"\rE\rAå (, \r  Aj6x (t"E\r  6t (x"Aj6x  Alj" 6A!A  Aj" O\r  j-AÛG\r@@ (x" (|"N@ AÀ Av j AL"6| (t Als"\rA! ($"\rE\rAå (, \r  Aj6x (t"E\r  6t (x"Aj6x  Alj" 6A!!A !  :  : B7   j"6@@ (x" (|"N@ AÀ Av j AL"6| (t Als"\rA! ($"\rE\rAå (, \r  Aj6x (t"E\r  6t (x"Aj6x  Alj"AÄ; B7  6  6@@ (x" (|"N@ AÀ Av j AL"6| (t Als"\rA! ($"\rE\rAå (, \r  Aj6x (t"E\r  6t (x"Aj6x  Alj"AÄ; B7  6  6 \n @@ E@  -\rA qAvq\r  E\r @@ (x" (|"N@ AÀ Av j AL"6| (t Als"\rA! ($"\rE\rAå (, \r  Aj6x (t"E\r  6t (x"Aj6x  Alj"A:  : B7  Aj"6  6 \n !@@@@@@@@@AA@@AA@@@@@@@@@@ > AþF\r AÞG\r -AqE\r Aj" !@  M\r@@  j-AÞG\r Aj" I\r ! !  kAG\r A! A!  j ( kA7"AÿM@ A F\r A F\r A kAM\rA  @@@  jAm"AtA°j("Au j"\nAtA°j(Aÿÿÿq K@ \nAk!   AvAq j"AtA°j(AÿÿÿqM\r Aj!  N\r A  \nAN\rA  A !  (F\r Q"AÿM\rA!A!@@@  jAm" AtA°j("Au j"\nAtA°j(Aÿÿÿq K@ \nAk!   AvAq j"AtA°j(AÿÿÿqM\r Aj!  N\r !  \nAN\r !  -A qE\r@ Aj" O\r  j" -A=F\r !  kAG\rA!A!AA ( kA7"AÿM@A! A F\r A F\r A kAI  @@@  jAm" AtA°j("Au j"\nAtA°j(Aÿÿÿq K@ \nAk!   AvAq j"AtA°j(AÿÿÿqM\r Aj!  N\r A!\n \nAN !  -A€qE\r@ Aj" O\r  j" -A+F\r !  kAG\rA!A!AA ( kA7"AÿM@A! A F\r A F\r A kAI  @@@  jAm" AtA°j("Au j"\nAtA°j(Aÿÿÿq K@ \nAk!   AvAq j"AtA°j(AÿÿÿqM\r Aj!  N\r A!\n \nAN !  A! Aj"\n!@  \nM"\r@  j-AþG" \r Aj" I\r ! @  k"AF@ ( "A€€qE\r@ \rA! A!  \nj ( \nkA7"AÿM@ A F\r A F\r A kAM\rA  @@@  jAm"AtA°j(" Au j"AtA°j(Aÿÿÿq K@ Ak!   AvAq j"AtA°j(AÿÿÿqM\r Aj!  N\r A  AN\rA  A !  (F\r Q"AÿM\r\rA!A!@@@  jAm" AtA°j(" Au j"AtA°j(Aÿÿÿq K@ Ak!   AvAq j"AtA°j(AÿÿÿqM\r Aj!  N\r !  AN\r !  ! AK\r ( ! ! A€qE\rA!  (M\r\n Q"AÿM\rA!\nA!@@@@  \njAm"AtA°j(" Au j" AtA°j(Aÿÿÿq K@ Ak!   AvAq j"\nAtA°j(AÿÿÿqM\r \nAj!\n  \nN\r  AN\r A!\nAä!@@@  \njAm"AtAÀ:j(" Au j" AtAÀ:j(Aÿÿÿq K@ Ak!   AvAq j"\nAtAÀ:j(AÿÿÿqM\r \nAj!\n  \nN\r A! AN @ Aj" O" E@  j-A$F\r !  kAK\rA!  (M\r Q"AÿM\rA!\nA!@@@@  \njAm"AtA°j(" Au j" AtA°j(Aÿÿÿq K@ Ak!   AvAq j"\nAtA°j(AÿÿÿqM\r \nAj!\n  \nN\r  AN\r A!\nAä!@@@  \njAm"AtAÀ:j(" Au j" AtAÀ:j(Aÿÿÿq K@ Ak!   AvAq j"\nAtAÀ:j(AÿÿÿqM\r \nAj!\n  \nN\r A! AN  @ Aj" O\r@  j-A k"AK\rA tA€€qE\r Aj" I\r ! A F@ !  kAI\r @@ (x" (|"N@ AÀ Av j AL"6| (t Als"\rA! ($"\rE\r!Aå (, \r ! Aj6x (t"E\r  6t (x"Aj6x  Alj"A:  : B7  6  6  @@ (x" (|"N@ AÀ Av j AL"6| (t Als"\rA! ($"\rE\r Aå (, \r Aj6x (t"E\r  6t (x"Aj6x  Alj"A€ ; B7  Aj"6  6  A! !  kAG\r A! !  kAG\r A F\r A F\r A\rkA}K\r AÿG AúKq A!kAI A:kAIr AÛkAIrr ! @ \rA! A!@  j" ( k" A7"AÿM@ A F\r A F\r A kAO\r  @@@  jAm"AtA°j("Au j"\nAtA°j(Aÿÿÿq K@ \nAk!   AvAq j"AtA°j(AÿÿÿqM\r Aj!  N\r  \nAN\r A!Aä! A7"AÿM@ AÿG AúKq A!kAI A:kAIr AÛkAIrr  @@@  jAm" AtAÀ:j("Au j"\nAtAÀ:j(Aÿÿÿq K@ \nAk!   AvAq j"AtAÀ:j(AÿÿÿqM\r Aj!  N\r A!\n \nAN ! !  Aq "E\r @@ (x" (|"N@ AÀ Av j AL"6| (t Als"\rA! ($"\rE\rAå (, \r  Aj6x (t"E\r  6t (x"Aj6x  Alj" : A$: B7  6  6  A F\r A F\r A\rkA}K\r AÿG AúKq A!kAI A:kAIr AÛkAIrr ! @ E\rA! A!@  j" ( k" A7"AÿM@ A F\r A F\r A kAO\r  @@@  jAm"AtA°j("Au j"\nAtA°j(Aÿÿÿq K@ \nAk!   AvAq j"AtA°j(AÿÿÿqM\r Aj!  N\r  \nAN\r A!Aä! A7"AÿM@ AÿG AúKq A!kAI A:kAIr AÛkAIrr  @@@  jAm" AtAÀ:j("Au j"\nAtAÀ:j(Aÿÿÿq K@ \nAk!   AvAq j"AtAÀ:j(AÿÿÿqM\r Aj!  N\r A!\n \nAN ! !  Aq "E\r @@ (x" (|"N@ AÀ Av j AL"6| (t Als"\rA! ($"\rE\rAå (, \r  Aj6x (t"E\r  6t (x"Aj6x  Alj" : Aþ: B7  6  6  A F\r A F\r A kAM\r !  A! ! AqE\r @@ (x" (|" N@ AÀ Av j AL"6| (t Als" \rA! ($"\rE\rAå (, \r  Aj6x (t" E\r  6t (x"Aj6x Alj" : Aþ: B7  \n6  6  @@  (F\r Q"AÿK@A!A!@@@  jAm" AtA°j("Au j"\nAtA°j(Aÿÿÿq K@ \nAk!   AvAq j"AtA°j(AÿÿÿqM\r Aj!  N\r !  \nAN\r !  A F\r A F\r A kAM\r !  A! ! AqE\r\r @@ (x" (|" N@ AÀ Av j AL"6| (t Als" \rA! ($"\rE\rAå (, \r  Aj6x (t" E\r  6t (x"Aj6x Alj" : A+: B7  6  6 \r @@  (F\r Q"AÿK@A!A!@@@  jAm" AtA°j("Au j"\nAtA°j(Aÿÿÿq K@ \nAk!   AvAq j"AtA°j(AÿÿÿqM\r Aj!  N\r !  \nAN\r !  A F\r A F\r A kAM\r !  A! ! AqE\r @@ (x" (|" N@ AÀ Av j AL"6| (t Als" \rA! ($"\rE\rAå (, \r  Aj6x (t" E\r  6t (x"Aj6x Alj" : A=: B7  6  6 A F\r A F\r A kAM\r !  A! ! AqE\r @@ (x" (|" N@ AÀ Av j AL"6| (t Als" \rA! ($"\rE\rAå (, \r  Aj6x (t" E\r  6t (x"Aj6x Alj" : AÞ: B7  6  6 @ Aj" O\r -A?G\rA!@ (ì" M@ !  !  \nAkO\r  Aj" \n \nI"M@  j/A¿üF\r@@ Aj! Aj" K\r " j/A¿üG\r  !  \nI Aj"Kq  Atj"(! (!  6ì ( ! @ M\r -A!G\r  j-AßqAÁkAÿqAK\r@ Aj" O\r@  j-AßqAÁkAÿqAK\r Aj" G\r ! A! (ð" K  \nAkOq\r Aj" \n \nI"M@  j-A>F\r@@ Aj! Aj" K\r " j-A>G\r  !  \nI Aj" Kq  Atj"(! (!  6ð ( ! @ A j" O\r )B¼ÂìšÄ¨ªÁ… 1Bۅ„BR\rA! (ô" K  \nAkOq\r@ Aj" \n \nI" M@  j"/Aݺs -A>srE\r@@ Aj! Aj" K\r  "j"/Aݺs -A>sr\r  !  \nI Aj"Kq@  Atj"(! (!  6ô ( !\n ! Aj" \nO\r@ Aj" \nO\r  j-" A0kAÿqA\nIA AßqAÁkAÿqAI E\r@  j-"A0kAÿqA\nI\r AßqAÁkAÿqAI\r A+k"AK\r AF\r Aj" \nF\r@  kA K\r  j-" A:F@  \nO\r@  j-" A k"AMAA tA€€q\r A>G@ A I\r AG\r  j-A-F\rAð! Aj !@@ (x" (|"N@ AÀ Av j AL"6| (t Als"\rA! ($"\rE\rAå (, \r  Aj6x (t"E\r  6t (x"Aj6x  Alj" Ar: A<: B7  6  6@@ (x" (|"N@ AÀ Av j AL"6| (t Als"\rA! ($"\rE\rAå (, \r  Aj6x (t"E\r  6t (x"Aj6x  Alj" Ar: A>: B7  6  Ak6 (t (x"Alj"A k Ak6 Ak Ak6 ! !   6 @@ (x" (|"N@ AÀ Av j AL"6| (t Als"E\r 6t (x"Aj6x  Aj6x (t"E\r\n  Alj"A¼(; B7  6  6@ (x" (|"N@ AÀ Av j AL"6| (t Als"E\r 6t (x"Aj6x  Aj6x (t"E\r\n ( !  Alj"A¾0; B7  6  6 (t (x"Alj"A k Ak6 Ak Ak6A! !  (M\r@@   jAv"Atj"( K@ E@ ! \n   AtjAk(K@ ! \n Ak!  ( O@ ! Aj!  M\r A!  ($"E\rAå (,   Aj!  A F\r A F\r A\rkA}K\r AÿG AúKq A!kAI A:kAIr AÛkAIrr "! As! A! @  \nF\rA!A!@@  \nj" ( \nk"A7"AÿM@ A G\r  @@@  jAm"AtA°j(" Au j" AtA°j(Aÿÿÿq K@ Ak!   AvAq j"AtA°j(AÿÿÿqM\r Aj!  N\r  A! AH\r  A F\r A kAI\r A!Aä!AA  A7"AÿM@ AÿG AúKq A!kAI A:kAIr AÛkAIrr  @@@  jAm"AtAÀ:j(" Au j" AtAÀ:j(Aÿÿÿq K@ Ak!   AvAq j"AtAÀ:j(AÿÿÿqM\r Aj!  N\r A! AN "! As! ! \n!A   q AßFq""A " rE\r  kAp!@@ (x" (|"N@ AÀ Av j AL"6| (t Als"\rA! ($"\rE\rAå (, \r  Aj6x (t"\r  6t (x"Aj6x  Alj" AtA# AG  MqAA  OA r" AFrA@k:  : B7  \n6  6 ! \n "M\r@@@ (x" (|"N@ AÀ Av j AL"6| (t Als"\rA! ($"\rE\r Aå (, \r Aj6x (t"\r  6t (x"Aj6x  Alj"AÄ; B7  6  6 Aj" \nI\r ! \n! @@ (x"\r (|"N@ AÀ Av j AL"6| (t Als"\rA! ($"\rE\rAå (, \r  \rAj6x (t"E\r  6t (x"\rAj6x (!  \rAlj"Aÿ ; B7  6  6A!  A! Aj$@ " \r A (xA¿AM@ (à"AN@ Ak! A€j! AØj! A j!@ (t" Al"j" (!\r@ -"AÄF\r ( !\nA! A! \rAN@   \rAlj" ( Alj! \nAl! (! -A q@ A!: Ak"6A!!  j!  I@ ( I\r  I@ ( I\r @  I@ A!G\r  AÛG\r ( A€€ÀqE\r (" (O\r ( j-AÞG\r Aj"6 (t j"(" M\r  ( j  kN"E\r ( "E@ (pAj"6p  6  (Aj6 6 (6 -A”r:  -A˜r: (! (! (  @@@@ -\rA qE\r -AÛG\r E\r (" ("kAG\r -AÛG\r E\r ( ("kAG\r -AÝG\r  AkG\r ( ("kAG\r  (AjG\r@@ Aj" \nN"@ !  (t! !@@  Alj" -"AÄF\r AüF\r ( kAäK\r -AqE\r ( ! Aj" \nH\r A!  (" F\r  kAäK\r  I@ (!@@  j-A\nk Aj" G\r A!@   Atj"("L@ (t!\r@ \r Alj( " N\r  6  ("L@ (t!\r@ \r Alj( " N\r  6 Aj"AG\r @@ ( (I@@  L\r  kAk! (t!\rA!  AsjAq"@@ \r AljAÄ; Aj! Aj" G\r AI\r@ \r Alj"AÄ;L AÄ;8 AÄ;$ AÄ; Aj" G\r -Ar: (6  @ \r \n kAk! (t!\rA! \n AsjAq"@@ \r AljAÄ; Aj! Aj" G\r AI\r@ \r Alj"AÄ;L AÄ;8 AÄ;$ AÄ; Aj" \nG\r  (6 (6  (6 (t"\r j" \n6 \r j"\r 6  -Ar: \r \r-Ar: (! (!  Aj \nO (  (6  (6 (t"\r j" \n6 \r j"\r 6  -Ar: \r \r-Ar: (! (! (  \r (!  (" ("G\r ( AjK@ -AÀq\r    ( AjP  -AÀq\r   (  AjP "AH\r E\r  (6 (t \rAlj(!\r  @  ("O\r (" j-A(G\r \nAj! A! !@@@   jAv"Atj"( K@ E\r  AtjAk( I\r Ak!   (M\r Aj!  O\r A! @@ Aj"  Atj(" I@@  j-A k"AK\rA tA€€qE\r Aj" G\r !  6@  O\r  j-A\nk Aj" O\r  Atj(!  6 @  O\r  j-A)G\rA! A6 B7  6  6 Aj! A    Atj"(" Aj Aj 8E\r@   k ( Aj A j A,j A(j A$j9@ ( " j! (," F!   j"Atj(! (!   ("6(  6$A!A! @  I@@  j-"A k"AK\rA tA€€qE\r Aj" G\r Aj" O\r   Atj("j-! AÿqA)G\r  Aj" 6 ($" (("M@A! A6 B7A   k! @ A6  6   j"6A  @ q"@  Atj!A! !@   Atj("  K" KA!  "kAq"@@  ( j-: Aj! Aj! Aj" G\r  kA|M@@  ( j-:  ( j-:  ( j-:  ( j-: Aj! Aj" G\r   O\r A\n: Aj!  Aj"Atj(! ($" E\rAÔ (,   A6  6   k6 (! A ! (x" L@  6  (t! !@  Alj"(K@@ -AqAG\r  ( "Alj( I\r \r r  Aj" H\r  6  L\r  \nkAk! (t! A!  \nAsjAq"@@ AljAÄ; Aj! Aj" G\r AI\r@ Alj"AÄ; AÄ;L AÄ;8 AÄ;$ Aj" G\r  -AÀq\r   ( ( AjP"AH\r E\r -Ar:  -Ar: Aj" (tj (6 (t j ( 6  (tj (6( Aj! (t! (@  Alj (Ð6 6Ð  Alj (6 (! (! -!  Aj \nO   AÛF"!   !   !   ! ! / AŒqE\r@@ "Aj! -$"AÄG" \r  I\r À! !@@ "\nAk! \r  K\r -%" AqE\r E\r ( (G\rA÷ ATE\r (  (tkAmG\r \nAk( (G\r AÄ:$  Aïq:% \nAkAÄ: \nAk" -Aïq: \r "AN\r B7à A6¸ @A! A6Ô (x"AL\r (t!\rA!@@ \r Alj"-"Aq\r -AüG\r ( (kAG\r A6  Ar:@ (ØAH@ 6Ø  \r (ÜAlj 6 6Ü Aj"6Ô Aj" G\r A!  A! A (xO  A! A0j$ Þ#Aðk"$ B7h B7` B7X B7P B7H B7@ B78 B70 A6( B7 B7 B7 B7A! @    Aj AÐj<\r  A Aj A0j<\r  6( (,! @  Aj (" E@A!  ($"E\rA (,    Aj (" E@A!  ($"E\rAæ (,  (`@ (Pr (Tr (Xr (@@ (0r (4r (8r Aðj$ Ý@ ( " ("I@ (!  ( Av jA "At"s"E@A ($"E\rAå (, A  6 ( Ajs"E@A ($"E\rAå (, A  6  6 ( ! ( Atj 6  ( Atj 6  ( Aj6 A Ü Aj!@@@ Aj" O\r j-A#G\r j-A rAøG\r !@@@@ j-"A0kAÿqA\nI\r AÁkAÿqAI\r  kAK\r AákAÿqAI\r   kAK\r Aj" G\r !  F\r  kAI\r  @ Aj" O"\r j-A#G\r !@@  kAK\r j-A0kAÿqA K\r Aj" G\r !  F\r  kAI\r   M\r j-AßqAÁkAÿqAK\r  \r@@ j-"AßqAÁkAÿqAO@   kA0K\r A0kAÿqA\nI\r    kA0M\r   Aj" G\r  " kAkA.K\r  M\r j-A;G\rA!  Aj6  ³#Ak"$@@  O\r (!@  j-A k"AK\rA tA€€qE\r Aj" G\r !   O\r (!@  jAk-A k"AK\rA tA€€qE\r Ak" K\r !  6  6  6@  Aj (, ("@ ($"E\rAŠ (,   AjA?"\r  Aj (, ("E@A!  ($"E\rA¯ (,  Aj$  Ê"#A0k"\r$  H@ A¸j! A€j! \rAr! \rAr! (!@@ (t" Al" j"-"Aq@ AqE\r@ E\r ,"E\r  `\r ( !  ,"\nAÿq"E\r  \n`E\r (" I\r@@@@@@@@@@@@@@@@@ A!k^  (! Aq@ AN@  Alj" -AÀr:  6  6  AH\r  Alj"( 6¸  6  6@ (ä" AN@  Alj 6  6à 6ä A6  Aj" (xN\r  Alj"-A;G\r (  (" \rKE\r  6  6  Ar:  -Ar:  6  AqE\r \r A€A˜ \nA*Fj"A j6 AÀq" AÀF@ ! A  \r Aj6 A€F\r ! A ! Aj6A! A°A´ ( kAFj!\n@ AqE\rA! \n("AH\r@   Atj"\n("L@@  Alj( " N\r \n 6  \n("L@@  Alj( " N\r \n 6 Aj" AG\r  Alj" 6  6  -Ar:  -Ar: \r AqE\r  \n(6 \n 6 @ AqE\rA! (Ä"AH\r@   Atj"\n("L@@  Alj( " N\r \n 6  \n("L@@  Alj( " N\r \n 6 Aj" AG\r  Alj" 6  6  -Ar:  -Ar: AqE\r  (Ä6 6Ä @ AqE\r (¼"AH\rA!  Alj" ( (k ( kG\r@   Atj"\n("L@@  Alj( " N\r \n 6  \n("L@@  Alj( " N\r \n 6 Aj" AG\r @ Aj" N\r  kAk!A!  AsjAq"\n@@  AljAÄ; Aj! Aj" \nG\r AI\r@  Alj" AÄ;L AÄ;8 AÄ;$ AÄ; Aj" G\r 6  6 -Ar:  -Ar: A6¼ AqE\r\n  (¼6 6¼ \n  Aj"Al"j"(! (! \r 6 \r ("6, \r 6( \r 6$ \nAÀF@   \rAAAA˜ \rA(jRAL\r\n \r(!  O\r Q" AÿM\rA!A!@@@  jAm" AtA°j("Au j"AtA°j(Aÿÿÿq K@ Ak!  AvAq j"AtA°j(AÿÿÿqM\r Aj!  N\r  AN\r  ( kAG\r@ AqE\rA! (À"AH\r@   Atj"\n("L@@  Alj( " N\r \n 6  \n("L@@  Alj( " N\r \n 6 Aj" AG\r  Alj" 6  6  -Ar:  -Ar: AqE\r  (À6 6À  ( kAG\r@ AqE\rA! (È"AH\r@   Atj"\n("L@@  Alj( " N\r \n 6  \n("L@@  Alj( " N\r \n 6 Aj" AG\r  Alj" 6  6  -Ar:  -Ar:  AqE\r  (È6 6È  ( kAG\r@ AqE\rA! (Ì"AH\r@   Atj"\n("L@@  Alj( " N\r \n 6  \n("L@@  Alj( " N\r \n 6 Aj" AG\r  Alj" 6  6  -Ar:  -Ar:  AqE\r  (Ì6 6Ì  A! A!  A F\r A F\r A kAI\r ( jAk,"@Aã AT\r \r((" I\r Ak!@@@ -AÄF\r ( K\r  (O\r -" Aq\r Ak" O\r  AqE\r   \rA,jAA.AAØ \rA$jRAH\r \r(,! @ \nAÀF\r@ O\r (" j-A/G\r Aj  \rA,jAA/AùA \rA$jRAH\r \r(," O\r  j-A/G\r \r Aj" 6, @ O\r ( j-A?G\r Aj  \rA,jAA&AóA• \rA$jRAH\r \r(,! O\r ( j-A#G\r Aj  \rA,jAAAAÛ \rA$jRAH\r \r(,! @ O\rA!A!@ ( j" ( kA7"AÿM@ A F\r A F\r A kAO\r  @@@  jAm" AtA°j("Au j"AtA°j(Aÿÿÿq K@ Ak!   AvAq j"AtA°j(AÿÿÿqM\r Aj!  N\r  AN\r ,"@A¡ A T\r \r($"  (xAlj"O\r@@@ -AÄF\r ( M\r ( K\r -"Aq\r Aj" I\r  AqE\r  6  6  \n:  6  6 (t" j" 6  j" 6  -Ar:  -Ar:  \r Atr 6 A q! Aj! AàqAàG@ \r Atj Aj6 Aj! As AGqE@ \r Atj Aj6 Aj!  \r(("AljA AN"! A !@ \r(("AH\r  Alj! @ ( (M\r ! ! @ AF\r@ \r(("AH\r  Alj! @ ( (M\r ! ! AF\r@ \r( ("AH\r  Alj! @ ( (M\r ! ! AF\r@ \r(("AH\r  Alj! @ ( (M\r ! ! AF\r \r((" AH\r  Alj! @ ( (M\r ! ! E\r (! (! (! !\n@@@@@@  Alj"-"A!k\n AÛk A€A˜ A*Fj -"AtAuA qj!\n@ AvAk \nAj!\n  A°A´ ( (kAFj!\n  \nAj!\n @ k" k"K@  (" (k k j"Al" j" (6  )7  )7  k"6  6 (t j \n(6 \n 6  O\r  Alj j" (6  )7  )7  k j"6  6 A!\n@   \nAtj"("L@@ AN@  (t Alj( "6  N\r \nAj"\nAG\r (t" Alj" 6  j" 6  -Ar:  -Ar:  AqE\r A€A˜ \nA*Fj AtAuA qj!@@@ AvAk Aj!  Aj!  (6  6 -"AqE\r Aq@ (t ( Alj(!  (! Aj" H\r \rA0j$ –#Ak"$@ (E\r  6! (  (pAtj("E@A!  @  ("I\r   ( (ljO\r  (F@ ( (  DE\r A!   6  6  6  Aj6 Aj!@ ("@@   Av"Atj"G"AH@   E\r Aj!  Asj "\r A! E@A!  (! Aj$  ý#A k"$ Aß: A¦Ô;A! ( "A€ q@ A$:A! A€€€q@ Aj rA=: Aj! A€€€q@ Aj rA+: Aj! A€€q@ Aj jAü: Aj! A€€q@ Aj jAÞ: Aj! A€„q@ Aj jAþ: Aj! A! Aj jA:    AjAM ( "AŒq@ Aq@ AÀ:A! Aq@  rA:: Aj! A€q  rA.: Aj  rA:     AjM A€jAÿAÐü A j$ • (TE@A Ak! Ak!AA ("\n j-A!F j!A!@@@   jAv"Atj" ( K@ E\r  AtjAk( I\r Ak!   (M\r Aj!  O\r A! @  ("M" E@@  kq"@ !@    K" KA!  "kAq"\n@@  ( j-: Aj! Aj! Aj" \nG\r  kA|M@@  ( j-:  ( j-:  ( j-:  ( j-: Aj! Aj" G\r   O\r A : Aj! Aj" Atj"(! (! A! ($"E\rAÔ (, A  k   \nj!  k ! A  Aj60A! Aj !\nA!@@ \n("E\r Aj p"AH\r   kK\r \nAj!\n  j" I\r A=! AH\r A \r  l E@A!  A! (0!\n@ \n(" E\r Aj p" j" K\r Aj h \nAj!\n  K\r A \r  A€Àsl \r   \rH!   AHq\r A=! +0 \r  m"AN\r \n -!\n Aj! \r E\rA!@  Atj("\n@  Atj \n jA! Aj"A\nG\r A\nO@A! \n @  Atj(\rA! Aj"A\nG\r A!   :\'A! ! !   k"  H" AÿÿÿÿsJ\rA=! \r  j"  \rH" \nK\r A   l  h A0   A€€sl A0  Al h A   A€Àsl ( Ak" O\r B€”ëÜT\r Ak" > @ "I@ Ak"(E\r (, k"6, ! AJ\r AH@ AjA nAj! AæF!@A A k" A O!\n@  M@AA (!  A€”ëÜ \nv!\rA \ntAs!A! !@  (" \nv j6  q \rl! Aj" I\r AA (! E\r  6 Aj! (, \nj"6,   j" " Atj   kAu J! AH\r A!@  M\r  kAuA l!A\n! ("A\nI\r@ Aj!  A\nl"O\r A AæGk AçF AGqk"  kAuA lA kH@ A0jA„`A¤b AHj A€Èj"A m"\rAtj!\nA\n!  \rA lk"AL@@ A\nl! Aj"AG\r @ \n("  n" lk"\rE \nAj" Fq\r@ AqE@D@C! A€”ëÜG\r \nO\r \nAk-AqE\r D@C! Dà?Dð?Dø?  FDø? \r Av"F \r I!@ \r -A-G\r š! š! \n  \rk"6    a\r \n  j"6 A€”ëÜO@@ \nA6 \nAk"\nK@ Ak" A6 \n \n(Aj"6 Aÿ“ëÜK\r  kAuA l!A\n! ("A\nI\r@ Aj!  A\nl"O\r \nAj"   K! @ " M"E@ Ak"(E\r @ AçG@ Aq!\r  AsA A " J A{Jq"\n j! AA~ \n j! Aq"\r\rAw!@ \r Ak("\nE\rA\n!A! \nA\np\r@ "\rAj! \n A\nl"pE\r \rAs!  kAuA l! A_qAÆF@A!\r  jA k"A AJ"  J!  A!\r  j jA k"A AJ"  J! A!\n AýÿÿÿAþÿÿÿ \rr"J\r AGjAj!@ A_q"AÆF@  AÿÿÿÿsJ\r A AJ!    Au"s k­ k"kAL@@ Ak"A0:  kAH\r Ak" : AkA-A+ AH:  k" AÿÿÿÿsJ\r  j" AÿÿÿÿsJ\r A   j" l  h A0   A€€sl@@@ AÆF@ AjA r!  K"! @ 5 k!@  G@  AjM\r@ Ak"A0:  AjK\r   G\r Ak"A0:   kh Aj" M\r @ AùAh  M\r AL\r@ 5 k" AjK@@ Ak"A0:  AjK\r A A Nh A k! Aj" O\r A J! ! \r  @ AH\r  Aj  K!\n AjA r! !@  5 k"F@ Ak"A0: @  G@  AjM\r@ Ak"A0:  AjK\r  Ah Aj! \rrE\r AùAh   k"  Hh k! Aj" \nO\r AN\r A0 AjAAl   kh  ! A0 A jA Al A   A€Àsl    J!\n   AtAuA qj!@ A K\rA k!D0@!@ D0@¢! Ak"\r -A-F@  š ¡ š!     ¡!  (," Au"s k­ k"F@ Ak"A0: (,! Ar!\r A q! Ak" Aj: AkA-A+ AH: AqE ALq! Aj!@ " ü"AÐÑj- r:  ·¡D0@¢!@ Aj" AjkAG\r Da q\r A.: Aj! Db\r A!\n Aýÿÿÿ \r  k" j"kJ\r A   Aj  Ajk" Ak H  "j" l  \rh A0   A€€sl Aj h A0  kAAl  h A   A€Àsl    J!\n A°j$ \n ¹~  (AjAxq"Aj6 )! )!#A k"$ Bÿÿÿÿÿÿ?ƒ!~ B0ˆBÿÿƒ"§"AøkAýM@ B† B<ˆ„! A€øk­!@ Bÿÿÿÿÿÿÿÿƒ"B€€€€€€€Z@ B|!  B€€€€€€€€R\r Bƒ |! B  BÿÿÿÿÿÿÿV"! ­ |  @  „P\r BÿÿR\r B† B<ˆ„B€€€€€€€„!Bÿ  Aþ‡K@B!Bÿ  A€øAø P"\n" k" AðJ@B!B   B€€€€€€À„ \n!A!\n G@ Aj! ! !@A€ k"AÀq@  A@j­†!B!  E\r  ­"† AÀ k­ˆ„!  †! 7 7 ) )„BR!\n @ AÀq@  A@j­ˆ!B!  E\r AÀ k­†  ­"ˆ„!  ˆ!  7  7 )B† )"B<ˆ„!@ \n­ Bÿÿÿÿÿÿÿÿƒ„"B€€€€€€€Z@ B|!  B€€€€€€€€R\r Bƒ |! B€€€€€€€…  BÿÿÿÿÿÿÿV"! ­ ! A j$ B€€€€€€€€€ƒ B4†„ „¿9 ¦ (T"(! (" ( ("k"  I"@   V  ( j"6  ( k"6    K"@   V  ( j"6  ( k6 A: (,"6 6  ¢ E@A @  AÿM\r@AìÞ((E@ A€qA€¿F\rAÐÝA6  AÿM@ A?qA€r: AvAÀr:A  A€@qA€ÀG A€°OqE@ A?qA€r: A vAàr: AvA?qA€r:A  A€€kAÿÿ?M@ A?qA€r: AvAðr: AvA?qA€r: A vA?qA€r:A  AÐÝA6 AA  :A Ñ( #Ak"\n$@@@@@@@@@@ AôM@Aß("A A jAøq A I"Av"v"Aq@@ AsAq j"At"A¸ßj" (À_"("F@Aß A~ wq6   6 6 Aj!  Ar6  j" (Ar6 A˜ß("M\r @@ tA t"A krqh"At"A¸ßj" (À_"("F@Aß A~ wq"6   6  6 Ar6 j"  k"Ar6 j 6 @ AxqA¸ßj!A¤ß(! A Avt"qE@Aß  r6   ( !  6  6  6  6 Aj!A¤ß 6A˜ß 6 A”ß(" E\r hAt(Àa"(Axq k! !@@ ("E@ ("E\r (Axq k"   K"!  ! !  (!  ( "G@ (" 6 6 \n (" Aj ("E\r Aj !@ ! "Aj! ("\r Aj! ("\r A6 A! A¿K\r A j"Axq!A”ß(" E\rA! AôÿÿM@ A& Avg"kvAq AtkA>j! A k!@@@ At(Àa"E@A!  A! A AvkA AGt!@@ (Axq k" O\r ! "\rA! !  ("   AvAqj("F ! At! "\r rE@A!A t"A kr q"E\r hAt(Àa! E\r @ (Axq k" I!   !  ! ("  ( "\r E\r A˜ß( kO\r (!  ( "G@ (" 6 6  (" Aj ("E\r Aj !@ ! "Aj! ("\r Aj! ("\r A6  A˜ß("M@A¤ß(!@ k"AO@  j" Ar6 j 6  Ar6   Ar6 j" (Ar6A!A! A˜ß 6A¤ß 6 Aj! Aœß("I@Aœß  k"6A¨ßA¨ß(" j"6  Ar6 Ar6 Aj! A! A/j"Aèâ(@Aðâ(  AôâB7AìâB€ €€€€7Aèâ \nA jApqAتժs6AüâA6AÌâA6A€ "j"A k"q" M\rAÈâ("@AÀâ(" j" M\r  I\r @AÌâ-AqE@@@@@A¨ß("@AÐâ!@ (" M@   (jI\r ("\r Au"AF\r !Aìâ("Ak" q@  k  jA kqj!  O\rAÈâ("@AÀâ(" j" M\r I\r u" G\r   k q"u" ( (jF\r ! AF\r A0j M@ !  Aðâ("  kjA kq"uAF\r  j! !  AG\r AÌâAÌâ(Ar6 u!Au! AF\r AF\r M\r k" A(jM\r AÀâAÀâ( j"6AÄâ( I@AÄâ 6 @A¨ß("@AÐâ!@  (" ("jF\r ("\r  A ß("A ME@A ß 6 A!AÔâ 6AÐâ 6A°ßA6A´ßAèâ(6AÜâA6@ At" A¸ßj"6À_  6Ä_ Aj"A G\r Aœß A(k"Ax kAq"k"6A¨ß  j"6  Ar6 jA(6A¬ßAøâ(6   O\r  I\r ( Aq\r  j6A¨ß Ax kAq"j"6AœßAœß( j" k"6  Ar6  jA(6A¬ßAøâ(6  A!  A!  A ß( K@A ß 6  j!AÐâ!@@  ("G@ ("\r  - AqE\r AÐâ!@@ (" M@   (j"I\r (!  Aœß A(k"Ax kAq"k"6A¨ß  j"6  Ar6 jA(6A¬ßAøâ(6  A\' kAqjA/k" AjI"A6 AØâ)7 AÐâ)7AØâ Aj6AÔâ 6AÐâ 6AÜâA6 Aj!@ A6 Aj! Aj!  I\r  F\r  (A~q6   k"Ar6  6 AÿM@ AøqA¸ßj!Aß("A Avt"qE@Aß  r6  ( ! 6  6 A !A  A! AÿÿÿM@ A& Avg"kvAq AtrA>s!  6 B7 AtAÀáj!@@A”ß("A t"qE@A”ß  r6  6  6  A AvkA AGt! (!@ "(Axq F\r Av! At!  Aqj"("\r  6  6 A! ! !A  (" 6  6  6A!A!A j 6  j 6 Aœß(" M\rAœß k"6A¨ßA¨ß(" j"6  Ar6 Ar6 Aj!  AÐÝA06A!  6 ( j6 Ax kAqj" Ar6 Ax kAqj"  j"k!@A¨ß( F@A¨ß 6AœßAœß( j"6  Ar6  A¤ß( F@A¤ß 6A˜ßA˜ß( j"6  Ar6  j 6  ("AqAF@ Axq! ( !@ AÿM@ (" F@AßAß(A~ Avwq6  6  6  (!@  G@ (" 6  6  @ (" Aj ("E\r Aj !@ ! "Aj! ("\r Aj! ("\r A6  A! E\r@ ("At"(Àa F@ AÀáj 6 \rA”ßA”ß(A~ wq6  @  (F@  6   6 E\r  6 ("@  6  6 ("E\r  6  6  j"(!  j!  A~q6  Ar6  j 6 AÿM@ AøqA¸ßj!Aß("A Avt"qE@Aß  r6   ( !  6  6  6  6  A! AÿÿÿM@ A& Avg"kvAq AtrA>s!  6 B7 AtAÀáj!@@A”ß("A t"qE@A”ß r6  6  6  A AvkA AGt! (!@ "(Axq F\r Av! At!  Aqj"("\r  6  6  6  6  (" 6  6 A6  6  6 Aj!  @ E\r@ ("At"(Àa F@ AÀáj 6 \rA”ß A~ wq" 6  @  (F@  6   6 E\r 6 ("@ 6  6 ("E\r 6  6 @ AM@   j"Ar6 j" (Ar6   Ar6  j" Ar6  j 6 AÿM@ AøqA¸ßj!Aß("A Avt"qE@Aß  r6  ( ! 6  6  6  6  A! AÿÿÿM@ A& Avg"kvAq AtrA>s!  6 B7 AtAÀáj!@@ A t"qE@A”ß  r6  6  6  A AvkA AGt! (!@ "(Axq F\r Av! At!  Aqj"("\r  6  6  6  6  (" 6  6 A6  6  6 Aj!  @ E\r@ ("At"(Àa F@ AÀáj 6 \rA”ß A~ wq6  @  (F@ 6  6 E\r 6 ("@ 6  6 ("E\r 6  6 @ AM@   j"Ar6 j" (Ar6   Ar6  j" Ar6  j 6 @ AxqA¸ßj!A¤ß(!A Avt" qE@Aß  r6   ( !  6  6 6 6 A¤ß 6A˜ß 6 Aj! \nAj$ ‚ @ E\r Ak" Ak("Axq"j!@ Aq\r AqE\r  ("k"A ß(I\r j!@@@A¤ß( G@ ( ! AÿM@  ("G\rAßAß(A~ Avwq6  (!  G@ (" 6  6  (" Aj ("E\r Aj !@ ! "Aj! ("\r Aj! ("\r A6  ("AqAG\rA˜ß 6  A~q6  Ar6  6  6  6  A! E\r@ ("At"(Àa F@ AÀáj 6 \rA”ßA”ß(A~ wq6  @  (F@  6   6 E\r  6 ("@  6  6 ("E\r  6  6  O\r ("AqE\r@@@@ AqE@A¨ß( F@A¨ß 6AœßAœß( j"6  Ar6 A¤ß(G\rA˜ßA6A¤ßA6 A¤ß(" F@A¤ß 6A˜ßA˜ß( j"6  Ar6 j 6 Axq j! ( ! AÿM@ (" F@AßAß(A~ Avwq6   6  6  (!  G@ (" 6  6  (" Aj ("E\r Aj !@ ! "Aj! ("\r Aj! ("\r A6   A~q6  Ar6 j 6  A! E\r@ ("At"(Àa F@ AÀáj 6 \rA”ßA”ß(A~ wq6  @  (F@  6   6 E\r  6 ("@  6  6 ("E\r  6  6  Ar6 j 6  G\rA˜ß 6 AÿM@ AøqA¸ßj!Aß("A Avt"qE@Aß r6   ( !  6 6  6  6 A! AÿÿÿM@ A& Avg"kvAq AtrA>s!  6 B7 AtAÀáj!@A”ß("A t"qE@A”ß  r6  6A!A  A AvkA AGt! (!@ "(Axq F\r Av! At!  Aqj"("\r  6A! !A ! !   (" 6  6A!A!A !  j 6  6 j 6A°ßA°ß(Ak"A 6 ‰  E@ q A@O@AÐÝA06A A A jAxq A I! Ak"("Axq!@ AqE@ A€I\r Aj M@ !  kAðâ(AtM\r A   j!@  O@  k"AI\r   AqrAr6  j" Ar6  (Ar6  t  A¨ß( F@Aœß( j" M\r   AqrAr6  j"  k"Ar6Aœß 6A¨ß 6  A¤ß( F@A˜ß( j" I\r@  k"AO@   AqrAr6  j" Ar6  j" 6  (A~q6   Aq rAr6  j" (Ar6A!A! A¤ß 6A˜ß 6  ("Aq\r Axq j"\n I\r \n k! ( !@ AÿM@ (" F@AßAß(A~ Avwq6   6  6  (! @  G@ (" 6  6  @ (" Aj ("E\r Aj !@ ! "Aj! ("\r Aj! ("\r A6  A! E\r@ ("At"(Àa F@ AÀáj 6 \rA”ßA”ß(A~ wq6  @  (F@ 6  6 E\r  6 ("@  6  6 ("E\r  6  6 AM@  Aq \nrAr6  \nj" (Ar6    AqrAr6  j" Ar6  \nj" (Ar6  t !  "@ Aj q"E@A  A|Ax Ak("Aq Axqj"   KV r  «  j!@@ ("Aq\r AqE\r (" j!@@@ k"A¤ß(G@ ( ! AÿM@  ("G\rAßAß(A~ Avwq6  (! G@ (" 6  6  (" Aj ("E\r Aj !@ ! "Aj! ("\r Aj! ("\r A6  ("AqAG\rA˜ß 6  A~q6 Ar6  6  6  6  A! E\r@ ("At"(Àa F@ AÀáj 6 \rA”ßA”ß(A~ wq6  @ (F@  6   6 E\r  6 ("@  6  6 ("E\r  6  6 @@@@ ("AqE@A¨ß( F@A¨ß 6AœßAœß( j"6 Ar6 A¤ß(G\rA˜ßA6A¤ßA6 A¤ß(" F@A¤ß 6A˜ßA˜ß( j"6 Ar6 j 6 Axq j! ( ! AÿM@ (" F@AßAß(A~ Avwq6   6  6  (!  G@ (" 6  6  (" Aj ("E\r Aj !@ ! "Aj! ("\r Aj! ("\r A6   A~q6 Ar6 j 6  A! E\r@ ("At"(Àa F@ AÀáj 6 \rA”ßA”ß(A~ wq6  @  (F@  6   6 E\r  6 ("@  6  6 ("E\r  6  6 Ar6 j 6 G\rA˜ß 6 AÿM@ AøqA¸ßj!Aß("A Avt"qE@Aß  r6   ( !  6  6 6 6 A! AÿÿÿM@ A& Avg"kvAq AtrA>s! 6 B7 AtAÀáj!@@A”ß("A t"qE@A”ß  r6  6 6  A AvkA AGt! (!@ "(Axq F\r Av! At!  Aqj"("\r  6 6 6 6 (" 6  6 A6 6 6 W~@ ­B|BøÿÿÿƒAÄÜ("­|"BÿÿÿÿX@ §"?AtM\r \r AÐÝA06A AÄÜ 6  ² #Ak"$  6 @ AÓM@AàÑA Ó A jx(!  A|O@y  AÒn"AÒl"k6A ÓAàÔ Ajx"( j! A ÓkAu!@A!@@@ "A/F\r At(àQ"n" I\r Aj!  lG\r A/I\r AÓ!@ n" I\r  lF\r A\nj"n" I\r  lF\r A j"n" I\r  lF\r Aj"n" I\r  lF\r Aj"n" I\r  lF\r Aj"n" I\r  lF\r Aj"n" I\r  lF\r Aj"n" I\r  lF\r A$j"n" I\r  lF\r A(j"n" I\r  lF\r A*j"n" I\r  lF\r A.j"n" I\r  lF\r A4j"n" I\r  lF\r A:j"n" I\r  lF\r A`-_.-+_\\\\({[,"children":[NANINFcatching a class without an object?)}].!?,;mailto:,"content":h6h5h4h3h2h1http://Too many link reference definition instantiations.Unsupported abi_version.Aborted from text() callback.Aborted from enter_span() callback.Aborted from leave_span() callback.Aborted from enter_block() callback.Aborted from leave_block() callback.malloc() failed.realloc() failed.@:..!#$%&\'*+/=?^_`{|}~-_.:-+-.-_+-_*(null)Suppressing too sparse table (see https://github.com/mity/md4c/issues/345)._=().+-_~%false && "unhandled NodeType in nodeTypeToString"\\"{"type":"[]!xX \n™Jìi¿e£<‡ ¢°üOs{±2‡+F< ÂÎ._VÉšAÜ ~ô H, „*NSt3__220__shared_ptr_emplaceIN8Markdown15MarkdownASTNodeENS_9allocatorIS2_EEEE8™(A€ ä A  f\r0\r`\r\r\rÐ\r\rp\r\r€ Àà\r\r @ \r\r\r\rA UmŒA° !þ\n-Aà Á™ÅA 5¨]¸‹;OAÐ 5F¾\n&’îa=A MM J G D A > ¬¨Að `A€ •EA  ܘóAÀ \r”nAà ¼¨A€ \r’A  ±Ô$AÀ Eu,’Ø–«gYA  A  ”A° —  € @\n €/ _ 0A@Z€µÀ@րØ@ހ@.€2@6€9@G€J@v€xy@}€‚„†‡‰Š‹Ž‘“”–—˜œŸ @¤€¦§©¬®¯±²³µ·¸¼ÄÅÇÈÊË@Û€Þ@î€ñòôö÷ø@€ "@2€:;=>ACDEF@N€Eprv†ˆ@Š€ŒŽ‘@¡€£@«€ÂÏÐÑÕÖØ@î€ðñôõ÷ùúý@ÿ€@€@/€`@€€Š@¾€ÀÁ@Í€Ð@.€1@V€ @Å€ÇÍø@ý€€‚ƒ„…†‡ˆ‰@º€½@¿€@”€› @þ€@€@€(@/€8@?€H@M€Y[]_h@o€¸¹º»¾È@Ë€ØÙÚÛèéêëìøùúû&!*!+!2!`!@o!€ƒ!¶$@Ï$€,@/,€`,b,c,d,g,@k,€m,n,o,p,r,u,~,,€,@â,€ë,í,ò,@¦@l¦€€¦@š¦€"§@.§€2§@n§€y§{§}§~§@†§€‹§§§’§–§@¨§€ª§«§¬§­§®§°§±§²§³§´§@§€Ä§Å§Æ§Ç§É§Ë§Ì§@Ú§€Ü§Ý§â§õ§l«m«p«@¿«€!ÿ@:ÿ€@\'€°@Ó€p@z€|@Š€Œ@’€”•€ @² €P\r@e\r€ @¿€@n@_n€ n@¸n€@ßHßJßMßQßhß@n߀rß@~߀é@!é€az¼àöøþ/37:HKwÿz~sSƒ…TˆVWŒÝY[’`cih™oru¡¥€¨ƒ­ˆ°Š‹´¶’¹½ÆÆÉÉÌÌÜßïóóõ•¿ùž#3e,<šf,B€‰ŒGO¹qswó¬­¯ÌÍαÁÃËÃײ¸ÆÀÙïºÁ¸µøòû{}P_0Oa‹¿ÏÂÎÑ/a†-%-\'---ðõ24>ABBJcK¦ŠÐúýÿ•a¡ÿ \'07@EQSUW`g°±pq¹ruÐÑvwàáz{åxy|}ÉkåN!p!!„!Ð$é$0,_,a,k}}h,l,QqPRs,v,?@,ã,ì,î,ó,A¦m¦¦›¦#§/§3§o§z§|§y§‡§Œ§e‘§“§—§©§f\\aljž‡S«µ§Ã§”§‚Žȧʧdͧۧ›w|ö§K«L« ïAÿZÿ(OØû—¡£±³¹»¼À ò p\r…\rÀß`nn»nÓnAßIßKßNßRßißoßsßß"éCéAÐ2 Óß0Ið‡–—˜™šžP€@‡€ˆ@€@—€˜@Ÿ€ @§€¨@¯€²³´¶¼ÂÃÄÆÌÖäæòóôöüûûûûûûûûûû•ßA°4 ¥ssi¼nj e‚h1tw\ny\na¾ssʹ¹¹ ¹\'¹ ¹\'¹`¹g¹`¹g¹p¹±¹¬¹±B±¹t¹·¹®¹·B·¹¹BÁÅB|¹É¹Î¹ÉBɹfffiflststtvtetk~vtmssAà7 À°RTV·ÇÒÓ×âãç÷ûû¹ÅÅÅÅB±B¹·B¹¹¹¹BÅÅÅBÉB¹ffifflА.P05à  A´: ½!@/€:@@€[@`€{@~€¡@©€«@¬€®@±€´¶@¸€»¿×÷Â@Å€Ò@߀å@ë€íï@ÿ€u~„@…€‡ö‚Z@_€‰@Š€@€¾ÀÃÆó@ô€@€@€j@m€ÔÞéý@þ€@\r€ö@ù€þ@ÿ€0@>€^ˆd @e €p ò @ó €ú @û €ý v\nð\n@ñ\n€p ó @ú €w  „ O\ry\rô\r?OZ@[€@€@€468:@=€…¾@Å€Ç@Ì€Î@Ú€J@O€ž@Ÿ€û`@h€@™€m@n€›@œ€ë@í€5@6€Ô@Ö€Ø@Û€@\n€@D@E€Þ@ÿ€@€ @¦€¨@­€N@O€Z@j€t@€ü@ÿ€;@?€~@€À@Ç€Ó½¿@Á€Í@Ï€Ý@߀í@ï€ý@þ€ @\' €0 @^ €z @~ €Š @Ž €  @Ä €!@!€!@!€!@ !€!!@!€!@#!€%!\'!)!.!:!@;!€@!@D!€J!@M!€O!Š!@‹!€!@)$€@$@J$€œ$@é$€%@u\'€”\'@s+€v+@ÿ+€å,@ê,€ù,@ü,€þ,@ÿ,€p-.@..€0.@].€`.@c.€€.@™.€›.@ó.€/@Õ/€ð/@ÿ/€0@0€0@ 0€0060@70€=0@?0€›0@œ0€ 0û01@‘1€–1@Ÿ1€À1@å1€ï12@2€*2@G2€P2`2@2€Š2@°2€À2@ÿ3€ÀM@ÿM€¤@Ƥ€þ¤@ÿ¤€\r¦@¦€s¦~¦ò¦@÷¦€§@§€ §@!§€‰§@Ч€(¨@+¨€6¨@9¨€t¨@w¨€Î¨@Ϩ€ø¨@ú¨€ü¨.©@/©€_©Á©@Í©€Þ©@ß©€\\ª@_ª€wª@yª€Þª@ߪ€ðª@ñª€[«j«@k«€ë«)û²û@Òû€>ý@Oý€ý@‘ý€Èý@Ïý€üý@ÿý€þ@þ€0þ@Rþ€Tþ@fþ€hþ@kþ€ÿ@ÿ€ÿ@ ÿ€;ÿ@@ÿ€[ÿ@eÿ€àÿ@æÿ€èÿ@îÿ€üÿ@ýÿ€@€7@?€y@‰€Œ@Ž€@œ€ Ð@ü€ŸÐoWw@x€ ? P\n@X\n€\nÈ\nð\n@ö\n€9 @? €™ @œ €n\rŽ\r@\r€­É@Ê€Ð@Ø€U@Y€†@‰€G@M€»@¼€¾@Á€@@C€t@u€Å@È€ÍÛÝ@߀8@=€©Ô@Õ€×@Ø€K@O€Z@[€]ÆÁ@×€A@C€`@l€¹<@?€;D@F€â?@F€š@œ€ž@¢€@ €áA@E€p@q€÷@ø€C@O€Õ@ñ€ÿp$@t$€ñ/@ò/€nj@oj€õj7k@?k€Dk@Ek€mm@om€—n@šn€âoœ¼Ÿ¼Ì@ïÌ€úÌ@üÌ€Í@³Î€ºÎ@Ð΀ÒÎ@Ô΀ÝÎ@ý΀PÏ@ÃÏ€Ð@õЀÑ@&Ñ€)Ñ@dÑ€jÑ@lÑ€ƒÑ@„Ñ€ŒÑ@©Ñ€®Ñ@AÒ€EÒSÒ@ZÒ€]Ò@^Ò€`Ò@Ò€Ó@VÓ€ÁÖÛÖûÖ×5×O×o׉ש×Ãר@ÿÙ€7Ú@:Ú€mÚ@tÚ€vÚ@ƒÚ€…Ú@‹Ú€Û@Û€Oáÿâÿå^é@_逬ì°ì.íðî@ñî€ð@+ð€0ð@“ð€ ð@®ð€±ð@¿ð€Áð@Ïð€Ñð@õð€\rñ@®ñ€æñ@ò€ò@;ò€@ò@Hò€Pò@Qò€`ò@eò€ó@Ùö€Üö@ìö€ðö@üö€÷@Û÷€à÷@ë÷€ð÷@ ø€ø@Gø€Pø@Yø€`ø@‡ø€ø@­ø€°ø@»ø€Àø@Áø€Ðø@Øø€ù@Wú€`ú@mú€pú@|ú€€ú@Æú€ÈúÌú@Ýú€ßú@ëú€ïú@úú€û@’û€”û@ïû€úû A€Î A  \n\n\n   AÑÎ ! \r\r  A‹Ï  A—Ï  AÅÏ AÑÏ  AÿÏ A‹Ð  AÂÐ  AóÐ AÿÐ  A­Ñ A¹Ñ € 0123456789ABCDEF \r%)+/5;=CGIOSYaegkmqƒ‰‹•—£§­³µ¿ÁÅÇÓ \r%)+/5;=CGIOSYaegkmqyƒ‰‹•—£§©­³µ»¿ÁÅÇÑ ,h*NSt3__214__shared_countE¤,œ*`*NSt3__219__shared_weak_countEAÄÕ ø\ndè\' †@B€–˜áõʚ;00010203040506070809101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899H,Ä+(.N10__cxxabiv116__shim_type_infoEH,ô+¸+N10__cxxabiv117__class_type_infoEè+ !"#$%&h,\'!"#()*H,t,è+N10__cxxabiv120__si_class_type_infoEÄ,+!"#,-.H,Ð,è+N10__cxxabiv121__vmi_class_type_infoEL-/0h-124-34 ,<-St9exceptionH,X-4-St9bad_allocH,t-L-St20bad_array_new_length¤-56H,°-4-St11logic_errorÔ-76H,à-¤-St12length_error.86H,.¤-St12out_of_range ,0.St9type_infoAÁÜ  1€'); + } + function getBinarySync(file) { + return file; +@@ -117,7 +118,7 @@ var createMd4cModule = (() => { + return imports; + } + async function createWasm() { +- function receiveInstance(instance) { ++ function receiveInstance(instance, module) { + wasmExports = instance.exports; + assignWasmExports(wasmExports); + updateMemoryViews(); +@@ -127,10 +128,11 @@ var createMd4cModule = (() => { + return receiveInstance(result["instance"]); + } + var info = getWasmImports(); +- var instantiateWasm = Module["instantiateWasm"]; +- if (instantiateWasm) { +- return new Promise(resolve => { +- instantiateWasm(info, inst => resolve(receiveInstance(inst))); ++ if (Module["instantiateWasm"]) { ++ return new Promise((resolve, reject) => { ++ Module["instantiateWasm"](info, (inst, mod) => { ++ resolve(receiveInstance(inst, mod)); ++ }); + }); + } + wasmBinaryFile ??= findWasmBinary(); +@@ -145,23 +147,15 @@ var createMd4cModule = (() => { + this.status = status; + } + } +- var HEAP16; +- var HEAP32; +- var HEAP64; +- var HEAP8; +- var HEAPF32; +- var HEAPF64; +- var HEAPU16; +- var HEAPU32; +- var HEAPU64; +- var HEAPU8; + var callRuntimeCallbacks = callbacks => { + while (callbacks.length > 0) { + callbacks.shift()(Module); + } + }; + var onPostRuns = []; ++ var addOnPostRun = cb => onPostRuns.push(cb); + var onPreRuns = []; ++ var addOnPreRun = cb => onPreRuns.push(cb); + var noExitRuntime = true; + var stackRestore = val => __emscripten_stack_restore(val); + var stackSave = () => _emscripten_stack_get_current(); +@@ -249,12 +243,14 @@ var createMd4cModule = (() => { + return HEAPU32[this.ptr + 16 >> 2]; + } + } ++ var exceptionLast = 0; + var uncaughtExceptionCount = 0; + var ___cxa_throw = (ptr, type, destructor) => { + var info = new ExceptionInfo(ptr); + info.init(type, destructor); ++ exceptionLast = ptr; + uncaughtExceptionCount++; +- abort(); ++ throw exceptionLast; + }; + var __abort_js = () => abort(""); + var getHeapMax = () => 536870912; +@@ -400,17 +396,24 @@ var createMd4cModule = (() => { + } + return (...args) => ccall(ident, returnType, argTypes, args, opts); + }; ++ for (var base64ReverseLookup = new Uint8Array(123), i = 25; i >= 0; --i) { ++ base64ReverseLookup[48 + i] = 52 + i; ++ base64ReverseLookup[65 + i] = i; ++ base64ReverseLookup[97 + i] = 26 + i; ++ } ++ base64ReverseLookup[43] = 62; ++ base64ReverseLookup[47] = 63; + { + if (Module["noExitRuntime"]) noExitRuntime = Module["noExitRuntime"]; + if (Module["print"]) out = Module["print"]; + if (Module["printErr"]) err = Module["printErr"]; +- if (Module["arguments"]) programArgs = Module["arguments"]; ++ if (Module["wasmBinary"]) wasmBinary = Module["wasmBinary"]; ++ if (Module["arguments"]) arguments_ = Module["arguments"]; + if (Module["thisProgram"]) thisProgram = Module["thisProgram"]; +- var preInit = Module["preInit"]; +- if (preInit) { +- if (typeof preInit == "function") Module["preInit"] = preInit = [preInit]; +- while (preInit.length > 0) { +- preInit.shift()(); ++ if (Module["preInit"]) { ++ if (typeof Module["preInit"] == "function") Module["preInit"] = [Module["preInit"]]; ++ while (Module["preInit"].length > 0) { ++ Module["preInit"].shift()(); + } + } + } +@@ -432,24 +435,39 @@ var createMd4cModule = (() => { + _abort_js: __abort_js, + emscripten_resize_heap: _emscripten_resize_heap + }; +- async function run() { ++ function run() { + preRun(); +- var setStatus = Module["setStatus"]; +- if (setStatus) { +- setStatus("Running..."); +- await new Promise(resolve => setTimeout(resolve, 1)); +- setTimeout(setStatus, 1, ""); +- } +- if (ABORT) return; +- initRuntime(); +- Module["onRuntimeInitialized"]?.(); +- postRun(); ++ function doRun() { ++ Module["calledRun"] = true; ++ if (ABORT) return; ++ initRuntime(); ++ readyPromiseResolve?.(Module); ++ Module["onRuntimeInitialized"]?.(); ++ postRun(); ++ } ++ if (Module["setStatus"]) { ++ Module["setStatus"]("Running..."); ++ setTimeout(() => { ++ setTimeout(() => Module["setStatus"](""), 1); ++ doRun(); ++ }, 1); ++ } else { ++ doRun(); ++ } + } + var wasmExports; + wasmExports = await createWasm(); +- await run(); ++ run(); ++ if (runtimeInitialized) { ++ moduleRtn = Module; ++ } else { ++ moduleRtn = new Promise((resolve, reject) => { ++ readyPromiseResolve = resolve; ++ readyPromiseReject = reject; ++ }); ++ } + ; +- return Module; ++ return moduleRtn; + }; + })(); + if (typeof exports === "object" && typeof module === "object") { +diff --git a/node_modules/react-native-enriched-markdown/lib/typescript/src/index.d.ts b/node_modules/react-native-enriched-markdown/lib/typescript/src/index.d.ts +index 60563bf..299e908 100644 +--- a/node_modules/react-native-enriched-markdown/lib/typescript/src/index.d.ts ++++ b/node_modules/react-native-enriched-markdown/lib/typescript/src/index.d.ts +@@ -1,3 +1,4 @@ ++export type { MarkdownRenderers, CodeBlockRendererProps, } from './types/MarkdownRenderers'; + export type { LinkVariantStyle, LinkPillStyle } from './types/MarkdownStyle'; + export type { LinkPillContent } from './types/MarkdownTextProps'; + export type { LinkContextMenuItem, LinkRecognition, } from './types/MarkdownTextProps'; +diff --git a/node_modules/react-native-enriched-markdown/lib/typescript/src/index.web.d.ts b/node_modules/react-native-enriched-markdown/lib/typescript/src/index.web.d.ts +index 17def30..eae0a3a 100644 +--- a/node_modules/react-native-enriched-markdown/lib/typescript/src/index.web.d.ts ++++ b/node_modules/react-native-enriched-markdown/lib/typescript/src/index.web.d.ts +@@ -1,3 +1,4 @@ ++export type { MarkdownRenderers, CodeBlockRendererProps, } from './types/MarkdownRenderers'; + export { EnrichedMarkdownText, default } from './web/EnrichedMarkdownText'; + export type { EnrichedMarkdownTextProps } from './types/MarkdownTextProps.web'; + export type { MarkdownStyle, Md4cFlags } from './types/MarkdownStyle'; +diff --git a/node_modules/react-native-enriched-markdown/lib/typescript/src/native/EnrichedMarkdownText.d.ts b/node_modules/react-native-enriched-markdown/lib/typescript/src/native/EnrichedMarkdownText.d.ts +index 3835048..1fcf936 100644 +--- a/node_modules/react-native-enriched-markdown/lib/typescript/src/native/EnrichedMarkdownText.d.ts ++++ b/node_modules/react-native-enriched-markdown/lib/typescript/src/native/EnrichedMarkdownText.d.ts +@@ -4,6 +4,6 @@ import type { LinkPressEvent, LinkLongPressEvent, ImagePressEvent, TaskListItemP + export type { MarkdownStyle, Md4cFlags }; + export type { EnrichedMarkdownTextProps, StreamingConfig, ContextMenuItem, SelectionMenuConfig, SelectionMenuPluralLabels, }; + export type { LinkPressEvent, LinkLongPressEvent, ImagePressEvent, TaskListItemPressEvent, CopyPressEvent, LatexErrorEvent, CodeBlockPressEvent, }; +-export declare const EnrichedMarkdownText: ({ markdown, markdownStyle, containerStyle, onLinkPress, onLinkLongPress, onImagePress, onTaskListItemPress, enableTaskListItemToggle, onCopyPress, onLatexError, onCodeBlockPress, enableBlockContextMenu, enableLinkPreview, selectable, md4cFlags, allowFontScaling, maxFontSizeMultiplier, allowTrailingMargin, flavor, streamingAnimation, streamingConfig, spoilerOverlay, contextMenuItems, linkContextMenuItems, imageRequestHeaders, linkPillContent, linkRecognition, selectionMenuConfig, accessibilityLabels, selectionColor, selectionHandleColor, textBreakStrategy, lineBreakStrategyIOS, writingDirection, numberOfLines, ellipsizeMode, ...rest }: EnrichedMarkdownTextProps) => import("react/jsx-runtime").JSX.Element; ++export declare const EnrichedMarkdownText: ({ markdown, markdownStyle, containerStyle, onLinkPress, onLinkLongPress, onImagePress, onTaskListItemPress, enableTaskListItemToggle, onCopyPress, onLatexError, onCodeBlockPress, enableBlockContextMenu, enableLinkPreview, selectable, md4cFlags, allowFontScaling, maxFontSizeMultiplier, allowTrailingMargin, flavor, streamingAnimation, streamingConfig, spoilerOverlay, contextMenuItems, linkContextMenuItems, imageRequestHeaders, linkPillContent, linkRecognition, selectionMenuConfig, accessibilityLabels, selectionColor, selectionHandleColor, textBreakStrategy, lineBreakStrategyIOS, writingDirection, numberOfLines, ellipsizeMode, renderers: _renderers, ...rest }: EnrichedMarkdownTextProps) => import("react/jsx-runtime").JSX.Element; + export default EnrichedMarkdownText; + //# sourceMappingURL=EnrichedMarkdownText.d.ts.map +\ No newline at end of file +diff --git a/node_modules/react-native-enriched-markdown/lib/typescript/src/types/MarkdownTextProps.d.ts b/node_modules/react-native-enriched-markdown/lib/typescript/src/types/MarkdownTextProps.d.ts +index afc9af3..f2bae28 100644 +--- a/node_modules/react-native-enriched-markdown/lib/typescript/src/types/MarkdownTextProps.d.ts ++++ b/node_modules/react-native-enriched-markdown/lib/typescript/src/types/MarkdownTextProps.d.ts +@@ -1,3 +1,4 @@ ++import type { MarkdownRenderers } from './MarkdownRenderers'; + import type { ColorValue, ViewProps, ViewStyle, TextStyle } from 'react-native'; + import type { MarkdownStyle, Md4cFlags } from './MarkdownStyle'; + import type { AccessibilityLabels } from './AccessibilityLabels'; +@@ -146,6 +147,8 @@ export interface EnrichedMarkdownTextProps extends Omit { + * @platform ios, android, web + */ + markdown: string; ++ /** Custom block components. Ignored on native. @platform web */ ++ renderers?: MarkdownRenderers; + /** + * Style configuration for markdown elements. + * @platform ios, android, web +diff --git a/node_modules/react-native-enriched-markdown/lib/typescript/src/types/MarkdownTextProps.web.d.ts b/node_modules/react-native-enriched-markdown/lib/typescript/src/types/MarkdownTextProps.web.d.ts +index 19b0d0d..7537e44 100644 +--- a/node_modules/react-native-enriched-markdown/lib/typescript/src/types/MarkdownTextProps.web.d.ts ++++ b/node_modules/react-native-enriched-markdown/lib/typescript/src/types/MarkdownTextProps.web.d.ts +@@ -1,3 +1,4 @@ ++import type { MarkdownRenderers } from './MarkdownRenderers'; + import type { LinkRecognition } from './MarkdownTextProps'; + import type { CSSProperties, HTMLAttributes } from 'react'; + import type { MarkdownStyle, Md4cFlags } from './MarkdownStyle'; +@@ -13,6 +14,8 @@ export interface EnrichedMarkdownTextProps extends Omit import("react/jsx-runtime").JSX.Element | null; ++export declare const EnrichedMarkdownText: ({ markdown, markdownStyle, md4cFlags, linkRecognition, renderers, onLinkPress, onLinkLongPress, onImagePress, onTaskListItemPress, onCodeBlockPress, enableTaskListItemToggle, allowTrailingMargin, containerStyle, selectable, dir, selectionColor, testID, ...rest }: EnrichedMarkdownTextProps) => import("react/jsx-runtime").JSX.Element | null; + export default EnrichedMarkdownText; + //# sourceMappingURL=EnrichedMarkdownText.d.ts.map +\ No newline at end of file +diff --git a/node_modules/react-native-enriched-markdown/lib/typescript/src/web/types.d.ts b/node_modules/react-native-enriched-markdown/lib/typescript/src/web/types.d.ts +index c83a25e..c3e28f5 100644 +--- a/node_modules/react-native-enriched-markdown/lib/typescript/src/web/types.d.ts ++++ b/node_modules/react-native-enriched-markdown/lib/typescript/src/web/types.d.ts +@@ -1,3 +1,4 @@ ++import type { MarkdownRenderers } from '../types/MarkdownRenderers'; + import type { ComponentType, ReactNode } from 'react'; + import type { MarkdownStyleInternal } from '../types/MarkdownStyleInternal'; + import type { Styles } from './styles'; +@@ -5,6 +6,7 @@ import type { LinkPressEvent, LinkLongPressEvent, ImagePressEvent, TaskListItemP + import type { KaTeXInstance } from './katex'; + export type NodeType = 'Document' | 'Paragraph' | 'Text' | 'Link' | 'Heading' | 'LineBreak' | 'SoftBreak' | 'Strong' | 'Emphasis' | 'Strikethrough' | 'Underline' | 'Superscript' | 'Subscript' | 'Highlight' | 'Code' | 'Image' | 'Blockquote' | 'UnorderedList' | 'OrderedList' | 'ListItem' | 'CodeBlock' | 'ThematicBreak' | 'Table' | 'TableHead' | 'TableBody' | 'TableRow' | 'TableHeaderCell' | 'TableCell' | 'LatexMathInline' | 'LatexMathDisplay' | 'BlankLine' | 'Admonition' | 'Video'; + export interface NodeAttributes { ++ start?: string; + level?: string; + url?: string; + /** Stamped by recognizeTextLinks() — not present in the raw WASM output. */ +@@ -44,6 +46,7 @@ export interface RendererCallbacks { + onCodeBlockPress?: (event: CodeBlockPressEvent) => void; + } + export interface RenderCapabilities { ++ renderers?: MarkdownRenderers; + katex: KaTeXInstance | null; + /** When false, task list checkboxes render read-only and ignore clicks. */ + enableTaskListItemToggle: boolean; +diff --git a/node_modules/react-native-enriched-markdown/src/index.tsx b/node_modules/react-native-enriched-markdown/src/index.tsx +index c3f90cd..c57baca 100644 +--- a/node_modules/react-native-enriched-markdown/src/index.tsx ++++ b/node_modules/react-native-enriched-markdown/src/index.tsx +@@ -1,3 +1,7 @@ ++export type { ++ MarkdownRenderers, ++ CodeBlockRendererProps, ++} from './types/MarkdownRenderers'; + export type { LinkVariantStyle, LinkPillStyle } from './types/MarkdownStyle'; + export type { LinkPillContent } from './types/MarkdownTextProps'; + export type { +diff --git a/node_modules/react-native-enriched-markdown/src/index.web.tsx b/node_modules/react-native-enriched-markdown/src/index.web.tsx +index 8628b37..de167b7 100644 +--- a/node_modules/react-native-enriched-markdown/src/index.web.tsx ++++ b/node_modules/react-native-enriched-markdown/src/index.web.tsx +@@ -1,3 +1,7 @@ ++export type { ++ MarkdownRenderers, ++ CodeBlockRendererProps, ++} from './types/MarkdownRenderers'; + export { EnrichedMarkdownText, default } from './web/EnrichedMarkdownText'; + export type { EnrichedMarkdownTextProps } from './types/MarkdownTextProps.web'; + export type { MarkdownStyle, Md4cFlags } from './types/MarkdownStyle'; +diff --git a/node_modules/react-native-enriched-markdown/src/native/EnrichedMarkdownText.tsx b/node_modules/react-native-enriched-markdown/src/native/EnrichedMarkdownText.tsx +index ca7157e..1ddd2e1 100644 +--- a/node_modules/react-native-enriched-markdown/src/native/EnrichedMarkdownText.tsx ++++ b/node_modules/react-native-enriched-markdown/src/native/EnrichedMarkdownText.tsx +@@ -170,6 +170,7 @@ export const EnrichedMarkdownText = ({ + writingDirection = 'first-strong', + numberOfLines, + ellipsizeMode, ++ renderers: _renderers, + ...rest + }: EnrichedMarkdownTextProps) => { + // An equal config keeps the previous object, so an inline literal does not +diff --git a/node_modules/react-native-enriched-markdown/src/types/MarkdownTextProps.web.ts b/node_modules/react-native-enriched-markdown/src/types/MarkdownTextProps.web.ts +index cac90b4..c308537 100644 +--- a/node_modules/react-native-enriched-markdown/src/types/MarkdownTextProps.web.ts ++++ b/node_modules/react-native-enriched-markdown/src/types/MarkdownTextProps.web.ts +@@ -1,3 +1,4 @@ ++import type { MarkdownRenderers } from './MarkdownRenderers'; + import type { LinkRecognition } from './MarkdownTextProps'; + import type { CSSProperties, HTMLAttributes } from 'react'; + import type { MarkdownStyle, Md4cFlags } from './MarkdownStyle'; +@@ -23,6 +24,8 @@ export interface EnrichedMarkdownTextProps extends Omit< + * @platform ios, android, web + */ + markdown: string; ++ /** Custom block components. Ignored on native. @platform web */ ++ renderers?: MarkdownRenderers; + /** + * Style configuration for markdown elements. + * @platform ios, android, web +diff --git a/node_modules/react-native-enriched-markdown/src/web/EnrichedMarkdownText.tsx b/node_modules/react-native-enriched-markdown/src/web/EnrichedMarkdownText.tsx +index 08dc7df..0bab2bb 100644 +--- a/node_modules/react-native-enriched-markdown/src/web/EnrichedMarkdownText.tsx ++++ b/node_modules/react-native-enriched-markdown/src/web/EnrichedMarkdownText.tsx +@@ -27,6 +27,7 @@ export const EnrichedMarkdownText = ({ + markdownStyle = {}, + md4cFlags = {}, + linkRecognition, ++ renderers, + onLinkPress, + onLinkLongPress, + onImagePress, +@@ -46,7 +47,10 @@ export const EnrichedMarkdownText = ({ + [markdownStyle] + ); + +- const [ast, setAst] = useState(null); ++ const [parsed, setParsed] = useState<{ ++ ast: ASTNode; ++ markdown: string; ++ } | null>(null); + const [katex, setKatex] = useState(null); + const [parseError, setParseError] = useState(false); + +@@ -92,13 +96,14 @@ export const EnrichedMarkdownText = ({ + + setParseError(false); + setKatex(katexInstance); +- setAst( +- recognizeTextLinks( ++ setParsed({ ++ markdown, ++ ast: recognizeTextLinks( + result, + linkRecognitionRef.current?.text, + linkRecognitionRef.current?.inlineCode +- ) +- ); ++ ), ++ }); + } + }) + .catch((error) => { +@@ -108,7 +113,7 @@ export const EnrichedMarkdownText = ({ + } + + setParseError(true); +- setAst(null); ++ setParsed(null); + setKatex(null); + } + }); +@@ -148,8 +153,8 @@ export const EnrichedMarkdownText = ({ + ); + + const capabilities = useMemo( +- () => ({ katex, enableTaskListItemToggle }), +- [katex, enableTaskListItemToggle] ++ () => ({ katex, enableTaskListItemToggle, renderers }), ++ [katex, enableTaskListItemToggle, renderers] + ); + + const lastChildStyle = useMemo( +@@ -196,13 +201,12 @@ export const EnrichedMarkdownText = ({ + ); + } + +- if (!ast) return null; +- +- const children = ast.children ?? []; ++ const children = parsed?.ast.children ?? []; + const lastIdx = children.length - 1; + + return ( +
{renderChildren(node)}; ++ return ( ++ ++ {renderChildren(node)} ++ ++ ); + } + + function UnorderedListRenderer(props: RendererProps) { +diff --git a/node_modules/react-native-enriched-markdown/src/web/types.ts b/node_modules/react-native-enriched-markdown/src/web/types.ts +index 781adb1..4d4fa0c 100644 +--- a/node_modules/react-native-enriched-markdown/src/web/types.ts ++++ b/node_modules/react-native-enriched-markdown/src/web/types.ts +@@ -1,3 +1,4 @@ ++import type { MarkdownRenderers } from '../types/MarkdownRenderers'; + import type { ComponentType, ReactNode } from 'react'; + import type { MarkdownStyleInternal } from '../types/MarkdownStyleInternal'; + import type { Styles } from './styles'; +@@ -46,6 +47,7 @@ export type NodeType = + | 'Video'; + + export interface NodeAttributes { ++ start?: string; + level?: string; + url?: string; + /** Stamped by recognizeTextLinks() — not present in the raw WASM output. */ +@@ -88,6 +90,7 @@ export interface RendererCallbacks { + } + + export interface RenderCapabilities { ++ renderers?: MarkdownRenderers; + katex: KaTeXInstance | null; + /** When false, task list checkboxes render read-only and ignore clicks. */ + enableTaskListItemToggle: boolean; +diff --git a/node_modules/react-native-enriched-markdown/src/web/wasm/md4c.js b/node_modules/react-native-enriched-markdown/src/web/wasm/md4c.js +index 26a61d7..4f82a68 100644 +--- a/node_modules/react-native-enriched-markdown/src/web/wasm/md4c.js ++++ b/node_modules/react-native-enriched-markdown/src/web/wasm/md4c.js +@@ -1,2 +1,2 @@ +-var createMd4cModule=(()=>{var _scriptName=globalThis.document?.currentScript?.src;return async function(moduleArg={}){var Module=moduleArg;var ENVIRONMENT_IS_WEB=true;var ENVIRONMENT_IS_WORKER=false;var programArgs=[];var thisProgram="./this.program";var scriptDirectory="";var readAsync;if(ENVIRONMENT_IS_WEB||ENVIRONMENT_IS_WORKER){try{scriptDirectory=new URL(".",_scriptName).href}catch{}{readAsync=async url=>{var response=await fetch(url,{credentials:"same-origin"});if(response.ok){return response.arrayBuffer()}throw new Error(response.status+" : "+response.url)}}}else{}var out=console.log.bind(console);var err=console.error.bind(console);var wasmBinary;var ABORT=false;class EmscriptenEH{}class EmscriptenSjLj extends EmscriptenEH{}function binaryDecode(bin){for(var i=0,l=bin.length,o=new Uint8Array(l),c;i>8&c}return o}var runtimeInitialized=false;function getMemoryBuffer(){return wasmMemory.buffer}function updateMemoryViews(){if(HEAP8?.buffer?.resizable)return;var b=getMemoryBuffer();HEAP8=new Int8Array(b);HEAP16=new Int16Array(b);HEAPU8=new Uint8Array(b);HEAPU16=new Uint16Array(b);HEAP32=new Int32Array(b);HEAPU32=new Uint32Array(b);HEAPF32=new Float32Array(b);HEAPF64=new Float64Array(b);HEAP64=new BigInt64Array(b);HEAPU64=new BigUint64Array(b)}function preRun(){var preRun=Module["preRun"];if(preRun){if(typeof preRun=="function")preRun=[preRun];onPreRuns.push(...preRun)}callRuntimeCallbacks(onPreRuns)}function initRuntime(){runtimeInitialized=true;wasmExports["__wasm_call_ctors"]()}function postRun(){var postRun=Module["postRun"];if(postRun){if(typeof postRun=="function")postRun=[postRun];onPostRuns.push(...postRun)}callRuntimeCallbacks(onPostRuns)}function abort(what){Module["onAbort"]?.(what);what=`Aborted(${what})`;err(what);ABORT=true;what+=". Build with -sASSERTIONS for more info.";var e=new WebAssembly.RuntimeError(what);throw e}var wasmBinaryFile;function findWasmBinary(){return binaryDecode('asm¬`````````````` ````||`~`|``Tenv __cxa_throwenv\r__assert_failenv _abort_js\nenvemscripten_resize_heap˜–\n\r\n\n\n   \r     \r \n  \n  p44€€@ A à€ ¢memory__wasm_call_ctors\rparseMarkdown__indirect_function_table_emscripten_stack_restore—_emscripten_stack_alloc˜emscripten_stack_get_current™ JA 3” \rq !"#$%&\'()s*B=DZfghq*‚‚„’†*‘އ*‹‰*“•–• (\nªº–æAàÚB7AèÚA6A÷ÚA:AìÚAØ\r(6AðÚAÜ\r-:AñÚA:AƒÛA:AøÚA±\r/;AúÚA³\r-:AûÚA:AÛA:A„ÛAŠ(6AˆÛAŽ-:A‰ÛA:A›ÛA :AÛA™ )7A˜ÛA¡ -:A™ÛA:A ÛB칻֮سå7A«ÛA:A¨ÛA:A·ÛA:A¬ÛAá\r(6A°ÛAå\r/;A²ÛA:A¸ÛAt"6A¼ÛB‹€€€€‚€€€7 AÆ(6 A¿)7 A: AÏÛA:AÄÛA¦ (6AÈÛAª -:AÉÛA:AÛÛA:AÐÛAÈ (6AÔÛAÌ -:AÕÛA:AÜÛAt"6AàÛBŽ€€€€‚€€€7 AÖ)7 AÐ)7 A: AëÚ,AH@AèÚ(AàÚ(k õ”!~#A0k"$@ E@@A€\\"AèÚ("AÿÿÿÿqAkA\nAëÚ,"AH""M@AàÚ(AàÚ !@  @ A€ ü\n AëÚ- Av ÀAH@AäÚ 6  AëÚ Aÿq:  jA:  AàÚ   kAäÚ( "A  A€x AàÚAàÚ(AëÚ,AN  A:,  AG:/  AG:.  AG:-  AG:+  AG:*  AG:)  AG:(  AG:\'A$t"A6 B7 B7 B7 B7 A j" 6 ! \\"A÷ÿÿÿO\r@@ A O@ Ar"Ajt!  Aÿÿÿÿk6  6  6   : A j! E\r E\r  ü\n  jA: Aj! A\'j!#AÐk"$@ A j"( , " AH"E@A<t"B7 Aì6 B7 B7 B7 B7$ B74 B€€€ü7,  6  A j6  (! AéOA4 Aèn" A4OA jA !A<t" B7 Aì6 B7 B7 B7 B7$ B74 B€€€ü7,  A j6 (!  6@ E\r (" Ak6 \r (( r Aj! ("\n! ( " \nG@@@ Ak(" E\r ("Ak6 \r (( r \n Ak" G\r (!  \n6 @A€  A€O" ( kAuM\r At"t! \n k"\n@ \nü\n   j6  \n j6  6 E\r k !@@@@ (" ("\nI@ (6 ("6 @  (Aj6 Aj!  ("k" Au"Aj" A€€€€O\rAÿÿÿÿ \n k"\nAu"  K \nAøÿÿÿO" A€€€€O\r At"\nt" j" (6 ("6  \nj!\n   (Aj6 ( ( ("k" Au  Atk! @   ü\n  \n6  Aj" 6  6 E\r k  6    Aj! @ ,AH@ (A: A6  A: A: A€z (" (  , AH6 -! -! -! -! -! -! -!\r -! -! B7( A6$ A6 A6 A6 A6 A6  A tAìžAà– r \rAtr Atr Atr Atr Atr Atr Atr6 (  , "AH"! ( ! !\n#A€k" $@ A j"(@A! ("E\rA›    A4jAAüü 6 6 )7 )7 )7 )7$ ( 6, \n60 AA ("Aq6œ A„j"AA€ü A;ã A:® A;à A:ß A: A;¿ A:ª A:¥ A:„ A€„q@ A:‚ A€€q@ A:â A€ q@ A:¨ A€€€q@ A:Á A€€€q@ A:¯ Aq@ A:Ä Aq@ A:¾ A€q@ A:² A€Âq@ A:€ Aq@A!@ A G A GqE@ jA: Ar"AýqA F@  jA: Aj"A€G\r A E\rA  jAk-"A\rF\r A\nF ! A6` A 6D 64 A€€  A€€OAt6X A„jAÿAÔü B7ä B7Ü@AA \n ("E@ AÌj!" A°jAr!# Aüj!$ AèjAr!% A°j!\rAè! A!@@ \r!@@@@ (" K@ B7ø B7ð B7èA! A°jAA \r A°jF"j \r \rF"!\r (¨!& (!@@ j-"A G A Gq\r A|qAj Aj A F! Aj" G\r ! A°jA,A j Aj " 6 A°jA$A j A j " 6A! A°jA A j Aj " A6@ (”"AL\r (œ! (! !\n@@  Alj"-A>F@ \n O\r O\r j-A>G\r Aj"!@ Aj" O\r@ j-"\nA G \nA Gq\r A|qAj Aj \nA F! Aj" G\r !   kAkA  G"\n6  6 !  \n ( "I\r  \n k"\n6 ! Aj" G\r ! ! 6ä@@ O\r j-A\nk  L\r (!@  Alj-A>F\r Aj" G\r ! " #  Aj !A!\nA!A! !@A k!A k! („"\' (üjAk!( (¬E! (!! ( !) (œ! (¤!@@ ("AG@ !   6@  ("M@ !  !@ O\r  ( j-!@  j- AÿqG\r Aj" G\r ! @  k )I\r@  O\r@@  j-A k Aj" G\r ! 6ä \rA6  6ä  F@   (k"A M6 \rA6  (! @@@@ AG\r AL\r  L\rA! A6¤  O\r  j"-A\nk @ Aj  Aäj+AL\r A6¤ AþÿÿÿqAG\r \rA6 A6 ! \rA6  @ AG\r  (”G\r \rA6  (" (œk"A O6  \rA6@@ AL"\r G\r ( AljAk-"A>G6¨  A6¨ @ ! ( AljAk-! AÿqA>F@ !  G@ !  (€\r („"A H\r (ü jAk-AG\r A6¬  @ Aq@ !    AL\r  (” G\r  ! Alj"Ak-A>F\r  G\r  (€\r  \'A H\r  (-AG\r  6A AF\r   A$k(k"A M6 Ak ! A6¬ A6¨@ (" O\r@ AG\r@ -"A-k  G\r@@ Aj" O\r@  j-F@  Aj"K\r  @@ j-A k Aj" G\r ! 6ä \rA6 AA A=F6   \nI\r -"E\rA­ ÀAPE\rA! Aj"\n O\r@@@  \nj-" AÿqF"\r A F\r A G\r j! \nAj"\n G\r !\n AI\r  AI\r A\nk @@@  L\r G\r@ Aj   Aàj Aèj,E\r -è"A>F\r ! Alj"-G\r (ð" ( K\r (ô" k j" !@ (à" O\r@ j-"A G A Gq\r A|qAj Aj A F! Aj" G\r 6ä   k6  6 !  6ä   k" 6  6 O\r@ j-A\nk M@ j"6ô A6  Aj"6ô  Ak6  ! @  O@ (AF@ !  \rA6   k6 A6 ! \n Aj   Aäj Aèj,! (!@@@@@ @ (ä!@ AG\r  G\r@@ O\r j-"A\rF\r -è! A\nG\r AÿqA>F\r  A>! -èA>G\r Aÿq"A.G A)Gq\r (ìAG\r (ô" (ðk j"!@ O\r@ j-"A G A Gq\r A|qAj Aj A F! Aj" G\r ! 6ä   k"6  6  ,è6@@@ O\r j-A\nk Aj6ô   M@  j6ô A6  Aj6ô  Ak6 E@ Aj j-\r (”! @ (˜" J@ (!  A Av j AL"6˜ ( All"E@ (("E\rAõ (0   6 (”! Aè  F! Aj! Aj6”  Alj" )ø7  )ð7  )è7 (”! (! (! ! \n A F  Fq\r (ä! ! O@ !  @ j"\n-"A#G@ ! AþF\r AàF\r !  Ak"\nF\rA!  Aj"j-A#F\r  @@  j- G\r Aj" G\r !  k"AI@ !  6 @@@  O\r@  j-A G\r Aj" G\r   O\r@@  j-"A\nk @ AàG\r \n-AàG\r !  Aj" G\r ! 6ä \rA6 A6 A6 \r \rA 6  \nF\rA!  Aj"j-A#G\r  \nF\rA!  Aj"j-A#G\r  \nF\rA!  Aj"j-A#G\r  \nF\rA!  Aj"j-A#G\r  \nF\rA!  Aj"j-A#G\r  \nF\rA! Aj!   k! ! AJ@ !  @  O"\n\r (Aq\r  j-A k"AK@ !  A tA“€€q\r !  @ \n\r@  j-"A G A Gq\r Aj" G\r !  6 6ä \rA6  6 @@@@@  O\r  j-AF\r Aj"\n O\r A/G\r \n j-A>F\r  AjM@A!  A! AA   A j>E\rA! ( "\n O\r (!@@  \nj-"A k"AK\rA tA€€qE\r \nAj"\n G\r  AAA A\nF A\rF! Aj$ 6¤@  (AG\r A6¤ ("A€qE\r (AG\r  (O\r ( j-"A-k Aj  Aäj+ F@ A6¤ A6 \rA6 AüG\r  (”G\r (€"E\r (AG\r Aäj! Aàj!A!A!@ Aj"(" M\r (" j-AüG\rA! Aj" O\r@  j-A k"\nAK\rA \ntA€€qE\r Aj" G\r ! @@@@  I@  ( j-A:Fj!  O\r ("\n j-A-G\r@@@  \nj-"A-G\r Aj" G\r Aj!  A! Aj!@  A:Fj" O\r@@  \nj-"A k"AK\rA tA€€qE\r  Aj"G\r  AüG\rA!A! Aj" O\r@  \nj-A k"AK\rA tA€€qE\r Aj" I\r !   O\r@  \nj-A\nk E\r  ! E\r  6  6A  A E\r  (à6 \rA\n6 \rA6 (”"  F  (AF! A€qE\r jAL\r ( Alj"Ak-"E\r A?K\rB ­†B€€€€ÀƒP\r@ (ä" ("\n Aj"  \nK"O\r (!@ j-"A G A Gq\r Aj" G\r ! Aj" \nO\r (" j-AÛG\r  Aj"j,"E\rAÓ APE\r  j-AÝG\r \n Aj"G@ j-A k"AK\r A tA“€€qE\r Ak % " -Ar: Ak $  6@ ("O\r (!@ j-A k"AK\rA tA€€qE\r Aj" G\r ! 6ä  6  Aj"6ô  6  6Aè!A! ! ! Aj.A! Aj A@k/\r -Aq@ Aj AÜj/\r AjA-\r Aj0\rA!#Ak" $ A6$ Aj"\rA6@ \r(€AJ@ Aøj!@@@@@@ \r(ø j" ("AÿqAk AsA\nvAq6 /:  (6 (AsA\nvAq6 /:  AÿÿK6 /: (6  A6 /At(€\\"6 /! 6 At(€6 Aj6 A$j6 @@ ("A€q@@ A€qE\r Aÿq A j \r(, \r("@ \r($"E\rA¿ \r(,   ("Aÿq"AK\rA tAŽ€qE\r \r \r(Ak6 (! A€qE\r Aÿq A j \r(, \r("@ \r($"E\rAš \r(,   @ ("AÿqAk \r(Œ \r(Alj" -Ar: \r \r(Aj6  B7` B7X B7P B7H B7@ B78 B70 B7(@ ("AÿqA\nG\r A€€„I\r ("A€ Av"nM\r Aq! Aj!A!\nA!@ "AO@ A|q!A!@  Atj"( j ( j (j (j ( ( ( (jjjk! Aj! Aj" G\r E\r @  Atj"( j (k! Aj! \nAj"\n G\r  n AvO\r \r($"A¸ \r(,  (  A€~qA r"6 A! A6ˆ B7€ B7x B7p B7h \r("@ \r(Œ AljAk-As! A!@@@@@@@ AÿqAk Av6h  A€€I\r \r("\n ("j-! ( !@@@  \r("O\r@  \nj- AÿqG\r Aj" G\r  M\r@  \nj-A G\r Aj" G\r ! @  M\r@  \njAk-A G\r Ak" K\r ! A! \r  \nj  kA Aèj AÈj9\r \r(! !\n@  M\r@  \nj-A k"AMAA tA€€q\r \nAj"\n G\r !\n \r  j \n kA  A(j9\r :ˆ (!A!  A6l Av6h (Ak6p  Av6h @@@@@@@  Aÿq"A FqE@  Aèj \r(, \r("\r (! AÿqAk \r($"E\rAš \r(,   (!@ AÿÿM@ E\r Aj!@ ( (F@ A j! Ak"\r  @  A lj"A k( Ak(G\r Ak"\r  Ak"E\r Aj! \rA  :"E\r  \rA Aj (:"E\r  (!@@ Av"Atj"E@A! \r($"E\rAä \r(,  k  @ (! ("! ! !@ !@ M\r \r(!@  j-A-F\r Aj" G\r ! A!  K@ \r( jAk-A:F! @ M\r \r(!@@  j-"\nA-G\r Aj" G\r !  Ar  \nA:F!  At( 96 Ak! Aj! AJ\r A A \r(, \r("@ \r($"E\rAš \r(,  k  \rA ( (  ;"\rA A \r(, \r("@ \r($"E\rA¿ \r(,  k  AI\r@A A \r(, \r("E@ Aj!A!  \r($"E\rAš \r(,  k  @ \rA  Atj"( (  ;"\r Aj" G\r A A \r(, \r("E\r \r($"E\rA¿ \r(,  k  k  \r Aj (<"\r A!  -"A Fq\r  Aèj \r(, \r("E\r \r($"@A¿ \r(,  ! \r (X@ (Hk (Lk (Pk (8E\r ((k (,k (0k \r (! -AkAM@ A l j!  At j!  \r(Œ \r(Alj" -Aþq A\nvAqr: \r \r(Aj6 Aj" \r(€H\r \rA6€A! Aj$ "\r -Aq@A!A!#A@j"\n$@ Aj"(`"E\r (pE\r (X AAR@@AA (, ("E@ (`E\r \nA0j!  ($"E\rAš (,   @ (X Alj"( E\r \nB7 \nB7 \nB7 \nB7 B7 B7 \n ( 6( \n (6,@ ( (A  \nAj9@A!  A \nA(j (, ("@ ($"E\rAš (,   ( (<"\rA \nA(j (, ("E@A!  ($"E\rA¿ (,  \n(@ \n(k \n( k \n(k \r Aj" (`I\r A!AA (, ("E\r ($"@A¿ (,  ! \nA@k$ "\r A!AA (0 ("E\r (("@A¿ (0  !  \n6ä \rA6 !  ! !  A6¨ ! @ (ä"Aj" ("O\r (!@@ j"-A\nk @ -A\nk @ -A\nk @  j-A\nk Aj! Aj!  I\r @ I@ (!@A!@ j-A\nk Aj" G\r ! A! A°jA(A j Aj "\n 6 " \r("AG\r@@ ("M\r (!@  jAk-"A G A Gq\r Ak" K\r  @  O\r (!@  jAk-A#G\r Ak" K\r   F\r ( jAk-"A F@ !  A F@ !  ! -AqE\r \n 6  !@ AkAI\r  ("M\r (!@  jAk-"A G A Gq\r \n Ak"6  K\r @ ( j-A\rFj! I@ ( j-A\nFj! @ &E\r E\r  j"AL\r ( Alj"Ak-A>F\r (ü Ak(j" (A€r6 @ \r  j" (”N\r Aj -\r AJ@ AjA Al" (j"(" -Aq ( j,A A1\r AjA (ü" -éAq" ( j,A A1\r ( j" -Aýq r: ( j 6 @ (”!@ -AqE\r ( Alj"Ak-A>G\r \r(AG\r \n(" ("k"AkA K\r (" j"-AÛG\r -A!G\r  jAk-AÝG\r Ak! Aj!A!@@@@ Ak @  j-"A k  AákAÿqAIAÿq -¹"A k  AákAÿqAIAÿqG\r Aj" G\r A  @  j-"A k  AákAÿqAIAÿq -Î "A k  AákAÿqAIAÿqG@ A\nk Aj" G\r A  A!@  j-"A k  AákAÿqAIAÿq -ú "A k  AákAÿqAIAÿqG@ A\nG\r  Aj" G\r A  A!@  j-"A k  AákAÿqAIAÿq -‡"A k  AákAÿqAIAÿqG@ A\nG\r -"A k  AákAÿqAIAÿqAÃG\r -"A k  AákAÿqAIAÿqAÁG\r -"A k  AákAÿqAIAÿqAÕG\r -"A k  AákAÿqAIAÿqAÔG\r -"A k  AákAÿqAIAÿqAÉG\r -"A k  AákAÿqAIAÿqAÏG\r -"A k  AákAÿqAIAÿqAÎG\rA  Aj" G\r A ! Ak" -Ar: ( (”AljAk" -AÇq r: \rA6 (”!  k!@A!@@A ( Alj"-A)kt"AqE@ A!qE\rA! Aj.  („6 Aj  ( ,A1\r AjA (" -Aq ( j,A A1\r  AjAA -"AqA AvAqA1\r Aj" (”H\r @ \r(E@ Aj.@A!  A!Aè! -AÀqE\r ( \n(I\r (ŒAj6Œ  (Œ@@ („"Aj" (ˆ"J@ A€ Av j AL"6ˆ (ü l"E@A! (("E\rAõ (0   (€"@   (ükj6€ 6ü („"Aj6„  6„ (ü"\rA!   j"A; (Œ! A6  AtAr6 A6Œ @ (E\r Aj.E\rA!  @@@@ \r("Ak\n A! Aj.\r („"Aj" (ˆ"J@ A€ Av j AL"6ˆ (ü l"E@ (("E\r\nAõ (0  \n (€"@   (ükj6€ („"Aj6„ 6ü  j  6„ (ü"E\r  j " \r(AÃÌj-"; (! A6   Atr6 6€ Aj \r2\r Aj.\rA!Aè!  (€A: (€ /; (€" (A€r6A! Aj \r2\r Aj.\rA! (€E@Aè!  \rA6 \r!  (€A\n: (€ /; A 6AA Aj \r2!  @  (F\r Aj.E\rA!  @ (€\r \r! @ Aj"(€"Aj"\n („"J@ A€ Av j AL"6„ (ø l"E@A ($"E\rAõ (, A  (ü"\n@   \n (økj6ü  6ø  (€"\nAj6€  \nj   \n6€ (ø"E\r  j " \r(AÃÌj-"\n; \r(! A6  \n Atr6  6üA  A E\rA!  Aj \r2! A! E\r  A!  (("E\rAš \n  (P"@ (T"A!@@ (P Atj("E\r (@" M@   (D (HljI\r k (T! Aj" I\r (P k (H@A!@ (@ Atj"-"Aq (k -  Aq@ ( k Aj" (HI\r (@k (l"@ (p"A!@@ (l Atj("E\r (\\" M@   (` (dljI\r k (p! Aj" I\r (l k (d@A!@ (\\ Alj(k Aj" (dI\r (\\k (8k (xk (ük (k A€j$ @A<t"B7 Aì6 B7 B7 B7 B7$ B74 B€€€ü7,  6  A j6  (@ (("E\r ((" ($"G A$j!A!\r@@@  \rAt"\nj("(AF@ ($" (("G\r \rAj!\r   k"Au!@@ ((AF\rA! AG@@ Aj!  Atj"((AF@  O\r Ak(!  Atj(! \r \r  "G\r A! A68 B70A!  A! A68 B70 A6L B7D  A0j6@  AÄj6<@@@@@@  At"j"(" (AG\r  Ak(A !  Aj"K@  Atj( rAG\r AF\r @ Aj" O\r -"A k  AákAÿqAIAÿqAÓG\r -"A k  AákAÿqAIAÿqAÃG\r -"A k  AákAÿqAIAÿqAÒG\r -"A k  AákAÿqAIAÿqAÉG\r -"A k  AákAÿqAIAÿqAÐG\r -"A k  AákAÿqAIAÿqAÔG\r  j-A>F\r @ Aj" O\r -"A k  AákAÿqAIAÿqAÓG\r -"A k  AákAÿqAIAÿqAÔG\r -"A k  AákAÿqAIAÿqAÙG\r -"A k  AákAÿqAIAÿqAÌG\r -"A k  AákAÿqAIAÿqAÅG\r  j-A>F\r A\nj" O\r -"A k AákAÿqAIAÿqAÔG\r -"A k AákAÿqAIAÿqAÅG\r -"A k AákAÿqAIAÿqAØG\r -"A k AákAÿqAIAÿqAÔG\r -"A k AákAÿqAIAÿqAÁG\r -"A k AákAÿqAIAÿqAÒG\r -"A k AákAÿqAIAÿqAÅG\r - "A k AákAÿqAIAÿqAÁG\r  j-A>F\r\r Aj" G\r !  6A Aj" O\r Ak! (!@@  j"-A\nk\n\n /A­Ús -A>srE\r Aj! Aj!  I\r !  Aj" O\r Ak! (!@@  j"-A\nk /A¿üF\r Aj! Aj!  I\r !  Aj" O\r Ak! (!@@@  j-"A\nk  A>F\r Aj! Aj!  I\r !  Aj" O\r Ak! (!@@  j"-A\nk /Aݺs -A>srE\r Aj! Aj!  I\r !  @@  O\rA! ( j-A\nk  6 ( !   6A  6A  6A  6A  6A  Aj6A Å@  ("O\r  (˜O\r @ (" j-"E\r A>F@ A>:  6  -Aüq: Aj! Aj  A?K\rB ­†B€€€€€ ƒP\r  Aj"K@  j-A k"AK\rA tA“€€qE\r  :  6  -Aüq: Aj  A! A6  A j"  I"\nO\rA! !@@ j-" A0kAÿqA K\r  A\nljA0k"6 Aj" \nG\r \n! M\r O\r@ j-" A)k  Aj"K@  j-A k"AK\rA tA“€€qE\r  :  6  -Aüq:  k jAj 6  6A!  ç  ("H@@A!@@@A (Œ Alj"Ak"-A)kt"AqE@ A!qE\rA! Ak(!A! Ak-Aq@ ( j,! A! A  A1\r A ,A1\r  AA Ak-"AqA AvAqA1E\rA!  (Ak"6  H\r A Ú#A0k"$@ (ü"E\r@@@ ("AÿqAk A€qE\r (" (O\r ( j-AÛG\r@ ("E\r Aj! AG\r !  @ \r@ A/G\r Aj" O\r \n j-A>F\r AL\r@ AßqAÁkAÿqAI\r AßF\r A:G\r  Aj"M@A!  @@@ \nj,"A0kAÿqA\nI\r A_qAÁkAÿqAI\r E@A!  A  APE\r A!  Aj"G\r  A!  A! @@ Ak A=G\r Aj!A!  A+!@@@@ A"k E\r Aá AP\r A\rF\r A)!  A*! Aj! I\r A! E\r \rF\rA  A)FA !  Aj" Atj"(! (" I\r   M@A A!  Aj6 „#Ak"$@  O\r@@@ j"  k A j4"AÿK@A!A!@@@  jAm"AtA j("\nAu j"AtA j(Aÿÿÿq K@ Ak!   \nAvAq j"AtA j(AÿÿÿqM\r Aj!  L\r  AH\r  A F\r A F\r A kAI\r -A\nk ( j" I\r Aj$  à@@@ AÿK@A­!@@@  jAm"AtAÀj("Au j"AtAÀj("Aÿÿÿq K@ Ak!  AvAq j"AtAÀj(AÿÿÿqM\r Aj!  N\r  AH\rAÐ8!  A6  A r AÁkAI6 A!A4!@@@  jAm"AtAÀ1j("Au j"AtAÀ1j("Aÿÿÿq M@ AvAq j"AtAÀ1j(Aÿÿÿq O@ AH\rAà8!  Aj!  Ak!  N\r A!A!@@@  jAm"AtAÐ6j("Au j"AtAÐ6j("Aÿÿÿq M@ AvAq j"AtAÐ6j(Aÿÿÿq O@ AH\rAð8!  Aj!  Ak!  N\r A6  6  ( ( " lAtj! At"@   ü\n  6 F\r ( Aÿÿÿq"AjF@  sAsAq j6  ( kj6 õ #A k"$ B7 B7 B7 B7 A ?! A ?! @@@  K@   M\r @ \nE@   AjC!A! AqE@   C! A!  Atj( Aj Atj(k"\r Aj" ( " I! Aj" (" I!\n  K\r  I\r  K\r  I\r A! A j$  iA!@ ("(" ("("I\rA!  K\r ( ( ( (A"\rA! (" ("I\r K!  ‰#Ak"$@@@  O\r j  k A j4! ( j! AÿK@A!@@@  jAm"AtA j("\nAu j" AtA j(Aÿÿÿq K@ Ak!   \nAvAq j"AtA j(AÿÿÿqM\r Aj!  L\r  AN\r  A F\r A F\r A kAO\r A6 A 6  ?!   @ Aj$  D ("(" ("("I@A  K@A ( ( ( (A ›”#A0k"$ A6x#Ak"$ AjAA€ü @@ E\r Ak! A€j!  AtjAk!@  Atj"(!@ !@ !@ Aj" ("O@ !  (! @  j"-j-@ !  -j-@ !  -j-@ !   j-j-@ !  Aj! Aj"!  K\r @  K@ (!@  j"-" j-\r Aj" G\r Aj" I\r  @AA@@@@@@ AÜF@A! Aj" (O\r@  j-"A!kAÿqAI\r@ AÿqA\nku Aj I\r Aj! @@ (x" (|"N@ AÀ Av j AL"6| (t All"\rA! ($" E\rAõ (,   Aj6x (t"\rA!  6t (x"Aj6x  Alj"AÜ ; B7  Aj"6  6 A!@@@@@@@@@@@@ A!k\\ \r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\n\r \r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r Aj"\r!\n@  \rM\r@  \nj- G\r \nAj"\n I\r !\n A!A!A!  (F\r N"AÿM\rA!A!@@@@  jAm" AtA j("Au j"AtA j(Aÿÿÿq K@ Ak!   AvAq j"AtA j(AÿÿÿqM\r Aj!  N\r  A! AN\r A!Aä!@@@  jAm" AtA°9j("Au j"AtA°9j(Aÿÿÿq K@ Ak!   AvAq j"AtA°9j(AÿÿÿqM\r Aj!  N\r A! AN   k! !@@  j-" AàG\r Aj" G\r !A  A F ! !  k"A K\r At j( "\r  AtjAk(O\rA!A!@ E@ ! !\n  !\n \r " I\r @@@@ \nO\r@  j-"\rA G@ \rAàF\rA! Aj" \nG\r A! \n  !\r \nO@A! !  @  \rj-AàG"@ ! \r!  \rAj"\r \nG\r ! \n! @  k"\rF@A!\n  Atj"("\r I@  jAk-A F!\n As  Gq\r \r \rF \nrE\r E\r Aj  @ F\rA! \rAK\r \rAt jA j"\r \r("\r \rK6 \r  Aj"M@A!   Atj"\n(! \n(!\n  ( ! \n@ Ak!  Ak(" ("O\r@  j-"A G A Gq\r Aj" G\r ! @@ (x" (|"N@ AÀ Av j AL"6| (t All"\rA! ($" E\rAõ (,   Aj6x (t"\rA!  6t (x"Aj6x  Alj"Aà; B7  6  6@@ (x" (|"N@ AÀ Av j AL"6| (t All"\rA! ($" E\rAõ (,   Aj6x (t"\rA!  6t (x"Aj6x  Alj"Aà; B7  6  6 (t (x"Alj"Ak" Ak6 A(k" Ak6  -Ar:  -Ar:A! ! ! ! (M\r@@@   jAv"Atj"( K@ E\r  AtjAk( I\r Ak!  (K\r ! !  Aj! @@ (x" (|"N@ AÀ Av j AL"6| (t All"\rA! ($" E\rAõ (,   Aj6x (t"\rA!  6t (x"Aj6x  Alj"A¦; B7  Aj"6  6  (x"AL\r (t" AljAk-A&G\r (|" L@ AÀ Av j AL"6|  All"E@A! ($" E\rAõ (,   6t (x! Aj6x  Alj"A»; B7  Aj6  6  ("\n - AÀq\r\n   k"  \n A j>\r (" AjM\r -A!G\r  Aj"j-A-G\r -A-G\r A! (è" K  \nAkOq\r @ Aj" \n \nI"\rM@  j" /A­Ús -A>srE\r\r@@ Aj! Aj" \rK\r " j" /A­Ús -A>sr\r  !  \nI  Aj" Kq@  Atj"(! (!  6è (! A! -AqE\r Aj" O\r  j-AüG\r@@ (x" (|"N@ AÀ Av j AL"6| (t All"\rA! ($" E\rAõ (,   Aj6x (t"\rA!  6t (x"Aj6x  Alj"Aü; B7  Aj"6  6  ("Aj K\r  Ak"\rj"/A÷îs -A÷sr\r @  \rF\r \rN"AÿK@A!A!@@@@  jAm" AtA j("\nAu j"AtA j(Aÿÿÿq K@ Ak!   \nAvAq j"AtA j(AÿÿÿqM\r Aj!  N\r  AN\r A!Aä!@@@  jAm" AtA°9j("\nAu j"AtA°9j(Aÿÿÿq K@ Ak!   \nAvAq j"AtA°9j(AÿÿÿqM\r Aj!  N\r  AH\r\r  A F\r A F\r A\rkA}K\r A!kAI\r A:kAI\r AÛkAI\r AÿG AúKqE\r @@ (x" (|"N@ AÀ Av j AL"6| (t All"\rA! ($" E\rAõ (,   Aj6x (t"\rA!  6t (x"Aj6x  Alj"A®; B7  Aj"6  \r6@@ (x" (|"N@ AÀ Av j AL"6| (t All"\rA! ($" E\rAõ (,   Aj6x (t"\rA!  6t (x"Aj6x  Alj" (6 (! AÄ; B7  6  Aj!@@ (" Aj K\r  Ak"\nj(AèèуG\r  M\r /A¯ÞF\r @ Aj K\r  Ak"\nj"\r(Aèèуs \r-Aósr\r  M\r /A¯ÞF\r Aj K\r  Ak"\nj"/Aæès -Aðsr\r  M\r /A¯ÞG\r @@ (x" (|"N@ AÀ Av j AL"6| (t All"\rA! ($" E\rAõ (,   Aj6x (t"\rA!  6t (x"Aj6x  Alj"Aº; B7  6  \n6@@ (x" (|"N@ AÀ Av j AL"6| (t All"\rA! ($" E\rAõ (,   Aj6x (t"\rA!  6t (x"Aj6x  Alj" (6 (! AÄ; B7  6 Aj!  (Aj K\r @ Ak-"AßqAÁkAÿqAO@ Aj O\r A0kAÿqA\nO\r  Aj O\r\n  Aj"j-"A0kAÿqA\nO@ A_qAÁkAÿqAK\r\n @@ (x" (|"N@ AÀ Av j AL"6| (t All"\rA! ($" E\rAõ (,   Aj6x (t"\rA!  6t (x"Aj6x  Alj"AÀ; B7  6  6@@ (x" (|"N@ AÀ Av j AL"6| (t All"\rA! ($" E\rAõ (,   Aj6x (t"\rA!  6t (x"Aj6x  Alj" (6 (! AÄ; B7  6 Aj!  @@ (x" (|"N@ AÀ Av j AL"6| (t All"\rA! ($" E\rAõ (,   Aj6x (t"\rA!  6t (x"Aj6x  Alj"AÝ; B7  Aj"6  6 \r @@ (x" (|"N@ AÀ Av j AL"6| (t All"\rA! ($" E\rAõ (,   Aj6x (t"\rA!  6t (x"Aj6x  Alj" 6A!A  Aj" O\r  j-AÛG\r@@ (x" (|"N@ AÀ Av j AL"6| (t All"\rA! ($" E\rAõ (,   Aj6x (t"\rA!  6t (x"Aj6x  Alj" 6A!!A !  :  : B7   j"6@@ (x" (|"N@ AÀ Av j AL"6| (t All"\rA! ($" E\rAõ (,   Aj6x (t"\rA!  6t (x"Aj6x  Alj"AÄ; B7  6  6@@ (x" (|"N@ AÀ Av j AL"6| (t All"\rA! ($" E\rAõ (,   Aj6x (t"\rA!  6t (x"Aj6x  Alj"AÄ; B7  6  6 \n @@ E@  -\rA qAvq\r  E\r @@ (x" (|"N@ AÀ Av j AL"6| (t All"\rA! ($" E\rAõ (,   Aj6x (t"\rA!  6t (x"Aj6x  Alj"A:  : B7  Aj"6  6 \n @@@@@@@@@@@AA@@AA@@@@@@@@@@@@@@ >\r"""""""" " """"""""""""""""""" """""""""""""""""""""""""" AÞF\r AþG\r!A! Aj"\n!@  \nM"\r@  j-AþG" \r Aj" I\r !  k"AG\r ( "A€€qE\r \rA! A!  \nj ( \nkA4"AÿK\r A F\r A F\r A kAM\rA  A! Aj" I\r -AqE\r Aj"!@  M\r@@  j-AÞG\r Aj" I\r ! !  kAG\r%A! A!  j ( kA4"AÿM@ A F\r A F\r A kAM\rA  @@@  jAm"AtA j("\rAu j"\nAtA j(Aÿÿÿq K@ \nAk!   \rAvAq j"AtA j(AÿÿÿqM\r Aj!  N\r A  \nAN\rA  A !  (F\r N"AÿM\rA!A!@@@  jAm" AtA j("\rAu j"\nAtA j(Aÿÿÿq K@ \nAk!   \rAvAq j"AtA j(AÿÿÿqM\r Aj!  N\r !  \nAN\r !  -A qE\r Aj" O\r@  j" -A=F@  Aj"K\r  !  kAG\r"A!A!AA ( kA4"AÿM@A! A F\r A F\r A kAI  @@@  jAm" AtA j("\rAu j"\nAtA j(Aÿÿÿq K@ \nAk!   \rAvAq j"AtA j(AÿÿÿqM\r Aj!  N\r A!\n \nAN !  -A€qE\r Aj" O\r@  j" -A+F@  Aj"K\r  !  kAG\r!A!A!AA ( kA4"AÿM@A! A F\r A F\r A kAI  @@@  jAm" AtA j("\rAu j"\nAtA j(Aÿÿÿq K@ \nAk!   \rAvAq j"AtA j(AÿÿÿqM\r Aj!  N\r A!\n \nAN !  @@@  jAm"AtA j("Au j"\rAtA j(Aÿÿÿq K@ \rAk!   AvAq j"AtA j(AÿÿÿqM\r Aj!  N\r A  \rAN\rA  A !  (F\r\r N"AÿM\r A!A!@@@  jAm" AtA j("Au j"\rAtA j(Aÿÿÿq K@ \rAk!   AvAq j"AtA j(AÿÿÿqM\r Aj!  N\r !  \rAN\r\r !  ! AK\r ( ! ! A€qE\rA!  (M\r N"AÿM\rA!\nA!@@@@  \njAm"\rAtA j(" Au \rj"AtA j(Aÿÿÿq K@ Ak!   AvAq \rj"\nAtA j(AÿÿÿqM\r \nAj!\n  \nN\r  AN\r\n A!\nAä!@@@  \njAm"\rAtA°9j(" Au \rj"AtA°9j(Aÿÿÿq K@ Ak!   AvAq \rj"\nAtA°9j(AÿÿÿqM\r \nAj!\n  \nN\r A! AN  @  j-A$F@  Aj"K\r  A! !  kAK\rA!  (M\r N"AÿM\rA!A! @@@@  jAm"\nAtA j("Au \nj"\rAtA j(Aÿÿÿq K@ \rAk!   AvAq \nj"AtA j(AÿÿÿqM\r Aj!  L\r  \rAN\r A!Aä! @@@  jAm"\nAtA°9j("Au \nj"\rAtA°9j(Aÿÿÿq K@ \rAk!   AvAq \nj"AtA°9j(AÿÿÿqM\r Aj!  L\r A!\r \rAN  @ Aj" O\r@  j-A k"AK\rA tA€€qE\r Aj" I\r ! A F@ !  kAI\r @@ (x" (|"N@ AÀ Av j AL"6| (t All"\rA! ($" E\r Aõ (,  Aj6x (t"\rA!  6t (x"Aj6x  Alj"A:  : B7  6  6  @@ (x" (|"N@ AÀ Av j AL"6| (t All"\rA! ($" E\rAõ (,   Aj6x (t"\rA!  6t (x"Aj6x  Alj"A€ ; B7  Aj"6  6  A F\r A F\r A\rkA}K\r AÿG AúKq A!kAI A:kAIr AÛkAIrr ! @ E\rA! A!@  j" ( k" A4"AÿM@ A F\r A F\r A kAO\r  @@@  jAm"AtA j("\rAu j"\nAtA j(Aÿÿÿq K@ \nAk!   \rAvAq j"AtA j(AÿÿÿqM\r Aj!  N\r  \nAN\r A!Aä!  A4"AÿM@ AÿG AúKq A!kAI A:kAIr AÛkAIrr  @@@  jAm" AtA°9j("\rAu j"\nAtA°9j(Aÿÿÿq K@ \nAk!   \rAvAq j"AtA°9j(AÿÿÿqM\r Aj!  N\r A!\n \nAN ! !  Aq "E\r @@ (x" (|"N@ AÀ Av j AL"6| (t All"\rA! ($" E\rAõ (,   Aj6x (t"\rA!  6t (x"Aj6x  Alj" : A$: B7  6  6  A F\r A F\r A\rkA}K\r AÿG AúKq A!kAI A:kAIr AÛkAIrr ! @ E\rA! A!@  j" ( k" A4"AÿM@ A F\r A F\r A kAO\r  @@@  jAm"AtA j("\rAu j"\nAtA j(Aÿÿÿq K@ \nAk!   \rAvAq j"AtA j(AÿÿÿqM\r Aj!  N\r  \nAN\r A!Aä!  A4"AÿM@ AÿG AúKq A!kAI A:kAIr AÛkAIrr  @@@  jAm" AtA°9j("\rAu j"\nAtA°9j(Aÿÿÿq K@ \nAk!   \rAvAq j"AtA°9j(AÿÿÿqM\r Aj!  N\r A!\n \nAN ! !  Aq "E\r @@ (x" (|"N@ AÀ Av j AL"6| (t All"\rA! ($" E\rAõ (,   Aj6x (t"\rA!  6t (x"Aj6x  Alj" : Aþ: B7  6  6  A F\r A F\r A kAM\r !  A! ! AqE\r @@ (x" (|" N@ AÀ Av j AL"6| (t All" \rA! ($" E\rAõ (,   Aj6x (t" \rA!  6t (x"Aj6x Alj" : Aþ: B7  \n6  6  A! !  kAG\r @@  (F\r N"AÿK@A!A!@@@  jAm" AtA j("\rAu j"\nAtA j(Aÿÿÿq K@ \nAk!   \rAvAq j"AtA j(AÿÿÿqM\r Aj!  N\r !  \nAN\r !  A F\r A F\r A kAM\r !  A! ! AqE\r @@ (x" (|" N@ AÀ Av j AL"6| (t All" \rA! ($" E\rAõ (,   Aj6x (t" \rA!  6t (x"Aj6x Alj" : A+: B7  6  6  A! !  kAG\r @@  (F\r N"AÿK@A!A!@@@  jAm" AtA j("\rAu j"\nAtA j(Aÿÿÿq K@ \nAk!   \rAvAq j"AtA j(AÿÿÿqM\r Aj!  N\r !  \nAN\r !  A F\r A F\r A kAM\r !  A! ! AqE\r @@ (x" (|" N@ AÀ Av j AL"6| (t All" \rA! ($" E\rAõ (,   Aj6x (t" \rA!  6t (x"Aj6x Alj" : A=: B7  6  6 A F\r A F\r A kAM\r !  A! ! AqE\r @@ (x" (|" N@ AÀ Av j AL"6| (t All" \rA! ($" E\rAõ (,   Aj6x (t" \rA!  6t (x"Aj6x Alj" : AÞ: B7  6  6 @ Aj" O\r -A?G\rA!@ (ì" M@ !  !  \nAkO\r  Aj" \n \nI"\rM@  j/A¿üF\r@@ Aj! Aj" \rK\r " j/A¿üG\r  !  \nI  Aj"Kq  Atj"(! (!  6ì ( ! @ M\r -A!G\r  j-AßqAÁkAÿqAK\r@ Aj" O\r@  j-AßqAÁkAÿqAK\r Aj" G\r ! A! (ð" K  \nAkOq\r Aj" \n \nI"\rM@  j-A>F\r@@ Aj! Aj" \rK\r " j-A>G\r  !  \nI  Aj" Kq  Atj"(! (!  6ð ( ! @ A j" O\r )B¼ÂìšÄ¨ªÁ… 1Bۅ„BR\rA!\r (ô" K  \nAkOq\r@ Aj" \n \nI" M@  j"/Aݺs -A>srE\r@@ Aj! Aj" K\r  "j"/Aݺs -A>sr\r  !  \nI  \rAj"\rKq@  \rAtj"(! (!  6ô ( !\n ! Aj" \nO\r@ Aj" \nO\r  j-" A0kAÿqA\nIA AßqAÁkAÿqAI E\r@  j-"A0kAÿqA\nI\r AßqAÁkAÿqAI\r A+k"AK\r AF\r Aj" \nF\r@  kA K\r  j-" A:F@  \nO\r@  j-" A k"AMAA tA€€q\r A>G@ A I\r AG\r  j-A-F\rAð! Aj !@@ (x" (|"N@ AÀ Av j AL"6| (t All"\rA! ($" E\r\rAõ (,  \r Aj6x (t"\rA! 6t (x"Aj6x  Alj" Ar: A<: B7  6  6@@ (x" (|"N@ AÀ Av j AL"6| (t All"\rA! ($" E\r\rAõ (,  \r Aj6x (t"\rA! 6t (x"Aj6x  Alj" Ar: A>: B7  6  Ak6 (t (x"Alj"A k Ak6 Ak Ak6 ! !   6 @@@ (x" (|"N@ AÀ Av j AL"6| (t All"E\r 6t (x"Aj6x  Aj6x (t"E\r  Alj"A¼(; B7  6  6@ (x" (|"N@ AÀ Av j AL"6| (t All"E\r 6t (x"Aj6x  Aj6x (t"E\r ( !  Alj"A¾0; B7  6  6 (t (x"Alj"A k Ak6 Ak Ak6A! !  (M\r@@   jAv"Atj"( K@ E@ !   AtjAk(K@ ! Ak!  ( O@ ! \n Aj!  M\r A!  ($"E\rAõ (,  A!  Aj!  A F\r A F\r A\rkA}K\r AÿG AúKq A!kAI A:kAIr AÛkAIrr "! As AßFq! @  \nF\rA!A!@  \nj" ( \nk"A4"AÿM@ A F\r A F\r A kAO\r  @@@  jAm"AtA j(" Au j"AtA j(Aÿÿÿq K@ Ak!   AvAq j"AtA j(AÿÿÿqM\r Aj!  N\r  AN\r A!Aä!AAA  A4"AÿM@ AÿG AúKq A!kAI A:kAIr AÛkAIrr  @@@  jAm"AtA°9j(" Au j"AtA°9j(Aÿÿÿq K@ Ak!   AvAq j"AtA°9j(AÿÿÿqM\r Aj!  N\r A! AN "Aq As qAq"!A  ! ! \n!  rE\r  kAp!@@ (x" (|"N@ AÀ Av j AL"6| (t All"\rA! ($" E\rAõ (,   Aj6x (t"\rA!  6t (x"Aj6x  Alj" AtA# AG  LqAA  NA AJr" AFrA@k:  : B7  \n6  6 \n \r"M\r@@@ (x" (|"N@ AÀ Av j AL"6| (t All"\rA! ($" E\rAõ (,   Aj6x (t"\rA!  6t (x"Aj6x  Alj"AÄ; B7  \r6  \r6 \rAj"\r \nI\r ! \n! @@ (x" (|"N@ AÀ Av j AL"6| (t All"\rA! ($" E\rAõ (,   Aj6x (t"\rA!  6t (x" Aj6x (!  Alj"Aÿ ; B7  6  6A! Aj$@ " \r A (xAÏAJ@ (à"AN@ Ak!\r A€j! AØj! A j!@ (t" Al"j" (! @ -"AÄF\r ( !A! A! AN@   Alj" ( Alj! Al! (! -A q@ A!: Ak"6A!!  j!  I@ ( I\r  I@ ( I\r @  I@ A!G\r  AÛG\r ( A€€ÀqE\r (" (O\r ( j-AÞG\r Aj"6 (t j"(" M\r  ( j  kK"E\r ( "E@ (pAj"6p  6  (Aj6 6 (6 -A”r:  -A˜r: (! (! (  @@@@ -\rA qE\r -AÛG\r E\r (" ("kAG\r -AÛG\r E\r ( ("kAG\r -AÝG\r  AkG\r ( ("kAG\r  (AjG\r@@ Aj" N"@ !  (t! !@@  Alj"\n-"AÄF\r AüF\r \n( kAäK\r \n-AqE\r \n( ! Aj" H\r A!\n \n  \n(" F\r  kAäK\r  I@ (!@@  j-A\nk Aj" G\r A!@   Atj"("L@ (t! @ Alj( " N\r  6  ("L@ (t! @ Alj( " N\r  6 Aj"AG\r \n@@ \n( (I@@  L\r  kAk! (t! A!  AsjAq"@@ AljAÄ; Aj! Aj" G\r AI\r@ Alj"AÄ;L AÄ;8 AÄ;$ AÄ; Aj" G\r \n \n-Ar: \n(6  @ \r  kAk! (t! A!  AsjAq"@@ AljAÄ; Aj! Aj" G\r AI\r@ Alj"AÄ;L AÄ;8 AÄ;$ AÄ; Aj" G\r  \n(6 (6  (6 (t" j" 6 j" 6  -Ar: -Ar: (! (!  Aj L (  (6  (6 (t" j" 6 j" 6  -Ar: -Ar: (! (! (  \r (!  (" ("G\r ( AjK@ -AÀq\r    ( AjM  -AÀq\r   (  AjM "AH\r E\r  (6 (t Alj(!  @  ("O\r (" j-A(G\r Aj!\nA! \r!@@@   jAv"Atj"( K@ E\r  AtjAk( I\r Ak!   (M\r Aj!  O\r A! @@ Aj"  Atj(" I@@  j-A k"AK\rA tA€€qE\r Aj" G\r !  6@  O\r  j-A\nk Aj" O\r  Atj(!  6 @  O\r  j-A)G\rA! A6 B7  6  6 Aj! A    Atj"(" Aj Aj 5E\r@   k ( Aj A j A,j A(j A$j6@ ( " j! (," F!   j"Atj(! (!   ("6(  6$A!A! @  I@@  j-"A k"AK\rA tA€€qE\r Aj" G\r Aj" O\r   Atj("j-! AÿqA)G\r  Aj" 6 ($" (("M@A! A6 B7A   k! @ A6  6   j"6A  @ j"@  Atj!A! !@   Atj("  K" KA!  "kAq"@@  ( j-: Aj! Aj! Aj" G\r  kA|M@@  ( j-:  ( j-:  ( j-:  ( j-: Aj! Aj" G\r   O\r A\n: Aj!  Aj"Atj(! ($" E\rAä (,   A6  6   k6 (! A ! (x" \nL@  6  (t! \n!@  Alj"(K@@ -AqAG\r  ( "Alj( I\r \r k  Aj" H\r  6  \nL\r  kAk! (t! A!  AsjAq"@@ \nAljAÄ; \nAj!\n Aj" G\r AI\r@ \nAlj"AÄ; AÄ;L AÄ;8 AÄ;$ \nAj"\n G\r  -AÀq\r   ( ( AjM"AH\r E\r -Ar:  -Ar: Aj" (tj (6 (t j ( 6  (tj (6( Aj! (t! (@  Alj (Ð6 6Ð  Alj (6 (! (! -!  Aj L   AÛF"!   !   !   ! ! / AŒqE\r@@ "Aj! -$"AÄG" \r  I\r À! !@@ "Ak! \r  K\r -%" AqE\r E\r ( (G\rA‡ APE\r (  (tkAmG\r Ak( (G\r AÄ:$  Aïq:% AkAÄ: Ak" -Aïq: "AN\r B7à A6¸ @A! A6Ô (x"AL\r (t! A!@@ Alj"-"Aq\r -AüG\r ( (kAG\r A6  Ar:@ (ØAH@ 6Ø  (ÜAlj 6 6Ü Aj"6Ô Aj" G\r A!  A! A (xL  A! A0j$ Þ#Aðk"$ B7h B7` B7X B7P B7H B7@ B78 B70 A6( B7 B7 B7 B7A! @    Aj AÐj9\r  A Aj A0j9\r  6( (,! @  Aj (" E@A!  ($"E\rAÒ (,    Aj (" E@A!  ($"E\rAö (,  (`@ (Pk (Tk (Xk (@@ (0k (4k (8k Aðj$ Ý@ ( " ("I@ (!  ( Av jA "At"l"E@A ($"E\rAõ (, A  6 ( Ajl"E@A ($"E\rAõ (, A  6  6 ( ! ( Atj 6  ( Atj 6  ( Aj6 A Ü Aj!@@@ Aj" O\r j-A#G\r j-A rAøG\r !@@@@ j-"A0kAÿqA\nI\r AÁkAÿqAI\r  kAK\r AákAÿqAI\r   kAK\r Aj" G\r !  F\r  kAI\r  @ Aj" O"\r j-A#G\r !@@  kAK\r j-A0kAÿqA K\r Aj" G\r !  F\r  kAI\r   M\r j-AßqAÁkAÿqAK\r  \r@@ j-"AßqAÁkAÿqAO@   kA0K\r A0kAÿqA\nI\r    kA0M\r   Aj" G\r  " kAkA.K\r  M\r j-A;G\rA!  Aj6  ³#Ak"$@@  O\r (!@  j-A k"AK\rA tA€€qE\r Aj" G\r !   O\r (!@  jAk-A k"AK\rA tA€€qE\r Ak" K\r !  6  6  6@  Aj (, ("@ ($"E\rAš (,   AjA<"\r  Aj (, ("E@A!  ($"E\rA¿ (,  Aj$  à!#A0k"\r$  H@ A¸j! A€j! \rAr! \rAr! (!@@ (t" Al" j" -"Aq@ AqE\r@ E\r ,"E\r  [\r ( !  ,"\nAÿq"E\r  \n[E\r (" I\r@@@@@@@@@@@@@@@@@ A!k^  (! Aq@ AN@  Alj" -AÀr: 6  6  AH\r  Alj"( 6¸  6 6@ (ä" AN@  Alj 6  6à 6ä A6  Aj" (xN\r  Alj"-A;G\r (  (" \rHE\r 6  6 Ar:  -Ar: 6  AqE\r \r A€A˜ \nA*Fj" A j6 AÀq"AÀF@A!   \r Aj6 A€F\rA!  Aj6A! A°A´ ( kAFj!\n@ AqE\rA! \n("AH\r@   Atj"\n("L@@  Alj( " N\r \n 6  \n("L@@  Alj( " N\r \n 6 Aj" AG\r  Alj" 6 6  -Ar: -Ar: \r AqE\r \n(6 \n 6 @ AqE\rA! (Ä"AH\r@   Atj"\n("L@@  Alj( " N\r \n 6  \n("L@@  Alj( " N\r \n 6 Aj" AG\r  Alj" 6 6  -Ar: -Ar: AqE\r (Ä6 6Ä @ AqE\r (¼"AH\rA!  Alj" ( (k ( kG\r@   Atj"\n("L@@  Alj( " N\r \n 6  \n("L@@  Alj( " N\r \n 6 Aj" AG\r @ Aj" N\r  kAk!A!  AsjAq"\n@@  AljAÄ; Aj! Aj" \nG\r AI\r@  Alj" AÄ;L AÄ;8 AÄ;$ AÄ; Aj" G\r 6 6 -Ar: -Ar: A6¼ AqE\r\n (¼6 6¼ \n  Aj"Al"j"(! (! \r 6 \r ("6, \r 6( \r 6$ \nAÀF@   \rAAAA¨ \rA(jOAL\r\n \r(!  O\r N"AÿM\rA!A!@@@  jAm" AtA j("Au j"AtA j(Aÿÿÿq K@ Ak!   AvAq j"AtA j(AÿÿÿqM\r Aj!  N\r  AN\r  ( kAG\r@ AqE\rA! (À"AH\r@   Atj"\n("L@@  Alj( " N\r \n 6  \n("L@@  Alj( " N\r \n 6 Aj" AG\r  Alj" 6 6  -Ar: -Ar: AqE\r (À6 6À  ( kAG\r@ AqE\rA! (È"AH\r@   Atj"\n("L@@  Alj( " N\r \n 6  \n("L@@  Alj( " N\r \n 6 Aj" AG\r  Alj" 6 6  -Ar: -Ar:  AqE\r (È6 6È  ( kAG\r@ AqE\rA! (Ì"AH\r@   Atj"\n("L@@  Alj( " N\r \n 6  \n("L@@  Alj( " N\r \n 6 Aj" AG\r  Alj" 6 6  -Ar: -Ar:  AqE\r (Ì6 6Ì  A!A!  A F\r A F\r A kAI\r ( jAk,"@Aó AP\r \r((" I\r Ak!@@@ -AÄF\r ( K\r  (O\r -" Aq\r Ak" O\r  AqE\r   \rA,jAA.AAè \rA$jOAH\r \r(,!@ \nAÀF\r@  O\r (" j-A/G\r Aj  \rA,jAA/A‰A \rA$jOAH\r \r(," O\r  j-A/G\r \r Aj"6, @  O\r ( j-A?G\r Aj  \rA,jAA&AƒA¥ \rA$jOAH\r \r(,!  O\r ( j-A#G\r Aj  \rA,jAAAAë \rA$jOAH\r \r(,! @  O\rA!A!@ ( j" ( kA4"AÿM@ A F\r A F\r A kAO\r  @@@  jAm" AtA j("Au j"AtA j(Aÿÿÿq K@ Ak!   AvAq j"AtA j(AÿÿÿqM\r Aj!  N\r  AN\r ,"@A± A P\r \r($"  (xAlj"O\r@@@ -AÄF\r ( M\r ( K\r -"Aq\r Aj" I\r  AqE\r 6 6  \n:  6  6 (t" j" 6  j" 6  -Ar:  -Ar:  \r Atr 6 A q! Aj! AàqAàG@ \r Atj Aj6 Aj! As AGqE@ \r Atj Aj6 Aj! Aq! Aq!A!A!A! A!@@ \r Atj"(("AH\r  Alj! @ ( (M\r ! ! @ (("AH\r  Alj! @ ( (M\r ! ! Aj! Aj" G\r @ E\r \r Atj((" AH\r  Alj! @ ( (M\r ! ! E\r (! (! (! !\n@@@@@@  Alj"-"A!k\n AÛk A€A˜ A*Fj -"AtAuA qj!\n@ AvAk \nAj!\n  A°A´ ( (kAFj!\n  \nAj!\n @  k"  k" K@  (" (k k j"Al"j" (6  )7  )7   k" 6  6 (t j \n(6 \n 6   O\r  Alj j" (6  )7  )7  k j"6  6 A!\n@   \nAtj"("L@@ AN@  (t Alj( "6  N\r \nAj"\nAG\r (t" Alj" 6  j" 6  -Ar:  -Ar:  AqE\r A€A˜ \nA*Fj AtAuA qj!@@@ AvAk Aj!  Aj! (6  6 -"AqE\r Aq@ (t ( Alj(!  (! Aj" H\r \rA0j$ –#Ak"$@ (E\r  3! (  (pAtj("E@A!  @  ("I\r   ( (ljO\r  (F@ ( (  AE\r A!   6  6  6  Aj6 Aj!@ ("@@   Av"Atj"D"AH@   E\r Aj!  Asj "\r A! E@A!  (! Aj$  ý#A k"$ Aß: A¦Ô;A! ( "A€ q@ A$:A! A€€€q@ Aj rA=: Aj! A€€€q@ Aj rA+: Aj! A€€q@ Aj jAü: Aj! A€€q@ Aj jAÞ: Aj! A€„q@ Aj jAþ: Aj! A! Aj jA:    AjAJ ( "AŒq@ Aq@ AÀ:A! Aq@  rA:: Aj! A€q  rA.: Aj  rA:     AjJ A€jAÿAÐü A j$ ‰ (TE@A Ak! Ak!AA ("\n j-A!F j!A!@@   jAv"Atj" ( K@ E\r  AtjAk( I\r Ak!   (M\r Aj!  @  (M" E@@  kj"@ !@  Atj("  K" KA!  "kAq"\n@@  ( j-: Aj! Aj! Aj" \nG\r  kA|M@@  ( j-:  ( j-:  ( j-:  ( j-: Aj! Aj" G\r   O\r A : Aj! Aj" Atj(! A! ($"E\rAä (, A  k   \nj!  k ! A  Aj60A! Aj !\nA!@@ \n("E\r Aj i"AH\r   kK\r \nAj!\n  j" I\r A=! AH\r A \r  e E@A!  A! (0!\n@ \n(" E\r Aj i" j" K\r Aj a \nAj!\n  K\r A \r  A€Àse \r   \rH!   AHq\r A=! +0 \r   f"AN\r \n -!\n Aj! \r E\rA!@  Atj("\n@  Atj \n cA! Aj"A\nG\r A\nO@A! \n @  Atj(\rA! Aj"A\nG\r A!   :\'A! ! !   k"  H" AÿÿÿÿsJ\rA=! \r  j"  \rH" \nK\r A   e  a A0   A€€se A0  Ae a A   A€Àse ( Ak" O\r B€”ëÜT\r Ak" > @ "I@ Ak"(E\r (¬ k"6¬ ! AJ\r AH@ AjA nAj! AæF!@A A k" A O!\n@  M@AA (!  A€”ëÜ \nv!\rA \ntAs!A! !@  (" \nv j6  q \rl! Aj" I\r AA (! E\r  6 Aj! (¬ \nj"6¬   j" " Atj   kAu J! AH\r A!@  M\r  kAuA l!A\n! ("A\nI\r@ Aj!  A\nl"O\r A AæGk AçF AGqk"  kAuA lA kH@ A„`Aìc AHj A€Èj"A m"\rAtj!\nA\n!  \rA lk"AL@@ A\nl! Aj"AG\r @ \n("  n" lk"\rE \nAj" Fq\r@ AqE@D@C! A€”ëÜG\r \nO\r \nAk-AqE\r D@C! Dà?Dð?Dø?  FDø? \r Av"F \r I!@ \r -A-G\r š! š! \n  \rk"6    a\r \n  j"6 A€”ëÜO@@ \nA6 \nAk"\nK@ Ak" A6 \n \n(Aj"6 Aÿ“ëÜK\r  kAuA l!A\n! ("A\nI\r@ Aj!  A\nl"O\r \nAj"   K! @ " M"E@ Ak"(E\r @ AçG@ Aq!\r  AsA A " J A{Jq"\n j! AA~ \n j! Aq"\r\rAw!@ \r Ak("\nE\rA\n!A! \nA\np\r@ "\rAj! \n A\nl"pE\r \rAs!  kAuA l! A_qAÆF@A!\r  jA k"A AJ"  J!  A!\r  j jA k"A AJ"  J! A!\n AýÿÿÿAþÿÿÿ \rr"J\r AGjAj!@ A_q"AÆF@  AÿÿÿÿsJ\r A AJ!    Au"s k­ d"kAL@@ Ak"A0:  kAH\r Ak" : AkA-A+ AH:  k" AÿÿÿÿsJ\r  j" AÿÿÿÿsJ\r A   j" e  a A0   A€€se@@@ AÆF@ AjA r!  K"! @ 5 d!@  G@  AjM\r@ Ak"A0:  AjK\r   G\r Ak"A0:   ka Aj" M\r @ A‰Aa  M\r AL\r@ 5 d" AjK@@ Ak"A0:  AjK\r A A Na A k! Aj" O\r A J! ! \r  @ AH\r  Aj  K!\n AjA r! !@  5 d"F@ Ak"A0: @  G@  AjM\r@ Ak"A0:  AjK\r  Aa Aj! \rrE\r A‰Aa   k"  Ha k! Aj" \nO\r AN\r A0 AjAAe   ka  ! A0 A jA Ae A   A€Àse    J!\n   AtAuA qj!@ A K\r -!Dð?!@A4 Atk"A€N@Dà! AÿI@ Aÿk!  Dð!Aý  AýOAþk!  AxJ\rD`! A¸pK@ AÉj!  D!Aðh  AðhMA’j!  Aÿj­B4†¿¢! A-F@  š ¡ š!     ¡!  (¬" Au"s k­ d"F@ Ak"A0: (¬! Ar! A q! Ak"\r Aj: AkA-A+ AH: AqE ALq! Aj!@ " ü"AÀÐj- r:  ·¡D0@¢!@ Aj" AjkAG\r Da q\r A.: Aj! Db\r A!\n Aûÿÿÿ   \rk" jkJ\r A  Aj  Ajk" Ak H  " jj" e  a A0   A€€se Aj a A0  kAAe \r a A   A€Àse    J!\n A°j$ \n ¹~  (AjAxq"Aj6 )! )!#A k"$ Bÿÿÿÿÿÿ?ƒ!~ B0ˆBÿÿƒ"§"AøkAýM@ B† B<ˆ„! A€øk­!@ Bÿÿÿÿÿÿÿÿƒ"B€€€€€€€Z@ B|!  B€€€€€€€€R\r Bƒ |! B  BÿÿÿÿÿÿÿV"! ­ |  @  „P\r BÿÿR\r B† B<ˆ„B€€€€€€€„!Bÿ  Aþ‡K@B!Bÿ  A€øAø P"\n" k" AðJ@B!B   B€€€€€€À„ \n!A!\n G@ Aj! ! !@A€ k"AÀq@  A@j­†!B!  E\r  ­"† AÀ k­ˆ„!  †! 7 7 ) )„BR!\n @ AÀq@  A@j­ˆ!B!  E\r AÀ k­†  ­"ˆ„!  ˆ!  7  7 )B† )"B<ˆ„!@ \n­ Bÿÿÿÿÿÿÿÿƒ„"B€€€€€€€Z@ B|!  B€€€€€€€€R\r Bƒ |! B€€€€€€€…  BÿÿÿÿÿÿÿV"! ­ ! A j$ B€€€€€€€€€ƒ B4†„ „¿9 ¦ (T"(! (" ( ("k"  I"@   Q  ( j"6  ( k"6    K"@   Q  ( j"6  ( k6 A: (,"6 6  ¢ E@A @  AÿM\r@AÔÚ((E@ A€qA€¿F\rAèÛA6  AÿM@ A?qA€r: AvAÀr:A  A€@qA€ÀG A€°OqE@ A?qA€r: A vAàr: AvA?qA€r:A  A€€kAÿÿ?M@ A?qA€r: AvAðr: AvA?qA€r: A vA?qA€r:A  AèÛA6 AA  :A Ñ( #Ak"\n$@@@@@@@@@@ AôM@A¤Ü("A A jAøq A I"Av"v"Aq@@ AsAq j"At"AÌÜj" (Ô\\"("F@A¤Ü A~ wq6   6 6 Aj!  Ar6  j" (Ar6 A¬Ü("M\r @@ tA t"A krqh"At"AÌÜj" (Ô\\"("F@A¤Ü A~ wq"6   6  6 Ar6 j"  k"Ar6 j 6 @ AxqAÌÜj!A¸Ü(! A Avt"qE@A¤Ü  r6   ( !  6  6  6  6 Aj!A¸Ü 6A¬Ü 6 A¨Ü(" E\r hAt(Ô^"(Axq k! !@@ ("E@ ("E\r (Axq k"   K"!  ! !  (!  ( "G@ (" 6 6 \n (" Aj ("E\r Aj !@ ! "Aj! ("\r Aj! ("\r A6 A! A¿K\r A j"Axq!A¨Ü(" E\rA! AôÿÿM@ A& Avg"kvAq AtkA>j! A k!@@@ At(Ô^"E@A!  A! A AvkA AGt!@@ (Axq k" O\r ! "\rA! !  ("   AvAqj("F ! At! "\r rE@A!A t"A kr q"E\r hAt(Ô^! E\r @ (Axq k" I!   !  ! ("  ( "\r E\r A¬Ü( kO\r (!  ( "G@ (" 6 6  (" Aj ("E\r Aj !@ ! "Aj! ("\r Aj! ("\r A6  A¬Ü("M@A¸Ü(!@ k"AO@  j" Ar6 j 6  Ar6   Ar6 j" (Ar6A!A! A¬Ü 6A¸Ü 6 Aj! A°Ü("I@A°Ü  k"6A¼ÜA¼Ü(" j"6  Ar6 Ar6 Aj! A! A/j"Aüß(@A„à(  AˆàB7A€àB€ €€€€7Aüß \nA jApqAتժs6AàA6AàßA6A€ "j"A k"q" M\rAÜß("@AÔß(" j" M\r  I\r @Aàß-AqE@@@@@A¼Ü("@Aäß!@ (" M@   (jI\r ("\r An"AF\r !A€à("Ak" q@  k  jA kqj!  O\rAÜß("@AÔß(" j" M\r I\r n" G\r   k q"n" ( (jF\r ! AF\r A0j M@ !  A„à("  kjA kq"nAF\r  j! !  AG\r AàßAàß(Ar6 n!An! AF\r AF\r M\r k" A(jM\r AÔßAÔß( j"6AØß( I@AØß 6 @A¼Ü("@Aäß!@  (" ("jF\r ("\r  A´Ü("A ME@A´Ü 6 A!Aèß 6Aäß 6AÄÜA6AÈÜAüß(6AðßA6@ At" AÌÜj"6Ô\\  6Ø\\ Aj"A G\r A°Ü A(k"Ax kAq"k"6A¼Ü  j"6  Ar6 jA(6AÀÜAŒà(6   O\r  I\r ( Aq\r  j6A¼Ü Ax kAq"j"6A°ÜA°Ü( j" k"6  Ar6  jA(6AÀÜAŒà(6  A!  A!  A´Ü( K@A´Ü 6  j!Aäß!@@  ("G@ ("\r  - AqE\r Aäß!@@ (" M@   (j"I\r (!  A°Ü A(k"Ax kAq"k"6A¼Ü  j"6  Ar6 jA(6AÀÜAŒà(6  A\' kAqjA/k" AjI"A6 Aìß)7 Aäß)7Aìß Aj6Aèß 6Aäß 6AðßA6 Aj!@ A6 Aj! Aj!  I\r  F\r  (A~q6   k"Ar6  6 AÿM@ AøqAÌÜj!A¤Ü("A Avt"qE@A¤Ü  r6  ( ! 6  6 A !A  A! AÿÿÿM@ A& Avg"kvAq AtrA>s!  6 B7 AtAÔÞj!@@A¨Ü("A t"qE@A¨Ü  r6  6  6  A AvkA AGt! (!@ "(Axq F\r Av! At!  Aqj"("\r  6  6 A! ! !A  (" 6  6  6A!A!A j 6  j 6 A°Ü(" M\rA°Ü k"6A¼ÜA¼Ü(" j"6  Ar6 Ar6 Aj!  AèÛA06A!  6 ( j6 Ax kAqj" Ar6 Ax kAqj"  j"k!@A¼Ü( F@A¼Ü 6A°ÜA°Ü( j"6  Ar6  A¸Ü( F@A¸Ü 6A¬ÜA¬Ü( j"6  Ar6  j 6  ("AqAF@ Axq! ( !@ AÿM@ (" F@A¤ÜA¤Ü(A~ Avwq6  6  6  (!@  G@ (" 6  6  @ (" Aj ("E\r Aj !@ ! "Aj! ("\r Aj! ("\r A6  A! E\r@ ("At"(Ô^ F@ AÔÞj 6 \rA¨ÜA¨Ü(A~ wq6  @  (F@  6   6 E\r  6 ("@  6  6 ("E\r  6  6  j"(!  j!  A~q6  Ar6  j 6 AÿM@ AøqAÌÜj!A¤Ü("A Avt"qE@A¤Ü  r6   ( !  6  6  6  6  A! AÿÿÿM@ A& Avg"kvAq AtrA>s!  6 B7 AtAÔÞj!@@A¨Ü("A t"qE@A¨Ü r6  6  6  A AvkA AGt! (!@ "(Axq F\r Av! At!  Aqj"("\r  6  6  6  6  (" 6  6 A6  6  6 Aj!  @ E\r@ ("At"(Ô^ F@ AÔÞj 6 \rA¨Ü A~ wq" 6  @  (F@  6   6 E\r 6 ("@ 6  6 ("E\r 6  6 @ AM@   j"Ar6 j" (Ar6   Ar6  j" Ar6  j 6 AÿM@ AøqAÌÜj!A¤Ü("A Avt"qE@A¤Ü  r6  ( ! 6  6  6  6  A! AÿÿÿM@ A& Avg"kvAq AtrA>s!  6 B7 AtAÔÞj!@@ A t"qE@A¨Ü  r6  6  6  A AvkA AGt! (!@ "(Axq F\r Av! At!  Aqj"("\r  6  6  6  6  (" 6  6 A6  6  6 Aj!  @ E\r@ ("At"(Ô^ F@ AÔÞj 6 \rA¨Ü A~ wq6  @  (F@ 6  6 E\r 6 ("@ 6  6 ("E\r 6  6 @ AM@   j"Ar6 j" (Ar6   Ar6  j" Ar6  j 6 @ AxqAÌÜj!A¸Ü(!A Avt" qE@A¤Ü  r6   ( !  6  6 6 6 A¸Ü 6A¬Ü 6 Aj! \nAj$ ‚ @ E\r Ak" Ak("Axq"j!@ Aq\r AqE\r  ("k"A´Ü(I\r j!@@@A¸Ü( G@ ( ! AÿM@  ("G\rA¤ÜA¤Ü(A~ Avwq6  (!  G@ (" 6  6  (" Aj ("E\r Aj !@ ! "Aj! ("\r Aj! ("\r A6  ("AqAG\rA¬Ü 6  A~q6  Ar6  6  6  6  A! E\r@ ("At"(Ô^ F@ AÔÞj 6 \rA¨ÜA¨Ü(A~ wq6  @  (F@  6   6 E\r  6 ("@  6  6 ("E\r  6  6  O\r ("AqE\r@@@@ AqE@A¼Ü( F@A¼Ü 6A°ÜA°Ü( j"6  Ar6 A¸Ü(G\rA¬ÜA6A¸ÜA6 A¸Ü(" F@A¸Ü 6A¬ÜA¬Ü( j"6  Ar6 j 6 Axq j! ( ! AÿM@ (" F@A¤ÜA¤Ü(A~ Avwq6   6  6  (!  G@ (" 6  6  (" Aj ("E\r Aj !@ ! "Aj! ("\r Aj! ("\r A6   A~q6  Ar6 j 6  A! E\r@ ("At"(Ô^ F@ AÔÞj 6 \rA¨ÜA¨Ü(A~ wq6  @  (F@  6   6 E\r  6 ("@  6  6 ("E\r  6  6  Ar6 j 6  G\rA¬Ü 6 AÿM@ AøqAÌÜj!A¤Ü("A Avt"qE@A¤Ü r6   ( !  6 6  6  6 A! AÿÿÿM@ A& Avg"kvAq AtrA>s!  6 B7 AtAÔÞj!@A¨Ü("A t"qE@A¨Ü  r6  6A!A  A AvkA AGt! (!@ "(Axq F\r Av! At!  Aqj"("\r  6A! !A ! !   (" 6  6A!A!A !  j 6  6 j 6AÄÜAÄÜ(Ak"A 6 ‰  E@ j A@O@AèÛA06A A A jAxq A I! Ak"("Axq!@ AqE@ A€I\r Aj M@ !  kA„à(AtM\r A   j!@  O@  k"AI\r   AqrAr6  j" Ar6  (Ar6  m  A¼Ü( F@A°Ü( j" M\r   AqrAr6  j"  k"Ar6A°Ü 6A¼Ü 6  A¸Ü( F@A¬Ü( j" I\r@  k"AO@   AqrAr6  j" Ar6  j" 6  (A~q6   Aq rAr6  j" (Ar6A!A! A¸Ü 6A¬Ü 6  ("Aq\r Axq j"\n I\r \n k! ( !@ AÿM@ (" F@A¤ÜA¤Ü(A~ Avwq6   6  6  (! @  G@ (" 6  6  @ (" Aj ("E\r Aj !@ ! "Aj! ("\r Aj! ("\r A6  A! E\r@ ("At"(Ô^ F@ AÔÞj 6 \rA¨ÜA¨Ü(A~ wq6  @  (F@ 6  6 E\r  6 ("@  6  6 ("E\r  6  6 AM@  Aq \nrAr6  \nj" (Ar6    AqrAr6  j" Ar6  \nj" (Ar6  m !  "@ Aj j"E@A  A|Ax Ak("Aq Axqj"   KQ k  «  j!@@ ("Aq\r AqE\r (" j!@@@ k"A¸Ü(G@ ( ! AÿM@  ("G\rA¤ÜA¤Ü(A~ Avwq6  (! G@ (" 6  6  (" Aj ("E\r Aj !@ ! "Aj! ("\r Aj! ("\r A6  ("AqAG\rA¬Ü 6  A~q6 Ar6  6  6  6  A! E\r@ ("At"(Ô^ F@ AÔÞj 6 \rA¨ÜA¨Ü(A~ wq6  @ (F@  6   6 E\r  6 ("@  6  6 ("E\r  6  6 @@@@ ("AqE@A¼Ü( F@A¼Ü 6A°ÜA°Ü( j"6 Ar6 A¸Ü(G\rA¬ÜA6A¸ÜA6 A¸Ü(" F@A¸Ü 6A¬ÜA¬Ü( j"6 Ar6 j 6 Axq j! ( ! AÿM@ (" F@A¤ÜA¤Ü(A~ Avwq6   6  6  (!  G@ (" 6  6  (" Aj ("E\r Aj !@ ! "Aj! ("\r Aj! ("\r A6   A~q6 Ar6 j 6  A! E\r@ ("At"(Ô^ F@ AÔÞj 6 \rA¨ÜA¨Ü(A~ wq6  @  (F@  6   6 E\r  6 ("@  6  6 ("E\r  6  6 Ar6 j 6 G\rA¬Ü 6 AÿM@ AøqAÌÜj!A¤Ü("A Avt"qE@A¤Ü  r6   ( !  6  6 6 6 A! AÿÿÿM@ A& Avg"kvAq AtrA>s! 6 B7 AtAÔÞj!@@A¨Ü("A t"qE@A¨Ü  r6  6 6  A AvkA AGt! (!@ "(Axq F\r Av! At!  Aqj"("\r  6 6 6 6 (" 6  6 A6 6 6 W~@ ­B|BøÿÿÿƒAØÚ("­|"BÿÿÿÿX@ §"?AtM\r \r AèÛA06A AØÚ 6 ß  #Ak"$  6 @ AÓM@AÐÐAÒ A jp(!  A|O@]  AÒn"AÒl"k6AÒAÐÓ Ajp"( j! AÒkAu!@A! !@@@ ! A/F@AÓ!@ n" I\r  lF\r A\nj"n" I\r  lF\r A j"n" I\r  lF\r Aj"n" I\r  lF\r Aj"n" I\r  lF\r Aj"n" I\r  lF\r Aj"n" I\r  lF\r Aj"n" I\r  lF\r A$j"n" I\r  lF\r A(j"n" I\r  lF\r A*j"n" I\r  lF\r A.j"n" I\r  lF\r A4j"n" I\r  lF\r A:j"n" I\r  lF\r AA AM!@@ j"\rA”à("E\r \n  "E@u ]  \\w ¬@  (AÿÿÿÿqAkA\n , "AH"" (  "kM@ E\r ( AH! @  j  ü\n  j!@ , AH@ 6  Aÿq:  jA:   j k  A  x …Aöÿÿÿ k O@A÷ÿÿÿ! ( , AH! AòÿÿÿM@A  j" At"  K"ArAj A I! t!@ E\r E\r  ü\n @ E\r E\r  j  ü\n   j"k!\n@  F\r \nE\r  j j  j j \nü\n A\nG@ k 6 A€€€€xr6  j \nj"6  jA: u Å@@@ , "AN@A\n! A\nF\r AjAÿq:  (" (AÿÿÿÿqAk"G\r !@ AöÿÿÿG@A÷ÿÿÿ! ( , AH! AòÿÿÿM@A Aj" At"  K"ArAj A I! t!@ E\r E\r   ü\n @  F\r  k"E\r  j  j ü\n A\nG@ k 6 A€€€€xr6  u 6 ! Aj6 (! j"A: : « A÷ÿÿÿI@ (AÿÿÿÿqAkA\n , "AH" I@A ArAj A I"t! (!@ (  "Aj"E\r E\r   AH ü\n AH@ k 6 6 A€€€€xr6  u h (Aÿÿÿÿq" K@ 6 (!@ E\r E\r   ü\n  jA: Ak  kAj ("A   x Q A\nM@ : @ E\r E\r  ü\n jA: A\n A\nk - Aÿq"A  x #A k"$#Ak"$ Aj" A j" Aj"k"A L@A= A ArgkAÑ lA v"  At(°TIkAjH\r  A¿„=M@ AÎM@ AãM@ A M@  A0r: Aj   At/àT; Aj  AçM@  AÿÿqAän"A0r:   AälkAÿÿqAt/àT; Aj   ~  AŸM@  AÎn"A0j: Aj  AÎlk~     AÿÁ×/M@ Aÿ¬âM@  AÀ„=n"A0j: Aj  AÀ„=lk   €  Aÿ“ëÜM@  A€Â×/n"A0j: Aj  A€Â×/lk€   A€Â×/n"At/àT; Aj  A€Â×/lk€ !A 6  6 A j" (6  ( 6 Aj$@ ( " Aj"k"A÷ÿÿÿI@@ A\nM@ :  Ar"Ajt! Aÿÿÿÿk6 6 6 !  k!@  F\r E\r  ü\n jA:  u A j$ 2 Aän"At/àT;  AälkAt/àT; Aj * AÎn"At/àT; Aj  AÎlk~ * AÀ„=n"At/àT; Aj  AÀ„=lk  AÐjjAÐj  t E@ ( (F F@A ("-!@ ("-"E\r G\r@ -! -"E\r Aj! Aj! F\r F Ú#AÐk"$@A ( (F\r#A@j"$  ("Ak("j!@ Ak("(AÜÖ(F@A  !   "L@ B7 A6 AØÖ6  6  6 B7 B7$ B7, A6< B€€€€€€€74  Aj  AA (( (\r B7 A6 A¨Ö6  6 AØÖ6 B7 B7$ B7, B73 A6< A:;  Aj AA (( A!@@ (( (A ($AFA ( AFA (,AF!  (AG@ (,\r ( AG\r ($AG\r (! A@k$A "E\r ("E\r AjAA8ü A:K A6  6  6 A6D  Aj A (( (,"AF@  ($6 AF ! AÐj$  A6 Aç6 AŽ 6] v ($"E@ 6 6 A6$ (86 @@ ( (8G\r ( G\r (AG\r 6 A:6 A6 Aj6$  ( ((F@   … 5 ( ((F@   … ("    (( { ("Aq! -7AF@ Au" E\r ( j(  Au E\r  ((68A!A ! ("   j A Aq (( r ( ((F@   … ( ! Aj"   ˆ@ AI\r  Atj! Aj!@   ˆ -6\r Aj" I\r š A:5@  (G\r A:4@ ("E@ A6$ 6 6 AG\r (0AF\r   F@ ("AF@ 6 ! (0AG\r AF\r  ($Aj6$ A:6 Â@ ( ƒ@  (G\r (AF\r  6 @ ( ƒ@@ ( G@  (G\r AG\r A6   6 (,AF\r Aj" ( Atj!@@@ @  M\r A;4    A Œ -6\r -5AG\r -4AF@ (AF\rA!A! -AqE\r  A! -Aq\rA  AA Aq 6, \r  A6,  Aj! ( ! Aj"     AI\r  Atj! Aj!@ ("AqE@ ($AG\r @ -6\r      Aj" I\r  AqE@@ -6\r ($AF\r      Aj" I\r  @ -6\r ($AF@ (AF\r      Aj" I\r   6  ((Aj6( ($AG\r (AG\r A:6 K ("Au! ("   Aq ( j(  j A Aq  (( I ("Au! ("  Aq ( j(  j A Aq  (( Š@ ( ƒ@  (G\r (AF\r  6 ( ƒ@@ ( G@  (G\r AG\r A6   6 @ (,AF\r A;4 ("   A  (( -5AF@ A6, -4E\r  A6,  6  ((Aj6( ($AG\r (AG\r A:6 ("     (( ¦@ ( ƒ@  (G\r (AF\r  6 ( ƒE\r@ ( G@  (G\r AG\r A6   6  6  ((Aj6(@ ($AG\r (AG\r A:6 A6, › ( ƒ@    Š -5! ( ! A:5 -4! A:4 Aj"     Œ  -4"\nr!  -5" r!@ AI\r Atj! Aj!@ -6\r@ \nAq@ (AF\r -Aq\r  AqE\r -AqE\r A;4      Œ -5" rAq! -4"\n rAq! Aj" I\r  Aq:5  Aq:4 9 ( ƒ@    Š ("      ((  ( ƒ@    Š A§ 7 AàÙ6 ("Ak" (Ak"6 AH@ A kk ” k  (  $ # kApq"$ # ‘P(A€ Þ{"type":"Document"},"attributes":{summarytbodyTableBodyLatexMathDisplay\\u%04x-+ 0X0x-0X+0X 0X-0x+0x 0xTableRowdivnavmenuTextUnorderedListOrderedListstartSuperscriptSubscripttfootcountbodyRowCountheadRowCountbasefontDocumentimportantdefaulttightrightHighlightleftframesetfieldsetdt\\taddressdetailsEmphasisnoframes%s:%d: %strvectordirhrfootercenterSpoilerheaderfenceChar\\roptgroupcolgroup/Users/ernest/react-native-enriched-markdown/packages/core/cpp/wasm/ASTSerializer.cpp/emsdk/emscripten/system/lib/libcxxabi/src/private_typeinfo.cpptipcautionoptionfigcaptionAdmonitionsectionmainalignnan\\nformmenuitemListItemparamulurlcolhtmlTableHeaderCellTableCellleveldlisTasklinkLinkCodeBlocktrackSoftBreakLineBreakThematicBreaklibad_array_new_lengthParagraphStrikethroughsearchdialogStrongbasic_stringnodeTypeToStringwarningHeadinginf\\ftrueblockquoteBlockquotenotefalsebasefigureadmonitionTypeUnderlineLatexMathInlineBlankLineiframetitlearticletableTableImageCodeasidetdlegendtaskCheckedddtheadTableHead\\b"\'=<>`-_.-+_\\\\({[,"children":[NANINFcatching a class without an object?)}].!?,;mailto:,"content":h6h5h4h3h2h1http://Too many link reference definition instantiations.Unsupported abi_version.Aborted from text() callback.Aborted from enter_span() callback.Aborted from leave_span() callback.Aborted from enter_block() callback.Aborted from leave_block() callback.malloc() failed.realloc() failed.@:..!#$%&\'*+/=?^_`{|}~-_.:-+-.-_+-_*(null)Suppressing too sparse table (see https://github.com/mity/md4c/issues/345)._=().+-_~%false && "unhandled NodeType in nodeTypeToString"\\"{"type":"[]!xX \nñ<‡í bOFß*$®ŒšŸòT2q¾Îé<Ь¸ùlAè ~€ ¸+Œ ô)NSt3__220__shared_ptr_emplaceIN8Markdown15MarkdownASTNodeENS_9allocatorIS2_EEEE"ƒA€ ¹Nú‡RA  f ° à \rˆ P\rˆ \rð\rˆ ˆ  @`€ˆ ˆ  Àˆ ˆ ˆ ˆ ˆ A ?/A° Äè£\n-Aà dƒµ¯A 5KG[uzÞ9AÐ 50a\nÉ|‘\'A MÝÚ×ÔÑÎO’y¹Að A€ 8$èA  ‚–AÀ \r~XAà ¶¦ZA€ \rP¨A  Tw$AÀ E,5Â9N\nküA ®A  —  € @\n €/ _ 0A@Z€µÀ@րØ@ހ@.€2@6€9@G€J@v€xy@}€‚„†‡‰Š‹Ž‘“”–—˜œŸ @¤€¦§©¬®¯±²³µ·¸¼ÄÅÇÈÊË@Û€Þ@î€ñòôö÷ø@€ "@2€:;=>ACDEF@N€Eprv†ˆ@Š€ŒŽ‘@¡€£@«€ÂÏÐÑÕÖØ@î€ðñôõ÷ùúý@ÿ€@€@/€`@€€Š@¾€ÀÁ@Í€Ð@.€1@V€ @Å€ÇÍø@ý€€‚ƒ„…†‡ˆ‰@º€½@¿€@”€› @þ€@€@€(@/€8@?€H@M€Y[]_h@o€¸¹º»¾È@Ë€ØÙÚÛèéêëìøùúû&!*!+!2!`!@o!€ƒ!¶$@Ï$€,@/,€`,b,c,d,g,@k,€m,n,o,p,r,u,~,,€,@â,€ë,í,ò,@¦@l¦€€¦@š¦€"§@.§€2§@n§€y§{§}§~§@†§€‹§§§’§–§@¨§€ª§«§¬§­§®§°§±§²§³§´§@§€Ä§Å§Æ§Ç§É§Ë§Ì§@Ú§€Ü§Ý§â§õ§l«m«p«@¿«€!ÿ@:ÿ€@\'€°@Ó€p@z€|@Š€Œ@’€”•€ @² €P\r@e\r€ @¿€@n@_n€ n@¸n€@ßHßJßMßQßhß@n߀rß@~߀é@!é€az¼àöøþ/37:HKwÿz~sSƒ…TˆVWŒÝY[’`cih™oru¡¥€¨ƒ­ˆ°Š‹´¶’¹½ÆÆÉÉÌÌÜßïóóõ•¿ùž#3e,<šf,B€‰ŒGO¹qswó¬­¯ÌÍαÁÃËÃײ¸ÆÀÙïºÁ¸µøòû{}P_0Oa‹¿ÏÂÎÑ/a†-%-\'---ðõ24>ABBJcK¦ŠÐúýÿ•a¡ÿ \'07@EQSUW`g°±pq¹ruÐÑvwàáz{åxy|}ÉkåN!p!!„!Ð$é$0,_,a,k}}h,l,QqPRs,v,?@,ã,ì,î,ó,A¦m¦¦›¦#§/§3§o§z§|§y§‡§Œ§e‘§“§—§©§f\\aljž‡S«µ§Ã§”§‚Žȧʧdͧۧ›w|ö§K«L« ïAÿZÿ(OØû—¡£±³¹»¼À ò p\r…\rÀß`nn»nÓnAßIßKßNßRßißoßsßß"éCéAÀ1 Óß0Ið‡–—˜™šžP€@‡€ˆ@€@—€˜@Ÿ€ @§€¨@¯€²³´¶¼ÂÃÄÆÌÖäæòóôöüûûûûûûûûûû•ßA 3 ¥ssi¼nj e‚h1tw\ny\na¾ssʹ¹¹ ¹\'¹ ¹\'¹`¹g¹`¹g¹p¹±¹¬¹±B±¹t¹·¹®¹·B·¹¹BÁÅB|¹É¹Î¹ÉBɹfffiflststtvtetk~vtmssAÐ6 À°RTV·ÇÒÓ×âãç÷ûû¹ÅÅÅÅB±B¹·B¹¹¹¹BÅÅÅBÉB¹ffiffl@.À 5P A¤9 ¬!@/€:@@€[@`€{@~€¡@©€«@¬€®@±€´¶@¸€»¿×÷Â@Å€Ò@߀å@ë€íï@ÿ€u~„@…€‡ö‚Z@_€‰@Š€@€¾ÀÃÆó@ô€@€@€j@m€ÔÞéý@þ€@\r€ö@ù€þ@ÿ€0@>€^ˆd @e €p ò @ó €ú @û €ý v\nð\n@ñ\n€p ó @ú €w  „ O\ry\rô\r?OZ@[€@€@€468:@=€…¾@Å€Ç@Ì€Î@Ú€J@O€ž@Ÿ€û`@h€@™€m@n€›@œ€ë@í€5@6€Ô@Ö€Ø@Û€@\n€@D@E€Þ@ÿ€@€ @¦€¨@­€N@O€Z@j€t@€ü@ÿ€;@?€~@€À@Ç€Ó½¿@Á€Í@Ï€Ý@߀í@ï€ý@þ€ @\' €0 @^ €z @~ €Š @Ž €  @Ä €!@!€!@!€!@ !€!!@!€!@#!€%!\'!)!.!:!@;!€@!@D!€J!@M!€O!Š!@‹!€!@)$€@$@J$€œ$@é$€%@u\'€”\'@s+€v+@ÿ+€å,@ê,€ù,@ü,€þ,@ÿ,€p-.@..€0.@].€`.@c.€€.@™.€›.@ó.€/@Õ/€ð/@ÿ/€0@0€0@ 0€0060@70€=0@?0€›0@œ0€ 0û01@‘1€–1@Ÿ1€À1@å1€ï12@2€*2@G2€P2`2@2€Š2@°2€À2@ÿ3€ÀM@ÿM€¤@Ƥ€þ¤@ÿ¤€\r¦@¦€s¦~¦ò¦@÷¦€§@§€ §@!§€‰§@Ч€(¨@+¨€6¨@9¨€t¨@w¨€Î¨@Ϩ€ø¨@ú¨€ü¨.©@/©€_©Á©@Í©€Þ©@ß©€\\ª@_ª€wª@yª€Þª@ߪ€ðª@ñª€[«j«@k«€ë«)û²û@Òû€>ý@Oý€ý@‘ý€Èý@Ïý€üý@ÿý€þ@þ€0þ@Rþ€Tþ@fþ€hþ@kþ€ÿ@ÿ€ÿ@ ÿ€;ÿ@@ÿ€[ÿ@eÿ€àÿ@æÿ€èÿ@îÿ€üÿ@ýÿ€@€7@?€y@‰€Œ@Ž€@œ€ Ð@ü€ŸÐoWw@x€ ? P\n@X\n€\nÈ\nð\n@ö\n€9 @? €™ @œ €n\rŽ\r@\r€­É@Ê€Ð@Ø€U@Y€†@‰€G@M€»@¼€¾@Á€@@C€t@u€Å@È€ÍÛÝ@߀8@=€©Ô@Õ€×@Ø€K@O€Z@[€]ÆÁ@×€A@C€`@l€¹<@?€;D@F€â?@F€š@œ€ž@¢€@ €áA@E€p@q€÷@ø€C@O€Õ@ñ€ÿp$@t$€ñ/@ò/€nj@oj€õj7k@?k€Dk@Ek€mm@om€—n@šn€âoœ¼Ÿ¼Ì@ïÌ€úÌ@üÌ€Í@³Î€ºÎ@Ð΀ÒÎ@Ô΀ÝÎ@ý΀PÏ@ÃÏ€Ð@õЀÑ@&Ñ€)Ñ@dÑ€jÑ@lÑ€ƒÑ@„Ñ€ŒÑ@©Ñ€®Ñ@AÒ€EÒSÒ@ZÒ€]Ò@^Ò€`Ò@Ò€Ó@VÓ€ÁÖÛÖûÖ×5×O×o׉ש×Ãר@ÿÙ€7Ú@:Ú€mÚ@tÚ€vÚ@ƒÚ€…Ú@‹Ú€Û@Û€Oáÿâÿå^é@_逬ì°ì.íðî@ñî€ð@+ð€0ð@“ð€ ð@®ð€±ð@¿ð€Áð@Ïð€Ñð@õð€\rñ@®ñ€æñ@ò€ò@;ò€@ò@Hò€Pò@Qò€`ò@eò€ó@Ùö€Üö@ìö€ðö@üö€÷@Û÷€à÷@ë÷€ð÷@ ø€ø@Gø€Pø@Yø€`ø@‡ø€ø@­ø€°ø@»ø€Àø@Áø€Ðø@Øø€ù@Wú€`ú@mú€pú@|ú€€ú@Æú€ÈúÌú@Ýú€ßú@ëú€ïú@úú€û@’û€”û@ïû€úû AãÌ  AðÌ A  \n\n\n   AÁÍ ! \r\r  AûÍ  A‡Î  AµÎ AÁÎ  AïÎ AûÎ  A²Ï  AãÏ AïÏ  AÐ A©Ð € 0123456789ABCDEF \r%)+/5;=CGIOSYaegkmqƒ‰‹•—£§­³µ¿ÁÅÇÓ \r%)+/5;=CGIOSYaegkmqyƒ‰‹•—£§©­³µ»¿ÁÅÇѐ+Ø)NSt3__214__shared_countE, *Ð)NSt3__219__shared_weak_countEA´Ô œ\ndè\' †@B€–˜áõʚ;00010203040506070809101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899¸+4+<-N10__cxxabiv116__shim_type_infoE¸+d+(+N10__cxxabiv117__class_type_infoEX+ !"#$%&Ø+\'!"#()*¸+ä+X+N10__cxxabiv120__si_class_type_infoE4,+!"#,-.¸+@,X+N10__cxxabiv121__vmi_class_type_infoE°,/0+„,St9exception¸+ ,|,St9bad_alloc¸+¼,”,St20bad_array_new_lengthì,12¸+ø,|,St11logic_error-32¸+(-ì,St12length_error+D-St9type_infoAÔÚ  . 0€')}function getBinarySync(file){return file}async function getWasmBinary(binaryFile){return getBinarySync(binaryFile)}async function instantiateArrayBuffer(binaryFile,imports){try{var binary=await getWasmBinary(binaryFile);var instance=await WebAssembly.instantiate(binary,imports);return instance}catch(reason){err(`failed to asynchronously prepare wasm: ${reason}`);abort(reason)}}async function instantiateAsync(binary,binaryFile,imports){return instantiateArrayBuffer(binaryFile,imports)}function getWasmImports(){var imports={env:wasmImports,wasi_snapshot_preview1:wasmImports};return imports}async function createWasm(){function receiveInstance(instance){wasmExports=instance.exports;assignWasmExports(wasmExports);updateMemoryViews();return wasmExports}function receiveInstantiationResult(result){return receiveInstance(result["instance"])}var info=getWasmImports();var instantiateWasm=Module["instantiateWasm"];if(instantiateWasm){return new Promise(resolve=>{instantiateWasm(info,inst=>resolve(receiveInstance(inst)))})}wasmBinaryFile??=findWasmBinary();var result=await instantiateAsync(wasmBinary,wasmBinaryFile,info);var exports=receiveInstantiationResult(result);return exports}class ExitStatus{name="ExitStatus";constructor(status){this.message=`Program terminated with exit(${status})`;this.status=status}}var HEAP16;var HEAP32;var HEAP64;var HEAP8;var HEAPF32;var HEAPF64;var HEAPU16;var HEAPU32;var HEAPU64;var HEAPU8;var callRuntimeCallbacks=callbacks=>{while(callbacks.length>0){callbacks.shift()(Module)}};var onPostRuns=[];var onPreRuns=[];var noExitRuntime=true;var stackRestore=val=>__emscripten_stack_restore(val);var stackSave=()=>_emscripten_stack_get_current();var UTF8Decoder=globalThis.TextDecoder&&new TextDecoder;var findStringEnd=(heapOrArray,idx,maxBytesToRead,ignoreNul)=>{var maxIdx=idx+maxBytesToRead;if(ignoreNul)return maxIdx;while(heapOrArray[idx]&&!(idx>=maxIdx))++idx;return idx};var UTF8ArrayToString=(heapOrArray,idx=0,maxBytesToRead,ignoreNul)=>{var endPtr=findStringEnd(heapOrArray,idx,maxBytesToRead,ignoreNul);if(endPtr-idx>16&&heapOrArray.buffer&&UTF8Decoder){return UTF8Decoder.decode(heapOrArray.subarray(idx,endPtr))}var str="";while(idx>10,56320|ch&1023)}}return str};var UTF8ToString=(ptr,maxBytesToRead,ignoreNul)=>ptr?UTF8ArrayToString(HEAPU8,ptr,maxBytesToRead,ignoreNul):"";var ___assert_fail=(condition,filename,line,func)=>abort(`Assertion failed: ${UTF8ToString(condition)}, at: `+[filename?UTF8ToString(filename):"unknown filename",line,func?UTF8ToString(func):"unknown function"]);class ExceptionInfo{constructor(excPtr){this.excPtr=excPtr;this.ptr=excPtr-24}set_type(type){HEAPU32[this.ptr+4>>2]=type}get_type(){return HEAPU32[this.ptr+4>>2]}set_destructor(destructor){HEAPU32[this.ptr+8>>2]=destructor}get_destructor(){return HEAPU32[this.ptr+8>>2]}set_caught(caught){caught=caught?1:0;HEAP8[this.ptr+12]=caught}get_caught(){return HEAP8[this.ptr+12]!=0}set_rethrown(rethrown){rethrown=rethrown?1:0;HEAP8[this.ptr+13]=rethrown}get_rethrown(){return HEAP8[this.ptr+13]!=0}init(type,destructor){this.set_adjusted_ptr(0);this.set_type(type);this.set_destructor(destructor)}set_adjusted_ptr(adjustedPtr){HEAPU32[this.ptr+16>>2]=adjustedPtr}get_adjusted_ptr(){return HEAPU32[this.ptr+16>>2]}}var uncaughtExceptionCount=0;var ___cxa_throw=(ptr,type,destructor)=>{var info=new ExceptionInfo(ptr);info.init(type,destructor);uncaughtExceptionCount++;abort()};var __abort_js=()=>abort("");var getHeapMax=()=>536870912;var alignMemory=(size,alignment)=>Math.ceil(size/alignment)*alignment;var growMemory=size=>{var oldHeapSize=wasmMemory.buffer.byteLength;var pages=(size-oldHeapSize+65535)/65536|0;try{wasmMemory.grow(pages);updateMemoryViews();return 1}catch(e){}};var _emscripten_resize_heap=requestedSize=>{var oldSize=HEAPU8.length;requestedSize>>>=0;var maxHeapSize=getHeapMax();if(requestedSize>maxHeapSize){return false}for(var cutDown=1;cutDown<=4;cutDown*=2){var overGrownHeapSize=oldSize*(1+.2/cutDown);overGrownHeapSize=Math.min(overGrownHeapSize,requestedSize+100663296);var newSize=Math.min(maxHeapSize,alignMemory(Math.max(requestedSize,overGrownHeapSize),65536));var replacement=growMemory(newSize);if(replacement){return true}}return false};var getCFunc=ident=>{var func=Module["_"+ident];return func};var writeArrayToMemory=(array,buffer)=>{HEAP8.set(array,buffer)};var lengthBytesUTF8=str=>{var len=0;for(var i=0;i=55296&&c<=57343){len+=4;++i}else{len+=3}}return len};var stringToUTF8Array=(str,heap,outIdx,maxBytesToWrite)=>{if(!(maxBytesToWrite>0))return 0;var startIdx=outIdx;var endIdx=outIdx+maxBytesToWrite-1;for(var i=0;i=endIdx)break;heap[outIdx++]=u}else if(u<=2047){if(outIdx+1>=endIdx)break;heap[outIdx++]=192|u>>6;heap[outIdx++]=128|u&63}else if(u<=65535){if(outIdx+2>=endIdx)break;heap[outIdx++]=224|u>>12;heap[outIdx++]=128|u>>6&63;heap[outIdx++]=128|u&63}else{if(outIdx+3>=endIdx)break;heap[outIdx++]=240|u>>18;heap[outIdx++]=128|u>>12&63;heap[outIdx++]=128|u>>6&63;heap[outIdx++]=128|u&63;i++}}heap[outIdx]=0;return outIdx-startIdx};var stringToUTF8=(str,outPtr,maxBytesToWrite)=>stringToUTF8Array(str,HEAPU8,outPtr,maxBytesToWrite);var stackAlloc=sz=>__emscripten_stack_alloc(sz);var stringToUTF8OnStack=str=>{var size=lengthBytesUTF8(str)+1;var ret=stackAlloc(size);stringToUTF8(str,ret,size);return ret};var ccall=(ident,returnType,argTypes,args,opts)=>{var toC={string:str=>{var ret=0;if(str!==null&&str!==undefined&&str!==0){ret=stringToUTF8OnStack(str)}return ret},array:arr=>{var ret=stackAlloc(arr.length);writeArrayToMemory(arr,ret);return ret}};function convertReturnValue(ret){if(returnType==="string"){return UTF8ToString(ret)}if(returnType==="boolean")return Boolean(ret);return ret}var func=getCFunc(ident);var cArgs=[];var stack=0;if(args){for(var i=0;i{var numericArgs=!argTypes||argTypes.every(type=>type==="number"||type==="boolean");var numericRet=returnType!=="string";if(numericRet&&numericArgs&&!opts){return getCFunc(ident)}return(...args)=>ccall(ident,returnType,argTypes,args,opts)};{if(Module["noExitRuntime"])noExitRuntime=Module["noExitRuntime"];if(Module["print"])out=Module["print"];if(Module["printErr"])err=Module["printErr"];if(Module["arguments"])programArgs=Module["arguments"];if(Module["thisProgram"])thisProgram=Module["thisProgram"];var preInit=Module["preInit"];if(preInit){if(typeof preInit=="function")Module["preInit"]=preInit=[preInit];while(preInit.length>0){preInit.shift()()}}}Module["ccall"]=ccall;Module["cwrap"]=cwrap;Module["UTF8ToString"]=UTF8ToString;var _parseMarkdown,__emscripten_stack_restore,__emscripten_stack_alloc,_emscripten_stack_get_current,memory,__indirect_function_table,wasmMemory;function assignWasmExports(wasmExports){_parseMarkdown=Module["_parseMarkdown"]=wasmExports["parseMarkdown"];__emscripten_stack_restore=wasmExports["_emscripten_stack_restore"];__emscripten_stack_alloc=wasmExports["_emscripten_stack_alloc"];_emscripten_stack_get_current=wasmExports["emscripten_stack_get_current"];memory=wasmMemory=wasmExports["memory"];__indirect_function_table=wasmExports["__indirect_function_table"]}var wasmImports={__assert_fail:___assert_fail,__cxa_throw:___cxa_throw,_abort_js:__abort_js,emscripten_resize_heap:_emscripten_resize_heap};async function run(){preRun();var setStatus=Module["setStatus"];if(setStatus){setStatus("Running...");await new Promise(resolve=>setTimeout(resolve,1));setTimeout(setStatus,1,"")}if(ABORT)return;initRuntime();Module["onRuntimeInitialized"]?.();postRun()}var wasmExports;wasmExports=await createWasm();await run(); +-;return Module}})();if(typeof exports==="object"&&typeof module==="object"){module.exports=createMd4cModule;module.exports.default=createMd4cModule}else if(typeof define==="function"&&define["amd"])define([],()=>createMd4cModule); ++var createMd4cModule=(()=>{var _scriptName=globalThis.document?.currentScript?.src;return async function(moduleArg={}){var moduleRtn;var Module=moduleArg;var ENVIRONMENT_IS_WEB=true;var ENVIRONMENT_IS_WORKER=false;var arguments_=[];var thisProgram="./this.program";var scriptDirectory="";var readAsync;if(ENVIRONMENT_IS_WEB||ENVIRONMENT_IS_WORKER){try{scriptDirectory=new URL(".",_scriptName).href}catch{}{readAsync=async url=>{var response=await fetch(url,{credentials:"same-origin"});if(response.ok){return response.arrayBuffer()}throw new Error(response.status+" : "+response.url)}}}else{}var out=console.log.bind(console);var err=console.error.bind(console);var wasmBinary;var ABORT=false;function binaryDecode(bin){for(var i=0,l=bin.length,o=new Uint8Array(l),c;i>8&c}return o}var readyPromiseResolve,readyPromiseReject;var HEAP8,HEAPU8,HEAP16,HEAPU16,HEAP32,HEAPU32,HEAPF32,HEAPF64;var HEAP64,HEAPU64;var runtimeInitialized=false;function updateMemoryViews(){var b=wasmMemory.buffer;HEAP8=new Int8Array(b);HEAP16=new Int16Array(b);HEAPU8=new Uint8Array(b);HEAPU16=new Uint16Array(b);HEAP32=new Int32Array(b);HEAPU32=new Uint32Array(b);HEAPF32=new Float32Array(b);HEAPF64=new Float64Array(b);HEAP64=new BigInt64Array(b);HEAPU64=new BigUint64Array(b)}function preRun(){if(Module["preRun"]){if(typeof Module["preRun"]=="function")Module["preRun"]=[Module["preRun"]];while(Module["preRun"].length){addOnPreRun(Module["preRun"].shift())}}callRuntimeCallbacks(onPreRuns)}function initRuntime(){runtimeInitialized=true;wasmExports["__wasm_call_ctors"]()}function postRun(){if(Module["postRun"]){if(typeof Module["postRun"]=="function")Module["postRun"]=[Module["postRun"]];while(Module["postRun"].length){addOnPostRun(Module["postRun"].shift())}}callRuntimeCallbacks(onPostRuns)}function abort(what){Module["onAbort"]?.(what);what="Aborted("+what+")";err(what);ABORT=true;what+=". Build with -sASSERTIONS for more info.";var e=new WebAssembly.RuntimeError(what);readyPromiseReject?.(e);throw e}var wasmBinaryFile;function findWasmBinary(){return binaryDecode('asm«````````````` `````||`~`|``Tenv __cxa_throwenv\r__assert_failenvemscripten_resize_heapenv _abort_js ¾¼   \r\r    \r\n     \n\n\n\np99€€@ Aã€ ¢memory__wasm_call_ctors\rparseMarkdown__indirect_function_table_emscripten_stack_restore½_emscripten_stack_alloc¾emscripten_stack_get_current¿ PA 8º \r{ !"#$%&\'()*+,~-E@G_mno{-cc¦¶³¨-µ²©-´¯¬-¸-¹-·»¼»» (\n¼è¼ AÈÜB7AÐÜA6AßÜA:AÔÜAµ(6AØÜA¹-:AÙÜA:AëÜA:AàÜAŽ/;AâÜA-:AãÜA:A÷ÜA:AìÜAç(6AðÜAë-:AñÜA:AƒÝA :AøÜA¯ )7A€ÝA· -:AÝA:AˆÝB칻֮سå7A“ÝA:AÝA:AŸÝA:A”ÝA¾(6A˜ÝAÂ/;AšÝA:A ÝA"6A¤ÝB‹€€€€‚€€€7 A£(6 Aœ)7 A: A·ÝA:A¬ÝA¼ (6A°ÝAÀ -:A±ÝA:AÃÝA:A¸ÝAÞ (6A¼ÝAâ -:A½ÝA:AÄÝA"6AÈÝBŽ€€€€‚€€€7 A³)7 A­)7 A:AìÞAôÝ6AÄÞA€€€6AÀÞAã€6A¤ÞA*6AÈÞAÀÜ(6 AÓÜ,AH@AÐÜ(AÈÜ(r ó‹!~#A0k"$@ E@AÈÜ„!AÈ܃!@ AO@AÈÜ…"A€A‰#Ak"$AÈ܃AÈÜAŠ A: Aj Aj‹ Aj$  AÈÜ A k A AA€† AÈÜAÈÜ(AÓÜ,AN  A:,  AG:/  AG:.  AG:-  AG:+  AG:*  AG:)  AG:(  AG:\'A4"A60 B7( B7 B7 B7 B7 B7 A j" 6 ! a"AøÿÿÿO\r@@ A O@ Ar"Aj!  Aÿÿÿÿk6  6  6   : A j! E\r E\r  ü\n  jA: Aj! A\'j!#AÐk"$@ A j"( , " AH"E@A<"B7 Aà6 B7 B7 B7 B7$ B74 B€€€ü7,  6  A j6  (! AéOA4 Aèn" A4OA jA !A<"\nB7 \nAà6 \nB7 \nB7 \nB7 \nB7$ \nB74 \nB€€€ü7,  \nA j6 (!  \n6@ E\r  ("\nAk6 \n\r  (( | Aj! ("! ( "\n G@@@ \nAk("E\r  ("Ak6 \r  (( |  \nAk"\nG\r (!  6 @A€  A€O"\n ( kAuM\r \nAt"!\n  k"@ \n  ü\n   \nj6  \n j6  \n6 E\r r !@@@@ ("\n ("I@ \n (6 \n ("6 @  (Aj6 \nAj!\n  \n ("k"\nAu"Aj"A€€€€O\rAÿÿÿÿ  k"Au"   K AøÿÿÿO"A€€€€O\r At"" \nj" (6  ("6  j!    (Aj6 ( ( ("k"\nAu  Atk! \n@   \nü\n  6  Aj"\n6  6 E\r r  \n6    Aj!\n@ ,AH@ (A: A6  A: A: \nA€” A:$@ ,3AH@ ((A: A6,  A:3 A:( (" (  , AH6 -! -! -! -! -! -! -!\n -! -! B7( A6$ A6 A6 A6 A6 A6  A tA̞AÀ– r \nAtr Atr Atr Atr Atr Atr Atr6 (  , "AH"! ( ! !\n#A€k" $@ A j"(@A! ("E\rA‹    A4jAAüü 6 6 )7 )7 )7 )7$ ( 6, \n60 AA ("Aq6œ A„j"AA€ü A;ã A:® A;à A:ß A: A;¿ A:ª A:¥ A:„ A€„q@ A:‚ A€€q@ A:â A€ q@ A:¨ A€€€q@ A:Á A€€€q@ A:¯ Aq@ A:Ä Aq@ A:¾ A€q@ A:² A€Âq@ A:€ Aq@A!@ A G A GqE@ jA: Ar"AýqA F@  jA: Aj"A€G\r A E\rA  jAk-"A\rF\r A\nF ! A6` A 6D 64 A€€  A€€OAt6X A„jAÿAÔü B7ä B7Ü@AA \n ("E@ AÌj!% A°jAr!& Aüj!\' AèjAr!( A°j!AÜ!A!@@ !@@ (" K@ B7ø B7ð B7èA! A°jAA  A°jF""j   F"#! (¨!) (!@@ j-"A G A Gq\r A|qAj Aj A F! Aj" G\r ! A°jA,A "j Aj #" 6 A°jA$A "j A j #" 6A! A°jA A "j Aj #"$A6@ (”"AL\r (œ! (! !\n@@  Alj"-A>F@ \n O\r O\r j-A>G\r Aj"!@ Aj" O\r@ j-"\nA G \nA Gq\r A|qAj Aj \nA F! Aj" G\r !   kAkA  G"\n6  6 !  \n ( "I\r  \n k"\n6 ! Aj" G\r ! ! 6ä@@ O\r j-A\nk  N\r (!@  Alj-A>F\r Aj" G\r ! % & " Aj #!A!\nA!A! !@A k!A k! („" (üjAk! (¬E! (! ( !\r (œ!! (¤!@@ ("AG@ !   6@ ! ("M@ !  @ " O\r  ( j-!@  j- AÿqG\r Aj" G\r ! @  k \rI\r@  O\r@@  j-A k Aj" G\r ! 6ä A6 A6¨  6ä  F@   (k"A M6 A6 !  (! @@@@ AG\r AL\r  N\rA! A6¤  O\r  j" -A\nk @ Aj  Aäj.AL\r A6¤ AþÿÿÿqAG\r A6 A6  A6 !  @ AG\r  (”G\r A6  (" (œk"A O6 A6¨  A6@@ AL"\r  G\r ( AljAk-"A>G6¨  A6¨ \r ( AljAk-! AÿqA>F\r  G\r (€@ !  („"A H@ !  (ü jAk-AG@ !  A6¬ !  @ Aq@ !    AL\r  (” G\r   Alj"Ak-A>F\r   G\r  (€\r  A H\r  -AG\r  6A AF\r   A$k(k"A M6 Ak ! A6¬ A6¨@ (" !O\r@ AG\r@ -"A-k !  G\r@@@ Aj" O\r j- F\r @@ j-A k Aj" G\r ! 6ä A6 AA A=F6 !   \nI\r -"" E\rA ÀATE\rA! Aj"\n O\r@@@  \nj-" F"\r A F\r A G\r j! \nAj"\n G\r !\n AI\r  AI\r A\nk @@@  N\r  G\r@ Aj  Aàj Aèj/E\r -è"A>F\r  Alj"-G\r (ð" ( K\r (ô" k j"!@ (à" O\r@ j-"A G A Gq\r A|qAj Aj A F! Aj" G\r 6ä   k6  6 !  6ä   k"6  6 O\r@ j-A\nk  !M@  j"6ô A6  Aj"6ô  Ak6  ! @ !O@ (AF@ !  A6  !k6 A6 !  Aj  Aäj Aèj/! (!@@ @ (ä!@ AG\r  G\r@@ O\r j-"A\rF\r -è! A\nG\r AÿqA>F\r   A>! -èA>F\r   Aÿq"A.G A)Gq\r (ìAF\r   (ô" (ðk j"!@ O\r@ j-"A G A Gq\r A|qAj Aj A F! Aj" G\r ! 6ä   k"6  6  ,è6@@@ O\r j-A\nk Aj6ô   !M@  j6ô A6  Aj6ô  Ak6 E@ Aj  j0\r (”! @ (˜" J@ (!  A Av j AL"6˜ ( Als"E@ (("E\rAå (0   6 (”! AÜ   F! Aj! Aj6”  Alj" )ø7  )ð7  )è7 (”! (! (! ! A F  Fq\r (ä!  ! O@ !  @ j"\n-"A#G@ ! AþF\r AàF\r !  Ak"\nF\rA  Aj"j-A#G\r  \nF\rA  Aj"j-A#G\r  \nF\rA  Aj"j-A#G\r  \nF\rA  Aj"j-A#G\r  \nF\rA  Aj"j-A#G\r  \nF\rA  Aj"j-A#G\r  \nF\r Aj!A  @@  j- G\r Aj" G\r !  k"AI@ !  6 @@@  M\r@  j-A G\r Aj" G\r   M\r@@  j-"A\nk @ AàG\r \n-AàG\r !  Aj" G\r ! 6ä A6 A6 $A6 \n A 6 ! " k "AJ@ !  @  M"\n\r (Aq\r  j-A k"AK@ !  A tA“€€q\r !  @ \n\r@  j-"A G A Gq\r Aj" G\r !  6 6ä A6  6  @@@@@  O\r  j-AF\r Aj" O\r A/G\r  j-A>F\r AjM@A!  A! \rAA  A jAE\rA! ( " O\r \r(!@@  j-"A k" AK\rA tA€€qE\r Aj" G\r  AAA A\nF A\rF! Aj$ "6¤@  (AG\r A6¤ ("A€qE\r (AG\r  (O\r ( j-"A-k Aj  Aäj. F@ A6¤ $A6 A6 AüG\r  (”G\r (€"E\r (AG\r Aäj!\n Aàj!A!A!\r@ Aj"(" M\r (" j-AüG\rA! Aj" O\r@  j-A k" AK\rA tA€€qE\r Aj" G\r ! @@@@  I@  ( j-A:Fj!  O\r (" j-A-G\r@@@  j-"A-G\r Aj" G\r \rAj!\r  A! \rAj!\r@  A:Fj" O\r@@  j-"A k"AK\rA tA€€qE\r  Aj"G\r  AüG\rA!A! Aj" O\r@  j-A k"AK\rA tA€€qE\r Aj" I\r !   O\r@  j-A\nk E\r  ! E\r \n 6  \r6A  A E\r  (à6 A\n6  A6 (”"   F  (AF! A€qE\r  jAL\r ( Alj"Ak-"E\r A?K\rB ­†B€€€€ÀƒP\r@ (ä" ("\n Aj"  \nK"O\r (!@ j-"A G A Gq\r Aj" G\r ! Aj" \nO\r (" j-AÛG\r  Aj"j,"E\rAà ATE\r  j-AÝG\r \n Aj"G@ j-A k"AK\rA tA“€€qE\r Ak ( " -Ar: Ak \'  6@ ("O\r (!@ j-A k"AK\rA tA€€qE\r Aj" G\r ! 6ä  6  Aj"6ô  6  6AÜ!A! ! ! Aj1A! Aj A@k2\r -Aq@ Aj AÜj2\r AjA0\r Aj3\rA!#Ak"\r$ \rA6$ Aj" A6@ (€AJ@ \rAøj!@@@@@@ (ø j"("AÿqAk \r AsA\nvAq6 \r /:  \r (6 \r (AsA\nvAq6 \r /:  \r AÿÿK6 \r /: \r (6  \rA6 \r /At(€a"6 /! \r 6 \r At(€6 \r \rAj6 \r \rA$j6 @@ ("A€q@@ A€qE\r Aÿq \rA j (, ("@ ($"E\rA¯ (,   ("Aÿq"AK\rA tAŽ€qE\r (Ak6 (! A€qE\r Aÿq \rA j (, ("@ ($"E\rAŠ (,   @ ("AÿqAk (Œ (Alj" -Ar: (Aj6  \rB7` \rB7X \rB7P \rB7H \rB7@ \rB78 \rB70 \rB7(@ ("AÿqA\nG\r A€€„I\r ("A€ Av"nM\r Aq! Aj!A!\nA!@ "AO@ A|q!A!@  Atj"( j ( j (j (j ( ( ( (jjjk! Aj! Aj" G\r E\r @  Atj"( j (k! Aj! \nAj"\n G\r  n AvO\r  ($"A¨ (,  (  A€~qA r"6 A! \rA6ˆ \rB7€ \rB7x \rB7p \rB7h ("@ (Œ AljAk-AqE! A!@@@@@@@ AÿqAk \r Av6h  A€€I\r ("\n ("j-! ( !@@@  ("O\r@  \nj- AÿqG\r Aj" G\r  M\r@  \nj-A G\r Aj" G\r ! @  M\r@  \njAk-A G\r Ak" K\r ! A!  \nj  kA \rAèj \rAÈj<\r (! !\n@  M\r@  \nj-A k"AMAA tA€€q\r \nAj"\n G\r !\n  j \n kA  \rA(j<\r \r :ˆ (!A!  \rA6l \r Av6h \r (Ak6p  \r Av6h @@@@@@@  Aÿq"A FqE@  \rAèj (, ("\r (! AÿqAk ($"E\rAŠ (,   (!@ AÿÿM@ E\r Aj!@ ( (F@ A j! Ak"\r  @  A lj"A k( Ak(G\r Ak"\r  Ak"E\r Aj! A  ="E\r  A Aj (="E\r  (!@@ Av"Atq"E@A! ($"E\rAÔ (,  r  @ (! ("! ! !@ !@ M\r (!@  j-A-F\r Aj" G\r ! A!  I@ ( jAk-A:F! @ M\r (!@@  j-"\nA-G\r Aj" G\r !  Ar  \nA:F!  At(°:6 Ak! Aj! AJ\r A A (, ("@ ($"E\rAŠ (,  r  A ( (  >"\rA A (, ("@ ($"E\rA¯ (,  r  AI\r@A A (, ("E@ Aj!A!  ($"E\rAŠ (,  r  @ A  Atj"( (  >"\r Aj" G\r A A (, ("E\r ($"E\rA¯ (,  r  r  Aj (?"\r A!  (Aÿq"A Fq\r  \rAèj (, ("E\r ($"@A¯ (,  ! \r \r(X@ \r(Hr \r(Lr \r(Pr \r(8E\r \r((r \r(,r \r(0r \r (! -AkAM@ A l j!  At j!  (Œ (Alj" -Aþq A\nvAqr: (Aj6 Aj" (€H\r A6€A! \rAj$ "\r -Aq@A!A!#A@j"$@ Aj"(`"E\r (pE\r (X AAW@@AA (, ("E@ (`E\r A0j!\n  ($"E\rAŠ (,   @ (X Alj"( E\r B7 B7 B7 B7 \nB7 \nB7  ( 6(  (6,@  ( (A \n Aj<@A!  A A(j (, ("@ ($"E\rAŠ (,    ( (?"\rA A(j (, ("E@A!  ($"E\rA¯ (,  (@ (r ( r (r \r Aj" (`I\r A!AA (, ("E\r ($"@A¯ (,  ! A@k$ \r A!AA (0 ("E\r (("@A¯ (0  !  \n6ä A6 ! @ (ä"Aj" ("O\r (!@@ j"-A\nk @ -A\nk @ -A\nk @  j-A\nk Aj! Aj!  I\r @ I@ (!@A!@ j-A\nk Aj" G\r ! A! A°jA(A "j Aj #"\n 6 " ("AG\r@@ ("M\r (!@  jAk-"A G A Gq\r Ak" K\r  @  O\r (!@  jAk-A#G\r Ak" K\r   F\r ( jAk-"A F@ !  A F@ !  ! -AqE\r \n 6  !@ AkAI\r  ("M\r (!@  jAk-"A G A Gq\r \n Ak"6  I\r @ ( j-A\rFj! I@ ( j-A\nFj! @ )E\r E\r  j"AL\r ( Alj"Ak-A>F\r (ü Ak(j" (A€r6 @ \r  j" (”N\r Aj 0\r AJ@ AjA Al" (j"(" -Aq ( j,A A4\r AjA (ü" -éAq" ( j,A A4\r ( j" -Aýq r: ( j 6 @ (”!@ -AqE\r ( Alj"Ak-A>G\r (AG\r \n(" ("k"AkA K\r (" j"-AÛG\r -A!G\r  jAk-AÝG\r Ak! Aj!A!@@@@ Ak @  j-"A k  AákAÿqAIAÿq -–"A k  AákAÿqAIAÿqG\r Aj" G\r A  @  j-"A k  AákAÿqAIAÿq -\r"A k  AákAÿqAIAÿqG@ A\nk Aj" G\r A  A!@  j-"A k  AákAÿqAIAÿq -\n"A k  AákAÿqAIAÿqG@ A\nG\r  Aj" G\r A  A!@  j-"A k  AákAÿqAIAÿq -ä"A k  AákAÿqAIAÿqG@ A\nG\r -"A k  AákAÿqAIAÿqAÃG\r -"A k  AákAÿqAIAÿqAÁG\r -"A k  AákAÿqAIAÿqAÕG\r -"A k  AákAÿqAIAÿqAÔG\r -"A k  AákAÿqAIAÿqAÉG\r -"A k  AákAÿqAIAÿqAÏG\r -"A k  AákAÿqAIAÿqAÎG\rA  Aj" G\r A ! Ak" -Ar: ( (”AljAk" -AÇq r: A6 (”!  k!@A!@@A ( Alj"-A)kt"AqE@ A!qE\rA! Aj1  („6 Aj  ( ,A4\r AjA (" -Aq ( j,A A4\r  AjAA -"AqA AvAqA4\r Aj" (”H\r @ (E@ Aj1@A!  A!AÜ! -AÀqE\r ( \n(I\r (ŒAj6Œ  (Œ@@ („"Aj" (ˆ"J@ A€ Av j AL"6ˆ (ü s"E@A! (("E\rAå (0   (€"@   (ükj6€ 6ü („"Aj6„  6„ (ü"\rA!   j"A; (Œ! A6  AtAr6 A6Œ @ $(E\r Aj1E\rA!  @@@@ ("Ak\n A! Aj1\r („"Aj" (ˆ"J@ A€ Av j AL"6ˆ (ü s"E@ (("E\r\nAå (0  \n (€"@   (ükj6€ („"Aj6„ 6ü  j  6„ (ü"E\r  j " (AtAÐÍj(" /Atr6 (! A6   Atr6 6€ Aj 5\r Aj1\rA!AÜ!  (€A: (€ /; (€" (A€r6A! Aj 5\r Aj1\rA! (€E@AÜ!  A6 !  (€A\n: (€ /; A 6AA Aj 5!  @  (F\r Aj1E\rA!  @ (€\r !@ Aj"\n(€"Aj" \n(„"J@ \nA€ Av j AL"6„ \n(ø s"E@A \n($"E\rAå \n(, A  \n(ü"@ \n   \n(økj6ü \n 6ø \n \n(€"Aj6€  j  \n 6€ \n(ø"E\r  j " (AtAÐÍj(" /Atr6 (! A6   Atr6 \n 6üA  A E\rA!  Aj 5! A! E\r  A!  (("E\rAŠ \n  (P"@ (T"A!@@ (P Atj("E\r (@" M@   (D (HljI\r r (T! Aj" I\r (P r (H@A!@ (@ Atj"-"Aq (r -  Aq@ ( r Aj" (HI\r (@r (l"@ (p"A!@@ (l Atj("E\r (\\" M@   (` (dljI\r r (p! Aj" I\r (l r (d@A!@ (\\ Alj(r Aj" (dI\r (\\r (8r (xr (ür (r A€j$ @A<"B7 Aà6 B7 B7 B7 B7$ B74 B€€€ü7,  6  A j6  (@ (("E\r ((" ($"G A$j!A!@@@  At"j("(AF@ ($" (("G\r Aj!   k"Au!@@ ((AF\rA! AG@@ Aj!  Atj"((AF@  O\r Ak(!  Atj(! \r \r !  G\r A! A68 B70A!  A! A68 B70 A6L B7D  A0j6@  AÄj6<@@@@@@  At"j"("(AG\r  Ak(A !  Aj"K@  Atj( rAG\r AF\r -A/F\r Aj" G\r ! F\r Aj  k” I@@ Aj ((  ,3AH j-bÀš Aj" G\r  (," ,3" AH"O\r Av! Aj!\n@ (  Aq!@  j"-S@ Aj" I\r  -A>F\r -A/F\r@@ K@ !@@  j"-A=F\r -S\r -A>F\r -A/F\r Aj" I\r !  G\r Aj!  A6P B7H AÈj  k”  I@@ AÈj ((  ,3AH j-bÀš Aj" I\r @@  (, ,3" AH""O\r (  !@@  j"-SE\r Aj" G\r  -A=G\r@@@@@@ "Aj"M\r  j"-S\r B78 A6@@ -A"F\r -A\'F\r !@  j" -S\r -A>F\r -A/F\r Aj" G\r !  Aj"!@ M\r -!@  j- F\r Aj" I\r ! k"  k" I"AøÿÿÿO\r@@ A O@ Ar" Aj!  Aÿÿÿÿk64  6,  60   :7 A,j! E\r E\r   j ü\n jA: ,CAH@ (@ (8r  (46@  ),78   (, ,3" AHIj!  A6@ B78 ! M\r k"  k" I"AøÿÿÿO\r@@ A O@ Ar" Aj!  Aÿÿÿÿk64  6,  60   :7 A,j! E\r E\r   j ü\n jA: ,CAH@ (@ (8r  (46@  ),78  AÈj6\\ A,j \n AÈj AÜj@ (,"Aj" A8jF\r ,C! ,AN@ AN@ (@6 )87  (8 (<™  (8 A8j AH" (<  ˜ ,CAN\r (@ (8r   A¤AƁ"AüÛ6 AˆÜA  !  AÈj6, A8j \n AÈj A,j (8",AH@ A6 (  A: Aj A: ,SAH@ (P (Hr (,! -3!   Aÿq ÀAH""I\r A!A!@ ( ," AHAG\r ( Aj AH"(Aöґ«s -Aïsr\r A:S A:K A»/;H A½-:J Aj!\nA! AÈj"( , " AH""! (  " !@ "AI\r Ak"Aq@ !  (A•ÓÇÞl"Av sA•ÓÇÞl A•ÓÇÞls! ! Aj ! AI\r !@ (A•ÓÇÞl"Av sA•ÓÇÞl (A•ÓÇÞl"Av sA•ÓÇÞl A•ÓÇÞlsA•ÓÇÞls! Aj! Ak"AK\r @@@@ Ak -At s! -At s!  -sA•ÓÇÞl! @ \n("E\r \n( A\rv sA•ÓÇÞl"Av s" Akq i"AM\r   K\r  p " Atj("E\r ("E\r Ak! AK!\n@@@  ("G@@ \nE@  q!   K\r  p!  G\r  ( ," AH" G\r ( Aj  U\r !  ("\r A  ! ,SAH@ (P (Hr E\r ( ," AHE\rA<"B7 A!6 Aà6 B7 B7 B7 A6( B74 B€€€ü7, A:S AŽ/;H A-:J A:K  AÈj6, A8j Aj AÈj A,j@ (8" F\r Aj! Aj! ,! ,AN@ AN@  (6  )7   ( (™   (  AH" (  ˜ ,SAH@ (P (Hr A j! Aj ,AH@ ( (r @ ,3AH@ ((A: A6,  A:3 A:( @ E\r ( ( "F\r Ak(!  6  6 A$j Aj@ ( "E\r ("Ak6 \r (( | A!  A! E\r  ("Ak6 \r  (( |  A! ( ( F\r  ( " (kA I\r@ Ak("E\r  ("Ak6 \r  (( |  Ak6 Aàj$ à%#A k"$@@ E@A!  @@@@@@@@@@@@@@@@ Ak\n \r  A<"B7 A6 Aà6 B7 B7 B7 A6( B74 B€€€ü7,  6Œ  A j6ˆ  )ˆ7    A<"B7 A6 Aà6 B7 B7 B7 A6( B74 B€€€ü7,@ E\r (! A:• AA  AL" ANA0r:” A”ja"AøÿÿÿO\r@@ A O@ Ar"Aj!  Aÿÿÿÿk6€  6ø  6ü   :ƒ Aøj! E\r E\r  A”j ü\n  jA: AÔÜ6˜ A¤j AjAÔÜ A˜j@ (¤"Aj" AøjF\r ,ƒ! ,AN@ AN@  (€6  )ø7   (ø (ü™   (ø Aøj AH" (ü  ˜ ,ƒAN\r (€ (ør  6ô  A j6ð (Aj6  )ð7  Aj ("Ak6 \r (( |  A<"B7 A 6 Aà6 B7 B7 B7 A6( B74 B€€€ü7,  6ì  A j6è  )è7  Aj \r A<"B7 A 6 Aà6 B7 B7 B7 A6( B74 B€€€ü7, A j!@ E\r ("E\r ("E\r AøÿÿÿO\r@ A O@ Ar"Aj!  Aÿÿÿÿk6€  6ø  6ü   :ƒ Aøj! @   ü\n  jA: (ü -ƒ" ÀAH@ AÄÝ Aøj ,ƒAN\r (€ (ør  6ä  6à (Aj6  )à7  Aj ("Ak6 \r (( | A<"B7 A\r6 Aà6 B7 B7 B7 A6( B74 B€€€ü7,  6Ü  A j6Ø  )Ø7  A j A<"B7 A6 Aà6 B7 B7 B7 A6( B74 B€€€ü7, A j!@ E\r ("AF\r Aøj œ A¬Ý Aøj ,ƒAN\r (€ (ør  6Ô  6Ð (Aj6  )Ð7(  A(j ("Ak6 \r\n (( | \n A<"B7 A6 Aà6 B7 B7 B7 A6( B74 B€€€ü7, A j!@ E\r (E\r A”Ý AøjAû" , AH@ ( (r A Ý AøjAûA› -AßqAØF" , AN\r ( (r  6Ì  6È (Aj6  )È70  A0j ("Ak6 \r (( | A<"B7 A6 Aà6 B7 B7 B7 A6( B74 B€€€ü7, A j!@ E\r@ - "E\r A:¥  :¤ AøÜ Aøj A¤j" , AN\r ( (r ("E\r ("E\r AøÿÿÿO\r\n@ A O@ Ar"Aj!  Aÿÿÿÿk6€  6ø  6ü   :ƒ Aøj! @   ü\n  jA: (ü -ƒ" ÀAH@ AˆÝ Aøj ,ƒAN\r (€ (ør  6Ä  6À (Aj6  )À78  A8j ("Ak6 \r (( |  A<"B7 A6 Aà6 B7 B7 B7 A6( B74 B€€€ü7,  6¼  A j6¸  )¸7@  A@k  A<"B7 A6 Aà6 B7 B7 B7 A6( B74 B€€€ü7, A j!@ E\r Aøj (œ A¸Ý6˜ A¤j AjA¸Ý A˜j@ (¤"Aj" AøjF\r ,ƒ! ,AN@ AN@  (€6  )ø7   (ø (ü™   (ø Aøj AH" (ü  ˜ ,ƒAN\r (€ (ør  6´  6° (Aj6  )°7H  AÈj ("Ak6 \r (( |  A<"B7 A6 Aà6 B7 B7 B7 A6( B74 B€€€ü7, A j!@ E\r A:€ BãÞ±›ô­·ô7ø A:ƒ A¤j (œ  Aøj6” A˜j Aj" Aøj A”j@ (˜"Aj" A¤jF\r ,¯! ,AN@ AN@  (¬6  )¤7   (¤ (¨™   (¤ A¤j AH" (¨  ˜ ,¯AH@ (¬ (¤r ,ƒAH@ (€ (ør A"6ø BŒ€€€€‚€€€7ü Aù (6 Añ )7 A: A¤j (œ  Aøj6” A˜j  Aøj A”j@ (˜"Aj" A¤jF\r ,¯! ,AN@ AN@  (¬6  )¤7   (¤ (¨™   (¤ A¤j AH" (¨  ˜ ,¯AH@ (¬ (¤r ,ƒAH@ (€ (ør A"6ø BŒ€€€€‚€€€7ü Aì (6 Aä )7 A: A¤j (œ  Aøj6” A˜j  Aøj A”j@ (˜"Aj" A¤jF\r ,¯! ,AN@ AN@  (¬6  )¤7   (¤ (¨™   (¤ A¤j AH" (¨  ˜ ,¯AH@ (¬ (¤r ,ƒAN\r (€ (ør  6   6œ (Aj6  )œ7P  AÐj ("Ak6 \r (( |  A<"B7 A6 Aà6 B7 B7 B7 A6( B74 B€€€ü7,  6˜  A j6”  )”7X  AØj  A<"B7 A6 Aà6 B7 B7 B7 A6( B74 B€€€ü7,  6  A j6Œ  )Œ7`  Aàj  A<"B7 A6 Aà6 B7 B7 B7 A6( B74 B€€€ü7,  6ˆ  A j6„  )„7h  Aèj  A<"B7 Aà6 B7 B7 B7 A6( B74 B€€€ü7, AA AF6 @ E\rAš\n! (Ak"AM@ At(Ð! A:ƒ A:ý Aá\r(6ø Aå\r-:ü a"AøÿÿÿO\r@@ A O@ Ar"Aj!  Aÿÿÿÿk6¬  6¤  6¨   :¯ A¤j! E\r E\r   ü\n jA:  Aøj6” A˜j Aj Aøj A”j@ (˜"Aj" A¤jF\r ,¯! ,AN@ AN@ (¬6 )¤7  (¤ (¨™  (¤ A¤j AH" (¨  ˜ ,¯AH@ (¬ (¤r ,ƒAN\r (€ (ør  6€  A j6|  (Aj6  )|7p  Aðj  ("Ak6 \r  (( |  A:$ ,3AH@ ((A: A6,  A:3 A:( A j$    #Ak"$@ ( ," AHE\r ( " (F\rA<"B74 B€€€ü7, A6 Aà6 B7 B7 B7$  Aj")7  (6 A: A: Ak(!  6  A j6 A$j Aj@ ( "E\r  ("Ak6 \r  (( | ,AH@ (A: A6  A: A: Aj$ ª~A!@@@ (Ak ( ," AH""E\r  ( Aj "j! !@@ 1"B?V\rB †B€Ì€€ƒP\r Aj" G\r  kAF A!  ¹  #Ak"$ ("(" ("kAu!@  F\r@  Atj(E\r Aj" G\r ! @@ " M\r  Ak"Atj(\r A<"B7 A6 Aà6 B7 B7 B7 A6( B74 B€€€ü7,  6  A j6 A0j!  At"j!  At"j!@  kAu" (" ("kAuM@ (" k"Au I@  j!  G@@ (! ("@  (Aj6  6 (!  6@ E\r  ("Ak6 \r  (( | Aj! Aj" G\r (! !  G@@  (6  ("6 @  (Aj6 Aj! Aj" G\r    kj6   G@@ (! ("@  (Aj6  6 (!  6@ E\r  ("Ak6 \r  (( | Aj! Aj" G\r (!  G@@@ Ak("E\r  ("Ak6 \r  (( |  Ak"G\r  6  @ ! (" G@@@ Ak("E\r  ("Ak6 \r  (( |  Ak"G\r ( (!  6 r A6 B7A! @ A€€€€O\rAÿÿÿÿ Au"  K AøÿÿÿO"A€€€€O\r  At""6  6   j6  G@@  (6  ("6 @  (Aj6 Aj! Aj" G\r  6   ( Aj@ ( "E\r  ("Ak6 \r  (( | ("(! (!  G@@@ Ak("E\r  ("Ak6 \r  (( |  Ak"G\r  6 Aj$ A„  )A¤€"A€Ú6 A”Ú6 AèÚA  ~ AL@ (" ("kAu N@  k"Au"\n H@@   j" F@ !  ! !@  (6  ("6 @  (Aj6 Aj! Aj" G\r 6 \nAL@  At"j! !   k"K@ !@  (6  (6 B7 Aj! Aj" I\r 6  G@@ Ak")! B7 Ak(! Ak" 7@ E\r  ("Ak6 \r  (( |  G\r  F@ !@ (! ("@  (Aj6  6 (!  6@ E\r  ("Ak6 \r  (( | Aj! Aj" G\r   At"j! !   k"K@ !@  (6  (6 B7 Aj! Aj" I\r 6  G@@ Ak")! B7 Ak(! Ak" 7@ E\r  ("Ak6 \r  (( |  G\r  Atj! !@ (! ("@  (Aj6  6 (!  6@ E\r  ("Ak6 \r  (( | Aj! Aj" G\r  @  ("kAu j"A€€€€I@Aÿÿÿÿ  k"Au"   I AøÿÿÿO" A€€€€O\r AtA "  kj! At"Ak"Aq   (6  ("6 @  (Aj6 Aj! Aj !  j! @@  (6  ("6 @  (Aj6  (6  ( "6 @  (Aj6 Aj! Aj" G\r ( k"@   ü\n 6   ("k"k! @   ü\n  j6 6 ( Atj6 @ r    Ý#A k"$ ($" (("G@@ ( Aj" G\r @ (AG\r ($" (("F\r !@@@@@ (("  k"AH\r  "6  6   j6 A6 B7  Aj6  Aj6@ Aj!@@ (("  Aj!    Aj"G\r  Aj" G\r    ($"@ " (("G@@@ Ak("E\r  ("Ak6 \r  (( |  Ak"G\r ($! 6( (, r (6$ (6( (6, A6 B7 ("E\r " ( "G@@@ Ak("E\r  ("Ak6 \r  (( |  Ak"G\r (!  6 ( r ("E\r " ("G@@@ Ak("E\r  ("Ak6 \r  (( |  Ak"G\r (!  6 ( r A j$ ¶ ~#A k"$@@@ ($" (("F\r@ ("("A G A GqE@  Aj" G\r ((" ($"F\r A$j! @  \nAt"\rj("(" A\rkAO \nAjA!@ ($" (("F\r !@@@ ("(AG\r (( ($"kAG\r ((A!F\r Aj" G\r  A6 B7 A6 B7@@@ ("(AG\r (( ($"kAG\r ((A!G\r (" ( "G@A<"B7  6 Aà6 B7 B7 B7 B€€€€€€€À?7(  6  A j6  (68  64  60 A6 B7 Aj @ ("E\r  ("Ak6 \r  (( | (($! (" ("I@  (6  ("6 @  (Aj6  Aj6   ("k"Au"Aj"A€€€€O\rAÿÿÿÿ  k"Au"   I AøÿÿÿO"A€€€€O\r At"" j" (6  ("6  j!    (Aj6 ( ( k"Au  Atk! @   ü\n  6  Aj"6  6 @ r  6  Aj  Aj" G\r @ (" ( "F\rA<"B7  6 Aà6 B7 B7 B7 B€€€€€€€À?7(  6  A j6  (68  64  60 A6 B7 Aj  ("E\r  ("Ak6 \r  (( | ($ \rj" ! Aj" (("G@@ )! B7 (!  7@ E\r  ("Ak6 \r  (( | Aj! Aj" G\r ((!  G@@@ Ak("E\r  ("Ak6 \r  (( |  Ak"G\r 6( (" ("  kAu (" ! ( k! ("@ " ( "G@@@ Ak("E\r  ("Ak6 \r  (( |  Ak"G\r (!  6 ( r (! @ " ("G@@@ Ak("E\r  ("Ak6 \r  (( |  Ak"G\r (!  6 ( r Au! ($! ((!  \nj !\n \n  kAuI\r A j$   Þ#Ak"$ @ ("E\r ( " (F\r Ak(! ("@  (Aj6 Aj!  6  6 A$j Aj@ ( "E\r ("Ak6 \r (( |   @ ("E\r  ("Ak6 \r  (( | Aj$ ¢  } ( , " AH""! (  " !@ "AI\r Ak"Aq@ !  (A•ÓÇÞl"Av sA•ÓÇÞl A•ÓÇÞls! ! Aj ! AI\r !@ (A•ÓÇÞl"Av sA•ÓÇÞl (A•ÓÇÞl"Av sA•ÓÇÞl A•ÓÇÞlsA•ÓÇÞls! Aj! Ak"AK\r @@@@ Ak -At s! -At s!  -sA•ÓÇÞl! A\rv sA•ÓÇÞl"Av s! @ ("E\r (  Akq i"AM\r   K\r  p "Atj("E\r ("E\r Ak!\n AK! @@  ("G@@ E@  \nq!   K\r  p!  F\r  ( ," AH G\rA ( Aj AH UE\r ("\r A " 6 A6 Aj!@ (", AN@  (6  )7  (! (!\n#Ak" $@@@ \n‘@ " \n’  \nA÷ÿÿÿK\r Aj \nŒAj“ (   ("6  (   \n6   \nAj‰ Aj$   A6 B7 *!\r ( Aj³!@ @ \r ³” ]E\r @A  AkqAG AIr Atr"  \r•ü"  K"AF\r   AkqE\r w " ("M@  M\r ( ³ *•ü! @ AI\r iAK\r AA Akgkt AI  w "  K" O\r !@@ "@ A€€€€O\r At"! (!  6 @ ( r (!  6 @ A ü ("E\r Aj! (!@ i" AM@  Akq!   I\r  p! Atj 6 ("E\r Ak!\n AK! @ (!@ E@  \nq!   I\r  p! @  F@ !  Atj" (E@ 6 ! !   (6  ((6 ( 6 ("\r  (! A6 @ ( r A6   (" Ak"qE@  q!   K@ !   p! @ (" Atj"("E@  (6  6  Aj6 ("E\r (!@  Ak"qE@  q!   K\r  p!  Atj 6   (6  6  ( Aj6 A : 6 ¡#Ak"$  6 Aj Aj  Aj@ ("Aj" F\r , ! ,AN@ AN@  (6  )7   ( (™   (  AH" (  ˜ Aj$ } a"AøÿÿÿI@@@ A O@ Ar"Aj! Aÿÿÿÿk6 6 6  : ! E\r E\r   ü\n  jA:   o ("@@ (! ,AH@ ( (r ,AH@ ( (r r "\r (! A6 @ ( r  (" ("I@  (6  (6 B7 Aj6 @  ("k"Au"Aj"A€€€€I@Aÿÿÿÿ  k"Au"   I AøÿÿÿO" A€€€€O\r AtA " j" (6  (6 B7  Atk! @   ü\n  Atj6 Aj"6 6 @ r 6   Î#Ak"$ @ ("E\r ( ( "F\r Ak(! ("@ (Aj6  6  6 A$j Aj ( "E\r ("Ak6 \r (( | @ ("E\r  ("Ak6 \r  (( | Aj$ Ý#A0k"$ ("(" ("G@ !@@@ (@  Aj"G\r  A<"B7 A6 Aà6 B7 B7 B7 A6( B74 B€€€ü7,  6  A j6 A: A¢\n(6 A¦\n-: A: A: AôäÕ«6 A:  A j6$ A(j Aj A j A$j@ (("Aj" F\r ,AN@  (6  )7   A˜ , AN\r ( (r ,AH@ ( ( r (! (0"@ " (4"G@@@ Ak("E\r  ("Ak6 \r  (( |  Ak"G\r (0!  64 (8 r A68 B70  (60  (64  (68 A6 B7 ( Aj ("E\r  ("Ak6 \r  (( |  @ (  Aj" G\r ("(" ("G@@@ Ak("E\r  ("Ak6 \r  (( | Ak"G\r  6 A0j$ AßÜ,AH@AÜÜ(AÔÜ(r AëÜ,AH@AèÜ(AàÜ(r A÷Ü,AH@AôÜ(AìÜ(r AƒÝ,AH@A€Ý(AøÜ(r A“Ý,AH@AÝ(AˆÝ(r AŸÝ,AH@AœÝ(A”Ý(r A«Ý,AH@A¨Ý(A Ý(r A·Ý,AH@A´Ý(A¬Ý(r AÃÝ,AH@AÀÝ(A¸Ý(r AÏÝ,AH@AÌÝ(AÄÝ(r Aà6  Aà6 r ¬#Ak"$ A j"($"@ " (("G@@@ Ak("E\r ("Ak6 \r (( |  Ak"G\r ($!  6( (, r Aj ,AH@ (  (r Aj$  r µ  (!@@@@@@@@@@@@ ( Ak  Aj"M@  6A Ak! (!@@ ! !@ j"-"AF\r @ Aj" O\r -"A k  AákAÿqAIAÿqAÓG\r -"A k  AákAÿqAIAÿqAÃG\r -"A k  AákAÿqAIAÿqAÒG\r -"A k  AákAÿqAIAÿqAÉG\r -"A k  AákAÿqAIAÿqAÐG\r -"A k  AákAÿqAIAÿqAÔG\r  j-A>F\r @ Aj" O\r -"A k  AákAÿqAIAÿqAÓG\r -"A k  AákAÿqAIAÿqAÔG\r -"A k  AákAÿqAIAÿqAÙG\r -"A k  AákAÿqAIAÿqAÌG\r -"A k  AákAÿqAIAÿqAÅG\r  j-A>F\r A\nj" O\r -"A k AákAÿqAIAÿqAÔG\r -"A k AákAÿqAIAÿqAÅG\r -"A k AákAÿqAIAÿqAØG\r -"A k AákAÿqAIAÿqAÔG\r -"A k AákAÿqAIAÿqAÁG\r -"A k AákAÿqAIAÿqAÒG\r -"A k AákAÿqAIAÿqAÅG\r - "A k AákAÿqAIAÿqAÁG\r  j-A>F\r\r Aj" G\r !  6A Aj" O\r Ak! (!@@  j"-A\nk\n\n /A­Ús -A>srE\r Aj! Aj!  I\r !  Aj" O\r Ak! (!@@  j"-A\nk /A¿üF\r Aj! Aj!  I\r !  Aj" O\r Ak! (!@@@  j-"A\nk  A>F\r Aj! Aj!  I\r !  Aj" O\r Ak! (!@@  j"-A\nk /Aݺs -A>srE\r Aj! Aj!  I\r !  @@  O\rA! ( j-A\nk  6 ( !   6A  6A  6A  6A  6A  Aj6A Å@  ("O\r  (˜O\r @ (" j-"E\r A>F@ A>:  6  -Aüq: Aj! Aj  A?K\rB ­†B€€€€€ ƒP\r  Aj"K@  j-A k"AK\rA tA“€€qE\r  :  6  -Aüq: Aj  A! A6  A j"  I"\nO\rA! !@@ j-" A0kAÿqA K\r  A\nljA0k"6 Aj" \nG\r \n! M\r O\r@ j-" A)k  Aj"K@  j-A k"AK\rA tA“€€qE\r  :  6  -Aüq:  k jAj 6  6A!  ç  ("H@@A!@@@A (Œ Alj"Ak"-A)kt"AqE@ A!qE\rA! Ak(!A! Ak-Aq@ ( j,! A! A  A4\r A ,A4\r  AA Ak-"AqA AvAqA4E\rA!  (Ak"6  H\r A Þ#A0k"$@ (ü"E\r@@@ ("AÿqAk A€qE\r (" (O\r ( j-AÛG\r@ ("E\r Aj! AG\r !  @ \r@ A/G\r Aj" O\r \n j-A>F\r AL\r@ AßqAÁkAÿqAI\r AßF\r A:G\r  Aj"M@A!  @@@ \nj,"A0kAÿqA\nI\r A_qAÁkAÿqAI\r E@A!  A ATE\r A!  Aj"G\r  A!  A! @@ Ak A=G\r Aj!A!  A+!@@@@ A"k E\r AÑ AT\r A\rF\r A)!  A*! Aj! I\r A! E\r \rF\rA  A)FA !  Aj" Atj"(! (" I\r   M@A A!  Aj6 „#Ak"$@  O\r@@@ j"  k A j7"AÿK@A!A!@@@  jAm"AtA°j("\nAu j"AtA°j(Aÿÿÿq K@ Ak!   \nAvAq j"AtA°j(AÿÿÿqM\r Aj!  L\r  AH\r  A F\r A F\r A kAI\r -A\nk ( j" I\r Aj$  à@@@ AÿK@A­!@@@  jAm"AtAÐj("Au j"AtAÐj("Aÿÿÿq K@ Ak!  AvAq j"AtAÐj(AÿÿÿqM\r Aj!  N\r  AH\rAà9!  A6  A r AÁkAI6 A!A4!@@@  jAm"AtAÐ2j("Au j"AtAÐ2j("Aÿÿÿq M@ AvAq j"AtAÐ2j(Aÿÿÿq O@ AH\rAð9!  Aj!  Ak!  N\r A!A!@@@  jAm"AtAà7j("Au j"AtAà7j("Aÿÿÿq M@ AvAq j"AtAà7j(Aÿÿÿq O@ AH\rA€:!  Aj!  Ak!  N\r A6  6  ( ( " lAtj! At"@   ü\n  6 F\r ( Aÿÿÿq"AjF@  sAsAq j6  ( kj6 õ #A k"$ B7 B7 B7 B7 A B! A B! @@@  K@   M\r @ \nE@   AjF!A! AqE@   F! A!  Atj( Aj Atj(k"\r Aj" ( " I! Aj" (" I!\n  K\r  I\r  K\r  I\r A! A j$  iA!@ ("(" ("("I\rA!  K\r ( ( ( (D"\rA! (" ("I\r K!  ‰#Ak"$@@@  O\r j  k A j7! ( j! AÿK@A!@@@  jAm"AtA°j("\nAu j" AtA°j(Aÿÿÿq K@ Ak!   \nAvAq j"AtA°j(AÿÿÿqM\r Aj!  L\r  AN\r  A F\r A F\r A kAO\r A6 A 6  B!   C Aj$  D ("(" ("("I@A  K@A ( ( ( (D ž“#A0k"$ A6x#Ak"$ AjAA€ü @@@ E\r Ak! A€j!\r  AtjAk!@  Atj"(!@ !@ !@ Aj" ("O@ !  (! @ \r  j"-j-@ !  \r -j-@ !  \r -j-@ !  \r  j-j-@ !  Aj! Aj"!  K\r @  K@ (!@ \r  j"-" j-\r Aj" G\r Aj" I\r  @AA@@@@@@ AÜF@A! Aj" (O\r@  j-"A!kAÿqAI\r@ AÿqA\nku Aj I\r Aj! @@ (x" (|"N@ AÀ Av j AL"6| (t Als"\rA! ($"\rE\rAå (, \r  Aj6x (t"\r  6t (x"Aj6x  Alj"AÜ ; B7  Aj"6  6 A!@@@@@@@@@@@@ A!k\\ \r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\n\r \r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r\r Aj"!\n@  M\r@  \nj- G\r \nAj"\n I\r !\n A!A!A!  (F\r Q"AÿM\rA! A!@@@@  jAm" AtA°j("Au j"AtA°j(Aÿÿÿq K@ Ak!   AvAq j" AtA°j(AÿÿÿqM\r Aj!  N\r  A! AN\r A! Aä!@@@  jAm" AtAÀ:j("Au j"AtAÀ:j(Aÿÿÿq K@ Ak!   AvAq j" AtAÀ:j(AÿÿÿqM\r Aj!  N\r A! AN   k! !@@  j-" AàG\r Aj" G\r !A  A F ! !  k"A K\r At j( "  AtjAk(O\rA!A!@ E@ ! !\n  !\n  " I\r @@@@ \nO\r@  j-"A G@ AàF\rA! Aj" \nG\r A! \n  ! \nO@A! !  @  j-AàG" @ ! !  Aj" \nG\r ! \n! @  k"F@A!\n  Atj" (" I@  jAk-A F!\n As  Gq\r \r F \nrE\r E\r Aj  @ F\rA! AK\r At jA j" (" K6 \r  Aj"M@A!   Atj"\n(! \n(!\n  ( ! \n@ Ak!  Ak(" ("O\r@  j-"A G A Gq\r Aj" G\r ! @@ (x" (|"N@ AÀ Av j AL"6| (t Als"\rA! ($"\rE\rAå (, \r  Aj6x (t"\r  6t (x"Aj6x  Alj"Aà; B7  6  6@@ (x" (|"N@ AÀ Av j AL"6| (t Als"\rA! ($"\rE\rAå (, \r  Aj6x (t"\r  6t (x"Aj6x  Alj"Aà; B7  6  6 (t (x"Alj"Ak" Ak6 A(k" Ak6  -Ar:  -Ar:A! ! ! ! (M\r@@@@   jAv"Atj"( K@ E\r  AtjAk( I\r Ak!  (K\r ! ! !  Aj! A!  M\r ! !  @@ (x" (|"N@ AÀ Av j AL"6| (t Als"\rA! ($"\rE\rAå (, \r  Aj6x (t"\r  6t (x"Aj6x  Alj"A¦; B7  Aj"6  6  (x"AL\r (t" AljAk-A&G\r (|" L@ AÀ Av j AL"6|  Als"E@A! ($"\rE\rAå (, \r  6t (x! Aj6x  Alj"A»; B7  Aj6  6  ("\n - AÀq\r\n   k"  \n A jA\r (" AjM\r -A!G\r  Aj"j-A-G\r -A-G\r A! (è" K  \nAkOq\r @ Aj" \n \nI"M@  j" /A­Ús -A>srE\r\r@@ Aj! Aj" K\r " j" /A­Ús -A>sr\r  !  \nI Aj" Kq@  Atj"(! (!  6è (! A! -AqE\r Aj" O\r  j-AüG\r@@ (x" (|"N@ AÀ Av j AL"6| (t Als"\rA! ($"\rE\rAå (, \r  Aj6x (t"E\r  6t (x"Aj6x  Alj"Aü; B7  Aj"6  6  ("Aj K\r  Ak"j"/A÷îs -A÷sr\r @  F\r Q"AÿK@A!A!@@@@  jAm" AtA°j("\nAu j"AtA°j(Aÿÿÿq K@ Ak!   \nAvAq j"AtA°j(AÿÿÿqM\r Aj!  N\r  AN\r A!Aä!@@@  jAm" AtAÀ:j("\nAu j"AtAÀ:j(Aÿÿÿq K@ Ak!   \nAvAq j"AtAÀ:j(AÿÿÿqM\r Aj!  N\r  AH\r\r  A F\r A F\r A\rkA}K\r A!kAI\r A:kAI\r AÛkAI\r AÿG AúKqE\r @@ (x" (|"N@ AÀ Av j AL"6| (t Als"\rA! ($"\rE\rAå (, \r  Aj6x (t"E\r  6t (x"Aj6x  Alj"A®; B7  Aj"6  6@@ (x" (|"N@ AÀ Av j AL"6| (t Als"\rA! ($"\rE\rAå (, \r  Aj6x (t"E\r  6t (x"Aj6x  Alj" (6 (! AÄ; B7  6  Aj!@@ (" Aj K\r  Ak"\nj(AèèуG\r  M\r /A¯ÞF\r @ Aj K\r  Ak"\nj"(Aèèуs -Aósr\r  M\r /A¯ÞF\r Aj K\r  Ak"\nj"/Aæès -Aðsr\r  M\r /A¯ÞG\r @@ (x" (|"N@ AÀ Av j AL"6| (t Als"\rA! ($"\rE\rAå (, \r  Aj6x (t"E\r  6t (x"Aj6x  Alj"Aº; B7  6  \n6@@ (x" (|"N@ AÀ Av j AL"6| (t Als"\rA! ($"\rE\rAå (, \r  Aj6x (t"E\r  6t (x"Aj6x  Alj" (6 (! AÄ; B7  6 Aj!  (Aj K\r @ Ak-"AßqAÁkAÿqAO@ Aj O\r A0kAÿqA\nO\r  Aj O\r\n  Aj"j-"A0kAÿqA\nO@ A_qAÁkAÿqAK\r\n @@ (x" (|"N@ AÀ Av j AL"6| (t Als"\rA! ($"\rE\rAå (, \r  Aj6x (t"E\r  6t (x"Aj6x  Alj"AÀ; B7  6  6@@ (x" (|"N@ AÀ Av j AL"6| (t Als"\rA! ($"\rE\rAå (, \r  Aj6x (t"E\r  6t (x"Aj6x  Alj" (6 (! AÄ; B7  6 Aj!  @@ (x" (|"N@ AÀ Av j AL"6| (t Als"\rA! ($"\rE\rAå (, \r  Aj6x (t"E\r  6t (x"Aj6x  Alj"AÝ; B7  Aj"6  6 \r @@ (x" (|"N@ AÀ Av j AL"6| (t Als"\rA! ($"\rE\rAå (, \r  Aj6x (t"E\r  6t (x"Aj6x  Alj" 6A!A  Aj" O\r  j-AÛG\r@@ (x" (|"N@ AÀ Av j AL"6| (t Als"\rA! ($"\rE\rAå (, \r  Aj6x (t"E\r  6t (x"Aj6x  Alj" 6A!!A !  :  : B7   j"6@@ (x" (|"N@ AÀ Av j AL"6| (t Als"\rA! ($"\rE\rAå (, \r  Aj6x (t"E\r  6t (x"Aj6x  Alj"AÄ; B7  6  6@@ (x" (|"N@ AÀ Av j AL"6| (t Als"\rA! ($"\rE\rAå (, \r  Aj6x (t"E\r  6t (x"Aj6x  Alj"AÄ; B7  6  6 \n @@ E@  -\rA qAvq\r  E\r @@ (x" (|"N@ AÀ Av j AL"6| (t Als"\rA! ($"\rE\rAå (, \r  Aj6x (t"E\r  6t (x"Aj6x  Alj"A:  : B7  Aj"6  6 \n !@@@@@@@@@AA@@AA@@@@@@@@@@ > AþF\r AÞG\r -AqE\r Aj" !@  M\r@@  j-AÞG\r Aj" I\r ! !  kAG\r A! A!  j ( kA7"AÿM@ A F\r A F\r A kAM\rA  @@@  jAm"AtA°j("Au j"\nAtA°j(Aÿÿÿq K@ \nAk!   AvAq j"AtA°j(AÿÿÿqM\r Aj!  N\r A  \nAN\rA  A !  (F\r Q"AÿM\rA!A!@@@  jAm" AtA°j("Au j"\nAtA°j(Aÿÿÿq K@ \nAk!   AvAq j"AtA°j(AÿÿÿqM\r Aj!  N\r !  \nAN\r !  -A qE\r@ Aj" O\r  j" -A=F\r !  kAG\rA!A!AA ( kA7"AÿM@A! A F\r A F\r A kAI  @@@  jAm" AtA°j("Au j"\nAtA°j(Aÿÿÿq K@ \nAk!   AvAq j"AtA°j(AÿÿÿqM\r Aj!  N\r A!\n \nAN !  -A€qE\r@ Aj" O\r  j" -A+F\r !  kAG\rA!A!AA ( kA7"AÿM@A! A F\r A F\r A kAI  @@@  jAm" AtA°j("Au j"\nAtA°j(Aÿÿÿq K@ \nAk!   AvAq j"AtA°j(AÿÿÿqM\r Aj!  N\r A!\n \nAN !  A! Aj"\n!@  \nM"\r@  j-AþG" \r Aj" I\r ! @  k"AF@ ( "A€€qE\r@ \rA! A!  \nj ( \nkA7"AÿM@ A F\r A F\r A kAM\rA  @@@  jAm"AtA°j(" Au j"AtA°j(Aÿÿÿq K@ Ak!   AvAq j"AtA°j(AÿÿÿqM\r Aj!  N\r A  AN\rA  A !  (F\r Q"AÿM\r\rA!A!@@@  jAm" AtA°j(" Au j"AtA°j(Aÿÿÿq K@ Ak!   AvAq j"AtA°j(AÿÿÿqM\r Aj!  N\r !  AN\r !  ! AK\r ( ! ! A€qE\rA!  (M\r\n Q"AÿM\rA!\nA!@@@@  \njAm"AtA°j(" Au j" AtA°j(Aÿÿÿq K@ Ak!   AvAq j"\nAtA°j(AÿÿÿqM\r \nAj!\n  \nN\r  AN\r A!\nAä!@@@  \njAm"AtAÀ:j(" Au j" AtAÀ:j(Aÿÿÿq K@ Ak!   AvAq j"\nAtAÀ:j(AÿÿÿqM\r \nAj!\n  \nN\r A! AN @ Aj" O" E@  j-A$F\r !  kAK\rA!  (M\r Q"AÿM\rA!\nA!@@@@  \njAm"AtA°j(" Au j" AtA°j(Aÿÿÿq K@ Ak!   AvAq j"\nAtA°j(AÿÿÿqM\r \nAj!\n  \nN\r  AN\r A!\nAä!@@@  \njAm"AtAÀ:j(" Au j" AtAÀ:j(Aÿÿÿq K@ Ak!   AvAq j"\nAtAÀ:j(AÿÿÿqM\r \nAj!\n  \nN\r A! AN  @ Aj" O\r@  j-A k"AK\rA tA€€qE\r Aj" I\r ! A F@ !  kAI\r @@ (x" (|"N@ AÀ Av j AL"6| (t Als"\rA! ($"\rE\r!Aå (, \r ! Aj6x (t"E\r  6t (x"Aj6x  Alj"A:  : B7  6  6  @@ (x" (|"N@ AÀ Av j AL"6| (t Als"\rA! ($"\rE\r Aå (, \r Aj6x (t"E\r  6t (x"Aj6x  Alj"A€ ; B7  Aj"6  6  A! !  kAG\r A! !  kAG\r A F\r A F\r A\rkA}K\r AÿG AúKq A!kAI A:kAIr AÛkAIrr ! @ \rA! A!@  j" ( k" A7"AÿM@ A F\r A F\r A kAO\r  @@@  jAm"AtA°j("Au j"\nAtA°j(Aÿÿÿq K@ \nAk!   AvAq j"AtA°j(AÿÿÿqM\r Aj!  N\r  \nAN\r A!Aä! A7"AÿM@ AÿG AúKq A!kAI A:kAIr AÛkAIrr  @@@  jAm" AtAÀ:j("Au j"\nAtAÀ:j(Aÿÿÿq K@ \nAk!   AvAq j"AtAÀ:j(AÿÿÿqM\r Aj!  N\r A!\n \nAN ! !  Aq "E\r @@ (x" (|"N@ AÀ Av j AL"6| (t Als"\rA! ($"\rE\rAå (, \r  Aj6x (t"E\r  6t (x"Aj6x  Alj" : A$: B7  6  6  A F\r A F\r A\rkA}K\r AÿG AúKq A!kAI A:kAIr AÛkAIrr ! @ E\rA! A!@  j" ( k" A7"AÿM@ A F\r A F\r A kAO\r  @@@  jAm"AtA°j("Au j"\nAtA°j(Aÿÿÿq K@ \nAk!   AvAq j"AtA°j(AÿÿÿqM\r Aj!  N\r  \nAN\r A!Aä! A7"AÿM@ AÿG AúKq A!kAI A:kAIr AÛkAIrr  @@@  jAm" AtAÀ:j("Au j"\nAtAÀ:j(Aÿÿÿq K@ \nAk!   AvAq j"AtAÀ:j(AÿÿÿqM\r Aj!  N\r A!\n \nAN ! !  Aq "E\r @@ (x" (|"N@ AÀ Av j AL"6| (t Als"\rA! ($"\rE\rAå (, \r  Aj6x (t"E\r  6t (x"Aj6x  Alj" : Aþ: B7  6  6  A F\r A F\r A kAM\r !  A! ! AqE\r @@ (x" (|" N@ AÀ Av j AL"6| (t Als" \rA! ($"\rE\rAå (, \r  Aj6x (t" E\r  6t (x"Aj6x Alj" : Aþ: B7  \n6  6  @@  (F\r Q"AÿK@A!A!@@@  jAm" AtA°j("Au j"\nAtA°j(Aÿÿÿq K@ \nAk!   AvAq j"AtA°j(AÿÿÿqM\r Aj!  N\r !  \nAN\r !  A F\r A F\r A kAM\r !  A! ! AqE\r\r @@ (x" (|" N@ AÀ Av j AL"6| (t Als" \rA! ($"\rE\rAå (, \r  Aj6x (t" E\r  6t (x"Aj6x Alj" : A+: B7  6  6 \r @@  (F\r Q"AÿK@A!A!@@@  jAm" AtA°j("Au j"\nAtA°j(Aÿÿÿq K@ \nAk!   AvAq j"AtA°j(AÿÿÿqM\r Aj!  N\r !  \nAN\r !  A F\r A F\r A kAM\r !  A! ! AqE\r @@ (x" (|" N@ AÀ Av j AL"6| (t Als" \rA! ($"\rE\rAå (, \r  Aj6x (t" E\r  6t (x"Aj6x Alj" : A=: B7  6  6 A F\r A F\r A kAM\r !  A! ! AqE\r @@ (x" (|" N@ AÀ Av j AL"6| (t Als" \rA! ($"\rE\rAå (, \r  Aj6x (t" E\r  6t (x"Aj6x Alj" : AÞ: B7  6  6 @ Aj" O\r -A?G\rA!@ (ì" M@ !  !  \nAkO\r  Aj" \n \nI"M@  j/A¿üF\r@@ Aj! Aj" K\r " j/A¿üG\r  !  \nI Aj"Kq  Atj"(! (!  6ì ( ! @ M\r -A!G\r  j-AßqAÁkAÿqAK\r@ Aj" O\r@  j-AßqAÁkAÿqAK\r Aj" G\r ! A! (ð" K  \nAkOq\r Aj" \n \nI"M@  j-A>F\r@@ Aj! Aj" K\r " j-A>G\r  !  \nI Aj" Kq  Atj"(! (!  6ð ( ! @ A j" O\r )B¼ÂìšÄ¨ªÁ… 1Bۅ„BR\rA! (ô" K  \nAkOq\r@ Aj" \n \nI" M@  j"/Aݺs -A>srE\r@@ Aj! Aj" K\r  "j"/Aݺs -A>sr\r  !  \nI Aj"Kq@  Atj"(! (!  6ô ( !\n ! Aj" \nO\r@ Aj" \nO\r  j-" A0kAÿqA\nIA AßqAÁkAÿqAI E\r@  j-"A0kAÿqA\nI\r AßqAÁkAÿqAI\r A+k"AK\r AF\r Aj" \nF\r@  kA K\r  j-" A:F@  \nO\r@  j-" A k"AMAA tA€€q\r A>G@ A I\r AG\r  j-A-F\rAð! Aj !@@ (x" (|"N@ AÀ Av j AL"6| (t Als"\rA! ($"\rE\rAå (, \r  Aj6x (t"E\r  6t (x"Aj6x  Alj" Ar: A<: B7  6  6@@ (x" (|"N@ AÀ Av j AL"6| (t Als"\rA! ($"\rE\rAå (, \r  Aj6x (t"E\r  6t (x"Aj6x  Alj" Ar: A>: B7  6  Ak6 (t (x"Alj"A k Ak6 Ak Ak6 ! !   6 @@ (x" (|"N@ AÀ Av j AL"6| (t Als"E\r 6t (x"Aj6x  Aj6x (t"E\r\n  Alj"A¼(; B7  6  6@ (x" (|"N@ AÀ Av j AL"6| (t Als"E\r 6t (x"Aj6x  Aj6x (t"E\r\n ( !  Alj"A¾0; B7  6  6 (t (x"Alj"A k Ak6 Ak Ak6A! !  (M\r@@   jAv"Atj"( K@ E@ ! \n   AtjAk(K@ ! \n Ak!  ( O@ ! Aj!  M\r A!  ($"E\rAå (,   Aj!  A F\r A F\r A\rkA}K\r AÿG AúKq A!kAI A:kAIr AÛkAIrr "! As! A! @  \nF\rA!A!@@  \nj" ( \nk"A7"AÿM@ A G\r  @@@  jAm"AtA°j(" Au j" AtA°j(Aÿÿÿq K@ Ak!   AvAq j"AtA°j(AÿÿÿqM\r Aj!  N\r  A! AH\r  A F\r A kAI\r A!Aä!AA  A7"AÿM@ AÿG AúKq A!kAI A:kAIr AÛkAIrr  @@@  jAm"AtAÀ:j(" Au j" AtAÀ:j(Aÿÿÿq K@ Ak!   AvAq j"AtAÀ:j(AÿÿÿqM\r Aj!  N\r A! AN "! As! ! \n!A   q AßFq""A " rE\r  kAp!@@ (x" (|"N@ AÀ Av j AL"6| (t Als"\rA! ($"\rE\rAå (, \r  Aj6x (t"\r  6t (x"Aj6x  Alj" AtA# AG  MqAA  OA r" AFrA@k:  : B7  \n6  6 ! \n "M\r@@@ (x" (|"N@ AÀ Av j AL"6| (t Als"\rA! ($"\rE\r Aå (, \r Aj6x (t"\r  6t (x"Aj6x  Alj"AÄ; B7  6  6 Aj" \nI\r ! \n! @@ (x"\r (|"N@ AÀ Av j AL"6| (t Als"\rA! ($"\rE\rAå (, \r  \rAj6x (t"E\r  6t (x"\rAj6x (!  \rAlj"Aÿ ; B7  6  6A!  A! Aj$@ " \r A (xA¿AM@ (à"AN@ Ak! A€j! AØj! A j!@ (t" Al"j" (!\r@ -"AÄF\r ( !\nA! A! \rAN@   \rAlj" ( Alj! \nAl! (! -A q@ A!: Ak"6A!!  j!  I@ ( I\r  I@ ( I\r @  I@ A!G\r  AÛG\r ( A€€ÀqE\r (" (O\r ( j-AÞG\r Aj"6 (t j"(" M\r  ( j  kN"E\r ( "E@ (pAj"6p  6  (Aj6 6 (6 -A”r:  -A˜r: (! (! (  @@@@ -\rA qE\r -AÛG\r E\r (" ("kAG\r -AÛG\r E\r ( ("kAG\r -AÝG\r  AkG\r ( ("kAG\r  (AjG\r@@ Aj" \nN"@ !  (t! !@@  Alj" -"AÄF\r AüF\r ( kAäK\r -AqE\r ( ! Aj" \nH\r A!  (" F\r  kAäK\r  I@ (!@@  j-A\nk Aj" G\r A!@   Atj"("L@ (t!\r@ \r Alj( " N\r  6  ("L@ (t!\r@ \r Alj( " N\r  6 Aj"AG\r @@ ( (I@@  L\r  kAk! (t!\rA!  AsjAq"@@ \r AljAÄ; Aj! Aj" G\r AI\r@ \r Alj"AÄ;L AÄ;8 AÄ;$ AÄ; Aj" G\r -Ar: (6  @ \r \n kAk! (t!\rA! \n AsjAq"@@ \r AljAÄ; Aj! Aj" G\r AI\r@ \r Alj"AÄ;L AÄ;8 AÄ;$ AÄ; Aj" \nG\r  (6 (6  (6 (t"\r j" \n6 \r j"\r 6  -Ar: \r \r-Ar: (! (!  Aj \nO (  (6  (6 (t"\r j" \n6 \r j"\r 6  -Ar: \r \r-Ar: (! (! (  \r (!  (" ("G\r ( AjK@ -AÀq\r    ( AjP  -AÀq\r   (  AjP "AH\r E\r  (6 (t \rAlj(!\r  @  ("O\r (" j-A(G\r \nAj! A! !@@@   jAv"Atj"( K@ E\r  AtjAk( I\r Ak!   (M\r Aj!  O\r A! @@ Aj"  Atj(" I@@  j-A k"AK\rA tA€€qE\r Aj" G\r !  6@  O\r  j-A\nk Aj" O\r  Atj(!  6 @  O\r  j-A)G\rA! A6 B7  6  6 Aj! A    Atj"(" Aj Aj 8E\r@   k ( Aj A j A,j A(j A$j9@ ( " j! (," F!   j"Atj(! (!   ("6(  6$A!A! @  I@@  j-"A k"AK\rA tA€€qE\r Aj" G\r Aj" O\r   Atj("j-! AÿqA)G\r  Aj" 6 ($" (("M@A! A6 B7A   k! @ A6  6   j"6A  @ q"@  Atj!A! !@   Atj("  K" KA!  "kAq"@@  ( j-: Aj! Aj! Aj" G\r  kA|M@@  ( j-:  ( j-:  ( j-:  ( j-: Aj! Aj" G\r   O\r A\n: Aj!  Aj"Atj(! ($" E\rAÔ (,   A6  6   k6 (! A ! (x" L@  6  (t! !@  Alj"(K@@ -AqAG\r  ( "Alj( I\r \r r  Aj" H\r  6  L\r  \nkAk! (t! A!  \nAsjAq"@@ AljAÄ; Aj! Aj" G\r AI\r@ Alj"AÄ; AÄ;L AÄ;8 AÄ;$ Aj" G\r  -AÀq\r   ( ( AjP"AH\r E\r -Ar:  -Ar: Aj" (tj (6 (t j ( 6  (tj (6( Aj! (t! (@  Alj (Ð6 6Ð  Alj (6 (! (! -!  Aj \nO   AÛF"!   !   !   ! ! / AŒqE\r@@ "Aj! -$"AÄG" \r  I\r À! !@@ "\nAk! \r  K\r -%" AqE\r E\r ( (G\rA÷ ATE\r (  (tkAmG\r \nAk( (G\r AÄ:$  Aïq:% \nAkAÄ: \nAk" -Aïq: \r "AN\r B7à A6¸ @A! A6Ô (x"AL\r (t!\rA!@@ \r Alj"-"Aq\r -AüG\r ( (kAG\r A6  Ar:@ (ØAH@ 6Ø  \r (ÜAlj 6 6Ü Aj"6Ô Aj" G\r A!  A! A (xO  A! A0j$ Þ#Aðk"$ B7h B7` B7X B7P B7H B7@ B78 B70 A6( B7 B7 B7 B7A! @    Aj AÐj<\r  A Aj A0j<\r  6( (,! @  Aj (" E@A!  ($"E\rA (,    Aj (" E@A!  ($"E\rAæ (,  (`@ (Pr (Tr (Xr (@@ (0r (4r (8r Aðj$ Ý@ ( " ("I@ (!  ( Av jA "At"s"E@A ($"E\rAå (, A  6 ( Ajs"E@A ($"E\rAå (, A  6  6 ( ! ( Atj 6  ( Atj 6  ( Aj6 A Ü Aj!@@@ Aj" O\r j-A#G\r j-A rAøG\r !@@@@ j-"A0kAÿqA\nI\r AÁkAÿqAI\r  kAK\r AákAÿqAI\r   kAK\r Aj" G\r !  F\r  kAI\r  @ Aj" O"\r j-A#G\r !@@  kAK\r j-A0kAÿqA K\r Aj" G\r !  F\r  kAI\r   M\r j-AßqAÁkAÿqAK\r  \r@@ j-"AßqAÁkAÿqAO@   kA0K\r A0kAÿqA\nI\r    kA0M\r   Aj" G\r  " kAkA.K\r  M\r j-A;G\rA!  Aj6  ³#Ak"$@@  O\r (!@  j-A k"AK\rA tA€€qE\r Aj" G\r !   O\r (!@  jAk-A k"AK\rA tA€€qE\r Ak" K\r !  6  6  6@  Aj (, ("@ ($"E\rAŠ (,   AjA?"\r  Aj (, ("E@A!  ($"E\rA¯ (,  Aj$  Ê"#A0k"\r$  H@ A¸j! A€j! \rAr! \rAr! (!@@ (t" Al" j"-"Aq@ AqE\r@ E\r ,"E\r  `\r ( !  ,"\nAÿq"E\r  \n`E\r (" I\r@@@@@@@@@@@@@@@@@ A!k^  (! Aq@ AN@  Alj" -AÀr:  6  6  AH\r  Alj"( 6¸  6  6@ (ä" AN@  Alj 6  6à 6ä A6  Aj" (xN\r  Alj"-A;G\r (  (" \rKE\r  6  6  Ar:  -Ar:  6  AqE\r \r A€A˜ \nA*Fj"A j6 AÀq" AÀF@ ! A  \r Aj6 A€F\r ! A ! Aj6A! A°A´ ( kAFj!\n@ AqE\rA! \n("AH\r@   Atj"\n("L@@  Alj( " N\r \n 6  \n("L@@  Alj( " N\r \n 6 Aj" AG\r  Alj" 6  6  -Ar:  -Ar: \r AqE\r  \n(6 \n 6 @ AqE\rA! (Ä"AH\r@   Atj"\n("L@@  Alj( " N\r \n 6  \n("L@@  Alj( " N\r \n 6 Aj" AG\r  Alj" 6  6  -Ar:  -Ar: AqE\r  (Ä6 6Ä @ AqE\r (¼"AH\rA!  Alj" ( (k ( kG\r@   Atj"\n("L@@  Alj( " N\r \n 6  \n("L@@  Alj( " N\r \n 6 Aj" AG\r @ Aj" N\r  kAk!A!  AsjAq"\n@@  AljAÄ; Aj! Aj" \nG\r AI\r@  Alj" AÄ;L AÄ;8 AÄ;$ AÄ; Aj" G\r 6  6 -Ar:  -Ar: A6¼ AqE\r\n  (¼6 6¼ \n  Aj"Al"j"(! (! \r 6 \r ("6, \r 6( \r 6$ \nAÀF@   \rAAAA˜ \rA(jRAL\r\n \r(!  O\r Q" AÿM\rA!A!@@@  jAm" AtA°j("Au j"AtA°j(Aÿÿÿq K@ Ak!  AvAq j"AtA°j(AÿÿÿqM\r Aj!  N\r  AN\r  ( kAG\r@ AqE\rA! (À"AH\r@   Atj"\n("L@@  Alj( " N\r \n 6  \n("L@@  Alj( " N\r \n 6 Aj" AG\r  Alj" 6  6  -Ar:  -Ar: AqE\r  (À6 6À  ( kAG\r@ AqE\rA! (È"AH\r@   Atj"\n("L@@  Alj( " N\r \n 6  \n("L@@  Alj( " N\r \n 6 Aj" AG\r  Alj" 6  6  -Ar:  -Ar:  AqE\r  (È6 6È  ( kAG\r@ AqE\rA! (Ì"AH\r@   Atj"\n("L@@  Alj( " N\r \n 6  \n("L@@  Alj( " N\r \n 6 Aj" AG\r  Alj" 6  6  -Ar:  -Ar:  AqE\r  (Ì6 6Ì  A! A!  A F\r A F\r A kAI\r ( jAk,"@Aã AT\r \r((" I\r Ak!@@@ -AÄF\r ( K\r  (O\r -" Aq\r Ak" O\r  AqE\r   \rA,jAA.AAØ \rA$jRAH\r \r(,! @ \nAÀF\r@ O\r (" j-A/G\r Aj  \rA,jAA/AùA \rA$jRAH\r \r(," O\r  j-A/G\r \r Aj" 6, @ O\r ( j-A?G\r Aj  \rA,jAA&AóA• \rA$jRAH\r \r(,! O\r ( j-A#G\r Aj  \rA,jAAAAÛ \rA$jRAH\r \r(,! @ O\rA!A!@ ( j" ( kA7"AÿM@ A F\r A F\r A kAO\r  @@@  jAm" AtA°j("Au j"AtA°j(Aÿÿÿq K@ Ak!   AvAq j"AtA°j(AÿÿÿqM\r Aj!  N\r  AN\r ,"@A¡ A T\r \r($"  (xAlj"O\r@@@ -AÄF\r ( M\r ( K\r -"Aq\r Aj" I\r  AqE\r  6  6  \n:  6  6 (t" j" 6  j" 6  -Ar:  -Ar:  \r Atr 6 A q! Aj! AàqAàG@ \r Atj Aj6 Aj! As AGqE@ \r Atj Aj6 Aj!  \r(("AljA AN"! A !@ \r(("AH\r  Alj! @ ( (M\r ! ! @ AF\r@ \r(("AH\r  Alj! @ ( (M\r ! ! AF\r@ \r( ("AH\r  Alj! @ ( (M\r ! ! AF\r@ \r(("AH\r  Alj! @ ( (M\r ! ! AF\r \r((" AH\r  Alj! @ ( (M\r ! ! E\r (! (! (! !\n@@@@@@  Alj"-"A!k\n AÛk A€A˜ A*Fj -"AtAuA qj!\n@ AvAk \nAj!\n  A°A´ ( (kAFj!\n  \nAj!\n @ k" k"K@  (" (k k j"Al" j" (6  )7  )7  k"6  6 (t j \n(6 \n 6  O\r  Alj j" (6  )7  )7  k j"6  6 A!\n@   \nAtj"("L@@ AN@  (t Alj( "6  N\r \nAj"\nAG\r (t" Alj" 6  j" 6  -Ar:  -Ar:  AqE\r A€A˜ \nA*Fj AtAuA qj!@@@ AvAk Aj!  Aj!  (6  6 -"AqE\r Aq@ (t ( Alj(!  (! Aj" H\r \rA0j$ –#Ak"$@ (E\r  6! (  (pAtj("E@A!  @  ("I\r   ( (ljO\r  (F@ ( (  DE\r A!   6  6  6  Aj6 Aj!@ ("@@   Av"Atj"G"AH@   E\r Aj!  Asj "\r A! E@A!  (! Aj$  ý#A k"$ Aß: A¦Ô;A! ( "A€ q@ A$:A! A€€€q@ Aj rA=: Aj! A€€€q@ Aj rA+: Aj! A€€q@ Aj jAü: Aj! A€€q@ Aj jAÞ: Aj! A€„q@ Aj jAþ: Aj! A! Aj jA:    AjAM ( "AŒq@ Aq@ AÀ:A! Aq@  rA:: Aj! A€q  rA.: Aj  rA:     AjM A€jAÿAÐü A j$ • (TE@A Ak! Ak!AA ("\n j-A!F j!A!@@@   jAv"Atj" ( K@ E\r  AtjAk( I\r Ak!   (M\r Aj!  O\r A! @  ("M" E@@  kq"@ !@    K" KA!  "kAq"\n@@  ( j-: Aj! Aj! Aj" \nG\r  kA|M@@  ( j-:  ( j-:  ( j-:  ( j-: Aj! Aj" G\r   O\r A : Aj! Aj" Atj"(! (! A! ($"E\rAÔ (, A  k   \nj!  k ! A  Aj60A! Aj !\nA!@@ \n("E\r Aj p"AH\r   kK\r \nAj!\n  j" I\r A=! AH\r A \r  l E@A!  A! (0!\n@ \n(" E\r Aj p" j" K\r Aj h \nAj!\n  K\r A \r  A€Àsl \r   \rH!   AHq\r A=! +0 \r  m"AN\r \n -!\n Aj! \r E\rA!@  Atj("\n@  Atj \n jA! Aj"A\nG\r A\nO@A! \n @  Atj(\rA! Aj"A\nG\r A!   :\'A! ! !   k"  H" AÿÿÿÿsJ\rA=! \r  j"  \rH" \nK\r A   l  h A0   A€€sl A0  Al h A   A€Àsl ( Ak" O\r B€”ëÜT\r Ak" > @ "I@ Ak"(E\r (, k"6, ! AJ\r AH@ AjA nAj! AæF!@A A k" A O!\n@  M@AA (!  A€”ëÜ \nv!\rA \ntAs!A! !@  (" \nv j6  q \rl! Aj" I\r AA (! E\r  6 Aj! (, \nj"6,   j" " Atj   kAu J! AH\r A!@  M\r  kAuA l!A\n! ("A\nI\r@ Aj!  A\nl"O\r A AæGk AçF AGqk"  kAuA lA kH@ A0jA„`A¤b AHj A€Èj"A m"\rAtj!\nA\n!  \rA lk"AL@@ A\nl! Aj"AG\r @ \n("  n" lk"\rE \nAj" Fq\r@ AqE@D@C! A€”ëÜG\r \nO\r \nAk-AqE\r D@C! Dà?Dð?Dø?  FDø? \r Av"F \r I!@ \r -A-G\r š! š! \n  \rk"6    a\r \n  j"6 A€”ëÜO@@ \nA6 \nAk"\nK@ Ak" A6 \n \n(Aj"6 Aÿ“ëÜK\r  kAuA l!A\n! ("A\nI\r@ Aj!  A\nl"O\r \nAj"   K! @ " M"E@ Ak"(E\r @ AçG@ Aq!\r  AsA A " J A{Jq"\n j! AA~ \n j! Aq"\r\rAw!@ \r Ak("\nE\rA\n!A! \nA\np\r@ "\rAj! \n A\nl"pE\r \rAs!  kAuA l! A_qAÆF@A!\r  jA k"A AJ"  J!  A!\r  j jA k"A AJ"  J! A!\n AýÿÿÿAþÿÿÿ \rr"J\r AGjAj!@ A_q"AÆF@  AÿÿÿÿsJ\r A AJ!    Au"s k­ k"kAL@@ Ak"A0:  kAH\r Ak" : AkA-A+ AH:  k" AÿÿÿÿsJ\r  j" AÿÿÿÿsJ\r A   j" l  h A0   A€€sl@@@ AÆF@ AjA r!  K"! @ 5 k!@  G@  AjM\r@ Ak"A0:  AjK\r   G\r Ak"A0:   kh Aj" M\r @ AùAh  M\r AL\r@ 5 k" AjK@@ Ak"A0:  AjK\r A A Nh A k! Aj" O\r A J! ! \r  @ AH\r  Aj  K!\n AjA r! !@  5 k"F@ Ak"A0: @  G@  AjM\r@ Ak"A0:  AjK\r  Ah Aj! \rrE\r AùAh   k"  Hh k! Aj" \nO\r AN\r A0 AjAAl   kh  ! A0 A jA Al A   A€Àsl    J!\n   AtAuA qj!@ A K\rA k!D0@!@ D0@¢! Ak"\r -A-F@  š ¡ š!     ¡!  (," Au"s k­ k"F@ Ak"A0: (,! Ar!\r A q! Ak" Aj: AkA-A+ AH: AqE ALq! Aj!@ " ü"AÐÑj- r:  ·¡D0@¢!@ Aj" AjkAG\r Da q\r A.: Aj! Db\r A!\n Aýÿÿÿ \r  k" j"kJ\r A   Aj  Ajk" Ak H  "j" l  \rh A0   A€€sl Aj h A0  kAAl  h A   A€Àsl    J!\n A°j$ \n ¹~  (AjAxq"Aj6 )! )!#A k"$ Bÿÿÿÿÿÿ?ƒ!~ B0ˆBÿÿƒ"§"AøkAýM@ B† B<ˆ„! A€øk­!@ Bÿÿÿÿÿÿÿÿƒ"B€€€€€€€Z@ B|!  B€€€€€€€€R\r Bƒ |! B  BÿÿÿÿÿÿÿV"! ­ |  @  „P\r BÿÿR\r B† B<ˆ„B€€€€€€€„!Bÿ  Aþ‡K@B!Bÿ  A€øAø P"\n" k" AðJ@B!B   B€€€€€€À„ \n!A!\n G@ Aj! ! !@A€ k"AÀq@  A@j­†!B!  E\r  ­"† AÀ k­ˆ„!  †! 7 7 ) )„BR!\n @ AÀq@  A@j­ˆ!B!  E\r AÀ k­†  ­"ˆ„!  ˆ!  7  7 )B† )"B<ˆ„!@ \n­ Bÿÿÿÿÿÿÿÿƒ„"B€€€€€€€Z@ B|!  B€€€€€€€€R\r Bƒ |! B€€€€€€€…  BÿÿÿÿÿÿÿV"! ­ ! A j$ B€€€€€€€€€ƒ B4†„ „¿9 ¦ (T"(! (" ( ("k"  I"@   V  ( j"6  ( k"6    K"@   V  ( j"6  ( k6 A: (,"6 6  ¢ E@A @  AÿM\r@AìÞ((E@ A€qA€¿F\rAÐÝA6  AÿM@ A?qA€r: AvAÀr:A  A€@qA€ÀG A€°OqE@ A?qA€r: A vAàr: AvA?qA€r:A  A€€kAÿÿ?M@ A?qA€r: AvAðr: AvA?qA€r: A vA?qA€r:A  AÐÝA6 AA  :A Ñ( #Ak"\n$@@@@@@@@@@ AôM@Aß("A A jAøq A I"Av"v"Aq@@ AsAq j"At"A¸ßj" (À_"("F@Aß A~ wq6   6 6 Aj!  Ar6  j" (Ar6 A˜ß("M\r @@ tA t"A krqh"At"A¸ßj" (À_"("F@Aß A~ wq"6   6  6 Ar6 j"  k"Ar6 j 6 @ AxqA¸ßj!A¤ß(! A Avt"qE@Aß  r6   ( !  6  6  6  6 Aj!A¤ß 6A˜ß 6 A”ß(" E\r hAt(Àa"(Axq k! !@@ ("E@ ("E\r (Axq k"   K"!  ! !  (!  ( "G@ (" 6 6 \n (" Aj ("E\r Aj !@ ! "Aj! ("\r Aj! ("\r A6 A! A¿K\r A j"Axq!A”ß(" E\rA! AôÿÿM@ A& Avg"kvAq AtkA>j! A k!@@@ At(Àa"E@A!  A! A AvkA AGt!@@ (Axq k" O\r ! "\rA! !  ("   AvAqj("F ! At! "\r rE@A!A t"A kr q"E\r hAt(Àa! E\r @ (Axq k" I!   !  ! ("  ( "\r E\r A˜ß( kO\r (!  ( "G@ (" 6 6  (" Aj ("E\r Aj !@ ! "Aj! ("\r Aj! ("\r A6  A˜ß("M@A¤ß(!@ k"AO@  j" Ar6 j 6  Ar6   Ar6 j" (Ar6A!A! A˜ß 6A¤ß 6 Aj! Aœß("I@Aœß  k"6A¨ßA¨ß(" j"6  Ar6 Ar6 Aj! A! A/j"Aèâ(@Aðâ(  AôâB7AìâB€ €€€€7Aèâ \nA jApqAتժs6AüâA6AÌâA6A€ "j"A k"q" M\rAÈâ("@AÀâ(" j" M\r  I\r @AÌâ-AqE@@@@@A¨ß("@AÐâ!@ (" M@   (jI\r ("\r Au"AF\r !Aìâ("Ak" q@  k  jA kqj!  O\rAÈâ("@AÀâ(" j" M\r I\r u" G\r   k q"u" ( (jF\r ! AF\r A0j M@ !  Aðâ("  kjA kq"uAF\r  j! !  AG\r AÌâAÌâ(Ar6 u!Au! AF\r AF\r M\r k" A(jM\r AÀâAÀâ( j"6AÄâ( I@AÄâ 6 @A¨ß("@AÐâ!@  (" ("jF\r ("\r  A ß("A ME@A ß 6 A!AÔâ 6AÐâ 6A°ßA6A´ßAèâ(6AÜâA6@ At" A¸ßj"6À_  6Ä_ Aj"A G\r Aœß A(k"Ax kAq"k"6A¨ß  j"6  Ar6 jA(6A¬ßAøâ(6   O\r  I\r ( Aq\r  j6A¨ß Ax kAq"j"6AœßAœß( j" k"6  Ar6  jA(6A¬ßAøâ(6  A!  A!  A ß( K@A ß 6  j!AÐâ!@@  ("G@ ("\r  - AqE\r AÐâ!@@ (" M@   (j"I\r (!  Aœß A(k"Ax kAq"k"6A¨ß  j"6  Ar6 jA(6A¬ßAøâ(6  A\' kAqjA/k" AjI"A6 AØâ)7 AÐâ)7AØâ Aj6AÔâ 6AÐâ 6AÜâA6 Aj!@ A6 Aj! Aj!  I\r  F\r  (A~q6   k"Ar6  6 AÿM@ AøqA¸ßj!Aß("A Avt"qE@Aß  r6  ( ! 6  6 A !A  A! AÿÿÿM@ A& Avg"kvAq AtrA>s!  6 B7 AtAÀáj!@@A”ß("A t"qE@A”ß  r6  6  6  A AvkA AGt! (!@ "(Axq F\r Av! At!  Aqj"("\r  6  6 A! ! !A  (" 6  6  6A!A!A j 6  j 6 Aœß(" M\rAœß k"6A¨ßA¨ß(" j"6  Ar6 Ar6 Aj!  AÐÝA06A!  6 ( j6 Ax kAqj" Ar6 Ax kAqj"  j"k!@A¨ß( F@A¨ß 6AœßAœß( j"6  Ar6  A¤ß( F@A¤ß 6A˜ßA˜ß( j"6  Ar6  j 6  ("AqAF@ Axq! ( !@ AÿM@ (" F@AßAß(A~ Avwq6  6  6  (!@  G@ (" 6  6  @ (" Aj ("E\r Aj !@ ! "Aj! ("\r Aj! ("\r A6  A! E\r@ ("At"(Àa F@ AÀáj 6 \rA”ßA”ß(A~ wq6  @  (F@  6   6 E\r  6 ("@  6  6 ("E\r  6  6  j"(!  j!  A~q6  Ar6  j 6 AÿM@ AøqA¸ßj!Aß("A Avt"qE@Aß  r6   ( !  6  6  6  6  A! AÿÿÿM@ A& Avg"kvAq AtrA>s!  6 B7 AtAÀáj!@@A”ß("A t"qE@A”ß r6  6  6  A AvkA AGt! (!@ "(Axq F\r Av! At!  Aqj"("\r  6  6  6  6  (" 6  6 A6  6  6 Aj!  @ E\r@ ("At"(Àa F@ AÀáj 6 \rA”ß A~ wq" 6  @  (F@  6   6 E\r 6 ("@ 6  6 ("E\r 6  6 @ AM@   j"Ar6 j" (Ar6   Ar6  j" Ar6  j 6 AÿM@ AøqA¸ßj!Aß("A Avt"qE@Aß  r6  ( ! 6  6  6  6  A! AÿÿÿM@ A& Avg"kvAq AtrA>s!  6 B7 AtAÀáj!@@ A t"qE@A”ß  r6  6  6  A AvkA AGt! (!@ "(Axq F\r Av! At!  Aqj"("\r  6  6  6  6  (" 6  6 A6  6  6 Aj!  @ E\r@ ("At"(Àa F@ AÀáj 6 \rA”ß A~ wq6  @  (F@ 6  6 E\r 6 ("@ 6  6 ("E\r 6  6 @ AM@   j"Ar6 j" (Ar6   Ar6  j" Ar6  j 6 @ AxqA¸ßj!A¤ß(!A Avt" qE@Aß  r6   ( !  6  6 6 6 A¤ß 6A˜ß 6 Aj! \nAj$ ‚ @ E\r Ak" Ak("Axq"j!@ Aq\r AqE\r  ("k"A ß(I\r j!@@@A¤ß( G@ ( ! AÿM@  ("G\rAßAß(A~ Avwq6  (!  G@ (" 6  6  (" Aj ("E\r Aj !@ ! "Aj! ("\r Aj! ("\r A6  ("AqAG\rA˜ß 6  A~q6  Ar6  6  6  6  A! E\r@ ("At"(Àa F@ AÀáj 6 \rA”ßA”ß(A~ wq6  @  (F@  6   6 E\r  6 ("@  6  6 ("E\r  6  6  O\r ("AqE\r@@@@ AqE@A¨ß( F@A¨ß 6AœßAœß( j"6  Ar6 A¤ß(G\rA˜ßA6A¤ßA6 A¤ß(" F@A¤ß 6A˜ßA˜ß( j"6  Ar6 j 6 Axq j! ( ! AÿM@ (" F@AßAß(A~ Avwq6   6  6  (!  G@ (" 6  6  (" Aj ("E\r Aj !@ ! "Aj! ("\r Aj! ("\r A6   A~q6  Ar6 j 6  A! E\r@ ("At"(Àa F@ AÀáj 6 \rA”ßA”ß(A~ wq6  @  (F@  6   6 E\r  6 ("@  6  6 ("E\r  6  6  Ar6 j 6  G\rA˜ß 6 AÿM@ AøqA¸ßj!Aß("A Avt"qE@Aß r6   ( !  6 6  6  6 A! AÿÿÿM@ A& Avg"kvAq AtrA>s!  6 B7 AtAÀáj!@A”ß("A t"qE@A”ß  r6  6A!A  A AvkA AGt! (!@ "(Axq F\r Av! At!  Aqj"("\r  6A! !A ! !   (" 6  6A!A!A !  j 6  6 j 6A°ßA°ß(Ak"A 6 ‰  E@ q A@O@AÐÝA06A A A jAxq A I! Ak"("Axq!@ AqE@ A€I\r Aj M@ !  kAðâ(AtM\r A   j!@  O@  k"AI\r   AqrAr6  j" Ar6  (Ar6  t  A¨ß( F@Aœß( j" M\r   AqrAr6  j"  k"Ar6Aœß 6A¨ß 6  A¤ß( F@A˜ß( j" I\r@  k"AO@   AqrAr6  j" Ar6  j" 6  (A~q6   Aq rAr6  j" (Ar6A!A! A¤ß 6A˜ß 6  ("Aq\r Axq j"\n I\r \n k! ( !@ AÿM@ (" F@AßAß(A~ Avwq6   6  6  (! @  G@ (" 6  6  @ (" Aj ("E\r Aj !@ ! "Aj! ("\r Aj! ("\r A6  A! E\r@ ("At"(Àa F@ AÀáj 6 \rA”ßA”ß(A~ wq6  @  (F@ 6  6 E\r  6 ("@  6  6 ("E\r  6  6 AM@  Aq \nrAr6  \nj" (Ar6    AqrAr6  j" Ar6  \nj" (Ar6  t !  "@ Aj q"E@A  A|Ax Ak("Aq Axqj"   KV r  «  j!@@ ("Aq\r AqE\r (" j!@@@ k"A¤ß(G@ ( ! AÿM@  ("G\rAßAß(A~ Avwq6  (! G@ (" 6  6  (" Aj ("E\r Aj !@ ! "Aj! ("\r Aj! ("\r A6  ("AqAG\rA˜ß 6  A~q6 Ar6  6  6  6  A! E\r@ ("At"(Àa F@ AÀáj 6 \rA”ßA”ß(A~ wq6  @ (F@  6   6 E\r  6 ("@  6  6 ("E\r  6  6 @@@@ ("AqE@A¨ß( F@A¨ß 6AœßAœß( j"6 Ar6 A¤ß(G\rA˜ßA6A¤ßA6 A¤ß(" F@A¤ß 6A˜ßA˜ß( j"6 Ar6 j 6 Axq j! ( ! AÿM@ (" F@AßAß(A~ Avwq6   6  6  (!  G@ (" 6  6  (" Aj ("E\r Aj !@ ! "Aj! ("\r Aj! ("\r A6   A~q6 Ar6 j 6  A! E\r@ ("At"(Àa F@ AÀáj 6 \rA”ßA”ß(A~ wq6  @  (F@  6   6 E\r  6 ("@  6  6 ("E\r  6  6 Ar6 j 6 G\rA˜ß 6 AÿM@ AøqA¸ßj!Aß("A Avt"qE@Aß  r6   ( !  6  6 6 6 A! AÿÿÿM@ A& Avg"kvAq AtrA>s! 6 B7 AtAÀáj!@@A”ß("A t"qE@A”ß  r6  6 6  A AvkA AGt! (!@ "(Axq F\r Av! At!  Aqj"("\r  6 6 6 6 (" 6  6 A6 6 6 W~@ ­B|BøÿÿÿƒAÄÜ("­|"BÿÿÿÿX@ §"?AtM\r \r AÐÝA06A AÄÜ 6  ² #Ak"$  6 @ AÓM@AàÑA Ó A jx(!  A|O@y  AÒn"AÒl"k6A ÓAàÔ Ajx"( j! A ÓkAu!@A!@@@ "A/F\r At(àQ"n" I\r Aj!  lG\r A/I\r AÓ!@ n" I\r  lF\r A\nj"n" I\r  lF\r A j"n" I\r  lF\r Aj"n" I\r  lF\r Aj"n" I\r  lF\r Aj"n" I\r  lF\r Aj"n" I\r  lF\r Aj"n" I\r  lF\r A$j"n" I\r  lF\r A(j"n" I\r  lF\r A*j"n" I\r  lF\r A.j"n" I\r  lF\r A4j"n" I\r  lF\r A:j"n" I\r  lF\r A`-_.-+_\\\\({[,"children":[NANINFcatching a class without an object?)}].!?,;mailto:,"content":h6h5h4h3h2h1http://Too many link reference definition instantiations.Unsupported abi_version.Aborted from text() callback.Aborted from enter_span() callback.Aborted from leave_span() callback.Aborted from enter_block() callback.Aborted from leave_block() callback.malloc() failed.realloc() failed.@:..!#$%&\'*+/=?^_`{|}~-_.:-+-.-_+-_*(null)Suppressing too sparse table (see https://github.com/mity/md4c/issues/345)._=().+-_~%false && "unhandled NodeType in nodeTypeToString"\\"{"type":"[]!xX \n™Jìi¿e£<‡ ¢°üOs{±2‡+F< ÂÎ._VÉšAÜ ~ô H, „*NSt3__220__shared_ptr_emplaceIN8Markdown15MarkdownASTNodeENS_9allocatorIS2_EEEE8™(A€ ä A  f\r0\r`\r\r\rÐ\r\rp\r\r€ Àà\r\r @ \r\r\r\rA UmŒA° !þ\n-Aà Á™ÅA 5¨]¸‹;OAÐ 5F¾\n&’îa=A MM J G D A > ¬¨Að `A€ •EA  ܘóAÀ \r”nAà ¼¨A€ \r’A  ±Ô$AÀ Eu,’Ø–«gYA  A  ”A° —  € @\n €/ _ 0A@Z€µÀ@րØ@ހ@.€2@6€9@G€J@v€xy@}€‚„†‡‰Š‹Ž‘“”–—˜œŸ @¤€¦§©¬®¯±²³µ·¸¼ÄÅÇÈÊË@Û€Þ@î€ñòôö÷ø@€ "@2€:;=>ACDEF@N€Eprv†ˆ@Š€ŒŽ‘@¡€£@«€ÂÏÐÑÕÖØ@î€ðñôõ÷ùúý@ÿ€@€@/€`@€€Š@¾€ÀÁ@Í€Ð@.€1@V€ @Å€ÇÍø@ý€€‚ƒ„…†‡ˆ‰@º€½@¿€@”€› @þ€@€@€(@/€8@?€H@M€Y[]_h@o€¸¹º»¾È@Ë€ØÙÚÛèéêëìøùúû&!*!+!2!`!@o!€ƒ!¶$@Ï$€,@/,€`,b,c,d,g,@k,€m,n,o,p,r,u,~,,€,@â,€ë,í,ò,@¦@l¦€€¦@š¦€"§@.§€2§@n§€y§{§}§~§@†§€‹§§§’§–§@¨§€ª§«§¬§­§®§°§±§²§³§´§@§€Ä§Å§Æ§Ç§É§Ë§Ì§@Ú§€Ü§Ý§â§õ§l«m«p«@¿«€!ÿ@:ÿ€@\'€°@Ó€p@z€|@Š€Œ@’€”•€ @² €P\r@e\r€ @¿€@n@_n€ n@¸n€@ßHßJßMßQßhß@n߀rß@~߀é@!é€az¼àöøþ/37:HKwÿz~sSƒ…TˆVWŒÝY[’`cih™oru¡¥€¨ƒ­ˆ°Š‹´¶’¹½ÆÆÉÉÌÌÜßïóóõ•¿ùž#3e,<šf,B€‰ŒGO¹qswó¬­¯ÌÍαÁÃËÃײ¸ÆÀÙïºÁ¸µøòû{}P_0Oa‹¿ÏÂÎÑ/a†-%-\'---ðõ24>ABBJcK¦ŠÐúýÿ•a¡ÿ \'07@EQSUW`g°±pq¹ruÐÑvwàáz{åxy|}ÉkåN!p!!„!Ð$é$0,_,a,k}}h,l,QqPRs,v,?@,ã,ì,î,ó,A¦m¦¦›¦#§/§3§o§z§|§y§‡§Œ§e‘§“§—§©§f\\aljž‡S«µ§Ã§”§‚Žȧʧdͧۧ›w|ö§K«L« ïAÿZÿ(OØû—¡£±³¹»¼À ò p\r…\rÀß`nn»nÓnAßIßKßNßRßißoßsßß"éCéAÐ2 Óß0Ið‡–—˜™šžP€@‡€ˆ@€@—€˜@Ÿ€ @§€¨@¯€²³´¶¼ÂÃÄÆÌÖäæòóôöüûûûûûûûûûû•ßA°4 ¥ssi¼nj e‚h1tw\ny\na¾ssʹ¹¹ ¹\'¹ ¹\'¹`¹g¹`¹g¹p¹±¹¬¹±B±¹t¹·¹®¹·B·¹¹BÁÅB|¹É¹Î¹ÉBɹfffiflststtvtetk~vtmssAà7 À°RTV·ÇÒÓ×âãç÷ûû¹ÅÅÅÅB±B¹·B¹¹¹¹BÅÅÅBÉB¹ffifflА.P05à  A´: ½!@/€:@@€[@`€{@~€¡@©€«@¬€®@±€´¶@¸€»¿×÷Â@Å€Ò@߀å@ë€íï@ÿ€u~„@…€‡ö‚Z@_€‰@Š€@€¾ÀÃÆó@ô€@€@€j@m€ÔÞéý@þ€@\r€ö@ù€þ@ÿ€0@>€^ˆd @e €p ò @ó €ú @û €ý v\nð\n@ñ\n€p ó @ú €w  „ O\ry\rô\r?OZ@[€@€@€468:@=€…¾@Å€Ç@Ì€Î@Ú€J@O€ž@Ÿ€û`@h€@™€m@n€›@œ€ë@í€5@6€Ô@Ö€Ø@Û€@\n€@D@E€Þ@ÿ€@€ @¦€¨@­€N@O€Z@j€t@€ü@ÿ€;@?€~@€À@Ç€Ó½¿@Á€Í@Ï€Ý@߀í@ï€ý@þ€ @\' €0 @^ €z @~ €Š @Ž €  @Ä €!@!€!@!€!@ !€!!@!€!@#!€%!\'!)!.!:!@;!€@!@D!€J!@M!€O!Š!@‹!€!@)$€@$@J$€œ$@é$€%@u\'€”\'@s+€v+@ÿ+€å,@ê,€ù,@ü,€þ,@ÿ,€p-.@..€0.@].€`.@c.€€.@™.€›.@ó.€/@Õ/€ð/@ÿ/€0@0€0@ 0€0060@70€=0@?0€›0@œ0€ 0û01@‘1€–1@Ÿ1€À1@å1€ï12@2€*2@G2€P2`2@2€Š2@°2€À2@ÿ3€ÀM@ÿM€¤@Ƥ€þ¤@ÿ¤€\r¦@¦€s¦~¦ò¦@÷¦€§@§€ §@!§€‰§@Ч€(¨@+¨€6¨@9¨€t¨@w¨€Î¨@Ϩ€ø¨@ú¨€ü¨.©@/©€_©Á©@Í©€Þ©@ß©€\\ª@_ª€wª@yª€Þª@ߪ€ðª@ñª€[«j«@k«€ë«)û²û@Òû€>ý@Oý€ý@‘ý€Èý@Ïý€üý@ÿý€þ@þ€0þ@Rþ€Tþ@fþ€hþ@kþ€ÿ@ÿ€ÿ@ ÿ€;ÿ@@ÿ€[ÿ@eÿ€àÿ@æÿ€èÿ@îÿ€üÿ@ýÿ€@€7@?€y@‰€Œ@Ž€@œ€ Ð@ü€ŸÐoWw@x€ ? P\n@X\n€\nÈ\nð\n@ö\n€9 @? €™ @œ €n\rŽ\r@\r€­É@Ê€Ð@Ø€U@Y€†@‰€G@M€»@¼€¾@Á€@@C€t@u€Å@È€ÍÛÝ@߀8@=€©Ô@Õ€×@Ø€K@O€Z@[€]ÆÁ@×€A@C€`@l€¹<@?€;D@F€â?@F€š@œ€ž@¢€@ €áA@E€p@q€÷@ø€C@O€Õ@ñ€ÿp$@t$€ñ/@ò/€nj@oj€õj7k@?k€Dk@Ek€mm@om€—n@šn€âoœ¼Ÿ¼Ì@ïÌ€úÌ@üÌ€Í@³Î€ºÎ@Ð΀ÒÎ@Ô΀ÝÎ@ý΀PÏ@ÃÏ€Ð@õЀÑ@&Ñ€)Ñ@dÑ€jÑ@lÑ€ƒÑ@„Ñ€ŒÑ@©Ñ€®Ñ@AÒ€EÒSÒ@ZÒ€]Ò@^Ò€`Ò@Ò€Ó@VÓ€ÁÖÛÖûÖ×5×O×o׉ש×Ãר@ÿÙ€7Ú@:Ú€mÚ@tÚ€vÚ@ƒÚ€…Ú@‹Ú€Û@Û€Oáÿâÿå^é@_逬ì°ì.íðî@ñî€ð@+ð€0ð@“ð€ ð@®ð€±ð@¿ð€Áð@Ïð€Ñð@õð€\rñ@®ñ€æñ@ò€ò@;ò€@ò@Hò€Pò@Qò€`ò@eò€ó@Ùö€Üö@ìö€ðö@üö€÷@Û÷€à÷@ë÷€ð÷@ ø€ø@Gø€Pø@Yø€`ø@‡ø€ø@­ø€°ø@»ø€Àø@Áø€Ðø@Øø€ù@Wú€`ú@mú€pú@|ú€€ú@Æú€ÈúÌú@Ýú€ßú@ëú€ïú@úú€û@’û€”û@ïû€úû A€Î A  \n\n\n   AÑÎ ! \r\r  A‹Ï  A—Ï  AÅÏ AÑÏ  AÿÏ A‹Ð  AÂÐ  AóÐ AÿÐ  A­Ñ A¹Ñ € 0123456789ABCDEF \r%)+/5;=CGIOSYaegkmqƒ‰‹•—£§­³µ¿ÁÅÇÓ \r%)+/5;=CGIOSYaegkmqyƒ‰‹•—£§©­³µ»¿ÁÅÇÑ ,h*NSt3__214__shared_countE¤,œ*`*NSt3__219__shared_weak_countEAÄÕ ø\ndè\' †@B€–˜áõʚ;00010203040506070809101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899H,Ä+(.N10__cxxabiv116__shim_type_infoEH,ô+¸+N10__cxxabiv117__class_type_infoEè+ !"#$%&h,\'!"#()*H,t,è+N10__cxxabiv120__si_class_type_infoEÄ,+!"#,-.H,Ð,è+N10__cxxabiv121__vmi_class_type_infoEL-/0h-124-34 ,<-St9exceptionH,X-4-St9bad_allocH,t-L-St20bad_array_new_length¤-56H,°-4-St11logic_errorÔ-76H,à-¤-St12length_error.86H,.¤-St12out_of_range ,0.St9type_infoAÁÜ  1€')}function getBinarySync(file){return file}async function getWasmBinary(binaryFile){return getBinarySync(binaryFile)}async function instantiateArrayBuffer(binaryFile,imports){try{var binary=await getWasmBinary(binaryFile);var instance=await WebAssembly.instantiate(binary,imports);return instance}catch(reason){err(`failed to asynchronously prepare wasm: ${reason}`);abort(reason)}}async function instantiateAsync(binary,binaryFile,imports){return instantiateArrayBuffer(binaryFile,imports)}function getWasmImports(){var imports={env:wasmImports,wasi_snapshot_preview1:wasmImports};return imports}async function createWasm(){function receiveInstance(instance,module){wasmExports=instance.exports;assignWasmExports(wasmExports);updateMemoryViews();return wasmExports}function receiveInstantiationResult(result){return receiveInstance(result["instance"])}var info=getWasmImports();if(Module["instantiateWasm"]){return new Promise((resolve,reject)=>{Module["instantiateWasm"](info,(inst,mod)=>{resolve(receiveInstance(inst,mod))})})}wasmBinaryFile??=findWasmBinary();var result=await instantiateAsync(wasmBinary,wasmBinaryFile,info);var exports=receiveInstantiationResult(result);return exports}class ExitStatus{name="ExitStatus";constructor(status){this.message=`Program terminated with exit(${status})`;this.status=status}}var callRuntimeCallbacks=callbacks=>{while(callbacks.length>0){callbacks.shift()(Module)}};var onPostRuns=[];var addOnPostRun=cb=>onPostRuns.push(cb);var onPreRuns=[];var addOnPreRun=cb=>onPreRuns.push(cb);var noExitRuntime=true;var stackRestore=val=>__emscripten_stack_restore(val);var stackSave=()=>_emscripten_stack_get_current();var UTF8Decoder=globalThis.TextDecoder&&new TextDecoder;var findStringEnd=(heapOrArray,idx,maxBytesToRead,ignoreNul)=>{var maxIdx=idx+maxBytesToRead;if(ignoreNul)return maxIdx;while(heapOrArray[idx]&&!(idx>=maxIdx))++idx;return idx};var UTF8ArrayToString=(heapOrArray,idx=0,maxBytesToRead,ignoreNul)=>{var endPtr=findStringEnd(heapOrArray,idx,maxBytesToRead,ignoreNul);if(endPtr-idx>16&&heapOrArray.buffer&&UTF8Decoder){return UTF8Decoder.decode(heapOrArray.subarray(idx,endPtr))}var str="";while(idx>10,56320|ch&1023)}}return str};var UTF8ToString=(ptr,maxBytesToRead,ignoreNul)=>ptr?UTF8ArrayToString(HEAPU8,ptr,maxBytesToRead,ignoreNul):"";var ___assert_fail=(condition,filename,line,func)=>abort(`Assertion failed: ${UTF8ToString(condition)}, at: `+[filename?UTF8ToString(filename):"unknown filename",line,func?UTF8ToString(func):"unknown function"]);class ExceptionInfo{constructor(excPtr){this.excPtr=excPtr;this.ptr=excPtr-24}set_type(type){HEAPU32[this.ptr+4>>2]=type}get_type(){return HEAPU32[this.ptr+4>>2]}set_destructor(destructor){HEAPU32[this.ptr+8>>2]=destructor}get_destructor(){return HEAPU32[this.ptr+8>>2]}set_caught(caught){caught=caught?1:0;HEAP8[this.ptr+12]=caught}get_caught(){return HEAP8[this.ptr+12]!=0}set_rethrown(rethrown){rethrown=rethrown?1:0;HEAP8[this.ptr+13]=rethrown}get_rethrown(){return HEAP8[this.ptr+13]!=0}init(type,destructor){this.set_adjusted_ptr(0);this.set_type(type);this.set_destructor(destructor)}set_adjusted_ptr(adjustedPtr){HEAPU32[this.ptr+16>>2]=adjustedPtr}get_adjusted_ptr(){return HEAPU32[this.ptr+16>>2]}}var exceptionLast=0;var uncaughtExceptionCount=0;var ___cxa_throw=(ptr,type,destructor)=>{var info=new ExceptionInfo(ptr);info.init(type,destructor);exceptionLast=ptr;uncaughtExceptionCount++;throw exceptionLast};var __abort_js=()=>abort("");var getHeapMax=()=>536870912;var alignMemory=(size,alignment)=>Math.ceil(size/alignment)*alignment;var growMemory=size=>{var oldHeapSize=wasmMemory.buffer.byteLength;var pages=(size-oldHeapSize+65535)/65536|0;try{wasmMemory.grow(pages);updateMemoryViews();return 1}catch(e){}};var _emscripten_resize_heap=requestedSize=>{var oldSize=HEAPU8.length;requestedSize>>>=0;var maxHeapSize=getHeapMax();if(requestedSize>maxHeapSize){return false}for(var cutDown=1;cutDown<=4;cutDown*=2){var overGrownHeapSize=oldSize*(1+.2/cutDown);overGrownHeapSize=Math.min(overGrownHeapSize,requestedSize+100663296);var newSize=Math.min(maxHeapSize,alignMemory(Math.max(requestedSize,overGrownHeapSize),65536));var replacement=growMemory(newSize);if(replacement){return true}}return false};var getCFunc=ident=>{var func=Module["_"+ident];return func};var writeArrayToMemory=(array,buffer)=>{HEAP8.set(array,buffer)};var lengthBytesUTF8=str=>{var len=0;for(var i=0;i=55296&&c<=57343){len+=4;++i}else{len+=3}}return len};var stringToUTF8Array=(str,heap,outIdx,maxBytesToWrite)=>{if(!(maxBytesToWrite>0))return 0;var startIdx=outIdx;var endIdx=outIdx+maxBytesToWrite-1;for(var i=0;i=endIdx)break;heap[outIdx++]=u}else if(u<=2047){if(outIdx+1>=endIdx)break;heap[outIdx++]=192|u>>6;heap[outIdx++]=128|u&63}else if(u<=65535){if(outIdx+2>=endIdx)break;heap[outIdx++]=224|u>>12;heap[outIdx++]=128|u>>6&63;heap[outIdx++]=128|u&63}else{if(outIdx+3>=endIdx)break;heap[outIdx++]=240|u>>18;heap[outIdx++]=128|u>>12&63;heap[outIdx++]=128|u>>6&63;heap[outIdx++]=128|u&63;i++}}heap[outIdx]=0;return outIdx-startIdx};var stringToUTF8=(str,outPtr,maxBytesToWrite)=>stringToUTF8Array(str,HEAPU8,outPtr,maxBytesToWrite);var stackAlloc=sz=>__emscripten_stack_alloc(sz);var stringToUTF8OnStack=str=>{var size=lengthBytesUTF8(str)+1;var ret=stackAlloc(size);stringToUTF8(str,ret,size);return ret};var ccall=(ident,returnType,argTypes,args,opts)=>{var toC={string:str=>{var ret=0;if(str!==null&&str!==undefined&&str!==0){ret=stringToUTF8OnStack(str)}return ret},array:arr=>{var ret=stackAlloc(arr.length);writeArrayToMemory(arr,ret);return ret}};function convertReturnValue(ret){if(returnType==="string"){return UTF8ToString(ret)}if(returnType==="boolean")return Boolean(ret);return ret}var func=getCFunc(ident);var cArgs=[];var stack=0;if(args){for(var i=0;i{var numericArgs=!argTypes||argTypes.every(type=>type==="number"||type==="boolean");var numericRet=returnType!=="string";if(numericRet&&numericArgs&&!opts){return getCFunc(ident)}return(...args)=>ccall(ident,returnType,argTypes,args,opts)};for(var base64ReverseLookup=new Uint8Array(123),i=25;i>=0;--i){base64ReverseLookup[48+i]=52+i;base64ReverseLookup[65+i]=i;base64ReverseLookup[97+i]=26+i}base64ReverseLookup[43]=62;base64ReverseLookup[47]=63;{if(Module["noExitRuntime"])noExitRuntime=Module["noExitRuntime"];if(Module["print"])out=Module["print"];if(Module["printErr"])err=Module["printErr"];if(Module["wasmBinary"])wasmBinary=Module["wasmBinary"];if(Module["arguments"])arguments_=Module["arguments"];if(Module["thisProgram"])thisProgram=Module["thisProgram"];if(Module["preInit"]){if(typeof Module["preInit"]=="function")Module["preInit"]=[Module["preInit"]];while(Module["preInit"].length>0){Module["preInit"].shift()()}}}Module["ccall"]=ccall;Module["cwrap"]=cwrap;Module["UTF8ToString"]=UTF8ToString;var _parseMarkdown,__emscripten_stack_restore,__emscripten_stack_alloc,_emscripten_stack_get_current,memory,__indirect_function_table,wasmMemory;function assignWasmExports(wasmExports){_parseMarkdown=Module["_parseMarkdown"]=wasmExports["parseMarkdown"];__emscripten_stack_restore=wasmExports["_emscripten_stack_restore"];__emscripten_stack_alloc=wasmExports["_emscripten_stack_alloc"];_emscripten_stack_get_current=wasmExports["emscripten_stack_get_current"];memory=wasmMemory=wasmExports["memory"];__indirect_function_table=wasmExports["__indirect_function_table"]}var wasmImports={__assert_fail:___assert_fail,__cxa_throw:___cxa_throw,_abort_js:__abort_js,emscripten_resize_heap:_emscripten_resize_heap};function run(){preRun();function doRun(){Module["calledRun"]=true;if(ABORT)return;initRuntime();readyPromiseResolve?.(Module);Module["onRuntimeInitialized"]?.();postRun()}if(Module["setStatus"]){Module["setStatus"]("Running...");setTimeout(()=>{setTimeout(()=>Module["setStatus"](""),1);doRun()},1)}else{doRun()}}var wasmExports;wasmExports=await (createWasm());run();if(runtimeInitialized){moduleRtn=Module}else{moduleRtn=new Promise((resolve,reject)=>{readyPromiseResolve=resolve;readyPromiseReject=reject})} ++;return moduleRtn}})();if(typeof exports==="object"&&typeof module==="object"){module.exports=createMd4cModule;module.exports.default=createMd4cModule}else if(typeof define==="function"&&define["amd"])define([],()=>createMd4cModule); diff --git a/patches/react-native-markdown-display+7.0.2.patch b/patches/react-native-markdown-display+7.0.2.patch deleted file mode 100644 index ad8ac0bf66..0000000000 --- a/patches/react-native-markdown-display+7.0.2.patch +++ /dev/null @@ -1,74 +0,0 @@ -diff --git a/node_modules/react-native-markdown-display/src/lib/AstRenderer.js b/node_modules/react-native-markdown-display/src/lib/AstRenderer.js -index 608e945..edafe12 100644 ---- a/node_modules/react-native-markdown-display/src/lib/AstRenderer.js -+++ b/node_modules/react-native-markdown-display/src/lib/AstRenderer.js -@@ -1,6 +1,5 @@ - import {StyleSheet} from 'react-native'; - --import getUniqueID from './util/getUniqueID'; - import convertAdditionalStyles from './util/convertAdditionalStyles'; - - import textStyleProps from './data/textStyleProps'; -@@ -180,7 +179,7 @@ export default class AstRenderer { - * @return {*} - */ - render = (nodes) => { -- const root = {type: 'body', key: getUniqueID(), children: nodes}; -+ const root = {type: 'body', key: 'rnmr_root', children: nodes}; - return this.renderNode(root, [], true); - }; - } -diff --git a/node_modules/react-native-markdown-display/src/lib/util/tokensToAST.js b/node_modules/react-native-markdown-display/src/lib/util/tokensToAST.js -index b0ed265..68e2150 100644 ---- a/node_modules/react-native-markdown-display/src/lib/util/tokensToAST.js -+++ b/node_modules/react-native-markdown-display/src/lib/util/tokensToAST.js -@@ -1,4 +1,3 @@ --import getUniqueID from './getUniqueID'; - import getTokenTypeByToken from './getTokenTypeByToken'; - - /** -@@ -7,9 +6,10 @@ import getTokenTypeByToken from './getTokenTypeByToken'; - * @param {number} tokenIndex - * @return {{type: string, content, tokenIndex: *, index: number, attributes: {}, children: *}} - */ --function createNode(token, tokenIndex) { -+function createNode(token, tokenIndex, parentKey) { - const type = getTokenTypeByToken(token); - const content = token.content; -+ const keyPath = parentKey ? `${parentKey}.${tokenIndex}` : `${tokenIndex}`; - - let attributes = {}; - -@@ -27,12 +27,12 @@ function createNode(token, tokenIndex) { - sourceMeta: token.meta, - block: token.block, - markup: token.markup, -- key: getUniqueID() + '_' + type, -+ key: `rnmr_${keyPath}_${type}`, - content, - tokenIndex, - index: 0, - attributes, -- children: tokensToAST(token.children), -+ children: tokensToAST(token.children, keyPath), - }; - } - -@@ -41,7 +41,7 @@ function createNode(token, tokenIndex) { - * @param {Array<{type: string, tag:string, content: string, children: *, attrs: Array}>}tokens - * @return {Array} - */ --export default function tokensToAST(tokens) { -+export default function tokensToAST(tokens, parentKey = '') { - let stack = []; - let children = []; - -@@ -51,7 +51,7 @@ export default function tokensToAST(tokens) { - - for (let i = 0; i < tokens.length; i++) { - const token = tokens[i]; -- const astNode = createNode(token, i); -+ const astNode = createNode(token, i, parentKey); - - if ( - !( diff --git a/scripts/postinstall-patches.mjs b/scripts/postinstall-patches.mjs index 3eeafc6371..a571614c5a 100644 --- a/scripts/postinstall-patches.mjs +++ b/scripts/postinstall-patches.mjs @@ -9,8 +9,9 @@ import { join, relative } from "node:path"; // the patch's node_modules/... paths relative to its working directory. const patchedPackages = [ { - nodeModulesPath: "node_modules/react-native-markdown-display", - patchPrefix: "react-native-markdown-display+", + nodeModulesPath: "packages/app/node_modules/react-native-enriched-markdown", + patchPrefix: "react-native-enriched-markdown+", + cwd: "packages/app", }, // Remove after react-native-unistyles ships // https://github.com/jpudysz/react-native-unistyles/pull/1203.