@pitlane/content 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/CHANGELOG.md +22 -0
- package/LICENSE +21 -0
- package/README.md +77 -0
- package/dist/codegen.d.mts +40 -0
- package/dist/codegen.mjs +131 -0
- package/dist/hot.d.mts +52 -0
- package/dist/hot.mjs +65 -0
- package/dist/index.d.mts +14 -0
- package/dist/index.mjs +313 -0
- package/dist/loaders.d.mts +38 -0
- package/dist/loaders.mjs +255 -0
- package/dist/manifest.d.mts +1 -0
- package/dist/manifest.mjs +4 -0
- package/dist/mdx.d.mts +35 -0
- package/dist/mdx.mjs +389 -0
- package/dist/parse-DoKe2tNa.mjs +57 -0
- package/dist/prebuild.d.mts +65 -0
- package/dist/prebuild.mjs +81 -0
- package/dist/render-U9dXN6f0.mjs +273 -0
- package/dist/satteri.d.mts +41 -0
- package/dist/satteri.mjs +119 -0
- package/dist/symbols-DmXlrDbX.mjs +29 -0
- package/dist/types-xSR1WTBq.d.mts +238 -0
- package/dist/vite.d.mts +18 -0
- package/dist/vite.mjs +262 -0
- package/package.json +104 -0
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
import { readEsm } from "./mdx.mjs";
|
|
2
|
+
import * as jsxRuntime from "remix/ui/jsx-runtime";
|
|
3
|
+
//#region src/render.ts
|
|
4
|
+
let rendered = /* @__PURE__ */ new WeakMap();
|
|
5
|
+
/**
|
|
6
|
+
* Renders one entry, once.
|
|
7
|
+
*
|
|
8
|
+
* Nothing parses Markdown until this is called, so loading a collection to list
|
|
9
|
+
* its titles never pays for the bodies it does not show. The result is cached
|
|
10
|
+
* per entry, because a page rendered twice should not compile twice.
|
|
11
|
+
*/
|
|
12
|
+
function renderedEntry(collection, entry) {
|
|
13
|
+
let existing = rendered.get(entry);
|
|
14
|
+
if (existing) return existing;
|
|
15
|
+
let pending = render(collection, entry);
|
|
16
|
+
rendered.set(entry, pending);
|
|
17
|
+
return pending;
|
|
18
|
+
}
|
|
19
|
+
async function render(collection, entry) {
|
|
20
|
+
if (entry.prebuilt) return fromBundle(entry.prebuilt);
|
|
21
|
+
if (entry.body) return await fromSource(entry, entry.body);
|
|
22
|
+
throw new Error(`Entry "${collection}/${entry.id}" has no renderable content.`);
|
|
23
|
+
}
|
|
24
|
+
/** A body the build compiled: a module for MDX, an HTML string for Markdown. */
|
|
25
|
+
function fromBundle(prebuilt) {
|
|
26
|
+
if (prebuilt.format === "md") return {
|
|
27
|
+
Content: htmlComponent(prebuilt.html),
|
|
28
|
+
headings: headingList(prebuilt.headings)
|
|
29
|
+
};
|
|
30
|
+
return {
|
|
31
|
+
Content: mdxComponent(prebuilt.module.default),
|
|
32
|
+
headings: headingList(prebuilt.module.headings)
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
/** A body still in source form, rendered through Sätteri on demand. */
|
|
36
|
+
async function fromSource(entry, body) {
|
|
37
|
+
let satteri = await loadSatteri(entry.filePath ?? entry.id);
|
|
38
|
+
let options = await runtimeOptions(entry);
|
|
39
|
+
if (body.format === "md") {
|
|
40
|
+
let result = await satteri.markdownToHtml(body.source, options);
|
|
41
|
+
return {
|
|
42
|
+
Content: htmlComponent(result.html),
|
|
43
|
+
headings: dataHeadings(result.data)
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
let where = entry.filePath ?? entry.id;
|
|
47
|
+
let imported = await importedBindings(satteri, body.source, where, options);
|
|
48
|
+
let compiled = await satteri.mdxToJs(imported.body, {
|
|
49
|
+
...options,
|
|
50
|
+
jsxImportSource: "remix/ui",
|
|
51
|
+
outputFormat: "function-body"
|
|
52
|
+
});
|
|
53
|
+
let names = [...imported.bindings.keys()];
|
|
54
|
+
let module = compile(names, compiled.code, entry.filePath ?? entry.id)({ ...jsxRuntime }, ...names.map((name) => imported.bindings.get(name)));
|
|
55
|
+
return {
|
|
56
|
+
Content: mdxComponent(module.default, imported.bindings),
|
|
57
|
+
headings: headingList(module.headings)
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* The compiled body, as a callable function.
|
|
62
|
+
*
|
|
63
|
+
* The engine reports a body it cannot compile against generated source the
|
|
64
|
+
* author never saw: `import.meta` in an expression, which is ordinary under a
|
|
65
|
+
* bundler, arrives as a bare `SyntaxError: Cannot use 'import.meta' outside a
|
|
66
|
+
* module` naming neither the document nor the reason. `readEsm` catches the
|
|
67
|
+
* form written in an `import`/`export` block; this catches every other one.
|
|
68
|
+
*/
|
|
69
|
+
function compile(names, code, where) {
|
|
70
|
+
try {
|
|
71
|
+
return new Function("__mdxRuntime", ...names, code);
|
|
72
|
+
} catch (error) {
|
|
73
|
+
let cause = error instanceof Error ? error.message : String(error);
|
|
74
|
+
throw new Error(`"${where}" has a body that cannot be compiled outside a bundler: ${cause}. The document becomes a function body here rather than a module. Add contentLayer() from @pitlane/content/vite so the build compiles this collection.`, { cause: error });
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* MDX compiles to a plain function of props; a Remix component is a factory
|
|
79
|
+
* returning a render function. This is the bridge, and it is why props on
|
|
80
|
+
* `<Content />` reach the content and `components` overrides work.
|
|
81
|
+
*
|
|
82
|
+
* A document's own imports are merged last, so a caller's `components` cannot
|
|
83
|
+
* replace one. Under a bundler an `import` is a real import and nothing can
|
|
84
|
+
* override it; the two paths have to agree.
|
|
85
|
+
*/
|
|
86
|
+
function mdxComponent(exported, imported = /* @__PURE__ */ new Map()) {
|
|
87
|
+
let component = exported;
|
|
88
|
+
if (imported.size === 0) return (handle) => () => component(handle?.props ?? {});
|
|
89
|
+
return (handle) => () => {
|
|
90
|
+
let props = handle?.props ?? {};
|
|
91
|
+
let components = {
|
|
92
|
+
...props.components,
|
|
93
|
+
...Object.fromEntries(imported)
|
|
94
|
+
};
|
|
95
|
+
return component({
|
|
96
|
+
...props,
|
|
97
|
+
components
|
|
98
|
+
});
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Markdown arrives as an HTML string, and `innerHTML` is an element prop, so
|
|
103
|
+
* the markup needs an element to land on. The wrapper is unavoidable.
|
|
104
|
+
*/
|
|
105
|
+
function htmlComponent(html) {
|
|
106
|
+
return () => () => jsxRuntime.jsx("div", { innerHTML: html });
|
|
107
|
+
}
|
|
108
|
+
function headingList(value) {
|
|
109
|
+
return Array.isArray(value) ? value : [];
|
|
110
|
+
}
|
|
111
|
+
function dataHeadings(data) {
|
|
112
|
+
if (data && typeof data === "object" && "headings" in data) return headingList(data.headings);
|
|
113
|
+
return [];
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* The runtime rendering options: the application's, with the three things both
|
|
117
|
+
* paths must agree on forced on.
|
|
118
|
+
*
|
|
119
|
+
* `headings` and `rawStyles` are the same plugins `vite-plugin-satteri` runs
|
|
120
|
+
* on the prebuilt path, which is what makes the two produce the same list and
|
|
121
|
+
* the same working CSS. `rawStyles` goes last so it sees the `<style>` a
|
|
122
|
+
* highlighter appended. Frontmatter parsing stays on because a `LiveLoader`
|
|
123
|
+
* may hand back a body that still carries a fence, and rendering that as a
|
|
124
|
+
* horizontal rule would be worse than parsing it away.
|
|
125
|
+
*/
|
|
126
|
+
async function runtimeOptions(entry) {
|
|
127
|
+
let { headings, rawStyles } = await import("./satteri.mjs");
|
|
128
|
+
let configured = entry.satteri ?? {};
|
|
129
|
+
return {
|
|
130
|
+
...configured,
|
|
131
|
+
features: {
|
|
132
|
+
...configured.features,
|
|
133
|
+
frontmatter: true
|
|
134
|
+
},
|
|
135
|
+
mdastPlugins: [headings(), ...configured.mdastPlugins ?? []],
|
|
136
|
+
hastPlugins: [...configured.hastPlugins ?? [], rawStyles()]
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* The components an MDX document imported, keyed by the local name it used,
|
|
141
|
+
* and the source with those import statements removed.
|
|
142
|
+
*
|
|
143
|
+
* Specifiers resolve relative to the document, which is what makes the same
|
|
144
|
+
* file behave identically whether the bundler compiled it or this did.
|
|
145
|
+
*/
|
|
146
|
+
async function importedBindings(satteri, original, where, options) {
|
|
147
|
+
let source = original.replace(/^\uFEFF/, "");
|
|
148
|
+
let blocks = esmBlocks(satteri.mdxToMdast(source, { features: options.features }));
|
|
149
|
+
let bindings = /* @__PURE__ */ new Map();
|
|
150
|
+
if (blocks.length === 0) return {
|
|
151
|
+
body: source,
|
|
152
|
+
bindings
|
|
153
|
+
};
|
|
154
|
+
let node = await nodeResolution();
|
|
155
|
+
let from = node.pathToFileURL(where);
|
|
156
|
+
let body = source;
|
|
157
|
+
for (let block of [...blocks].reverse()) {
|
|
158
|
+
let { imports, remainder } = await readEsm(block.value, where);
|
|
159
|
+
body = body.slice(0, block.start) + remainder + body.slice(block.end);
|
|
160
|
+
for (let { specifier, bindings: names, attributes } of imports) {
|
|
161
|
+
let module = await importFrom(specifier, from, where, node, attributes);
|
|
162
|
+
for (let [local, exported] of names) {
|
|
163
|
+
if (!(exported in module)) throw missingExport(exported, specifier, where);
|
|
164
|
+
bindings.set(local, module[exported]);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return {
|
|
169
|
+
body,
|
|
170
|
+
bindings
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* An export the module does not have would otherwise arrive as `undefined` and
|
|
175
|
+
* render as nothing, which is the failure this whole path exists to remove.
|
|
176
|
+
*/
|
|
177
|
+
function missingExport(exported, specifier, where) {
|
|
178
|
+
let name = exported === "default" ? "a default export" : `\`${exported}\``;
|
|
179
|
+
return /* @__PURE__ */ new Error(`"${where}" imports ${name} from "${specifier}", which that module does not export.`);
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* The top-level ESM blocks, with the offsets Sätteri measured them at.
|
|
183
|
+
*
|
|
184
|
+
* Offsets rather than a text search: a page may quote its own import in a
|
|
185
|
+
* fenced code block, and removing the first textual match would strike the
|
|
186
|
+
* fence and leave the real statement behind.
|
|
187
|
+
*
|
|
188
|
+
* The end comes from the block's own text rather than its reported end, which
|
|
189
|
+
* runs to the start of the next node and so swallows the blank line between
|
|
190
|
+
* them. MDX needs that blank line to tell an ESM block from the body.
|
|
191
|
+
*/
|
|
192
|
+
function esmBlocks(tree) {
|
|
193
|
+
let children = tree.children ?? [];
|
|
194
|
+
let blocks = [];
|
|
195
|
+
for (let node of children) {
|
|
196
|
+
if (node.type !== "mdxjsEsm" || typeof node.value !== "string") continue;
|
|
197
|
+
let start = node.position?.start?.offset;
|
|
198
|
+
if (typeof start !== "number") continue;
|
|
199
|
+
blocks.push({
|
|
200
|
+
value: node.value,
|
|
201
|
+
start,
|
|
202
|
+
end: start + node.value.length
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
return blocks;
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* `node:module` and `node:url`, loaded on demand.
|
|
209
|
+
*
|
|
210
|
+
* A static import would put them in the chain `index.ts` pulls in, and the
|
|
211
|
+
* package documents itself as safe to import on any host. Only a runtime
|
|
212
|
+
* `.mdx` render reaches here, and that path is already Node, Bun, and Deno
|
|
213
|
+
* only because it needs `new Function`.
|
|
214
|
+
*/
|
|
215
|
+
async function nodeResolution() {
|
|
216
|
+
let [{ createRequire }, { pathToFileURL }] = await Promise.all([import("node:module"), import("node:url")]);
|
|
217
|
+
return {
|
|
218
|
+
createRequire,
|
|
219
|
+
pathToFileURL
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
async function importFrom(specifier, from, where, node, attributes) {
|
|
223
|
+
let resolved = specifier.startsWith(".") ? new URL(specifier, from).href : resolveBare(specifier, from, where, node);
|
|
224
|
+
try {
|
|
225
|
+
return await (attributes ? import(resolved, { with: attributes }) : import(resolved));
|
|
226
|
+
} catch (error) {
|
|
227
|
+
let cause = error instanceof Error ? error.message : String(error);
|
|
228
|
+
throw new Error(`"${where}" imports "${specifier}", which could not be loaded: ${cause}`, { cause: error });
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Resolves a bare or subpath specifier from the document's own location.
|
|
233
|
+
*
|
|
234
|
+
* `createRequire` rather than `import.meta.resolve`, whose parent argument is
|
|
235
|
+
* not part of the stable API: resolution has to start at the MDX file so that
|
|
236
|
+
* `#/ui/counter.tsx` means what it means in a controller of the same app. The
|
|
237
|
+
* cost is that it resolves under `require` conditions, so a dependency that
|
|
238
|
+
* publishes only an `import` condition fails here — loudly, naming the file
|
|
239
|
+
* and the specifier.
|
|
240
|
+
*/
|
|
241
|
+
function resolveBare(specifier, from, where, node) {
|
|
242
|
+
try {
|
|
243
|
+
return node.pathToFileURL(node.createRequire(from).resolve(specifier)).href;
|
|
244
|
+
} catch (error) {
|
|
245
|
+
let cause = error instanceof Error ? error.message : String(error);
|
|
246
|
+
throw new Error(`"${where}" imports "${specifier}", which could not be resolved: ${cause}`, { cause: error });
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* `satteri` is an optional peer dependency: an application whose content is
|
|
251
|
+
* prebuilt never renders at runtime and so never needs it. The import has to be
|
|
252
|
+
* dynamic for that to be true, and the failure has to name both ways out.
|
|
253
|
+
*
|
|
254
|
+
* The specifier is a variable because a literal one is not dynamic to a
|
|
255
|
+
* bundler — Rolldown resolves it at build time and pulls the whole compiler
|
|
256
|
+
* into the output. Sätteri is a native addon, so a Worker bundle that reaches
|
|
257
|
+
* it fails to build: workerd resolves under the `browser` condition, which
|
|
258
|
+
* sends `satteri` to its WASM binding and on to an optional package that is
|
|
259
|
+
* not installed on a native platform.
|
|
260
|
+
*/
|
|
261
|
+
async function loadSatteri(where) {
|
|
262
|
+
let specifier = "satteri";
|
|
263
|
+
try {
|
|
264
|
+
return await import(
|
|
265
|
+
/* @vite-ignore */
|
|
266
|
+
specifier
|
|
267
|
+
);
|
|
268
|
+
} catch {
|
|
269
|
+
throw new Error(`Rendering "${where}" needs the optional peer dependency "satteri"; install it, or add contentLayer() from @pitlane/content/vite so the build compiles this collection.`);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
//#endregion
|
|
273
|
+
export { renderedEntry };
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { HastPluginEntry, MdastPluginEntry } from "satteri";
|
|
2
|
+
//#region src/satteri.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* Collects every heading of a document as `{ depth, slug, text }`, publishes the
|
|
5
|
+
* list as `data.headings`, and gives each heading an `id` matching its slug so an
|
|
6
|
+
* anchor link lands on it. On MDX the list is also appended to the tree as
|
|
7
|
+
* `export const headings`, so the compiled module carries its own table of
|
|
8
|
+
* contents.
|
|
9
|
+
*
|
|
10
|
+
* The plugin is a factory rather than a definition because Sätteri resolves a
|
|
11
|
+
* factory once per compile: a single `headings()` shared by a whole build gets
|
|
12
|
+
* fresh slug state per document, instead of the second document's `notes`
|
|
13
|
+
* reading `notes-1`.
|
|
14
|
+
*
|
|
15
|
+
* The definition is written out rather than passed through Sätteri's
|
|
16
|
+
* `defineMdastPlugin`, whose entire body checks that `name` is set. Calling it
|
|
17
|
+
* would make this module import the `satteri` package, and `render.ts` imports
|
|
18
|
+
* this module: a Worker bundle whose collections `contentLayer()` already prebuilt
|
|
19
|
+
* would then have to resolve a native addon it can never load.
|
|
20
|
+
*/
|
|
21
|
+
declare function headings(): MdastPluginEntry;
|
|
22
|
+
/**
|
|
23
|
+
* Hands a `<style>` element's CSS to Remix as markup rather than as text, so
|
|
24
|
+
* the stylesheet survives being rendered.
|
|
25
|
+
*
|
|
26
|
+
* `@remix-run/ui` escapes `&`, `<`, and `>` in the text children of every
|
|
27
|
+
* element except `<script>`, and emits an `innerHTML` prop verbatim. `<style>`
|
|
28
|
+
* is a raw-text element, which is exactly the case where a browser does not
|
|
29
|
+
* undo that escaping: a rule written `pre > code` reaches the page as
|
|
30
|
+
* `pre > code`, matches nothing, and says nothing. Expressive Code is how
|
|
31
|
+
* most applications meet this, because it ships its theme as one such element.
|
|
32
|
+
*
|
|
33
|
+
* A workaround, not a design. The fix belongs in `@remix-run/ui`, which
|
|
34
|
+
* already special-cases `<script>` and should treat `<style>` the same way.
|
|
35
|
+
* `<script>` is deliberately not touched here: routing it through `innerHTML`
|
|
36
|
+
* would skip `escapeScriptTextContent`, which keeps a `</script>` inside a
|
|
37
|
+
* string from ending the element early.
|
|
38
|
+
*/
|
|
39
|
+
declare function rawStyles(): HastPluginEntry;
|
|
40
|
+
//#endregion
|
|
41
|
+
export { headings, rawStyles };
|
package/dist/satteri.mjs
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
//#region src/satteri.ts
|
|
2
|
+
/**
|
|
3
|
+
* The base slug for a heading with no letters or digits at all (`## ---`). An
|
|
4
|
+
* empty `id` is invalid HTML and makes the anchor a bare `#`, which lands
|
|
5
|
+
* nowhere; a fixed word plus the usual collision suffix keeps such headings
|
|
6
|
+
* addressable and distinct.
|
|
7
|
+
*/
|
|
8
|
+
const FALLBACK_SLUG = "heading";
|
|
9
|
+
/**
|
|
10
|
+
* The slug for `text`, kept distinct from every slug `taken` already holds by
|
|
11
|
+
* suffixing `-1`, `-2`, … The map remembers the last suffix tried for a base so
|
|
12
|
+
* a document of a hundred identical headings stays linear, and the loop covers
|
|
13
|
+
* the case where the suffixed candidate is itself a real heading's slug.
|
|
14
|
+
*/
|
|
15
|
+
function uniqueSlug(text, taken) {
|
|
16
|
+
let base = text.toLowerCase().replace(/[^\p{L}\p{N}\p{M}]+/gu, "-").replace(/^-+|-+$/g, "");
|
|
17
|
+
if (!base) base = FALLBACK_SLUG;
|
|
18
|
+
let suffix = taken.get(base) ?? 0;
|
|
19
|
+
let slug = base;
|
|
20
|
+
while (taken.has(slug)) {
|
|
21
|
+
suffix += 1;
|
|
22
|
+
slug = `${base}-${suffix}`;
|
|
23
|
+
}
|
|
24
|
+
taken.set(base, suffix);
|
|
25
|
+
if (!taken.has(slug)) taken.set(slug, 0);
|
|
26
|
+
return slug;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Collects every heading of a document as `{ depth, slug, text }`, publishes the
|
|
30
|
+
* list as `data.headings`, and gives each heading an `id` matching its slug so an
|
|
31
|
+
* anchor link lands on it. On MDX the list is also appended to the tree as
|
|
32
|
+
* `export const headings`, so the compiled module carries its own table of
|
|
33
|
+
* contents.
|
|
34
|
+
*
|
|
35
|
+
* The plugin is a factory rather than a definition because Sätteri resolves a
|
|
36
|
+
* factory once per compile: a single `headings()` shared by a whole build gets
|
|
37
|
+
* fresh slug state per document, instead of the second document's `notes`
|
|
38
|
+
* reading `notes-1`.
|
|
39
|
+
*
|
|
40
|
+
* The definition is written out rather than passed through Sätteri's
|
|
41
|
+
* `defineMdastPlugin`, whose entire body checks that `name` is set. Calling it
|
|
42
|
+
* would make this module import the `satteri` package, and `render.ts` imports
|
|
43
|
+
* this module: a Worker bundle whose collections `contentLayer()` already prebuilt
|
|
44
|
+
* would then have to resolve a native addon it can never load.
|
|
45
|
+
*/
|
|
46
|
+
function headings() {
|
|
47
|
+
return (factoryContext) => {
|
|
48
|
+
let collected = [];
|
|
49
|
+
let taken = /* @__PURE__ */ new Map();
|
|
50
|
+
factoryContext.data.headings = collected;
|
|
51
|
+
return {
|
|
52
|
+
name: "pitlane-headings",
|
|
53
|
+
heading(node, context) {
|
|
54
|
+
let text = context.textContent(node);
|
|
55
|
+
let slug = uniqueSlug(text, taken);
|
|
56
|
+
collected.push({
|
|
57
|
+
depth: node.depth,
|
|
58
|
+
slug,
|
|
59
|
+
text
|
|
60
|
+
});
|
|
61
|
+
context.setProperty(node, "data", { hProperties: { id: slug } });
|
|
62
|
+
},
|
|
63
|
+
after(root, context) {
|
|
64
|
+
if (context.sourceFormat !== "mdx") return;
|
|
65
|
+
context.appendChild(root, {
|
|
66
|
+
type: "mdxjsEsm",
|
|
67
|
+
value: `export const headings = ${JSON.stringify(collected)};`
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Hands a `<style>` element's CSS to Remix as markup rather than as text, so
|
|
75
|
+
* the stylesheet survives being rendered.
|
|
76
|
+
*
|
|
77
|
+
* `@remix-run/ui` escapes `&`, `<`, and `>` in the text children of every
|
|
78
|
+
* element except `<script>`, and emits an `innerHTML` prop verbatim. `<style>`
|
|
79
|
+
* is a raw-text element, which is exactly the case where a browser does not
|
|
80
|
+
* undo that escaping: a rule written `pre > code` reaches the page as
|
|
81
|
+
* `pre > code`, matches nothing, and says nothing. Expressive Code is how
|
|
82
|
+
* most applications meet this, because it ships its theme as one such element.
|
|
83
|
+
*
|
|
84
|
+
* A workaround, not a design. The fix belongs in `@remix-run/ui`, which
|
|
85
|
+
* already special-cases `<script>` and should treat `<style>` the same way.
|
|
86
|
+
* `<script>` is deliberately not touched here: routing it through `innerHTML`
|
|
87
|
+
* would skip `escapeScriptTextContent`, which keeps a `<\/script>` inside a
|
|
88
|
+
* string from ending the element early.
|
|
89
|
+
*/
|
|
90
|
+
function rawStyles() {
|
|
91
|
+
return (factoryContext) => {
|
|
92
|
+
if (factoryContext.sourceFormat !== "mdx") return false;
|
|
93
|
+
return {
|
|
94
|
+
name: "pitlane-raw-styles",
|
|
95
|
+
element: {
|
|
96
|
+
filter: ["style"],
|
|
97
|
+
visit(node) {
|
|
98
|
+
let css = "";
|
|
99
|
+
for (let child of node.children) {
|
|
100
|
+
if (child.type !== "text") return;
|
|
101
|
+
css += child.value;
|
|
102
|
+
}
|
|
103
|
+
if (!css) return;
|
|
104
|
+
return {
|
|
105
|
+
type: "element",
|
|
106
|
+
tagName: "style",
|
|
107
|
+
properties: {
|
|
108
|
+
...node.properties,
|
|
109
|
+
innerHTML: css
|
|
110
|
+
},
|
|
111
|
+
children: []
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
//#endregion
|
|
119
|
+
export { headings, rawStyles };
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
//#region src/symbols.ts
|
|
2
|
+
/**
|
|
3
|
+
* The two globals `contentLayer()` and the runtime use to find each other.
|
|
4
|
+
*
|
|
5
|
+
* They are named here rather than at each site because a drifted spelling fails
|
|
6
|
+
* in the quietest possible way: the emitted manifest assigns one symbol,
|
|
7
|
+
* `prebuiltManifest()` reads another, every collection silently falls back to
|
|
8
|
+
* its loader, and that works on Node — so the mistake only surfaces on a host
|
|
9
|
+
* with no filesystem, in production.
|
|
10
|
+
*
|
|
11
|
+
* `manifest.ts` cannot import these through `prebuild.ts`, which imports it
|
|
12
|
+
* back for its side effect, so this module holds nothing but the names.
|
|
13
|
+
*/
|
|
14
|
+
/** Set by `contentLayer()` before it runs the content entry; read to decide whether to prebuild. */
|
|
15
|
+
const PREBUILD_CHANNEL = Symbol.for("pitlane.content.prebuild");
|
|
16
|
+
/** Assigned by the module `contentLayer()` emits in place of `manifest.ts`. */
|
|
17
|
+
const PREBUILT_MANIFEST = Symbol.for("pitlane.content.manifest");
|
|
18
|
+
/** The spelling the emitted module writes, which has to match {@link PREBUILT_MANIFEST}. */
|
|
19
|
+
const PREBUILT_MANIFEST_KEY = "pitlane.content.manifest";
|
|
20
|
+
/**
|
|
21
|
+
* Hangs the development watcher's handle off a collection.
|
|
22
|
+
*
|
|
23
|
+
* A symbol rather than a method because `invalidate()` on the public
|
|
24
|
+
* `Collection` would let any caller empty a collection someone else is
|
|
25
|
+
* reading. `@pitlane/content/hot` is the only intended reader.
|
|
26
|
+
*/
|
|
27
|
+
const HOT_COLLECTION = Symbol.for("pitlane.content.hot");
|
|
28
|
+
//#endregion
|
|
29
|
+
export { PREBUILT_MANIFEST_KEY as i, PREBUILD_CHANNEL as n, PREBUILT_MANIFEST as r, HOT_COLLECTION as t };
|