radiant-docs 0.1.22 → 0.1.25
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/package.json +1 -1
- package/template/astro.config.mjs +9 -1
- package/template/src/components/user/Image.astro +8 -2
- package/template/src/lib/mdx/rehype-external-links.ts +57 -0
- package/template/src/lib/mdx/remark-code-block-component.ts +2 -0
- package/template/src/lib/mdx/remark-standalone-image-component.ts +89 -0
- package/template/src/lib/utils.ts +2 -0
package/package.json
CHANGED
|
@@ -9,6 +9,8 @@ import path from "path";
|
|
|
9
9
|
import { getConfig, validateMdxContent } from "./src/lib/validation";
|
|
10
10
|
import remarkCodeBlockComponent from "./src/lib/mdx/remark-code-block-component";
|
|
11
11
|
import remarkDemoteH1 from "./src/lib/mdx/remark-demote-h1";
|
|
12
|
+
import remarkStandaloneImageComponent from "./src/lib/mdx/remark-standalone-image-component";
|
|
13
|
+
import rehypeExternalLinks from "./src/lib/mdx/rehype-external-links";
|
|
12
14
|
import remarkGfm from "remark-gfm";
|
|
13
15
|
import rehypeSlug from "rehype-slug";
|
|
14
16
|
import rehypeAutolinkHeadings from "rehype-autolink-headings";
|
|
@@ -270,9 +272,15 @@ export default defineConfig({
|
|
|
270
272
|
},
|
|
271
273
|
integrations: [
|
|
272
274
|
mdx({
|
|
273
|
-
remarkPlugins: [
|
|
275
|
+
remarkPlugins: [
|
|
276
|
+
remarkGfm,
|
|
277
|
+
remarkDemoteH1,
|
|
278
|
+
remarkStandaloneImageComponent,
|
|
279
|
+
remarkCodeBlockComponent,
|
|
280
|
+
],
|
|
274
281
|
rehypePlugins: [
|
|
275
282
|
rehypeSlug,
|
|
283
|
+
rehypeExternalLinks,
|
|
276
284
|
[
|
|
277
285
|
rehypeAutolinkHeadings,
|
|
278
286
|
{
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
import type { HTMLAttributes } from "astro/types";
|
|
3
3
|
import { validateProps } from "../../lib/component-error";
|
|
4
|
+
import { renderMarkdown } from "../../lib/utils";
|
|
4
5
|
|
|
5
6
|
interface Props extends HTMLAttributes<"img"> {
|
|
6
7
|
src: string;
|
|
@@ -8,6 +9,11 @@ interface Props extends HTMLAttributes<"img"> {
|
|
|
8
9
|
|
|
9
10
|
const { title, ...attrs } = Astro.props;
|
|
10
11
|
|
|
12
|
+
const captionHtml =
|
|
13
|
+
typeof title === "string" && title.trim().length > 0
|
|
14
|
+
? (await renderMarkdown(title)).replace(/^<p>([\s\S]*)<\/p>\n?$/, "$1")
|
|
15
|
+
: "";
|
|
16
|
+
|
|
11
17
|
validateProps(
|
|
12
18
|
"Image",
|
|
13
19
|
Astro.props as Record<string, any>,
|
|
@@ -21,7 +27,7 @@ validateProps(
|
|
|
21
27
|
---
|
|
22
28
|
|
|
23
29
|
<figure
|
|
24
|
-
class="p-1.5 pb-2
|
|
30
|
+
class="p-1.5 pb-2 group border border-neutral-200/80 dark:border-neutral-800 shadow-xs bg-neutral-50 dark:bg-neutral-900 rounded-2xl"
|
|
25
31
|
x-data="{
|
|
26
32
|
open: false,
|
|
27
33
|
showZoomed: false,
|
|
@@ -96,7 +102,7 @@ validateProps(
|
|
|
96
102
|
{
|
|
97
103
|
title && (
|
|
98
104
|
<figcaption class="mt-2 text-center text-sm text-neutral-500 dark:text-neutral-400 font-medium whitespace-pre-line leading-relaxed px-2">
|
|
99
|
-
{
|
|
105
|
+
<span set:html={captionHtml} />
|
|
100
106
|
</figcaption>
|
|
101
107
|
)
|
|
102
108
|
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { Plugin } from "unified";
|
|
2
|
+
|
|
3
|
+
type HastNode = {
|
|
4
|
+
type?: string;
|
|
5
|
+
tagName?: string;
|
|
6
|
+
properties?: Record<string, unknown>;
|
|
7
|
+
children?: HastNode[];
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
function isExternalHttpLink(href: unknown): href is string {
|
|
11
|
+
return typeof href === "string" && /^https?:\/\//i.test(href);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function normalizeRelTokens(value: unknown): string[] {
|
|
15
|
+
if (Array.isArray(value)) {
|
|
16
|
+
return value
|
|
17
|
+
.flatMap((entry) =>
|
|
18
|
+
typeof entry === "string" ? entry.split(/\s+/) : [],
|
|
19
|
+
)
|
|
20
|
+
.filter(Boolean);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (typeof value === "string") {
|
|
24
|
+
return value.split(/\s+/).filter(Boolean);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return [];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function visitNodes(node: HastNode, visitor: (node: HastNode) => void): void {
|
|
31
|
+
visitor(node);
|
|
32
|
+
if (!Array.isArray(node.children)) return;
|
|
33
|
+
for (const child of node.children) {
|
|
34
|
+
visitNodes(child, visitor);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export const rehypeExternalLinks: Plugin<[], HastNode> = () => {
|
|
39
|
+
return (tree) => {
|
|
40
|
+
visitNodes(tree, (node) => {
|
|
41
|
+
if (node.type !== "element" || node.tagName !== "a") return;
|
|
42
|
+
|
|
43
|
+
const props = (node.properties ??= {});
|
|
44
|
+
const href = props.href;
|
|
45
|
+
if (!isExternalHttpLink(href)) return;
|
|
46
|
+
|
|
47
|
+
props.target = "_blank";
|
|
48
|
+
|
|
49
|
+
const relTokens = normalizeRelTokens(props.rel);
|
|
50
|
+
if (!relTokens.includes("noopener")) relTokens.push("noopener");
|
|
51
|
+
if (!relTokens.includes("noreferrer")) relTokens.push("noreferrer");
|
|
52
|
+
props.rel = relTokens.join(" ");
|
|
53
|
+
});
|
|
54
|
+
};
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export default rehypeExternalLinks;
|
|
@@ -6,6 +6,7 @@ import { gfm } from "micromark-extension-gfm";
|
|
|
6
6
|
import { mdxjs } from "micromark-extension-mdxjs";
|
|
7
7
|
import type { Plugin } from "unified";
|
|
8
8
|
import { visitParents } from "unist-util-visit-parents";
|
|
9
|
+
import { transformStandaloneImageParagraphs } from "./remark-standalone-image-component";
|
|
9
10
|
|
|
10
11
|
type CodeNode = {
|
|
11
12
|
type: "code";
|
|
@@ -275,6 +276,7 @@ function parseComponentPreviewChildren(rawCode: string): unknown[] {
|
|
|
275
276
|
mdastExtensions: [gfmFromMarkdown(), mdxFromMarkdown()],
|
|
276
277
|
}) as Root;
|
|
277
278
|
|
|
279
|
+
transformStandaloneImageParagraphs(parsedTree);
|
|
278
280
|
transformCodeBlockNodes(parsedTree);
|
|
279
281
|
|
|
280
282
|
return transformPreviewChildren(
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import type { Root } from "mdast";
|
|
2
|
+
import type { Plugin } from "unified";
|
|
3
|
+
import { visitParents } from "unist-util-visit-parents";
|
|
4
|
+
|
|
5
|
+
type ParagraphNode = {
|
|
6
|
+
type: "paragraph";
|
|
7
|
+
children?: unknown[];
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
type ImageNode = {
|
|
11
|
+
type: "image";
|
|
12
|
+
url?: string | null;
|
|
13
|
+
alt?: string | null;
|
|
14
|
+
title?: string | null;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
type ParentNode = {
|
|
18
|
+
children?: unknown[];
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
type MdxJsxAttributeNode = {
|
|
22
|
+
type: "mdxJsxAttribute";
|
|
23
|
+
name: string;
|
|
24
|
+
value: string | null;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
type MdxJsxFlowElementNode = {
|
|
28
|
+
type: "mdxJsxFlowElement";
|
|
29
|
+
name: string;
|
|
30
|
+
attributes: MdxJsxAttributeNode[];
|
|
31
|
+
children: unknown[];
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
function createAttribute(name: string, value: string): MdxJsxAttributeNode {
|
|
35
|
+
return {
|
|
36
|
+
type: "mdxJsxAttribute",
|
|
37
|
+
name,
|
|
38
|
+
value,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function transformStandaloneImageParagraphs(tree: Root): void {
|
|
43
|
+
visitParents(tree, "paragraph", (node, ancestors) => {
|
|
44
|
+
const paragraph = node as ParagraphNode;
|
|
45
|
+
if (!Array.isArray(paragraph.children) || paragraph.children.length !== 1) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const onlyChild = paragraph.children[0] as ImageNode;
|
|
50
|
+
if (onlyChild.type !== "image") return;
|
|
51
|
+
if (typeof onlyChild.url !== "string" || onlyChild.url.trim().length === 0) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const parent = ancestors[ancestors.length - 1] as ParentNode | undefined;
|
|
56
|
+
if (!Array.isArray(parent?.children)) return;
|
|
57
|
+
|
|
58
|
+
const currentIndex = parent.children.indexOf(node);
|
|
59
|
+
if (currentIndex < 0) return;
|
|
60
|
+
|
|
61
|
+
const attributes: MdxJsxAttributeNode[] = [
|
|
62
|
+
createAttribute("src", onlyChild.url),
|
|
63
|
+
];
|
|
64
|
+
|
|
65
|
+
if (typeof onlyChild.alt === "string") {
|
|
66
|
+
attributes.push(createAttribute("alt", onlyChild.alt));
|
|
67
|
+
}
|
|
68
|
+
if (typeof onlyChild.title === "string") {
|
|
69
|
+
attributes.push(createAttribute("title", onlyChild.title));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const replacementNode: MdxJsxFlowElementNode = {
|
|
73
|
+
type: "mdxJsxFlowElement",
|
|
74
|
+
name: "Image",
|
|
75
|
+
attributes,
|
|
76
|
+
children: [],
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
parent.children[currentIndex] = replacementNode;
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export const remarkStandaloneImageComponent: Plugin<[], Root> = () => {
|
|
84
|
+
return (tree) => {
|
|
85
|
+
transformStandaloneImageParagraphs(tree);
|
|
86
|
+
};
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
export default remarkStandaloneImageComponent;
|
|
@@ -3,6 +3,7 @@ import remarkParse from "remark-parse";
|
|
|
3
3
|
import remarkGfm from "remark-gfm";
|
|
4
4
|
import remarkRehype from "remark-rehype";
|
|
5
5
|
import rehypeStringify from "rehype-stringify";
|
|
6
|
+
import rehypeExternalLinks from "./mdx/rehype-external-links";
|
|
6
7
|
import path from "node:path";
|
|
7
8
|
|
|
8
9
|
export function slugify(text: string): string {
|
|
@@ -25,6 +26,7 @@ export async function renderMarkdown(markdown: string): Promise<string> {
|
|
|
25
26
|
.use(remarkParse)
|
|
26
27
|
.use(remarkGfm) // GitHub Flavored Markdown (for lists, etc.)
|
|
27
28
|
.use(remarkRehype, { allowDangerousHtml: false })
|
|
29
|
+
.use(rehypeExternalLinks)
|
|
28
30
|
.use(rehypeStringify)
|
|
29
31
|
.process(markdown);
|
|
30
32
|
|