@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.
- package/LICENSE +29 -0
- package/README.md +175 -2
- package/dist/attachment-links.d.ts +20 -0
- package/dist/attachment-links.d.ts.map +1 -0
- package/dist/attachment-links.js +27 -0
- package/dist/attachment-links.js.map +1 -0
- package/dist/content.d.ts +31 -0
- package/dist/content.d.ts.map +1 -0
- package/dist/content.js +78 -0
- package/dist/content.js.map +1 -0
- package/dist/embeds.d.ts +18 -0
- package/dist/embeds.d.ts.map +1 -0
- package/dist/embeds.js +59 -0
- package/dist/embeds.js.map +1 -0
- package/dist/external-links.d.ts +23 -0
- package/dist/external-links.d.ts.map +1 -0
- package/dist/external-links.js +43 -0
- package/dist/external-links.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/markdown-processor.d.ts +23 -0
- package/dist/lib/markdown-processor.d.ts.map +1 -0
- package/dist/lib/markdown-processor.js +35 -0
- package/dist/lib/markdown-processor.js.map +1 -0
- package/dist/lib/rehype-details.d.ts +13 -0
- package/dist/lib/rehype-details.d.ts.map +1 -0
- package/dist/lib/rehype-details.js +256 -0
- package/dist/lib/rehype-details.js.map +1 -0
- package/dist/lib/rehype-source-lines.d.ts +23 -0
- package/dist/lib/rehype-source-lines.d.ts.map +1 -0
- package/dist/lib/rehype-source-lines.js +77 -0
- package/dist/lib/rehype-source-lines.js.map +1 -0
- package/dist/lib/remark-frontmatter-table.d.ts +119 -0
- package/dist/lib/remark-frontmatter-table.d.ts.map +1 -0
- package/dist/lib/remark-frontmatter-table.js +212 -0
- package/dist/lib/remark-frontmatter-table.js.map +1 -0
- package/dist/lib/remark-rejected-urls.d.ts +23 -0
- package/dist/lib/remark-rejected-urls.d.ts.map +1 -0
- package/dist/lib/remark-rejected-urls.js +79 -0
- package/dist/lib/remark-rejected-urls.js.map +1 -0
- package/dist/markdown.d.ts +30 -0
- package/dist/markdown.d.ts.map +1 -0
- package/dist/markdown.js +146 -0
- package/dist/markdown.js.map +1 -0
- package/dist/plugin.d.ts +39 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/plugin.js +2 -0
- package/dist/plugin.js.map +1 -0
- package/package.json +66 -3
- 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
|
-
#
|
|
1
|
+
# @orz/ui-markdown
|
|
2
2
|
|
|
3
|
-
|
|
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"} />
|
|
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"}
|
package/dist/content.js
ADDED
|
@@ -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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"}
|
package/dist/embeds.d.ts
ADDED
|
@@ -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 `` 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 `` 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"}
|
package/dist/index.d.ts
ADDED
|
@@ -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"}
|