notro-loader 0.0.1 → 0.0.2

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.
@@ -1,27 +1,27 @@
1
- /**
2
- * Passthrough components for standard HTML elements.
3
- *
4
- * Uses astro/jsx-runtime to create lightweight wrappers that inject
5
- * an optional class string without requiring individual .astro files.
6
- *
7
- * makeHtmlElement(tag, cls?) bakes the class in at creation time.
8
- * The headless notro package uses this with no default classes;
9
- * notro-ui's NotionMarkdownRenderer passes Tailwind classes at creation time.
10
- *
11
- * Note on <code>:
12
- * The `code` element is intentionally omitted here. Both inline `code` and
13
- * code-block `pre > code` share the same element, so distinguishing them
14
- * requires the `:not(pre) > code` CSS selector — handled in notro-theme.css.
15
- */
16
- import { jsx } from 'astro/jsx-runtime';
17
- import { __astro_tag_component__ } from 'astro/runtime/server/index.js';
18
-
19
- export function makeHtmlElement(tag: string, cls?: string) {
20
- function HtmlElement({ class: className, ...rest }: Record<string, unknown>) {
21
- const combined = [cls, className].filter(Boolean).join(' ') || undefined;
22
- return jsx(tag, combined !== undefined ? { ...rest, class: combined } : rest);
23
- }
24
- __astro_tag_component__(HtmlElement, 'astro:jsx');
25
- return HtmlElement;
26
- }
27
-
1
+ /**
2
+ * Passthrough components for standard HTML elements.
3
+ *
4
+ * Uses astro/jsx-runtime to create lightweight wrappers that inject
5
+ * an optional class string without requiring individual .astro files.
6
+ *
7
+ * makeHtmlElement(tag, cls?) bakes the class in at creation time.
8
+ * The headless notro package uses this with no default classes;
9
+ * notro-ui's NotionMarkdownRenderer passes Tailwind classes at creation time.
10
+ *
11
+ * Note on <code>:
12
+ * The `code` element is intentionally omitted here. Both inline `code` and
13
+ * code-block `pre > code` share the same element, so distinguishing them
14
+ * requires the `:not(pre) > code` CSS selector — handled in notro-theme.css.
15
+ */
16
+ import { jsx } from 'astro/jsx-runtime';
17
+ import { __astro_tag_component__ } from 'astro/runtime/server/index.js';
18
+
19
+ export function makeHtmlElement(tag: string, cls?: string) {
20
+ function HtmlElement({ class: className, ...rest }: Record<string, unknown>) {
21
+ const combined = [cls, className].filter(Boolean).join(' ') || undefined;
22
+ return jsx(tag, combined !== undefined ? { ...rest, class: combined } : rest);
23
+ }
24
+ __astro_tag_component__(HtmlElement, 'astro:jsx');
25
+ return HtmlElement;
26
+ }
27
+
@@ -1,159 +1,159 @@
1
- /**
2
- * MDX → Astro component compiler.
3
- *
4
- * Astro integration layer: wires the MDX plugin pipeline from mdx-pipeline.ts
5
- * into Astro's jsx-runtime, registers the result with Astro's component
6
- * renderer, and caches compiled output by content hash.
7
- */
8
-
9
- import { evaluate } from '@mdx-js/mdx';
10
- import { createHash } from 'node:crypto';
11
- import { buildMdxPlugins } from './mdx-pipeline.ts';
12
- import type { LinkToPages } from '../types.ts';
13
-
14
- // Import Astro's jsx-runtime so evaluate() produces Astro VNodes.
15
- // (astro/src/jsx-runtime/index.ts:94)
16
- import { Fragment, jsx, jsxs } from 'astro/jsx-runtime';
17
-
18
- // Required to register Content as an 'astro:jsx' renderer.
19
- // Equivalent to annotateContentExport() in vite-plugin-mdx-postprocess.ts.
20
- import { __astro_tag_component__ } from 'astro/runtime/server/index.js';
21
-
22
- // ── Core compile function ──────────────────────────────────────────────────
23
-
24
- /**
25
- * Compiles a preprocessed Notion markdown string into an Astro component
26
- * that can be rendered with <Content components={notionComponents} />.
27
- *
28
- * @param mdxSource - Preprocessed markdown string (from loader store)
29
- * @param options.linkToPages - Optional map for resolving Notion page URLs
30
- *
31
- * @example
32
- * ```astro
33
- * ---
34
- * const Content = await compileMdxForAstro(entry.data.markdown, { linkToPages });
35
- * ---
36
- * <Content components={notionComponents} />
37
- * ```
38
- */
39
- export async function compileMdxForAstro(
40
- mdxSource: string,
41
- options: { linkToPages?: LinkToPages } = {},
42
- ) {
43
- const { linkToPages = {} } = options;
44
- const { remarkPlugins, rehypePlugins } = buildMdxPlugins(linkToPages);
45
-
46
- // evaluate() compiles + executes the MDX using Astro's jsx-runtime,
47
- // producing a function that returns Astro VNodes.
48
- // Wrapped in try-catch so a broken page does not crash the entire build.
49
- let mod: Awaited<ReturnType<typeof evaluate>>;
50
- try {
51
- mod = await evaluate(mdxSource, {
52
- jsx,
53
- jsxs,
54
- Fragment,
55
- remarkPlugins,
56
- rehypePlugins,
57
- });
58
- } catch (error) {
59
- console.warn(
60
- `[notro] MDX compilation failed for markdown (${mdxSource.length} chars):`,
61
- error,
62
- );
63
- // Return a fallback component that renders an error message so the build
64
- // continues and the problem is visible in the output without a 500 error.
65
- const errorMessage = error instanceof Error ? error.message : String(error);
66
- const FallbackContent = (props: Record<string, unknown> = {}) => {
67
- void props;
68
- return jsx('div', {
69
- style: 'border:2px solid red;padding:1em;color:red;white-space:pre-wrap',
70
- children: `[notro] MDX compilation error:\n${errorMessage}`,
71
- });
72
- };
73
- FallbackContent[Symbol.for('astro.needsHeadRendering')] = true;
74
- __astro_tag_component__(FallbackContent, 'astro:jsx');
75
- return FallbackContent;
76
- }
77
- const MDXContent = mod.default;
78
-
79
- // Pick up any `export const components = {...}` defined inside the MDX source.
80
- const mdxInternalComponents = (mod as Record<string, unknown>).components ?? {};
81
-
82
- // Wraps MDXContent so props.components are merged in priority order:
83
- // 1. Caller's <Content components={{...}} />
84
- // 2. MDX-internal `export const components`
85
- // 3. Fragment (required)
86
- const Content = (props: Record<string, unknown> = {}) =>
87
- MDXContent({
88
- ...props,
89
- components: {
90
- Fragment,
91
- ...(mdxInternalComponents as Record<string, unknown>),
92
- ...(props.components as Record<string, unknown> | undefined),
93
- },
94
- });
95
-
96
- // Tag the component so Astro's rendering pipeline handles it correctly.
97
- // Symbol.for("astro.needsHeadRendering") is checked by Astro's component renderer.
98
- // __astro_tag_component__ sets Symbol.for("astro:renderer") = 'astro:jsx',
99
- // which routes rendering through Astro's built-in JSX renderer.
100
- Content[Symbol.for('astro.needsHeadRendering')] = true;
101
- __astro_tag_component__(Content, 'astro:jsx');
102
-
103
- return Content;
104
- }
105
-
106
- // ── Cached variant ─────────────────────────────────────────────────────────
107
-
108
- // In-memory promise cache: keyed by SHA-256(mdxSource + linkToPages JSON).
109
- // Stores the Promise itself (not the resolved value) so that concurrent calls
110
- // with the same key share the in-flight compilation instead of launching
111
- // duplicate evaluate() calls. The cache is intentionally module-scoped and
112
- // lives for the duration of the build process.
113
- //
114
- // Known limitation: JSON.stringify(linkToPages) is insertion-order dependent.
115
- // Two objects with the same key/value pairs but different insertion order
116
- // produce different cache keys. In practice this is not a problem because
117
- // `buildLinkToPages()` always produces a consistent insertion order, but
118
- // custom linkToPages objects constructed in different orders would create
119
- // redundant cache entries rather than sharing results.
120
-
121
- // Maximum number of entries kept in compilationCache.
122
- // When the limit is reached, the oldest entry (first inserted) is evicted
123
- // using Map's guaranteed insertion-order iteration (FIFO eviction).
124
- const MAX_CACHE_SIZE = 500;
125
- const compilationCache = new Map<string, ReturnType<typeof compileMdxForAstro>>();
126
-
127
- export async function compileMdxCached(
128
- mdxSource: string,
129
- options: { linkToPages?: LinkToPages } = {},
130
- ) {
131
- const linkToPages = options.linkToPages ?? {};
132
- const key = createHash('sha256')
133
- .update(mdxSource)
134
- .update(JSON.stringify(linkToPages))
135
- .digest('hex');
136
-
137
- let entry = compilationCache.get(key);
138
- if (!entry) {
139
- // Evict the oldest entry (FIFO) before inserting a new one to keep
140
- // memory usage bounded. Eviction is skipped when the current key is
141
- // already present (cache hit), but a cache miss always inserts a new
142
- // entry, so we check the size here before that insertion.
143
- if (compilationCache.size >= MAX_CACHE_SIZE) {
144
- const oldestKey = compilationCache.keys().next().value;
145
- if (oldestKey !== undefined) {
146
- compilationCache.delete(oldestKey);
147
- }
148
- }
149
-
150
- // Store the promise immediately so concurrent callers share the same
151
- // in-flight compilation. On error, evict the cache entry so the next
152
- // request retries compilation rather than replaying the failure.
153
- const promise = compileMdxForAstro(mdxSource, options);
154
- compilationCache.set(key, promise);
155
- promise.catch(() => compilationCache.delete(key));
156
- entry = promise;
157
- }
158
- return entry;
159
- }
1
+ /**
2
+ * MDX → Astro component compiler.
3
+ *
4
+ * Astro integration layer: wires the MDX plugin pipeline from mdx-pipeline.ts
5
+ * into Astro's jsx-runtime, registers the result with Astro's component
6
+ * renderer, and caches compiled output by content hash.
7
+ */
8
+
9
+ import { evaluate } from '@mdx-js/mdx';
10
+ import { createHash } from 'node:crypto';
11
+ import { buildMdxPlugins } from './mdx-pipeline.ts';
12
+ import type { LinkToPages } from '../types.ts';
13
+
14
+ // Import Astro's jsx-runtime so evaluate() produces Astro VNodes.
15
+ // (astro/src/jsx-runtime/index.ts:94)
16
+ import { Fragment, jsx, jsxs } from 'astro/jsx-runtime';
17
+
18
+ // Required to register Content as an 'astro:jsx' renderer.
19
+ // Equivalent to annotateContentExport() in vite-plugin-mdx-postprocess.ts.
20
+ import { __astro_tag_component__ } from 'astro/runtime/server/index.js';
21
+
22
+ // ── Core compile function ──────────────────────────────────────────────────
23
+
24
+ /**
25
+ * Compiles a preprocessed Notion markdown string into an Astro component
26
+ * that can be rendered with <Content components={notionComponents} />.
27
+ *
28
+ * @param mdxSource - Preprocessed markdown string (from loader store)
29
+ * @param options.linkToPages - Optional map for resolving Notion page URLs
30
+ *
31
+ * @example
32
+ * ```astro
33
+ * ---
34
+ * const Content = await compileMdxForAstro(entry.data.markdown, { linkToPages });
35
+ * ---
36
+ * <Content components={notionComponents} />
37
+ * ```
38
+ */
39
+ export async function compileMdxForAstro(
40
+ mdxSource: string,
41
+ options: { linkToPages?: LinkToPages } = {},
42
+ ) {
43
+ const { linkToPages = {} } = options;
44
+ const { remarkPlugins, rehypePlugins } = buildMdxPlugins(linkToPages);
45
+
46
+ // evaluate() compiles + executes the MDX using Astro's jsx-runtime,
47
+ // producing a function that returns Astro VNodes.
48
+ // Wrapped in try-catch so a broken page does not crash the entire build.
49
+ let mod: Awaited<ReturnType<typeof evaluate>>;
50
+ try {
51
+ mod = await evaluate(mdxSource, {
52
+ jsx,
53
+ jsxs,
54
+ Fragment,
55
+ remarkPlugins,
56
+ rehypePlugins,
57
+ });
58
+ } catch (error) {
59
+ console.warn(
60
+ `[notro] MDX compilation failed for markdown (${mdxSource.length} chars):`,
61
+ error,
62
+ );
63
+ // Return a fallback component that renders an error message so the build
64
+ // continues and the problem is visible in the output without a 500 error.
65
+ const errorMessage = error instanceof Error ? error.message : String(error);
66
+ const FallbackContent = (props: Record<string, unknown> = {}) => {
67
+ void props;
68
+ return jsx('div', {
69
+ style: 'border:2px solid red;padding:1em;color:red;white-space:pre-wrap',
70
+ children: `[notro] MDX compilation error:\n${errorMessage}`,
71
+ });
72
+ };
73
+ FallbackContent[Symbol.for('astro.needsHeadRendering')] = true;
74
+ __astro_tag_component__(FallbackContent, 'astro:jsx');
75
+ return FallbackContent;
76
+ }
77
+ const MDXContent = mod.default;
78
+
79
+ // Pick up any `export const components = {...}` defined inside the MDX source.
80
+ const mdxInternalComponents = (mod as Record<string, unknown>).components ?? {};
81
+
82
+ // Wraps MDXContent so props.components are merged in priority order:
83
+ // 1. Caller's <Content components={{...}} />
84
+ // 2. MDX-internal `export const components`
85
+ // 3. Fragment (required)
86
+ const Content = (props: Record<string, unknown> = {}) =>
87
+ MDXContent({
88
+ ...props,
89
+ components: {
90
+ Fragment,
91
+ ...(mdxInternalComponents as Record<string, unknown>),
92
+ ...(props.components as Record<string, unknown> | undefined),
93
+ },
94
+ });
95
+
96
+ // Tag the component so Astro's rendering pipeline handles it correctly.
97
+ // Symbol.for("astro.needsHeadRendering") is checked by Astro's component renderer.
98
+ // __astro_tag_component__ sets Symbol.for("astro:renderer") = 'astro:jsx',
99
+ // which routes rendering through Astro's built-in JSX renderer.
100
+ Content[Symbol.for('astro.needsHeadRendering')] = true;
101
+ __astro_tag_component__(Content, 'astro:jsx');
102
+
103
+ return Content;
104
+ }
105
+
106
+ // ── Cached variant ─────────────────────────────────────────────────────────
107
+
108
+ // In-memory promise cache: keyed by SHA-256(mdxSource + linkToPages JSON).
109
+ // Stores the Promise itself (not the resolved value) so that concurrent calls
110
+ // with the same key share the in-flight compilation instead of launching
111
+ // duplicate evaluate() calls. The cache is intentionally module-scoped and
112
+ // lives for the duration of the build process.
113
+ //
114
+ // Known limitation: JSON.stringify(linkToPages) is insertion-order dependent.
115
+ // Two objects with the same key/value pairs but different insertion order
116
+ // produce different cache keys. In practice this is not a problem because
117
+ // `buildLinkToPages()` always produces a consistent insertion order, but
118
+ // custom linkToPages objects constructed in different orders would create
119
+ // redundant cache entries rather than sharing results.
120
+
121
+ // Maximum number of entries kept in compilationCache.
122
+ // When the limit is reached, the oldest entry (first inserted) is evicted
123
+ // using Map's guaranteed insertion-order iteration (FIFO eviction).
124
+ const MAX_CACHE_SIZE = 500;
125
+ const compilationCache = new Map<string, ReturnType<typeof compileMdxForAstro>>();
126
+
127
+ export async function compileMdxCached(
128
+ mdxSource: string,
129
+ options: { linkToPages?: LinkToPages } = {},
130
+ ) {
131
+ const linkToPages = options.linkToPages ?? {};
132
+ const key = createHash('sha256')
133
+ .update(mdxSource)
134
+ .update(JSON.stringify(linkToPages))
135
+ .digest('hex');
136
+
137
+ let entry = compilationCache.get(key);
138
+ if (!entry) {
139
+ // Evict the oldest entry (FIFO) before inserting a new one to keep
140
+ // memory usage bounded. Eviction is skipped when the current key is
141
+ // already present (cache hit), but a cache miss always inserts a new
142
+ // entry, so we check the size here before that insertion.
143
+ if (compilationCache.size >= MAX_CACHE_SIZE) {
144
+ const oldestKey = compilationCache.keys().next().value;
145
+ if (oldestKey !== undefined) {
146
+ compilationCache.delete(oldestKey);
147
+ }
148
+ }
149
+
150
+ // Store the promise immediately so concurrent callers share the same
151
+ // in-flight compilation. On error, evict the cache entry so the next
152
+ // request retries compilation rather than replaying the failure.
153
+ const promise = compileMdxForAstro(mdxSource, options);
154
+ compilationCache.set(key, promise);
155
+ promise.catch(() => compilationCache.delete(key));
156
+ entry = promise;
157
+ }
158
+ return entry;
159
+ }
@@ -1,62 +1,62 @@
1
- import { makeHtmlElement } from "./HtmlElements.ts";
2
-
3
- /**
4
- * Default headless component map for NotroContent.
5
- *
6
- * Maps all Notion block types and standard HTML elements to semantic HTML
7
- * equivalents with no Tailwind classes. Spread and override to customize:
8
- *
9
- * @example
10
- * ```ts
11
- * import { defaultComponents, NotroContent } from 'notro';
12
- * // Use as-is for unstyled output:
13
- * <NotroContent markdown={md} components={defaultComponents} />
14
- * // Or extend with your own components:
15
- * <NotroContent markdown={md} components={{ ...defaultComponents, callout: MyCallout }} />
16
- * ```
17
- */
18
- export const defaultComponents = {
19
- // ── Notion block elements (PascalCase) ────────────────────────────────────
20
- // These use PascalCase keys because MDX only generates a components-map
21
- // lookup (_jsx(Video, ...)) for PascalCase names. Lowercase names compile
22
- // as plain HTML string literals (_jsx("video", ...)), which bypass the
23
- // `components` prop entirely. rehypeBlockElementsPlugin renames the
24
- // mdxJsxFlowElement nodes to these PascalCase names before MDX compiles.
25
- TableOfContents: makeHtmlElement("nav"),
26
- Video: makeHtmlElement("figure"),
27
- Audio: makeHtmlElement("figure"),
28
- FileBlock: makeHtmlElement("div"),
29
- PdfBlock: makeHtmlElement("figure"),
30
- Columns: makeHtmlElement("div"),
31
- Column: makeHtmlElement("div"),
32
- PageRef: makeHtmlElement("a"),
33
- DatabaseRef: makeHtmlElement("a"),
34
- Details: makeHtmlElement("details"),
35
- Summary: makeHtmlElement("summary"),
36
- EmptyBlock: makeHtmlElement("div"),
37
- // callout is created by remarkNfm (a remark-level plugin via data.hName),
38
- // not from raw HTML, so MDX tracks it in _components and lowercase works.
39
- callout: makeHtmlElement("aside"),
40
- // ── Inline mention components (PascalCase) ────────────────────────────────
41
- // Same PascalCase requirement — rehypeInlineMentionsPlugin renames these.
42
- MentionUser: makeHtmlElement("span"),
43
- MentionPage: makeHtmlElement("span"),
44
- MentionDatabase: makeHtmlElement("span"),
45
- MentionDataSource: makeHtmlElement("span"),
46
- MentionAgent: makeHtmlElement("span"),
47
- MentionDate: makeHtmlElement("time"),
48
-
49
- // ── Standard HTML element pass-throughs ──────────────────────────────────
50
- span: makeHtmlElement("span"),
51
- p: makeHtmlElement("p"),
52
- ul: makeHtmlElement("ul"),
53
- ol: makeHtmlElement("ol"),
54
- li: makeHtmlElement("li"),
55
- pre: makeHtmlElement("pre"),
56
- hr: makeHtmlElement("hr"),
57
- th: makeHtmlElement("th"),
58
- a: makeHtmlElement("a"),
59
- strong: makeHtmlElement("strong"),
60
- em: makeHtmlElement("em"),
61
- del: makeHtmlElement("del"),
62
- } as const;
1
+ import { makeHtmlElement } from "./HtmlElements.ts";
2
+
3
+ /**
4
+ * Default headless component map for NotroContent.
5
+ *
6
+ * Maps all Notion block types and standard HTML elements to semantic HTML
7
+ * equivalents with no Tailwind classes. Spread and override to customize:
8
+ *
9
+ * @example
10
+ * ```ts
11
+ * import { defaultComponents, NotroContent } from 'notro';
12
+ * // Use as-is for unstyled output:
13
+ * <NotroContent markdown={md} components={defaultComponents} />
14
+ * // Or extend with your own components:
15
+ * <NotroContent markdown={md} components={{ ...defaultComponents, callout: MyCallout }} />
16
+ * ```
17
+ */
18
+ export const defaultComponents = {
19
+ // ── Notion block elements (PascalCase) ────────────────────────────────────
20
+ // These use PascalCase keys because MDX only generates a components-map
21
+ // lookup (_jsx(Video, ...)) for PascalCase names. Lowercase names compile
22
+ // as plain HTML string literals (_jsx("video", ...)), which bypass the
23
+ // `components` prop entirely. rehypeBlockElementsPlugin renames the
24
+ // mdxJsxFlowElement nodes to these PascalCase names before MDX compiles.
25
+ TableOfContents: makeHtmlElement("nav"),
26
+ Video: makeHtmlElement("figure"),
27
+ Audio: makeHtmlElement("figure"),
28
+ FileBlock: makeHtmlElement("div"),
29
+ PdfBlock: makeHtmlElement("figure"),
30
+ Columns: makeHtmlElement("div"),
31
+ Column: makeHtmlElement("div"),
32
+ PageRef: makeHtmlElement("a"),
33
+ DatabaseRef: makeHtmlElement("a"),
34
+ Details: makeHtmlElement("details"),
35
+ Summary: makeHtmlElement("summary"),
36
+ EmptyBlock: makeHtmlElement("div"),
37
+ // callout is created by remarkNfm (a remark-level plugin via data.hName),
38
+ // not from raw HTML, so MDX tracks it in _components and lowercase works.
39
+ callout: makeHtmlElement("aside"),
40
+ // ── Inline mention components (PascalCase) ────────────────────────────────
41
+ // Same PascalCase requirement — rehypeInlineMentionsPlugin renames these.
42
+ MentionUser: makeHtmlElement("span"),
43
+ MentionPage: makeHtmlElement("span"),
44
+ MentionDatabase: makeHtmlElement("span"),
45
+ MentionDataSource: makeHtmlElement("span"),
46
+ MentionAgent: makeHtmlElement("span"),
47
+ MentionDate: makeHtmlElement("time"),
48
+
49
+ // ── Standard HTML element pass-throughs ──────────────────────────────────
50
+ span: makeHtmlElement("span"),
51
+ p: makeHtmlElement("p"),
52
+ ul: makeHtmlElement("ul"),
53
+ ol: makeHtmlElement("ol"),
54
+ li: makeHtmlElement("li"),
55
+ pre: makeHtmlElement("pre"),
56
+ hr: makeHtmlElement("hr"),
57
+ th: makeHtmlElement("th"),
58
+ a: makeHtmlElement("a"),
59
+ strong: makeHtmlElement("strong"),
60
+ em: makeHtmlElement("em"),
61
+ del: makeHtmlElement("del"),
62
+ } as const;