radiant-docs 0.1.21 → 0.1.24

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "radiant-docs",
3
- "version": "0.1.21",
3
+ "version": "0.1.24",
4
4
  "description": "CLI tool for previewing Radiant documentation locally",
5
5
  "type": "module",
6
6
  "bin": {
@@ -9,6 +9,7 @@ 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 rehypeExternalLinks from "./src/lib/mdx/rehype-external-links";
12
13
  import remarkGfm from "remark-gfm";
13
14
  import rehypeSlug from "rehype-slug";
14
15
  import rehypeAutolinkHeadings from "rehype-autolink-headings";
@@ -273,6 +274,7 @@ export default defineConfig({
273
274
  remarkPlugins: [remarkGfm, remarkDemoteH1, remarkCodeBlockComponent],
274
275
  rehypePlugins: [
275
276
  rehypeSlug,
277
+ rehypeExternalLinks,
276
278
  [
277
279
  rehypeAutolinkHeadings,
278
280
  {
@@ -103,9 +103,15 @@ import CodeTabEdge from "../ui/CodeTabEdge.astro";
103
103
  const filename =
104
104
  itemElement.getAttribute("data-rd-tab-filename") ??
105
105
  `file-name-${index + 1}.txt`;
106
+ const hideTabIcon =
107
+ itemElement.getAttribute("data-rd-tab-hide-icon") === "true";
106
108
  const tabIconTemplate = itemElement.querySelector(
107
109
  "template[data-rd-code-group-tab-icon]",
108
110
  );
111
+ const hasTabIcon =
112
+ !hideTabIcon &&
113
+ !!tabIconTemplate &&
114
+ tabIconTemplate.innerHTML.trim().length > 0;
109
115
 
110
116
  const tabWrapper = document.createElement("div");
111
117
  tabWrapper.className = "relative z-10 text-xs font-medium";
@@ -113,25 +119,25 @@ import CodeTabEdge from "../ui/CodeTabEdge.astro";
113
119
  const tabButton = document.createElement("button");
114
120
  tabButton.type = "button";
115
121
  tabButton.className =
116
- "relative inline-flex h-9 items-center gap-2 border-0 bg-transparent px-3 py-1.5 text-xs font-medium text-neutral-600 transition-colors duration-150 focus:outline-none focus-visible:outline-none cursor-pointer";
122
+ `relative inline-flex h-9 items-center border-0 bg-transparent px-3 py-1.5 text-xs font-medium text-neutral-600 transition-colors duration-150 focus:outline-none focus-visible:outline-none cursor-pointer ${hasTabIcon ? "gap-2" : ""}`;
117
123
  tabButton.setAttribute("aria-label", filename);
118
124
  tabButton.setAttribute("data-rd-code-group-tab", String(index));
119
125
 
120
- const iconContainer = document.createElement("span");
121
- iconContainer.className =
122
- "pointer-events-none inline-flex shrink-0 items-center rounded-[4px] transition-opacity duration-150";
123
-
124
- if (tabIconTemplate && tabIconTemplate.innerHTML.trim().length > 0) {
126
+ let iconContainer = null;
127
+ if (hasTabIcon) {
128
+ iconContainer = document.createElement("span");
129
+ iconContainer.className =
130
+ "pointer-events-none inline-flex shrink-0 items-center rounded-[4px] transition-opacity duration-150";
125
131
  iconContainer.innerHTML = tabIconTemplate.innerHTML;
126
- } else {
127
- iconContainer.classList.add("hidden");
128
132
  }
129
133
 
130
134
  const labelElement = document.createElement("span");
131
135
  labelElement.className = "whitespace-pre leading-none";
132
136
  labelElement.textContent = filename;
133
137
 
134
- tabButton.appendChild(iconContainer);
138
+ if (iconContainer) {
139
+ tabButton.appendChild(iconContainer);
140
+ }
135
141
  tabButton.appendChild(labelElement);
136
142
  tabWrapper.appendChild(tabButton);
137
143
  tabsElement.appendChild(tabWrapper);
@@ -169,7 +175,7 @@ import CodeTabEdge from "../ui/CodeTabEdge.astro";
169
175
  tabButton.classList.toggle("text-neutral-900", isActive);
170
176
  tabButton.classList.toggle("text-neutral-600", !isActive);
171
177
 
172
- if (iconContainer.classList.contains("hidden")) return;
178
+ if (!iconContainer) return;
173
179
  iconContainer.classList.toggle("opacity-100", isActive);
174
180
  iconContainer.classList.toggle("opacity-80", !isActive);
175
181
  });
@@ -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>,
@@ -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
- {title}
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;
@@ -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