mirror of
https://github.com/jackyzha0/quartz.git
synced 2025-12-27 06:44:07 -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.ContentPage(),
|
||||||
Plugin.FolderPage(),
|
Plugin.FolderPage(),
|
||||||
Plugin.TagPage(),
|
Plugin.TagPage(),
|
||||||
|
Plugin.CanvasPage(),
|
||||||
Plugin.ContentIndex({
|
Plugin.ContentIndex({
|
||||||
enableSiteMap: true,
|
enableSiteMap: true,
|
||||||
enableRSS: 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"
|
import * as Component from "./quartz/components"
|
||||||
|
|
||||||
// components shared across all pages
|
// components shared across all pages
|
||||||
@ -48,3 +48,8 @@ export const defaultListPageLayout: PageLayout = {
|
|||||||
],
|
],
|
||||||
right: [],
|
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")
|
perf.addEvent("glob")
|
||||||
const allFiles = await glob("**/*.*", argv.directory, cfg.configuration.ignorePatterns)
|
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(
|
console.log(
|
||||||
`Found ${fps.length} input files from \`${argv.directory}\` in ${perf.timeSince("glob")}`,
|
`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 PageLayout = Pick<FullPageLayout, "beforeBody" | "left" | "right">
|
||||||
export type SharedLayout = Pick<FullPageLayout, "head" | "header" | "footer" | "afterBody">
|
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 Content from "./pages/Content"
|
||||||
import TagContent from "./pages/TagContent"
|
import TagContent from "./pages/TagContent"
|
||||||
import FolderContent from "./pages/FolderContent"
|
import FolderContent from "./pages/FolderContent"
|
||||||
|
import CanvasContent from "./pages/CanvasContent"
|
||||||
import NotFound from "./pages/404"
|
import NotFound from "./pages/404"
|
||||||
import ArticleTitle from "./ArticleTitle"
|
import ArticleTitle from "./ArticleTitle"
|
||||||
import Darkmode from "./Darkmode"
|
import Darkmode from "./Darkmode"
|
||||||
@ -26,6 +27,7 @@ export {
|
|||||||
Content,
|
Content,
|
||||||
TagContent,
|
TagContent,
|
||||||
FolderContent,
|
FolderContent,
|
||||||
|
CanvasContent,
|
||||||
Darkmode,
|
Darkmode,
|
||||||
Head,
|
Head,
|
||||||
PageTitle,
|
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", () => {
|
document.addEventListener("nav", () => {
|
||||||
const links = [...document.getElementsByClassName("internal")] as HTMLAnchorElement[]
|
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)
|
link.addEventListener("mouseenter", mouseEnterHandler)
|
||||||
window.addCleanup(() => link.removeEventListener("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> = (
|
export type QuartzComponentConstructor<Options extends object | undefined = undefined> = (
|
||||||
opts: Options,
|
opts: Options,
|
||||||
) => QuartzComponent
|
) => 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 { ContentPage } from "./contentPage"
|
||||||
export { TagPage } from "./tagPage"
|
export { TagPage } from "./tagPage"
|
||||||
export { FolderPage } from "./folderPage"
|
export { FolderPage } from "./folderPage"
|
||||||
|
export { CanvasPage } from "./canvasPage"
|
||||||
export { ContentIndex } from "./contentIndex"
|
export { ContentIndex } from "./contentIndex"
|
||||||
export { AliasRedirects } from "./aliases"
|
export { AliasRedirects } from "./aliases"
|
||||||
export { Assets } from "./assets"
|
export { Assets } from "./assets"
|
||||||
|
|||||||
@ -66,7 +66,7 @@ export function slugifyFilePath(fp: FilePath, excludeExt?: boolean): FullSlug {
|
|||||||
fp = stripSlashes(fp) as FilePath
|
fp = stripSlashes(fp) as FilePath
|
||||||
let ext = _getFileExtension(fp)
|
let ext = _getFileExtension(fp)
|
||||||
const withoutFileExt = fp.replace(new RegExp(ext + "$"), "")
|
const withoutFileExt = fp.replace(new RegExp(ext + "$"), "")
|
||||||
if (excludeExt || [".md", ".html", undefined].includes(ext)) {
|
if (excludeExt || [".md", ".html", ".canvas", undefined].includes(ext)) {
|
||||||
ext = ""
|
ext = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user