mirror of
https://github.com/jackyzha0/quartz.git
synced 2025-12-24 21:34:06 -06:00
Merge 2fb4b7474d into b050162f82
This commit is contained in:
commit
6f9fac15d2
13
docs/features/Canvas.canvas
Normal file
13
docs/features/Canvas.canvas
Normal file
@ -0,0 +1,13 @@
|
||||
{
|
||||
"nodes":[
|
||||
{"id":"a85fff7a7493ef52","type":"group","x":-600,"y":-240,"width":1340,"height":980,"color":"4","label":"Canvas Group"},
|
||||
{"id":"05e0c45c85417543","type":"link","url":"https://jsoncanvas.org/spec/1.0/","x":300,"y":300,"width":401,"height":400,"color":"3"},
|
||||
{"id":"5997bc7558e18f0b","type":"text","text":"Plain text note","x":-560,"y":470,"width":250,"height":60,"color":"5"},
|
||||
{"id":"f583da2f9411eca1","x":-200,"y":-200,"width":400,"height":400,"color":"1","type":"file","file":"features/popover previews.md"}
|
||||
],
|
||||
"edges":[
|
||||
{"id":"185431e9b6c045dc","fromNode":"5997bc7558e18f0b","fromSide":"right","toNode":"05e0c45c85417543","toSide":"left","fromEnd":"arrow"},
|
||||
{"id":"be55c44ecde61a30","fromNode":"f583da2f9411eca1","fromSide":"right","toNode":"05e0c45c85417543","toSide":"top","color":"2","label":"Specifications"},
|
||||
{"id":"1ff7941319ca11b8","fromNode":"f583da2f9411eca1","fromSide":"left","toNode":"5997bc7558e18f0b","toSide":"top","color":"6"}
|
||||
]
|
||||
}
|
||||
@ -81,6 +81,7 @@ const config: QuartzConfig = {
|
||||
Plugin.ContentPage(),
|
||||
Plugin.FolderPage(),
|
||||
Plugin.TagPage(),
|
||||
Plugin.CanvasPage(),
|
||||
Plugin.ContentIndex({
|
||||
enableSiteMap: true,
|
||||
enableRSS: true,
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { PageLayout, SharedLayout } from "./quartz/cfg"
|
||||
import { PageLayout, SharedLayout, CanvasLayout } from "./quartz/cfg"
|
||||
import * as Component from "./quartz/components"
|
||||
|
||||
// components shared across all pages
|
||||
@ -48,3 +48,8 @@ export const defaultListPageLayout: PageLayout = {
|
||||
],
|
||||
right: [],
|
||||
}
|
||||
|
||||
// components for canvas pages
|
||||
export const defaultCanvasPageLayout: CanvasLayout = {
|
||||
left: [Component.PageTitle(), Component.Search(), Component.Darkmode(), Component.Explorer()],
|
||||
}
|
||||
|
||||
@ -70,7 +70,7 @@ async function buildQuartz(argv: Argv, mut: Mutex, clientRefresh: () => void) {
|
||||
|
||||
perf.addEvent("glob")
|
||||
const allFiles = await glob("**/*.*", argv.directory, cfg.configuration.ignorePatterns)
|
||||
const fps = allFiles.filter((fp) => fp.endsWith(".md")).sort()
|
||||
const fps = allFiles.filter((fp) => fp.endsWith(".md") || fp.endsWith(".canvas")).sort()
|
||||
console.log(
|
||||
`Found ${fps.length} input files from \`${argv.directory}\` in ${perf.timeSince("glob")}`,
|
||||
)
|
||||
|
||||
@ -95,3 +95,4 @@ export interface FullPageLayout {
|
||||
|
||||
export type PageLayout = Pick<FullPageLayout, "beforeBody" | "left" | "right">
|
||||
export type SharedLayout = Pick<FullPageLayout, "head" | "header" | "footer" | "afterBody">
|
||||
export type CanvasLayout = Pick<FullPageLayout, "left">
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import Content from "./pages/Content"
|
||||
import TagContent from "./pages/TagContent"
|
||||
import FolderContent from "./pages/FolderContent"
|
||||
import CanvasContent from "./pages/CanvasContent"
|
||||
import NotFound from "./pages/404"
|
||||
import ArticleTitle from "./ArticleTitle"
|
||||
import Darkmode from "./Darkmode"
|
||||
@ -26,6 +27,7 @@ export {
|
||||
Content,
|
||||
TagContent,
|
||||
FolderContent,
|
||||
CanvasContent,
|
||||
Darkmode,
|
||||
Head,
|
||||
PageTitle,
|
||||
|
||||
105
quartz/components/pages/CanvasContent.tsx
Normal file
105
quartz/components/pages/CanvasContent.tsx
Normal file
@ -0,0 +1,105 @@
|
||||
import { ComponentChildren } from "preact"
|
||||
import { htmlToJsx } from "../../util/jsx"
|
||||
import d3 from "d3"
|
||||
import {
|
||||
QuartzComponent,
|
||||
QuartzComponentConstructor,
|
||||
QuartzComponentProps,
|
||||
QuartzCanvasComponent,
|
||||
CanvasNode,
|
||||
CanvasEdge,
|
||||
CanvasTextNode,
|
||||
CanvasFileNode,
|
||||
CanvasLinkNode,
|
||||
CanvasGroupNode,
|
||||
} from "../types"
|
||||
import { type FilePath, slugifyFilePath } from "../../util/path"
|
||||
import fs from "fs"
|
||||
|
||||
function loadCanvas(file: FilePath): QuartzCanvasComponent {
|
||||
const data = fs.readFileSync(file, "utf8") ?? "{}"
|
||||
console.log(data)
|
||||
return JSON.parse(data) as QuartzCanvasComponent
|
||||
}
|
||||
|
||||
function renderTextNodes(nodes: CanvasTextNode[]): ComponentChildren {
|
||||
return nodes.map((node) => (
|
||||
<div
|
||||
class="canvas canvas-node canvas-text-node"
|
||||
style={{
|
||||
position: "fixed",
|
||||
width: node.width,
|
||||
height: node.height,
|
||||
left: node.x,
|
||||
top: node.y,
|
||||
}}
|
||||
>
|
||||
{node["text"]}
|
||||
</div>
|
||||
))
|
||||
}
|
||||
|
||||
function renderFileNodes(nodes: CanvasFileNode[]): ComponentChildren {
|
||||
return nodes.map((node) => (
|
||||
<div
|
||||
class="internal alias internal-canvas"
|
||||
data-slug={slugifyFilePath(node.file as FilePath)}
|
||||
data-x={node.x}
|
||||
data-y={node.y}
|
||||
style={{
|
||||
position: "fixed",
|
||||
width: node.width,
|
||||
height: node.height,
|
||||
left: node.x,
|
||||
top: node.y,
|
||||
}}
|
||||
></div>
|
||||
))
|
||||
}
|
||||
|
||||
function renderLinkNodes(nodes: CanvasLinkNode[]): ComponentChildren {
|
||||
return nodes.map((node) => (
|
||||
<iframe
|
||||
src={node["url"]}
|
||||
style={{
|
||||
position: "fixed",
|
||||
width: node.width,
|
||||
height: node.height,
|
||||
left: node.x,
|
||||
top: node.y,
|
||||
}}
|
||||
/>
|
||||
))
|
||||
}
|
||||
|
||||
const CanvasContent: QuartzComponent = ({ fileData, tree }: QuartzComponentProps) => {
|
||||
//const content = htmlToJsx(fileData.filePath!, tree) as ComponentChildren
|
||||
const canvas = loadCanvas(fileData.filePath!)
|
||||
const canvasNodes = (canvas["nodes"] ?? []) as CanvasNode[]
|
||||
const canvasEdges = (canvas["edges"] ?? []) as CanvasEdge[]
|
||||
const classes: string[] = fileData.frontmatter?.cssclasses ?? []
|
||||
const classString = ["popover-hint", ...classes].join(" ")
|
||||
|
||||
// Canvas parts
|
||||
const textNodes = (canvasNodes.filter((node) => node["type"] === "text") ??
|
||||
[]) as CanvasTextNode[]
|
||||
const fileNodes = (canvasNodes.filter((node) => node["type"] === "file") ??
|
||||
[]) as CanvasFileNode[]
|
||||
const linkNodes = (canvasNodes.filter((node) => node["type"] === "link") ??
|
||||
[]) as CanvasLinkNode[]
|
||||
const groupNodes = (canvasNodes.filter((node) => node["type"] === "group") ??
|
||||
[]) as CanvasGroupNode[]
|
||||
|
||||
const result = (
|
||||
<article class={classString}>
|
||||
{renderTextNodes(textNodes)}
|
||||
{renderFileNodes(fileNodes)}
|
||||
{renderLinkNodes(linkNodes)}
|
||||
</article>
|
||||
)
|
||||
|
||||
//TODO: Implement canvas rendering
|
||||
return <article class={classString}>{result}</article>
|
||||
}
|
||||
|
||||
export default (() => CanvasContent) satisfies QuartzComponentConstructor
|
||||
266
quartz/components/renderCanvas.tsx
Normal file
266
quartz/components/renderCanvas.tsx
Normal file
@ -0,0 +1,266 @@
|
||||
import { render } from "preact-render-to-string"
|
||||
import { QuartzComponent, QuartzComponentProps } from "./types"
|
||||
import HeaderConstructor from "./Header"
|
||||
import BodyConstructor from "./Body"
|
||||
import { JSResourceToScriptElement, StaticResources } from "../util/resources"
|
||||
import { clone, FullSlug, RelativeURL, joinSegments, normalizeHastElement } from "../util/path"
|
||||
import { visit } from "unist-util-visit"
|
||||
import { Root, Element, ElementContent } from "hast"
|
||||
import { GlobalConfiguration } from "../cfg"
|
||||
import { i18n } from "../i18n"
|
||||
// @ts-ignore
|
||||
import mermaidScript from "./scripts/mermaid.inline"
|
||||
import mermaidStyle from "./styles/mermaid.inline.scss"
|
||||
import { QuartzPluginData } from "../plugins/vfile"
|
||||
|
||||
interface RenderComponents {
|
||||
head: QuartzComponent
|
||||
header: QuartzComponent[]
|
||||
beforeBody: QuartzComponent[]
|
||||
pageBody: QuartzComponent
|
||||
afterBody: QuartzComponent[]
|
||||
left: QuartzComponent[]
|
||||
right: QuartzComponent[]
|
||||
footer: QuartzComponent
|
||||
}
|
||||
|
||||
const headerRegex = new RegExp(/h[1-6]/)
|
||||
export function pageResources(
|
||||
baseDir: FullSlug | RelativeURL,
|
||||
fileData: QuartzPluginData,
|
||||
staticResources: StaticResources,
|
||||
): StaticResources {
|
||||
const contentIndexPath = joinSegments(baseDir, "static/contentIndex.json")
|
||||
const contentIndexScript = `const fetchData = fetch("${contentIndexPath}").then(data => data.json())`
|
||||
|
||||
const resources: StaticResources = {
|
||||
css: [
|
||||
{
|
||||
content: joinSegments(baseDir, "index.css"),
|
||||
},
|
||||
...staticResources.css,
|
||||
],
|
||||
js: [
|
||||
{
|
||||
src: joinSegments(baseDir, "prescript.js"),
|
||||
loadTime: "beforeDOMReady",
|
||||
contentType: "external",
|
||||
},
|
||||
{
|
||||
loadTime: "beforeDOMReady",
|
||||
contentType: "inline",
|
||||
spaPreserve: true,
|
||||
script: contentIndexScript,
|
||||
},
|
||||
...staticResources.js,
|
||||
],
|
||||
}
|
||||
|
||||
if (fileData.hasMermaidDiagram) {
|
||||
resources.js.push({
|
||||
script: mermaidScript,
|
||||
loadTime: "afterDOMReady",
|
||||
moduleType: "module",
|
||||
contentType: "inline",
|
||||
})
|
||||
resources.css.push({ content: mermaidStyle, inline: true })
|
||||
}
|
||||
|
||||
// NOTE: we have to put this last to make sure spa.inline.ts is the last item.
|
||||
resources.js.push({
|
||||
src: joinSegments(baseDir, "postscript.js"),
|
||||
loadTime: "afterDOMReady",
|
||||
moduleType: "module",
|
||||
contentType: "external",
|
||||
})
|
||||
|
||||
return resources
|
||||
}
|
||||
|
||||
export function renderCanvas(
|
||||
cfg: GlobalConfiguration,
|
||||
slug: FullSlug,
|
||||
componentData: QuartzComponentProps,
|
||||
components: RenderComponents,
|
||||
pageResources: StaticResources,
|
||||
): string {
|
||||
// make a deep copy of the tree so we don't remove the transclusion references
|
||||
// for the file cached in contentMap in build.ts
|
||||
const root = clone(componentData.tree) as Root
|
||||
|
||||
console.log(root)
|
||||
|
||||
// process transcludes in componentData
|
||||
visit(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
|
||||
const transcludeTarget = inner.properties["data-slug"] as FullSlug
|
||||
const page = componentData.allFiles.find((f) => f.slug === transcludeTarget)
|
||||
if (!page) {
|
||||
return
|
||||
}
|
||||
|
||||
let blockRef = node.properties.dataBlock as string | undefined
|
||||
if (blockRef?.startsWith("#^")) {
|
||||
// block transclude
|
||||
blockRef = blockRef.slice("#^".length)
|
||||
let blockNode = page.blocks?.[blockRef]
|
||||
if (blockNode) {
|
||||
if (blockNode.tagName === "li") {
|
||||
blockNode = {
|
||||
type: "element",
|
||||
tagName: "ul",
|
||||
properties: {},
|
||||
children: [blockNode],
|
||||
}
|
||||
}
|
||||
|
||||
node.children = [
|
||||
normalizeHastElement(blockNode, slug, transcludeTarget),
|
||||
{
|
||||
type: "element",
|
||||
tagName: "a",
|
||||
properties: { href: inner.properties?.href, class: ["internal", "transclude-src"] },
|
||||
children: [
|
||||
{ type: "text", value: i18n(cfg.locale).components.transcludes.linkToOriginal },
|
||||
],
|
||||
},
|
||||
]
|
||||
}
|
||||
} else if (blockRef?.startsWith("#") && page.htmlAst) {
|
||||
// header transclude
|
||||
blockRef = blockRef.slice(1)
|
||||
let startIdx = undefined
|
||||
let startDepth = undefined
|
||||
let endIdx = undefined
|
||||
for (const [i, el] of page.htmlAst.children.entries()) {
|
||||
// skip non-headers
|
||||
if (!(el.type === "element" && el.tagName.match(headerRegex))) continue
|
||||
const depth = Number(el.tagName.substring(1))
|
||||
|
||||
// lookin for our blockref
|
||||
if (startIdx === undefined || startDepth === undefined) {
|
||||
// skip until we find the blockref that matches
|
||||
if (el.properties?.id === blockRef) {
|
||||
startIdx = i
|
||||
startDepth = depth
|
||||
}
|
||||
} else if (depth <= startDepth) {
|
||||
// looking for new header that is same level or higher
|
||||
endIdx = i
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (startIdx === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
node.children = [
|
||||
...(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", "transclude-src"] },
|
||||
children: [
|
||||
{ type: "text", value: i18n(cfg.locale).components.transcludes.linkToOriginal },
|
||||
],
|
||||
},
|
||||
]
|
||||
} else if (page.htmlAst) {
|
||||
// page transclude
|
||||
node.children = [
|
||||
{
|
||||
type: "element",
|
||||
tagName: "h1",
|
||||
properties: {},
|
||||
children: [
|
||||
{
|
||||
type: "text",
|
||||
value:
|
||||
page.frontmatter?.title ??
|
||||
i18n(cfg.locale).components.transcludes.transcludeOf({
|
||||
targetSlug: page.slug!,
|
||||
}),
|
||||
},
|
||||
],
|
||||
},
|
||||
...(page.htmlAst.children as ElementContent[]).map((child) =>
|
||||
normalizeHastElement(child as Element, slug, transcludeTarget),
|
||||
),
|
||||
{
|
||||
type: "element",
|
||||
tagName: "a",
|
||||
properties: { href: inner.properties?.href, class: ["internal", "transclude-src"] },
|
||||
children: [
|
||||
{ type: "text", value: i18n(cfg.locale).components.transcludes.linkToOriginal },
|
||||
],
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// set componentData.tree to the edited html that has transclusions rendered
|
||||
componentData.tree = root
|
||||
|
||||
const {
|
||||
head: Head,
|
||||
header,
|
||||
beforeBody,
|
||||
pageBody: CanvasContent,
|
||||
afterBody,
|
||||
left,
|
||||
right,
|
||||
footer: Footer,
|
||||
} = components
|
||||
const Header = HeaderConstructor()
|
||||
const Body = BodyConstructor()
|
||||
|
||||
const LeftComponent = (
|
||||
<div class="left sidebar">
|
||||
{left.map((BodyComponent) => (
|
||||
<BodyComponent {...componentData} />
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
|
||||
const lang = componentData.fileData.frontmatter?.lang ?? cfg.locale?.split("-")[0] ?? "en"
|
||||
const doc = (
|
||||
<html lang={lang}>
|
||||
<Head {...componentData} />
|
||||
<body data-slug={slug}>
|
||||
<div id="quartz-root" class="page canvas">
|
||||
<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>
|
||||
<CanvasContent {...componentData} />
|
||||
</div>
|
||||
</Body>
|
||||
</div>
|
||||
</body>
|
||||
{pageResources.js
|
||||
.filter((resource) => resource.loadTime === "afterDOMReady")
|
||||
.map((res) => JSResourceToScriptElement(res))}
|
||||
</html>
|
||||
)
|
||||
|
||||
return "<!DOCTYPE html>\n" + render(doc)
|
||||
}
|
||||
@ -100,10 +100,121 @@ async function mouseEnterHandler(
|
||||
}
|
||||
}
|
||||
|
||||
async function navigationHandler(this: HTMLAnchorElement) {
|
||||
const link = this
|
||||
if (link.dataset.noPopover === "true") {
|
||||
return
|
||||
}
|
||||
|
||||
const canvasX = Number(link.getAttribute("data-x") ?? "0")
|
||||
const canvasY = Number(link.getAttribute("data-y") ?? "0")
|
||||
|
||||
async function setPosition(popoverElement: HTMLElement) {
|
||||
const { x, y } = await computePosition(link, popoverElement, {
|
||||
middleware: [inline({ x: canvasX, y: canvasY }), shift(), flip()],
|
||||
})
|
||||
Object.assign(popoverElement.style, {
|
||||
left: `${x}px`,
|
||||
top: `${y}px`,
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
})
|
||||
}
|
||||
|
||||
const hasAlreadyBeenFetched = () =>
|
||||
[...link.children].some((child) => child.classList.contains("popover"))
|
||||
|
||||
// dont refetch if there's already a popover
|
||||
if (hasAlreadyBeenFetched()) {
|
||||
return setPosition(link.lastChild as HTMLElement)
|
||||
}
|
||||
|
||||
const thisUrl = new URL(document.location.href)
|
||||
thisUrl.hash = ""
|
||||
thisUrl.search = ""
|
||||
const targetUrl = new URL(link.href)
|
||||
const hash = decodeURIComponent(targetUrl.hash)
|
||||
targetUrl.hash = ""
|
||||
targetUrl.search = ""
|
||||
|
||||
const response = await fetchCanonical(targetUrl).catch((err) => {
|
||||
console.error(err)
|
||||
})
|
||||
|
||||
// bailout if another popover exists
|
||||
if (hasAlreadyBeenFetched()) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!response) return
|
||||
const [contentType] = response.headers.get("Content-Type")!.split(";")
|
||||
const [contentTypeCategory, typeInfo] = contentType.split("/")
|
||||
|
||||
const popoverElement = document.createElement("div")
|
||||
popoverElement.classList.add("popover")
|
||||
const popoverInner = document.createElement("div")
|
||||
popoverInner.classList.add("popover-inner")
|
||||
popoverElement.appendChild(popoverInner)
|
||||
|
||||
popoverInner.dataset.contentType = contentType ?? undefined
|
||||
|
||||
switch (contentTypeCategory) {
|
||||
case "image":
|
||||
const img = document.createElement("img")
|
||||
img.src = targetUrl.toString()
|
||||
img.alt = targetUrl.pathname
|
||||
|
||||
popoverInner.appendChild(img)
|
||||
break
|
||||
case "application":
|
||||
switch (typeInfo) {
|
||||
case "pdf":
|
||||
const pdf = document.createElement("iframe")
|
||||
pdf.src = targetUrl.toString()
|
||||
popoverInner.appendChild(pdf)
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
break
|
||||
default:
|
||||
const contents = await response.text()
|
||||
const html = p.parseFromString(contents, "text/html")
|
||||
normalizeRelativeURLs(html, targetUrl)
|
||||
const elts = [...html.getElementsByClassName("popover-hint")]
|
||||
if (elts.length === 0) return
|
||||
|
||||
elts.forEach((elt) => popoverInner.appendChild(elt))
|
||||
}
|
||||
|
||||
setPosition(popoverElement)
|
||||
link.appendChild(popoverElement)
|
||||
|
||||
if (hash !== "") {
|
||||
const heading = popoverInner.querySelector(hash) as HTMLElement | null
|
||||
if (heading) {
|
||||
// leave ~12px of buffer when scrolling to a heading
|
||||
popoverInner.scroll({ top: heading.offsetTop - 12, behavior: "instant" })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener("nav", () => {
|
||||
const links = [...document.getElementsByClassName("internal")] as HTMLAnchorElement[]
|
||||
for (const link of links) {
|
||||
console.log(links)
|
||||
const pageLinks = links.filter((link) =>
|
||||
link.classList.contains("internal-canvas"),
|
||||
) as HTMLAnchorElement[]
|
||||
const canvasLinks = links.filter(
|
||||
(link) => !link.classList.contains("internal-canvas"),
|
||||
) as HTMLAnchorElement[]
|
||||
for (const link of pageLinks) {
|
||||
link.addEventListener("mouseenter", mouseEnterHandler)
|
||||
window.addCleanup(() => link.removeEventListener("mouseenter", mouseEnterHandler))
|
||||
}
|
||||
//TODO: Fix canvas loading of popovers
|
||||
for (const link of canvasLinks) {
|
||||
link.addEventListener("load", navigationHandler)
|
||||
window.addCleanup(() => link.removeEventListener("load", navigationHandler))
|
||||
}
|
||||
})
|
||||
|
||||
@ -27,3 +27,55 @@ export type QuartzComponent = ComponentType<QuartzComponentProps> & {
|
||||
export type QuartzComponentConstructor<Options extends object | undefined = undefined> = (
|
||||
opts: Options,
|
||||
) => QuartzComponent
|
||||
|
||||
export type QuartzCanvasComponent = {
|
||||
nodes?: CanvasNode[]
|
||||
edges?: CanvasEdge[]
|
||||
}
|
||||
|
||||
export type CanvasNode = {
|
||||
id: string
|
||||
type: "text" | "file" | "link" | "group"
|
||||
x: number
|
||||
y: number
|
||||
width: number
|
||||
height: number
|
||||
color?: CanvasColor
|
||||
}
|
||||
|
||||
export type CanvasTextNode = CanvasNode & {
|
||||
type: "text"
|
||||
text: string
|
||||
}
|
||||
|
||||
export type CanvasFileNode = CanvasNode & {
|
||||
type: "file"
|
||||
file: string
|
||||
subpath?: string
|
||||
}
|
||||
|
||||
export type CanvasLinkNode = CanvasNode & {
|
||||
type: "link"
|
||||
url: string
|
||||
}
|
||||
|
||||
export type CanvasGroupNode = CanvasNode & {
|
||||
type: "group"
|
||||
label?: string
|
||||
background?: string
|
||||
backgroundStyle?: "cover" | "ratio" | "repeat"
|
||||
}
|
||||
|
||||
export type CanvasEdge = {
|
||||
id: string
|
||||
fromNode: string
|
||||
toNode: string
|
||||
fromSide?: "top" | "bottom" | "left" | "right"
|
||||
toSide?: "top" | "bottom" | "left" | "right"
|
||||
fromEnd?: "none" | "arrow"
|
||||
toEnd?: "none" | "arrow"
|
||||
color?: CanvasColor
|
||||
label?: string
|
||||
}
|
||||
|
||||
export type CanvasColor = "1" | "2" | "3" | "4" | "5" | "6" | "#${string}"
|
||||
|
||||
141
quartz/plugins/emitters/canvasPage.tsx
Normal file
141
quartz/plugins/emitters/canvasPage.tsx
Normal file
@ -0,0 +1,141 @@
|
||||
import path from "path"
|
||||
import { visit } from "unist-util-visit"
|
||||
import { Root } from "hast"
|
||||
import { VFile } from "vfile"
|
||||
import { QuartzEmitterPlugin } from "../types"
|
||||
import { QuartzComponentProps } from "../../components/types"
|
||||
import HeaderConstructor from "../../components/Header"
|
||||
import BodyConstructor from "../../components/Body"
|
||||
import { pageResources, renderCanvas } from "../../components/renderCanvas"
|
||||
import { FullPageLayout } from "../../cfg"
|
||||
import { Argv } from "../../util/ctx"
|
||||
import { FilePath, isRelativeURL, joinSegments, pathToRoot } from "../../util/path"
|
||||
import { defaultContentPageLayout, sharedPageComponents } from "../../../quartz.layout"
|
||||
import { CanvasContent } from "../../components"
|
||||
import chalk from "chalk"
|
||||
import { write } from "./helpers"
|
||||
import DepGraph from "../../depgraph"
|
||||
|
||||
// get all the dependencies for the markdown file
|
||||
// eg. images, scripts, stylesheets, transclusions
|
||||
const parseDependencies = (argv: Argv, hast: Root, file: VFile): string[] => {
|
||||
const dependencies: string[] = []
|
||||
|
||||
visit(hast, "element", (elem): void => {
|
||||
let ref: string | null = null
|
||||
|
||||
if (
|
||||
["script", "img", "audio", "video", "source", "iframe"].includes(elem.tagName) &&
|
||||
elem?.properties?.src
|
||||
) {
|
||||
ref = elem.properties.src.toString()
|
||||
} else if (["a", "link"].includes(elem.tagName) && elem?.properties?.href) {
|
||||
// transclusions will create a tags with relative hrefs
|
||||
ref = elem.properties.href.toString()
|
||||
}
|
||||
|
||||
// if it is a relative url, its a local file and we need to add
|
||||
// it to the dependency graph. otherwise, ignore
|
||||
if (ref === null || !isRelativeURL(ref)) {
|
||||
return
|
||||
}
|
||||
|
||||
let fp = path.join(file.data.filePath!, path.relative(argv.directory, ref)).replace(/\\/g, "/")
|
||||
// markdown files have the .md extension stripped in hrefs, add it back here
|
||||
if (!fp.split("/").pop()?.includes(".")) {
|
||||
fp += ".md"
|
||||
}
|
||||
dependencies.push(fp)
|
||||
})
|
||||
|
||||
return dependencies
|
||||
}
|
||||
|
||||
export const CanvasPage: QuartzEmitterPlugin<Partial<FullPageLayout>> = (userOpts) => {
|
||||
const opts: FullPageLayout = {
|
||||
...sharedPageComponents,
|
||||
...defaultContentPageLayout,
|
||||
pageBody: CanvasContent(),
|
||||
...userOpts,
|
||||
}
|
||||
|
||||
const { head: Head, header, beforeBody, pageBody, afterBody, left, right, footer: Footer } = opts
|
||||
const Header = HeaderConstructor()
|
||||
const Body = BodyConstructor()
|
||||
|
||||
return {
|
||||
name: "CanvasPage",
|
||||
getQuartzComponents() {
|
||||
return [
|
||||
Head,
|
||||
Header,
|
||||
Body,
|
||||
...header,
|
||||
...beforeBody,
|
||||
pageBody,
|
||||
...afterBody,
|
||||
...left,
|
||||
...right,
|
||||
Footer,
|
||||
]
|
||||
},
|
||||
async getDependencyGraph(ctx, content, _resources) {
|
||||
const graph = new DepGraph<FilePath>()
|
||||
|
||||
for (const [tree, file] of content) {
|
||||
const sourcePath = file.data.filePath!
|
||||
const slug = file.data.slug!
|
||||
graph.addEdge(sourcePath, joinSegments(ctx.argv.output, slug + ".html") as FilePath)
|
||||
|
||||
parseDependencies(ctx.argv, tree as Root, file).forEach((dep) => {
|
||||
graph.addEdge(dep as FilePath, sourcePath)
|
||||
})
|
||||
}
|
||||
|
||||
return graph
|
||||
},
|
||||
async emit(ctx, content, resources): Promise<FilePath[]> {
|
||||
const cfg = ctx.cfg.configuration
|
||||
const fps: FilePath[] = []
|
||||
const allFiles = content.map((c) => c[1].data)
|
||||
|
||||
let containsCanvas = false
|
||||
for (const [tree, file] of content) {
|
||||
if (!file.data.filePath?.endsWith(".canvas")) continue
|
||||
const slug = file.data.slug!
|
||||
containsCanvas = true
|
||||
|
||||
const externalResources = pageResources(pathToRoot(slug), file.data, resources)
|
||||
const componentData: QuartzComponentProps = {
|
||||
ctx,
|
||||
fileData: file.data,
|
||||
externalResources,
|
||||
cfg,
|
||||
children: [],
|
||||
tree,
|
||||
allFiles,
|
||||
}
|
||||
|
||||
const content = renderCanvas(cfg, slug, componentData, opts, externalResources)
|
||||
const fp = await write({
|
||||
ctx,
|
||||
content,
|
||||
slug,
|
||||
ext: ".html",
|
||||
})
|
||||
|
||||
fps.push(fp)
|
||||
}
|
||||
|
||||
if (!containsCanvas && !ctx.argv.fastRebuild) {
|
||||
console.log(
|
||||
chalk.yellow(
|
||||
`\nWarning: No canvas files detected in \`${ctx.argv.directory}\`. Skipping.`,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
return fps
|
||||
},
|
||||
}
|
||||
}
|
||||
@ -1,6 +1,7 @@
|
||||
export { ContentPage } from "./contentPage"
|
||||
export { TagPage } from "./tagPage"
|
||||
export { FolderPage } from "./folderPage"
|
||||
export { CanvasPage } from "./canvasPage"
|
||||
export { ContentIndex } from "./contentIndex"
|
||||
export { AliasRedirects } from "./aliases"
|
||||
export { Assets } from "./assets"
|
||||
|
||||
@ -66,7 +66,7 @@ export function slugifyFilePath(fp: FilePath, excludeExt?: boolean): FullSlug {
|
||||
fp = stripSlashes(fp) as FilePath
|
||||
let ext = _getFileExtension(fp)
|
||||
const withoutFileExt = fp.replace(new RegExp(ext + "$"), "")
|
||||
if (excludeExt || [".md", ".html", undefined].includes(ext)) {
|
||||
if (excludeExt || [".md", ".html", ".canvas", undefined].includes(ext)) {
|
||||
ext = ""
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user