quartz-research-note/quartz/components/renderPage.tsx

245 lines
7.4 KiB
TypeScript
Raw Normal View History

2023-07-23 00:27:41 +00:00
import { render } from "preact-render-to-string"
import { QuartzComponent, QuartzComponentProps } from "./types"
2023-07-01 07:03:01 +00:00
import HeaderConstructor from "./Header"
import BodyConstructor from "./Body"
import { JSResourceToScriptElement, StaticResources } from "../util/resources"
2024-01-05 08:29:34 +00:00
import { FullSlug, RelativeURL, joinSegments, normalizeHastElement } from "../util/path"
import { visit } from "unist-util-visit"
import { Root, Element, ElementContent } from "hast"
2024-01-05 08:29:34 +00:00
import { QuartzPluginData } from "../plugins/vfile"
2024-02-05 10:45:36 +00:00
import { GlobalConfiguration } from "../cfg"
import { i18n } from "../i18n"
2023-07-01 07:03:01 +00:00
interface RenderComponents {
head: QuartzComponent
2023-07-23 00:27:41 +00:00
header: QuartzComponent[]
beforeBody: QuartzComponent[]
pageBody: QuartzComponent
left: QuartzComponent[]
right: QuartzComponent[]
footer: QuartzComponent
2023-07-01 07:03:01 +00:00
}
export function pageResources(
baseDir: FullSlug | RelativeURL,
staticResources: StaticResources,
): StaticResources {
const contentIndexPath = joinSegments(baseDir, "static/contentIndex.json")
2024-01-05 08:29:34 +00:00
const contentIndexScript = `const fetchData = fetch("${contentIndexPath}").then(data => data.json())`
2023-07-02 20:08:29 +00:00
2023-07-01 07:03:01 +00:00
return {
css: [joinSegments(baseDir, "index.css"), ...staticResources.css],
2023-07-01 07:03:01 +00:00
js: [
{
2023-08-19 23:28:44 +00:00
src: joinSegments(baseDir, "prescript.js"),
loadTime: "beforeDOMReady",
contentType: "external",
},
2023-07-23 00:27:41 +00:00
{
loadTime: "beforeDOMReady",
contentType: "inline",
spaPreserve: true,
script: contentIndexScript,
},
2023-07-01 07:03:01 +00:00
...staticResources.js,
2023-07-23 00:27:41 +00:00
{
2023-08-19 23:28:44 +00:00
src: joinSegments(baseDir, "postscript.js"),
2023-07-23 00:27:41 +00:00
loadTime: "afterDOMReady",
moduleType: "module",
contentType: "external",
},
],
2023-07-01 07:03:01 +00:00
}
}
2024-01-05 08:29:34 +00:00
let pageIndex: Map<FullSlug, QuartzPluginData> | undefined = undefined
function getOrComputeFileIndex(allFiles: QuartzPluginData[]): Map<FullSlug, QuartzPluginData> {
if (!pageIndex) {
pageIndex = new Map()
for (const file of allFiles) {
pageIndex.set(file.slug!, file)
}
}
return pageIndex
}
2023-07-23 00:27:41 +00:00
export function renderPage(
2024-02-05 10:45:36 +00:00
cfg: GlobalConfiguration,
slug: FullSlug,
2023-07-23 00:27:41 +00:00
componentData: QuartzComponentProps,
components: RenderComponents,
pageResources: StaticResources,
): string {
// process transcludes in componentData
visit(componentData.tree as Root, "element", (node, _index, _parent) => {
if (node.tagName === "blockquote") {
const classNames = (node.properties?.className ?? []) as string[]
if (classNames.includes("transclude")) {
const inner = node.children[0] as Element
2024-01-05 08:29:34 +00:00
const transcludeTarget = inner.properties["data-slug"] as FullSlug
const page = getOrComputeFileIndex(componentData.allFiles).get(transcludeTarget)
if (!page) {
return
}
2024-01-05 08:29:34 +00:00
let blockRef = node.properties.dataBlock as string | undefined
if (blockRef?.startsWith("#^")) {
// block transclude
2024-01-05 08:29:34 +00:00
blockRef = blockRef.slice("#^".length)
let blockNode = page.blocks?.[blockRef]
if (blockNode) {
if (blockNode.tagName === "li") {
blockNode = {
type: "element",
tagName: "ul",
2024-01-05 08:29:34 +00:00
properties: {},
children: [blockNode],
}
}
node.children = [
2024-01-05 08:29:34 +00:00
normalizeHastElement(blockNode, slug, transcludeTarget),
{
type: "element",
tagName: "a",
properties: { href: inner.properties?.href, class: ["internal"] },
children: [{ type: "text", value: `Link to original` }],
},
]
}
} else if (blockRef?.startsWith("#") && page.htmlAst) {
// header transclude
blockRef = blockRef.slice(1)
let startIdx = undefined
let endIdx = undefined
for (const [i, el] of page.htmlAst.children.entries()) {
if (el.type === "element" && el.tagName.match(/h[1-6]/)) {
if (endIdx) {
break
}
2024-01-05 08:29:34 +00:00
if (startIdx !== undefined) {
endIdx = i
} else if (el.properties?.id === blockRef) {
startIdx = i
}
}
}
2024-01-05 08:29:34 +00:00
if (startIdx === undefined) {
return
}
node.children = [
2024-01-05 08:29:34 +00:00
...(page.htmlAst.children.slice(startIdx, endIdx) as ElementContent[]).map((child) =>
normalizeHastElement(child as Element, slug, transcludeTarget),
),
{
type: "element",
tagName: "a",
properties: { href: inner.properties?.href, class: ["internal"] },
2024-02-05 10:45:36 +00:00
children: [
{ type: "text", value: i18n(cfg.locale).components.transcludes.linkToOriginal },
],
},
]
} else if (page.htmlAst) {
// page transclude
node.children = [
{
type: "element",
tagName: "h1",
2024-01-05 08:29:34 +00:00
properties: {},
children: [
2024-02-05 10:45:36 +00:00
{
type: "text",
value:
page.frontmatter?.title ??
i18n(cfg.locale).components.transcludes.transcludeOf({
targetSlug: page.slug!,
}),
},
],
},
2024-01-05 08:29:34 +00:00
...(page.htmlAst.children as ElementContent[]).map((child) =>
normalizeHastElement(child as Element, slug, transcludeTarget),
),
{
type: "element",
tagName: "a",
properties: { href: inner.properties?.href, class: ["internal"] },
2024-02-05 10:45:36 +00:00
children: [
{ type: "text", value: i18n(cfg.locale).components.transcludes.linkToOriginal },
],
},
]
}
}
}
})
2023-07-23 00:27:41 +00:00
const {
head: Head,
header,
beforeBody,
pageBody: Content,
left,
right,
footer: Footer,
} = components
2023-07-01 07:03:01 +00:00
const Header = HeaderConstructor()
const Body = BodyConstructor()
2023-07-23 00:27:41 +00:00
const LeftComponent = (
2023-07-05 00:14:15 +00:00
<div class="left sidebar">
2023-07-23 00:27:41 +00:00
{left.map((BodyComponent) => (
<BodyComponent {...componentData} />
))}
2023-07-02 20:08:29 +00:00
</div>
2023-07-23 00:27:41 +00:00
)
2023-07-02 20:08:29 +00:00
2023-07-23 00:27:41 +00:00
const RightComponent = (
2023-07-05 00:14:15 +00:00
<div class="right sidebar">
2023-07-23 00:27:41 +00:00
{right.map((BodyComponent) => (
<BodyComponent {...componentData} />
))}
2023-07-02 20:08:29 +00:00
</div>
2023-07-23 00:27:41 +00:00
)
2023-07-02 20:08:29 +00:00
2023-07-23 00:27:41 +00:00
const doc = (
<html>
<Head {...componentData} />
<body data-slug={slug}>
<div id="quartz-root" class="page">
<Body {...componentData}>
{LeftComponent}
<div class="center">
<div class="page-header">
<Header {...componentData}>
{header.map((HeaderComponent) => (
<HeaderComponent {...componentData} />
))}
</Header>
<div class="popover-hint">
{beforeBody.map((BodyComponent) => (
<BodyComponent {...componentData} />
))}
</div>
</div>
2023-07-23 00:27:41 +00:00
<Content {...componentData} />
</div>
2023-07-23 00:27:41 +00:00
{RightComponent}
</Body>
<Footer {...componentData} />
</div>
</body>
{pageResources.js
.filter((resource) => resource.loadTime === "afterDOMReady")
.map((res) => JSResourceToScriptElement(res))}
</html>
)
2023-07-01 07:03:01 +00:00
return "<!DOCTYPE html>\n" + render(doc)
}