@orz/ui-markdown 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/LICENSE +29 -0
  2. package/README.md +175 -2
  3. package/dist/attachment-links.d.ts +20 -0
  4. package/dist/attachment-links.d.ts.map +1 -0
  5. package/dist/attachment-links.js +27 -0
  6. package/dist/attachment-links.js.map +1 -0
  7. package/dist/content.d.ts +31 -0
  8. package/dist/content.d.ts.map +1 -0
  9. package/dist/content.js +78 -0
  10. package/dist/content.js.map +1 -0
  11. package/dist/embeds.d.ts +18 -0
  12. package/dist/embeds.d.ts.map +1 -0
  13. package/dist/embeds.js +59 -0
  14. package/dist/embeds.js.map +1 -0
  15. package/dist/external-links.d.ts +23 -0
  16. package/dist/external-links.d.ts.map +1 -0
  17. package/dist/external-links.js +43 -0
  18. package/dist/external-links.js.map +1 -0
  19. package/dist/index.d.ts +7 -0
  20. package/dist/index.d.ts.map +1 -0
  21. package/dist/index.js +6 -0
  22. package/dist/index.js.map +1 -0
  23. package/dist/lib/markdown-processor.d.ts +23 -0
  24. package/dist/lib/markdown-processor.d.ts.map +1 -0
  25. package/dist/lib/markdown-processor.js +35 -0
  26. package/dist/lib/markdown-processor.js.map +1 -0
  27. package/dist/lib/rehype-details.d.ts +13 -0
  28. package/dist/lib/rehype-details.d.ts.map +1 -0
  29. package/dist/lib/rehype-details.js +256 -0
  30. package/dist/lib/rehype-details.js.map +1 -0
  31. package/dist/lib/rehype-source-lines.d.ts +23 -0
  32. package/dist/lib/rehype-source-lines.d.ts.map +1 -0
  33. package/dist/lib/rehype-source-lines.js +77 -0
  34. package/dist/lib/rehype-source-lines.js.map +1 -0
  35. package/dist/lib/remark-frontmatter-table.d.ts +119 -0
  36. package/dist/lib/remark-frontmatter-table.d.ts.map +1 -0
  37. package/dist/lib/remark-frontmatter-table.js +212 -0
  38. package/dist/lib/remark-frontmatter-table.js.map +1 -0
  39. package/dist/lib/remark-rejected-urls.d.ts +23 -0
  40. package/dist/lib/remark-rejected-urls.d.ts.map +1 -0
  41. package/dist/lib/remark-rejected-urls.js +79 -0
  42. package/dist/lib/remark-rejected-urls.js.map +1 -0
  43. package/dist/markdown.d.ts +30 -0
  44. package/dist/markdown.d.ts.map +1 -0
  45. package/dist/markdown.js +146 -0
  46. package/dist/markdown.js.map +1 -0
  47. package/dist/plugin.d.ts +39 -0
  48. package/dist/plugin.d.ts.map +1 -0
  49. package/dist/plugin.js +2 -0
  50. package/dist/plugin.js.map +1 -0
  51. package/package.json +66 -3
  52. package/styles.css +293 -0
package/LICENSE ADDED
@@ -0,0 +1,29 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright 2026 the orzui team
4
+ Copyright 2026 the todou team
5
+
6
+ Redistribution and use in source and binary forms, with or without
7
+ modification, are permitted provided that the following conditions are met:
8
+
9
+ 1. Redistributions of source code must retain the above copyright notice, this
10
+ list of conditions and the following disclaimer.
11
+
12
+ 2. Redistributions in binary form must reproduce the above copyright notice,
13
+ this list of conditions and the following disclaimer in the documentation
14
+ and/or other materials provided with the distribution.
15
+
16
+ 3. Neither the name of the copyright holder nor the names of its
17
+ contributors may be used to endorse or promote products derived from
18
+ this software without specific prior written permission.
19
+
20
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
package/README.md CHANGED
@@ -1,3 +1,176 @@
1
- # Temporary Holding Version
1
+ # @orz/ui-markdown
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ GitHub-style markdown on [react-markdown](https://www.npmjs.com/package/react-markdown), extended by
4
+ markdown plugins. Code blocks and embedded files are drawn through
5
+ `@orz/ui-content`'s registry, so markdown reaches code, diagrams and other
6
+ documents without importing any of them.
7
+
8
+ ## Install
9
+
10
+ ```bash
11
+ pnpm add @orz/ui-markdown @orz/ui-content @orz/ui react
12
+ ```
13
+
14
+ `@orz/ui-content` is a peer dependency so that the app, this package and every other content package
15
+ share one registry. `@orz/ui` provides the Retry button and the theme tokens the stylesheet paints
16
+ with.
17
+
18
+ ## CSS
19
+
20
+ ```css
21
+ @import "tailwindcss";
22
+ @import "@orz/ui/styles.css";
23
+ @import "@orz/ui-markdown/styles.css";
24
+ ```
25
+
26
+ Import it after `@orz/ui/styles.css`. It holds the `.markdown-body` typography, in
27
+ `@layer components`, and no Tailwind directives: the package's own elements use no utility classes,
28
+ and the one exception, the Retry button, is `@orz/ui`'s `Button`, which `@orz/ui/styles.css`
29
+ already registers.
30
+
31
+ ## `<Markdown>`
32
+
33
+ ```tsx
34
+ import { Markdown } from "@orz/ui-markdown";
35
+
36
+ <Markdown>{source}</Markdown>
37
+ <Markdown plugins={plugins} className="text-sm" ref={rootRef}>{source}</Markdown>
38
+ ```
39
+
40
+ The fixed chain is: GFM; links whose protocol react-markdown refuses, drawn as their source text;
41
+ YAML and TOML frontmatter as a two-column table; raw HTML escaped to its source except
42
+ `<details>`/`<summary>` folds. Plugins append to it and cannot remove any of it. A consumer who needs
43
+ a different chain composes react-markdown with the `lib/*` pieces below.
44
+
45
+ A `<pre>` holding one `<code>` is a code block. It renders inside
46
+ `<div class="markdown-fence">`, which carries the `<pre>`'s classes and, under `rehypeSourceLines`,
47
+ its `data-loc` stamp plus `data-loc-content-start`, the line the code starts on. Inside, the block is
48
+ `<Content source={{ kind: "inline", text, lang }} placement="document">`: whichever renderer is
49
+ registered draws it, and with none it is a plain `<pre><code>`. Registering `@orz/ui-code`'s
50
+ `codeContent` highlights every fence:
51
+
52
+ ```tsx
53
+ import { codeContent } from "@orz/ui-code";
54
+ import { ContentRenderers } from "@orz/ui-content";
55
+
56
+ const RENDERERS = [codeContent];
57
+
58
+ <ContentRenderers renderers={RENDERERS}>
59
+ <Markdown>{source}</Markdown>
60
+ </ContentRenderers>;
61
+ ```
62
+
63
+ A renderer for one language, such as `@orz/ui-mermaid`'s `mermaidContent`, goes ahead of it in the
64
+ list.
65
+
66
+ **Keep `plugins` stable**: a module constant, or a `useMemo`. A fresh array, or a fresh plugin object
67
+ in it, makes react-markdown reparse the document and React remount every overridden element, and
68
+ either one collapses a live text selection inside it.
69
+
70
+ ## Writing a plugin
71
+
72
+ ```ts
73
+ type MarkdownPlugin = {
74
+ name: string;
75
+ remarkPlugins?: PluggableList; // after the fixed syntax chain
76
+ rehypePlugins?: PluggableList; // after rehypeDetails
77
+ components?: { [Tag]?: ComponentType<ElementProps<Tag> & { Next }> }; // any tag but pre
78
+ fence?: ComponentType<FenceProps & { Next: ComponentType<FenceProps> }>;
79
+ };
80
+ ```
81
+
82
+ The first plugin in the list is outermost: its wrapper renders first, and its `Next` renders the
83
+ element the way the plugins after it would. The bottom of every element chain is the plain element;
84
+ the bottom of the fence chain is the registry.
85
+
86
+ ```tsx
87
+ import { rehypeSourceLines } from "@orz/ui-markdown/lib/rehype-source-lines";
88
+
89
+ const anchors: MarkdownPlugin = {
90
+ name: "anchors",
91
+ rehypePlugins: [rehypeSourceLines],
92
+ components: {
93
+ h2: ({ Next, ...props }) => (
94
+ <a href={`#${slugOf(props.children)}`}>
95
+ <Next {...props} />
96
+ </a>
97
+ ),
98
+ },
99
+ // FenceProps: { code, lang, loc }; lang is the info string's first word as written.
100
+ fence: ({ Next, ...props }) =>
101
+ props.lang === "todo" ? <TodoList text={props.code} /> : <Next {...props} />,
102
+ };
103
+
104
+ const PLUGINS = [anchors];
105
+ ```
106
+
107
+ Element wrappers receive react-markdown's props, including the hast `node`; pass them on to `Next`.
108
+
109
+ ## Embeds and markdown files
110
+
111
+ ```tsx
112
+ import { codeContent } from "@orz/ui-code";
113
+ import { ContentRenderers } from "@orz/ui-content";
114
+ import { Markdown, markdownContent, markdownEmbeds } from "@orz/ui-markdown";
115
+
116
+ const RENDERERS = [markdownContent(), codeContent];
117
+
118
+ function Doc({ source }: { source: string }) {
119
+ const plugins = useMemo(() => [markdownEmbeds({ resolve })], []);
120
+ return <Markdown plugins={plugins}>{source}</Markdown>;
121
+ }
122
+
123
+ <ContentRenderers renderers={RENDERERS}>
124
+ <Doc source={"See the plan:\n\n![plan](files/plan.md)"} />
125
+ </ContentRenderers>;
126
+ ```
127
+
128
+ - **`markdownEmbeds({ resolve })`.** `resolve(href)` turns an image address into a
129
+ `ContentSource`, or null to leave the `<img>` alone. It must be synchronous and pure. An image
130
+ whose source some registered renderer accepts is drawn by it, keeping the `<img>`'s class. A
131
+ paragraph carrying a block embed becomes `div.markdown-paragraph`, decided from the markdown,
132
+ `resolve` and the renderers' `accepts`, never from loaded data, so it cannot flip when the text
133
+ arrives. Links are not touched.
134
+ - **`markdownContent({ plugins?, maxDepth? })`** accepts files named `*.md`/`*.markdown` or typed
135
+ `text/markdown`, in either placement. It shows the fallback link while loading, the message and a
136
+ Retry button on failure, and the document once loaded, with the plugins of the enclosing
137
+ `<Markdown>` (else `options.plugins`). In `"document"` placement, a document deeper than `maxDepth`
138
+ (else the provider's `maxDepth`, else 1) is a link: a document inside a document shows its own
139
+ documents as links, which is what stops a document that embeds itself. A `"viewer"` placement is
140
+ never limited. A "source" toggle in the title bar a surface lends (the viewer, `attachmentCard`)
141
+ swaps the rendered document for its source, drawn by the renderers after this one
142
+ (`codeContent`). A document over `MARKDOWN_RENDER_MAX_CHARS` (128K characters) opens as source,
143
+ with "Too large to render automatically.", since rendering it would hold the tab for seconds;
144
+ with no title bar lent, such a document gets the toggle above it.
145
+ - **`attachmentLinks({ resolve, boxed? })`** draws a link whose address `resolve` turns into a
146
+ file as an `AttachmentLink` chip from `@orz/ui-content/attachment`, which opens the nearest
147
+ `AttachmentViewerProvider`'s viewer on a plain click. Every other link goes on to the next
148
+ plugin. `resolve` has `markdownEmbeds`' signature, so one function serves both:
149
+ `[markdownEmbeds({ resolve }), attachmentLinks({ resolve })]`. A file `resolve` marks `deleted`
150
+ is a `DeletedAttachmentMark` in either plugin, inline in its paragraph, rather than a link to
151
+ nothing or a broken image. Using it means installing what
152
+ `@orz/ui-content/attachment` needs (its README, "Attachments").
153
+ - **`externalLinks({ origin? })`** opens a link to an http(s) address on another origin (default
154
+ `window.location.origin`) in a new tab, with `rel="noopener noreferrer"`; `styles.css` draws a ↗
155
+ after it, except on an image link. Relative, same-origin, fragment and `mailto:` links are left
156
+ alone. Put it after `attachmentLinks`, so file links are claimed first, and leave it out of the
157
+ list to keep every link in this tab. `isExternalHref(href, origin?)` is the test it applies.
158
+
159
+ Register `markdownContent` ahead of any renderer that takes every text file, since the first renderer
160
+ that accepts wins. `react-markdown` refuses some URL schemes (`file:` among them), and an address it
161
+ refuses is drawn as source text before `resolve` sees it: use relative addresses or `https:`.
162
+
163
+ ## Entry points
164
+
165
+ | Import | Contents |
166
+ |---|---|
167
+ | `@orz/ui-markdown` | `Markdown`, `MarkdownPluginsContext`, `markdownEmbeds`, `attachmentLinks`, `externalLinks`, `isExternalHref`, `markdownContent`, `MARKDOWN_RENDER_MAX_CHARS`, types (`MarkdownPlugin`, `FenceProps`, `ElementProps`, …) |
168
+ | `@orz/ui-markdown/lib/markdown-processor` | `MARKDOWN_SYNTAX_PLUGINS`, `parseMarkdown` |
169
+ | `@orz/ui-markdown/lib/remark-frontmatter-table` | `remarkFrontmatterTable`, `frontmatterFields`, `FRONTMATTER_FLAVOURS`, node types |
170
+ | `@orz/ui-markdown/lib/remark-rejected-urls` | `remarkRejectedUrlsAsText` |
171
+ | `@orz/ui-markdown/lib/rehype-source-lines` | `rehypeSourceLines`, `SOURCE_LINE_ATTR`, `CODE_CONTENT_START_ATTR`, `parseSourceLoc` |
172
+ | `@orz/ui-markdown/lib/rehype-details` | `rehypeDetails` |
173
+ | `@orz/ui-markdown/styles.css` | the `.markdown-body` typography |
174
+
175
+ The unified pieces are exported one by one so that code rebuilding the same tree outside React (an
176
+ index over the source, a diff) runs exactly the renderer's chain.
@@ -0,0 +1,20 @@
1
+ import type { ContentSource } from "@orz/ui-content";
2
+ import type { MarkdownPlugin } from "./plugin.ts";
3
+ export type AttachmentLinksOptions = {
4
+ /**
5
+ * Turns a link address into the file it points at, or null to leave the link alone. The same
6
+ * function `markdownEmbeds` takes, so an app passes one resolver to both.
7
+ */
8
+ resolve: (href: string) => ContentSource | null;
9
+ /** Draw the chips with a border. Default false: underlined on hover. */
10
+ boxed?: boolean;
11
+ };
12
+ /**
13
+ * Draws a markdown link to a file as an `AttachmentLink` chip, which opens the nearest attachment
14
+ * viewer on a plain click, or as a `DeletedAttachmentMark` when `resolve` marks the file
15
+ * `deleted`. Links that do not resolve to a file go on to the plugins after this one.
16
+ *
17
+ * Returns a new plugin on every call, so memoise the call along with the rest of the plugin list.
18
+ */
19
+ export declare function attachmentLinks({ resolve, boxed, }: AttachmentLinksOptions): MarkdownPlugin;
20
+ //# sourceMappingURL=attachment-links.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"attachment-links.d.ts","sourceRoot":"","sources":["../src/attachment-links.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAKrD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAElD,MAAM,MAAM,sBAAsB,GAAG;IACnC;;;OAGG;IACH,OAAO,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,aAAa,GAAG,IAAI,CAAC;IAChD,wEAAwE;IACxE,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB,CAAC;AAEF;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,EAC9B,OAAO,EACP,KAAK,GACN,EAAE,sBAAsB,GAAG,cAAc,CA8BzC"}
@@ -0,0 +1,27 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { AttachmentLink, DeletedAttachmentMark, } from "@orz/ui-content/attachment";
3
+ /**
4
+ * Draws a markdown link to a file as an `AttachmentLink` chip, which opens the nearest attachment
5
+ * viewer on a plain click, or as a `DeletedAttachmentMark` when `resolve` marks the file
6
+ * `deleted`. Links that do not resolve to a file go on to the plugins after this one.
7
+ *
8
+ * Returns a new plugin on every call, so memoise the call along with the rest of the plugin list.
9
+ */
10
+ export function attachmentLinks({ resolve, boxed, }) {
11
+ return {
12
+ name: "attachment-links",
13
+ components: {
14
+ a: function AttachmentAnchor({ Next, node, ...props }) {
15
+ const source = typeof props.href === "string" ? resolve(props.href) : null;
16
+ if (source === null || source.kind !== "file") {
17
+ return _jsx(Next, { node: node, ...props });
18
+ }
19
+ if (source.deleted) {
20
+ return (_jsx(DeletedAttachmentMark, { name: source.name, className: props.className }));
21
+ }
22
+ return (_jsx(AttachmentLink, { file: source, boxed: boxed, className: props.className, children: props.children }));
23
+ },
24
+ },
25
+ };
26
+ }
27
+ //# sourceMappingURL=attachment-links.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"attachment-links.js","sourceRoot":"","sources":["../src/attachment-links.tsx"],"names":[],"mappings":";AACA,OAAO,EACL,cAAc,EACd,qBAAqB,GACtB,MAAM,4BAA4B,CAAC;AAapC;;;;;;GAMG;AACH,MAAM,UAAU,eAAe,CAAC,EAC9B,OAAO,EACP,KAAK,GACkB;IACvB,OAAO;QACL,IAAI,EAAE,kBAAkB;QACxB,UAAU,EAAE;YACV,CAAC,EAAE,SAAS,gBAAgB,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,GAAG,KAAK,EAAE;gBACnD,MAAM,MAAM,GACV,OAAO,KAAK,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;gBAC9D,IAAI,MAAM,KAAK,IAAI,IAAI,MAAM,CAAC,IAAI,KAAK,MAAM,EAAE,CAAC;oBAC9C,OAAO,KAAC,IAAI,IAAC,IAAI,EAAE,IAAI,KAAM,KAAK,GAAI,CAAC;gBACzC,CAAC;gBACD,IAAI,MAAM,CAAC,OAAO,EAAE,CAAC;oBACnB,OAAO,CACL,KAAC,qBAAqB,IACpB,IAAI,EAAE,MAAM,CAAC,IAAI,EACjB,SAAS,EAAE,KAAK,CAAC,SAAS,GAC1B,CACH,CAAC;gBACJ,CAAC;gBACD,OAAO,CACL,KAAC,cAAc,IACb,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,KAAK,EACZ,SAAS,EAAE,KAAK,CAAC,SAAS,YAEzB,KAAK,CAAC,QAAQ,GACA,CAClB,CAAC;YACJ,CAAC;SACF;KACF,CAAC;AACJ,CAAC"}
@@ -0,0 +1,31 @@
1
+ import { type ContentRenderer } from "@orz/ui-content";
2
+ import type { MarkdownPlugin } from "./plugin.ts";
3
+ export type MarkdownContentOptions = {
4
+ /**
5
+ * Plugins for a document drawn outside any `<Markdown>`, such as in a viewer. Inside one, the
6
+ * enclosing document's plugins win, so an embed is drawn the way its host is.
7
+ */
8
+ plugins?: readonly MarkdownPlugin[];
9
+ /**
10
+ * How many embedded documents deep a document still renders in `"document"` placement; past it,
11
+ * the document is a link. Defaults to the provider's `maxDepth`, else 1: a document inside a
12
+ * document shows its own documents as links, which is what stops a document that embeds itself.
13
+ */
14
+ maxDepth?: number;
15
+ };
16
+ /**
17
+ * Past this a markdown file opens as its source, and renders on request. Highlighting can move
18
+ * off the main thread, react-markdown's tree cannot: 64 KiB of dense markdown blocked the tab for
19
+ * 1.0 s and 256 KiB for 3.4 s. Counted in characters, since the text is already decoded by then.
20
+ */
21
+ export declare const MARKDOWN_RENDER_MAX_CHARS: number;
22
+ /**
23
+ * The registry entry for markdown files, in either placement. Register it ahead of any renderer
24
+ * that takes every text file, since the first renderer that accepts wins; that renderer draws the
25
+ * file's source when the reader toggles to it, and a file over `MARKDOWN_RENDER_MAX_CHARS` opens
26
+ * that way.
27
+ *
28
+ * Returns a new renderer on every call, so call it once, where the renderer list is built.
29
+ */
30
+ export declare function markdownContent(options?: MarkdownContentOptions): ContentRenderer;
31
+ //# sourceMappingURL=content.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"content.d.ts","sourceRoot":"","sources":["../src/content.tsx"],"names":[],"mappings":"AAEA,OAAO,EAGL,KAAK,eAAe,EAMrB,MAAM,iBAAiB,CAAC;AAKzB,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAElD,MAAM,MAAM,sBAAsB,GAAG;IACnC;;;OAGG;IACH,OAAO,CAAC,EAAE,SAAS,cAAc,EAAE,CAAC;IACpC;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,CAAC;AAIF;;;;GAIG;AACH,eAAO,MAAM,yBAAyB,QAAa,CAAC;AAgFpD;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAC7B,OAAO,GAAE,sBAA2B,GACnC,eAAe,CA8DjB"}
@@ -0,0 +1,78 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Button } from "@orz/ui/components/button";
3
+ import { cn } from "@orz/ui/lib/utils";
4
+ import { ContentFallback, useContentDepth, useContentMaxDepth, useContentTitleBar, useSourceText, } from "@orz/ui-content";
5
+ import { CodeIcon } from "lucide-react";
6
+ import { useContext, useState } from "react";
7
+ import { createPortal } from "react-dom";
8
+ import { Markdown, MarkdownPluginsContext } from "./markdown.js";
9
+ const NOTHING = { kind: "inline", text: "" };
10
+ /**
11
+ * Past this a markdown file opens as its source, and renders on request. Highlighting can move
12
+ * off the main thread, react-markdown's tree cannot: 64 KiB of dense markdown blocked the tab for
13
+ * 1.0 s and 256 KiB for 3.4 s. Counted in characters, since the text is already decoded by then.
14
+ */
15
+ export const MARKDOWN_RENDER_MAX_CHARS = 128 * 1024;
16
+ /**
17
+ * Rendered, or the markdown as written, drawn by the renderers after this one (`codeContent`). A
18
+ * large one opens as written. The toggle goes into the title bar the surface lends (a viewer, an
19
+ * embed card); a large document with none gets it above.
20
+ */
21
+ function MarkdownDocument({ source, placement, className, text, plugins, Next, }) {
22
+ const titleBar = useContentTitleBar(source);
23
+ const large = text.length > MARKDOWN_RENDER_MAX_CHARS;
24
+ const [showSource, setShowSource] = useState(large);
25
+ const control = (_jsxs(Button, { type: "button", size: "xs", variant: "ghost", "aria-pressed": showSource, title: showSource ? "Back to the rendered document" : "The markdown as written", className: cn("font-normal text-muted-foreground", showSource && "bg-muted text-foreground"), onClick: () => setShowSource((shown) => !shown), children: [_jsx(CodeIcon, {}), "source"] }));
26
+ const body = showSource ? (_jsxs("div", { className: className, children: [large && (_jsx("p", { className: "mb-2 text-xs text-muted-foreground", children: "Too large to render automatically." })), _jsx(Next, { source: source, placement: placement })] })) : (_jsx(Markdown, { plugins: plugins, className: className, children: text }));
27
+ if (titleBar === undefined) {
28
+ // A document drawn in another one's flow, with no surface of its own,
29
+ // gets a row for the toggle only when it opened as source.
30
+ if (!large)
31
+ return body;
32
+ return (_jsxs(_Fragment, { children: [_jsx("div", { className: "mb-1 flex justify-end", children: control }), body] }));
33
+ }
34
+ return (_jsxs(_Fragment, { children: [titleBar !== null && createPortal(control, titleBar), body] }));
35
+ }
36
+ function isMarkdownFile(source) {
37
+ if (source.kind !== "file")
38
+ return false;
39
+ const mime = source.mime?.split(";")[0]?.trim().toLowerCase();
40
+ return /\.(md|markdown)$/i.test(source.name) || mime === "text/markdown";
41
+ }
42
+ /**
43
+ * The registry entry for markdown files, in either placement. Register it ahead of any renderer
44
+ * that takes every text file, since the first renderer that accepts wins; that renderer draws the
45
+ * file's source when the reader toggles to it, and a file over `MARKDOWN_RENDER_MAX_CHARS` opens
46
+ * that way.
47
+ *
48
+ * Returns a new renderer on every call, so call it once, where the renderer list is built.
49
+ */
50
+ export function markdownContent(options = {}) {
51
+ function MarkdownFile({ source, placement, className, Next, }) {
52
+ const depth = useContentDepth();
53
+ const providerMaxDepth = useContentMaxDepth();
54
+ const hostPlugins = useContext(MarkdownPluginsContext);
55
+ const maxDepth = options.maxDepth ?? providerMaxDepth ?? 1;
56
+ // A viewer is never limited: it is what a reader opens on purpose.
57
+ const limited = placement === "document" && depth > maxDepth;
58
+ // A document past the limit is drawn as a link, so nothing is loaded for it.
59
+ const text = useSourceText(limited ? NOTHING : source);
60
+ if (limited) {
61
+ return (_jsx(ContentFallback, { source: source, placement: placement, className: className }));
62
+ }
63
+ if (text.status === "loading") {
64
+ return (_jsx(ContentFallback, { source: source, placement: placement, className: className }));
65
+ }
66
+ if (text.status === "error") {
67
+ return (_jsxs("div", { className: className, children: [_jsx(ContentFallback, { source: source, placement: placement }), " ", _jsx("span", { children: text.error.message }), " ", _jsx(Button, { size: "xs", variant: "outline", onClick: text.retry, children: "Retry" })] }));
68
+ }
69
+ return (_jsx(MarkdownDocument, { source: source, placement: placement, className: className, text: text.text, plugins: hostPlugins ?? options.plugins, Next: Next }));
70
+ }
71
+ return {
72
+ name: "markdown",
73
+ accepts: isMarkdownFile,
74
+ display: "block",
75
+ Component: MarkdownFile,
76
+ };
77
+ }
78
+ //# sourceMappingURL=content.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"content.js","sourceRoot":"","sources":["../src/content.tsx"],"names":[],"mappings":"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"}
@@ -0,0 +1,18 @@
1
+ import { type ContentSource } from "@orz/ui-content";
2
+ import type { MarkdownPlugin } from "./plugin.ts";
3
+ export type MarkdownEmbedsOptions = {
4
+ /**
5
+ * Turns an image address into a source to draw, or null to leave the `<img>` alone. It must
6
+ * be synchronous and pure: whether a paragraph is a `<p>` is decided from it during render.
7
+ */
8
+ resolve: (href: string) => ContentSource | null;
9
+ };
10
+ /**
11
+ * Draws a markdown image through `@orz/ui-content` when its address resolves to a source some
12
+ * registered renderer accepts, such as `![spec](attachment-address)` resolving to a markdown file.
13
+ * A file `resolve` marks `deleted` is a `DeletedAttachmentMark` instead.
14
+ *
15
+ * Returns a new plugin on every call, so memoise the call along with the rest of the plugin list.
16
+ */
17
+ export declare function markdownEmbeds({ resolve, }: MarkdownEmbedsOptions): MarkdownPlugin;
18
+ //# sourceMappingURL=embeds.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"embeds.d.ts","sourceRoot":"","sources":["../src/embeds.tsx"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,aAAa,EAEnB,MAAM,iBAAiB,CAAC;AAEzB,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAElD,MAAM,MAAM,qBAAqB,GAAG;IAClC;;;OAGG;IACH,OAAO,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,aAAa,GAAG,IAAI,CAAC;CACjD,CAAC;AAEF;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,EAC7B,OAAO,GACR,EAAE,qBAAqB,GAAG,cAAc,CAiExC"}
package/dist/embeds.js ADDED
@@ -0,0 +1,59 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { Content, useContentDisplayOf, } from "@orz/ui-content";
3
+ import { DeletedAttachmentMark } from "@orz/ui-content/attachment";
4
+ /**
5
+ * Draws a markdown image through `@orz/ui-content` when its address resolves to a source some
6
+ * registered renderer accepts, such as `![spec](attachment-address)` resolving to a markdown file.
7
+ * A file `resolve` marks `deleted` is a `DeletedAttachmentMark` instead.
8
+ *
9
+ * Returns a new plugin on every call, so memoise the call along with the rest of the plugin list.
10
+ */
11
+ export function markdownEmbeds({ resolve, }) {
12
+ const sourceOf = (src) => typeof src === "string" ? resolve(src) : null;
13
+ return {
14
+ name: "embeds",
15
+ components: {
16
+ img: function EmbedImage({ Next, node, ...props }) {
17
+ const displayOf = useContentDisplayOf();
18
+ const source = sourceOf(props.src);
19
+ if (source?.kind === "file" && source.deleted) {
20
+ return (_jsx(DeletedAttachmentMark, { name: source.name, className: props.className }));
21
+ }
22
+ if (source === null || displayOf(source, "document") === null) {
23
+ return _jsx(Next, { node: node, ...props });
24
+ }
25
+ // The <img>'s class survives the swap: a decoration pass marks the
26
+ // element the embed was written as (T-223).
27
+ return (_jsx(Content, { source: source, placement: "document", className: props.className }));
28
+ },
29
+ // Block content is forbidden inside a <p>, so a paragraph carrying a
30
+ // block embed becomes a <div> with the paragraph's typography. This is
31
+ // answered from the markdown, `resolve` and the renderers' synchronous
32
+ // `accepts`, never from loaded data: an element type that flipped when
33
+ // data arrived would unmount and rebuild the whole paragraph (T-60).
34
+ // The cost: a paragraph whose embed turns out to point nowhere stays a
35
+ // `div.markdown-paragraph`.
36
+ p: function EmbedParagraph({ Next, node, ...props }) {
37
+ const displayOf = useContentDisplayOf();
38
+ const carriesBlock = (node?.children ?? []).some((child) => {
39
+ if (child.type !== "element" || child.tagName !== "img")
40
+ return false;
41
+ const source = sourceOf(child.properties.src);
42
+ if (source === null)
43
+ return false;
44
+ // A deleted file is an inline mark, whatever would draw it.
45
+ if (source.kind === "file" && source.deleted)
46
+ return false;
47
+ return displayOf(source, "document") === "block";
48
+ });
49
+ if (!carriesBlock)
50
+ return _jsx(Next, { node: node, ...props });
51
+ const { children, className, ...rest } = props;
52
+ return (_jsx("div", { ...rest, className: className
53
+ ? `markdown-paragraph ${className}`
54
+ : "markdown-paragraph", children: children }));
55
+ },
56
+ },
57
+ };
58
+ }
59
+ //# sourceMappingURL=embeds.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"embeds.js","sourceRoot":"","sources":["../src/embeds.tsx"],"names":[],"mappings":";AAAA,OAAO,EACL,OAAO,EAEP,mBAAmB,GACpB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,qBAAqB,EAAE,MAAM,4BAA4B,CAAC;AAWnE;;;;;;GAMG;AACH,MAAM,UAAU,cAAc,CAAC,EAC7B,OAAO,GACe;IACtB,MAAM,QAAQ,GAAG,CAAC,GAAY,EAAwB,EAAE,CACtD,OAAO,GAAG,KAAK,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;IAEhD,OAAO;QACL,IAAI,EAAE,QAAQ;QACd,UAAU,EAAE;YACV,GAAG,EAAE,SAAS,UAAU,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,GAAG,KAAK,EAAE;gBAC/C,MAAM,SAAS,GAAG,mBAAmB,EAAE,CAAC;gBACxC,MAAM,MAAM,GAAG,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;gBACnC,IAAI,MAAM,EAAE,IAAI,KAAK,MAAM,IAAI,MAAM,CAAC,OAAO,EAAE,CAAC;oBAC9C,OAAO,CACL,KAAC,qBAAqB,IACpB,IAAI,EAAE,MAAM,CAAC,IAAI,EACjB,SAAS,EAAE,KAAK,CAAC,SAAS,GAC1B,CACH,CAAC;gBACJ,CAAC;gBACD,IAAI,MAAM,KAAK,IAAI,IAAI,SAAS,CAAC,MAAM,EAAE,UAAU,CAAC,KAAK,IAAI,EAAE,CAAC;oBAC9D,OAAO,KAAC,IAAI,IAAC,IAAI,EAAE,IAAI,KAAM,KAAK,GAAI,CAAC;gBACzC,CAAC;gBACD,mEAAmE;gBACnE,4CAA4C;gBAC5C,OAAO,CACL,KAAC,OAAO,IACN,MAAM,EAAE,MAAM,EACd,SAAS,EAAC,UAAU,EACpB,SAAS,EAAE,KAAK,CAAC,SAAS,GAC1B,CACH,CAAC;YACJ,CAAC;YACD,qEAAqE;YACrE,uEAAuE;YACvE,uEAAuE;YACvE,uEAAuE;YACvE,qEAAqE;YACrE,uEAAuE;YACvE,4BAA4B;YAC5B,CAAC,EAAE,SAAS,cAAc,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,GAAG,KAAK,EAAE;gBACjD,MAAM,SAAS,GAAG,mBAAmB,EAAE,CAAC;gBACxC,MAAM,YAAY,GAAG,CAAC,IAAI,EAAE,QAAQ,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE;oBACzD,IAAI,KAAK,CAAC,IAAI,KAAK,SAAS,IAAI,KAAK,CAAC,OAAO,KAAK,KAAK;wBAAE,OAAO,KAAK,CAAC;oBACtE,MAAM,MAAM,GAAG,QAAQ,CAAC,KAAK,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC;oBAC9C,IAAI,MAAM,KAAK,IAAI;wBAAE,OAAO,KAAK,CAAC;oBAClC,4DAA4D;oBAC5D,IAAI,MAAM,CAAC,IAAI,KAAK,MAAM,IAAI,MAAM,CAAC,OAAO;wBAAE,OAAO,KAAK,CAAC;oBAC3D,OAAO,SAAS,CAAC,MAAM,EAAE,UAAU,CAAC,KAAK,OAAO,CAAC;gBACnD,CAAC,CAAC,CAAC;gBACH,IAAI,CAAC,YAAY;oBAAE,OAAO,KAAC,IAAI,IAAC,IAAI,EAAE,IAAI,KAAM,KAAK,GAAI,CAAC;gBAC1D,MAAM,EAAE,QAAQ,EAAE,SAAS,EAAE,GAAG,IAAI,EAAE,GAAG,KAAK,CAAC;gBAC/C,OAAO,CACL,iBACM,IAAI,EACR,SAAS,EACP,SAAS;wBACP,CAAC,CAAC,sBAAsB,SAAS,EAAE;wBACnC,CAAC,CAAC,oBAAoB,YAGzB,QAAQ,GACL,CACP,CAAC;YACJ,CAAC;SACF;KACF,CAAC;AACJ,CAAC"}
@@ -0,0 +1,23 @@
1
+ import type { MarkdownPlugin } from "./plugin.ts";
2
+ export type ExternalLinksOptions = {
3
+ /** The origin a link has to leave to count as external. Default `window.location.origin`. */
4
+ origin?: string;
5
+ };
6
+ /**
7
+ * Whether `href` leaves the app: an http(s) address on another origin. A relative or same-origin
8
+ * URL is still the app's, and `mailto:` and friends hand off to another app rather than a page,
9
+ * so a blank tab left behind would be all the reader got.
10
+ */
11
+ export declare function isExternalHref(href: string | undefined, origin?: string): boolean;
12
+ /**
13
+ * Opens external links in a new tab, with `rel="noopener noreferrer"`; `styles.css` draws a ↗
14
+ * after each one but image links. Links stay real `<a href>`s, and every other link is untouched.
15
+ * Leave it out of the list to keep every link in this tab, as when the reader has not asked for
16
+ * new tabs.
17
+ *
18
+ * Put it after the plugins that draw links of their own (`attachmentLinks`), so those claim
19
+ * theirs first. Returns a new plugin on every call, so memoise the call along with the rest of
20
+ * the plugin list.
21
+ */
22
+ export declare function externalLinks(options?: ExternalLinksOptions): MarkdownPlugin;
23
+ //# sourceMappingURL=external-links.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"external-links.d.ts","sourceRoot":"","sources":["../src/external-links.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAElD,MAAM,MAAM,oBAAoB,GAAG;IACjC,6FAA6F;IAC7F,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF;;;;GAIG;AACH,wBAAgB,cAAc,CAC5B,IAAI,EAAE,MAAM,GAAG,SAAS,EACxB,MAAM,GAAE,MAA+B,GACtC,OAAO,CAYT;AAED;;;;;;;;;GASG;AACH,wBAAgB,aAAa,CAC3B,OAAO,GAAE,oBAAyB,GACjC,cAAc,CAYhB"}
@@ -0,0 +1,43 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ /**
3
+ * Whether `href` leaves the app: an http(s) address on another origin. A relative or same-origin
4
+ * URL is still the app's, and `mailto:` and friends hand off to another app rather than a page,
5
+ * so a blank tab left behind would be all the reader got.
6
+ */
7
+ export function isExternalHref(href, origin = window.location.origin) {
8
+ if (href === undefined)
9
+ return false;
10
+ let url;
11
+ try {
12
+ url = new URL(href, origin);
13
+ }
14
+ catch {
15
+ return false;
16
+ }
17
+ return ((url.protocol === "http:" || url.protocol === "https:") &&
18
+ url.origin !== origin);
19
+ }
20
+ /**
21
+ * Opens external links in a new tab, with `rel="noopener noreferrer"`; `styles.css` draws a ↗
22
+ * after each one but image links. Links stay real `<a href>`s, and every other link is untouched.
23
+ * Leave it out of the list to keep every link in this tab, as when the reader has not asked for
24
+ * new tabs.
25
+ *
26
+ * Put it after the plugins that draw links of their own (`attachmentLinks`), so those claim
27
+ * theirs first. Returns a new plugin on every call, so memoise the call along with the rest of
28
+ * the plugin list.
29
+ */
30
+ export function externalLinks(options = {}) {
31
+ return {
32
+ name: "external-links",
33
+ components: {
34
+ a: function ExternalAnchor({ Next, ...props }) {
35
+ if (!isExternalHref(props.href, options.origin)) {
36
+ return _jsx(Next, { ...props });
37
+ }
38
+ return _jsx(Next, { ...props, target: "_blank", rel: "noopener noreferrer" });
39
+ },
40
+ },
41
+ };
42
+ }
43
+ //# sourceMappingURL=external-links.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"external-links.js","sourceRoot":"","sources":["../src/external-links.tsx"],"names":[],"mappings":";AAOA;;;;GAIG;AACH,MAAM,UAAU,cAAc,CAC5B,IAAwB,EACxB,MAAM,GAAW,MAAM,CAAC,QAAQ,CAAC,MAAM;IAEvC,IAAI,IAAI,KAAK,SAAS;QAAE,OAAO,KAAK,CAAC;IACrC,IAAI,GAAQ,CAAC;IACb,IAAI,CAAC;QACH,GAAG,GAAG,IAAI,GAAG,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;IAC9B,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;IACD,OAAO,CACL,CAAC,GAAG,CAAC,QAAQ,KAAK,OAAO,IAAI,GAAG,CAAC,QAAQ,KAAK,QAAQ,CAAC;QACvD,GAAG,CAAC,MAAM,KAAK,MAAM,CACtB,CAAC;AACJ,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,aAAa,CAC3B,OAAO,GAAyB,EAAE;IAElC,OAAO;QACL,IAAI,EAAE,gBAAgB;QACtB,UAAU,EAAE;YACV,CAAC,EAAE,SAAS,cAAc,CAAC,EAAE,IAAI,EAAE,GAAG,KAAK,EAAE;gBAC3C,IAAI,CAAC,cAAc,CAAC,KAAK,CAAC,IAAI,EAAE,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;oBAChD,OAAO,KAAC,IAAI,OAAK,KAAK,GAAI,CAAC;gBAC7B,CAAC;gBACD,OAAO,KAAC,IAAI,OAAK,KAAK,EAAE,MAAM,EAAC,QAAQ,EAAC,GAAG,EAAC,qBAAqB,GAAG,CAAC;YACvE,CAAC;SACF;KACF,CAAC;AACJ,CAAC"}
@@ -0,0 +1,7 @@
1
+ export { type AttachmentLinksOptions, attachmentLinks, } from "./attachment-links.tsx";
2
+ export { MARKDOWN_RENDER_MAX_CHARS, type MarkdownContentOptions, markdownContent, } from "./content.tsx";
3
+ export { type MarkdownEmbedsOptions, markdownEmbeds } from "./embeds.tsx";
4
+ export { type ExternalLinksOptions, externalLinks, isExternalHref, } from "./external-links.tsx";
5
+ export { Markdown, MarkdownPluginsContext, type MarkdownProps, } from "./markdown.tsx";
6
+ export type { ElementProps, ElementWrapper, FenceProps, MarkdownPlugin, WrappableTag, } from "./plugin.ts";
7
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,sBAAsB,EAC3B,eAAe,GAChB,MAAM,wBAAwB,CAAC;AAChC,OAAO,EACL,yBAAyB,EACzB,KAAK,sBAAsB,EAC3B,eAAe,GAChB,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,KAAK,qBAAqB,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAC1E,OAAO,EACL,KAAK,oBAAoB,EACzB,aAAa,EACb,cAAc,GACf,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EACL,QAAQ,EACR,sBAAsB,EACtB,KAAK,aAAa,GACnB,MAAM,gBAAgB,CAAC;AACxB,YAAY,EACV,YAAY,EACZ,cAAc,EACd,UAAU,EACV,cAAc,EACd,YAAY,GACb,MAAM,aAAa,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,6 @@
1
+ export { attachmentLinks, } from "./attachment-links.js";
2
+ export { MARKDOWN_RENDER_MAX_CHARS, markdownContent, } from "./content.js";
3
+ export { markdownEmbeds } from "./embeds.js";
4
+ export { externalLinks, isExternalHref, } from "./external-links.js";
5
+ export { Markdown, MarkdownPluginsContext, } from "./markdown.js";
6
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,eAAe,GAChB,MAAM,uBAAwB,CAAC;AAChC,OAAO,EACL,yBAAyB,EAEzB,eAAe,GAChB,MAAM,cAAe,CAAC;AACvB,OAAO,EAA8B,cAAc,EAAE,MAAM,aAAc,CAAC;AAC1E,OAAO,EAEL,aAAa,EACb,cAAc,GACf,MAAM,qBAAsB,CAAC;AAC9B,OAAO,EACL,QAAQ,EACR,sBAAsB,GAEvB,MAAM,eAAgB,CAAC"}
@@ -0,0 +1,23 @@
1
+ import type { Nodes } from "mdast";
2
+ import { type PluggableList } from "unified";
3
+ /**
4
+ * Static Markdown syntax shared by every parser and renderer.
5
+ *
6
+ * Keep runtime-dependent plugins (references, say) in the caller: their
7
+ * options belong to the app's context. Keeping
8
+ * this array at module scope also lets renderers retain plugin-list identity
9
+ * instead of rebuilding the invariant prefix on every render.
10
+ *
11
+ * The order is significant. Rejected URLs are restored to source text before
12
+ * later tokenizers see them, and `remarkFrontmatterTable` consumes the nodes
13
+ * produced by `remarkFrontmatter`. Without that tokenizer, frontmatter lines
14
+ * that begin with digits are parsed as ordered-list items (T-383).
15
+ */
16
+ export declare const MARKDOWN_SYNTAX_PLUGINS: PluggableList;
17
+ /**
18
+ * `runSync`, not `parse` alone: `parse` stops at the tokenizer and runs no
19
+ * transformer, so `remarkFrontmatterTable` — which is one — would never fire
20
+ * and the caller would hold the `yaml` leaf while the DOM held a table.
21
+ */
22
+ export declare function parseMarkdown(source: string): Nodes;
23
+ //# sourceMappingURL=markdown-processor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"markdown-processor.d.ts","sourceRoot":"","sources":["../../src/lib/markdown-processor.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,OAAO,CAAC;AAInC,OAAO,EAAE,KAAK,aAAa,EAAW,MAAM,SAAS,CAAC;AAOtD;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,uBAAuB,EAAE,aAKrC,CAAC;AAGF;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG,KAAK,CAEnD"}