various polish
This commit is contained in:
parent
ba6df2d95f
commit
83c5fa64ea
@ -4,12 +4,7 @@ import * as Plugin from "./quartz/plugins"
|
||||
|
||||
const sharedPageComponents = {
|
||||
head: Component.Head(),
|
||||
header: [
|
||||
Component.PageTitle(),
|
||||
Component.Spacer(),
|
||||
Component.Search(),
|
||||
Component.Darkmode()
|
||||
],
|
||||
header: [],
|
||||
footer: Component.Footer({
|
||||
authorName: "Jacky",
|
||||
links: {
|
||||
@ -25,11 +20,15 @@ const contentPageLayout: PageLayout = {
|
||||
Component.ReadingTime(),
|
||||
Component.TagList(),
|
||||
],
|
||||
left: [],
|
||||
left: [
|
||||
Component.PageTitle(),
|
||||
Component.Search(),
|
||||
Component.TableOfContents(),
|
||||
Component.Darkmode()
|
||||
],
|
||||
right: [
|
||||
Component.Graph(),
|
||||
Component.TableOfContents(),
|
||||
Component.Backlinks()
|
||||
Component.Backlinks(),
|
||||
],
|
||||
}
|
||||
|
||||
@ -37,7 +36,11 @@ const listPageLayout: PageLayout = {
|
||||
beforeBody: [
|
||||
Component.ArticleTitle()
|
||||
],
|
||||
left: [],
|
||||
left: [
|
||||
Component.PageTitle(),
|
||||
Component.Search(),
|
||||
Component.Darkmode()
|
||||
],
|
||||
right: [],
|
||||
}
|
||||
|
||||
@ -46,6 +49,9 @@ const config: QuartzConfig = {
|
||||
pageTitle: "🪴 Quartz 4.0",
|
||||
enableSPA: true,
|
||||
enablePopovers: true,
|
||||
analytics: {
|
||||
provider: 'plausible',
|
||||
},
|
||||
canonicalUrl: "quartz.jzhao.xyz",
|
||||
ignorePatterns: ["private", "templates"],
|
||||
theme: {
|
||||
@ -102,16 +108,16 @@ const config: QuartzConfig = {
|
||||
...contentPageLayout,
|
||||
pageBody: Component.Content(),
|
||||
}),
|
||||
Plugin.TagPage({
|
||||
...sharedPageComponents,
|
||||
...listPageLayout,
|
||||
pageBody: Component.TagContent(),
|
||||
}),
|
||||
Plugin.FolderPage({
|
||||
...sharedPageComponents,
|
||||
...listPageLayout,
|
||||
pageBody: Component.FolderContent(),
|
||||
}),
|
||||
Plugin.TagPage({
|
||||
...sharedPageComponents,
|
||||
...listPageLayout,
|
||||
pageBody: Component.TagContent(),
|
||||
}),
|
||||
Plugin.ContentIndex({
|
||||
enableSiteMap: true,
|
||||
enableRSS: true,
|
||||
|
@ -64,7 +64,7 @@ yargs(hideBin(process.argv))
|
||||
packages: "external",
|
||||
plugins: [
|
||||
sassPlugin({
|
||||
type: 'css-text'
|
||||
type: 'css-text',
|
||||
}),
|
||||
{
|
||||
name: 'inline-script-loader',
|
||||
|
@ -2,12 +2,23 @@ import { QuartzComponent } from "./components/types"
|
||||
import { PluginTypes } from "./plugins/types"
|
||||
import { Theme } from "./theme"
|
||||
|
||||
export type Analytics = null
|
||||
| {
|
||||
provider: 'plausible'
|
||||
}
|
||||
| {
|
||||
provider: 'google',
|
||||
tagId: string
|
||||
}
|
||||
|
||||
export interface GlobalConfiguration {
|
||||
pageTitle: string,
|
||||
/** Whether to enable single-page-app style rendering. this prevents flashes of unstyled content and improves smoothness of Quartz */
|
||||
enableSPA: boolean,
|
||||
/** Whether to display Wikipedia-style popovers when hovering over links */
|
||||
enablePopovers: boolean,
|
||||
/** Analytics mode */
|
||||
analytics: Analytics
|
||||
/** Glob patterns to not search */
|
||||
ignorePatterns: string[],
|
||||
/** Base URL to use for CNAME files, sitemaps, and RSS feeds that require an absolute URL.
|
||||
|
@ -2,9 +2,8 @@ import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
|
||||
|
||||
function ArticleTitle({ fileData }: QuartzComponentProps) {
|
||||
const title = fileData.frontmatter?.title
|
||||
const displayTitle = fileData.slug === "index" ? undefined : title
|
||||
if (displayTitle) {
|
||||
return <h1 class="article-title">{displayTitle}</h1>
|
||||
if (title) {
|
||||
return <h1 class="article-title">{title}</h1>
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
|
@ -14,7 +14,7 @@ export default ((opts?: Options) => {
|
||||
return <>
|
||||
<hr />
|
||||
<footer>
|
||||
<p>Made by {name} using <a>Quartz</a>, © {year}</p>
|
||||
<p>Made by {name} using <a href="https://quartz.jzhao.xyz/">Quartz</a>, © {year}</p>
|
||||
<ul>{Object.entries(links).map(([text, link]) => <li>
|
||||
<a href={link}>{text}</a>
|
||||
</li>)}</ul>
|
||||
|
@ -2,15 +2,7 @@ import { resolveToRoot } from "../path"
|
||||
import { JSResourceToScriptElement } from "../resources"
|
||||
import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
|
||||
|
||||
interface Options {
|
||||
prefetchContentIndex: boolean
|
||||
}
|
||||
|
||||
const defaultOptions: Options = {
|
||||
prefetchContentIndex: true
|
||||
}
|
||||
|
||||
export default ((opts?: Options) => {
|
||||
export default (() => {
|
||||
function Head({ fileData, externalResources }: QuartzComponentProps) {
|
||||
const slug = fileData.slug!
|
||||
const title = fileData.frontmatter?.title ?? "Untitled"
|
||||
@ -20,10 +12,6 @@ export default ((opts?: Options) => {
|
||||
const iconPath = baseDir + "/static/icon.png"
|
||||
const ogImagePath = baseDir + "/static/og-image.png"
|
||||
|
||||
const prefetchContentIndex = opts?.prefetchContentIndex ?? defaultOptions.prefetchContentIndex
|
||||
const contentIndexPath = baseDir + "/static/contentIndex.json"
|
||||
const contentIndexScript = `const fetchData = fetch(\`${contentIndexPath}\`).then(data => data.json())`
|
||||
|
||||
return <head>
|
||||
<title>{title}</title>
|
||||
<meta charSet="utf-8" />
|
||||
@ -36,9 +24,8 @@ export default ((opts?: Options) => {
|
||||
<link rel="icon" href={iconPath} />
|
||||
<meta name="description" content={description} />
|
||||
<meta name="generator" content="Quartz" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" />
|
||||
{prefetchContentIndex && <script spa-preserve>{contentIndexScript}</script>}
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com"/>
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com"/>
|
||||
{css.map(href => <link key={href} href={href} rel="stylesheet" type="text/css" spa-preserve />)}
|
||||
{js.filter(resource => resource.loadTime === "beforeDOMReady").map(res => JSResourceToScriptElement(res, true))}
|
||||
</head>
|
||||
|
@ -12,6 +12,7 @@ header {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
margin: 2em 0;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
header h1 {
|
||||
|
@ -23,7 +23,7 @@ function byDateAndAlphabetical(f1: QuartzPluginData, f2: QuartzPluginData): numb
|
||||
|
||||
export function PageList({ fileData, allFiles }: QuartzComponentProps) {
|
||||
const slug = fileData.slug!
|
||||
return <ul class="section-ul">
|
||||
return <ul class="section-ul popover-hint">
|
||||
{allFiles.sort(byDateAndAlphabetical).map(page => {
|
||||
const title = page.frontmatter?.title
|
||||
const pageSlug = page.slug!
|
||||
@ -36,9 +36,8 @@ export function PageList({ fileData, allFiles }: QuartzComponentProps) {
|
||||
<div class="desc">
|
||||
<h3><a href={stripIndex(relativeToRoot(slug, pageSlug))} class="internal">{title}</a></h3>
|
||||
</div>
|
||||
<div class="spacer"></div>
|
||||
<ul class="tags">
|
||||
{tags.map(tag => <li><a href={relativeToRoot(slug, `tags/${tag}`)}>#{tag}</a></li>)}
|
||||
{tags.map(tag => <li><a class="internal" href={relativeToRoot(slug, `tags/${tag}`)}>#{tag}</a></li>)}
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
|
@ -11,7 +11,7 @@ function TagList({ fileData }: QuartzComponentProps) {
|
||||
const display = `#${tag}`
|
||||
const linkDest = baseDir + `/tags/${slugAnchor(tag)}`
|
||||
return <li>
|
||||
<a href={linkDest}>{display}</a>
|
||||
<a href={linkDest} class="internal">{display}</a>
|
||||
</li>
|
||||
})}</ul>
|
||||
} else {
|
||||
@ -25,17 +25,18 @@ TagList.css = `
|
||||
display: flex;
|
||||
padding-left: 0;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
|
||||
.tags > li {
|
||||
display: inline-block;
|
||||
margin: 0;
|
||||
overflow-wrap: normal;
|
||||
}
|
||||
|
||||
& > li {
|
||||
display: inline-block;
|
||||
margin: 0;
|
||||
|
||||
& > a {
|
||||
border-radius: 8px;
|
||||
border: var(--lightgray) 1px solid;
|
||||
padding: 0.2rem 0.5rem;
|
||||
}
|
||||
}
|
||||
.tags > li > a {
|
||||
border-radius: 8px;
|
||||
background-color: var(--highlight);
|
||||
padding: 0.2rem 0.5rem;
|
||||
}
|
||||
`
|
||||
|
||||
|
@ -5,7 +5,7 @@ import { toJsxRuntime } from "hast-util-to-jsx-runtime"
|
||||
function Content({ tree }: QuartzComponentProps) {
|
||||
// @ts-ignore (preact makes it angry)
|
||||
const content = toJsxRuntime(tree, { Fragment, jsx, jsxs, elementAttributeNameCase: 'html' })
|
||||
return <article>{content}</article>
|
||||
return <article class="popover-hint">{content}</article>
|
||||
}
|
||||
|
||||
export default (() => Content) satisfies QuartzComponentConstructor
|
||||
export default (() => Content) satisfies QuartzComponentConstructor
|
||||
|
@ -17,10 +17,15 @@ interface RenderComponents {
|
||||
|
||||
export function pageResources(slug: string, staticResources: StaticResources): StaticResources {
|
||||
const baseDir = resolveToRoot(slug)
|
||||
|
||||
const contentIndexPath = baseDir + "/static/contentIndex.json"
|
||||
const contentIndexScript = `const fetchData = fetch(\`${contentIndexPath}\`).then(data => data.json())`
|
||||
|
||||
return {
|
||||
css: [baseDir + "/index.css", ...staticResources.css],
|
||||
js: [
|
||||
{ src: baseDir + "/prescript.js", loadTime: "beforeDOMReady", contentType: "external" },
|
||||
{ loadTime: "afterDOMReady", contentType: "inline", spaPreserve: true, script: contentIndexScript },
|
||||
...staticResources.js,
|
||||
{ src: baseDir + "/postscript.js", loadTime: "afterDOMReady", moduleType: 'module', contentType: "external" }
|
||||
]
|
||||
@ -32,28 +37,40 @@ export function renderPage(slug: string, componentData: QuartzComponentProps, co
|
||||
const Header = HeaderConstructor()
|
||||
const Body = BodyConstructor()
|
||||
|
||||
const LeftComponent =
|
||||
<div class="left">
|
||||
<div class="left-inner">
|
||||
{left.map(BodyComponent => <BodyComponent {...componentData} />)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
const RightComponent =
|
||||
<div class="right">
|
||||
<div class="right-inner">
|
||||
{right.map(BodyComponent => <BodyComponent {...componentData} />)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
const doc = <html>
|
||||
<Head {...componentData} />
|
||||
<body data-slug={slug}>
|
||||
<div id="quartz-root" class="page">
|
||||
<Header {...componentData} >
|
||||
{header.map(HeaderComponent => <HeaderComponent {...componentData} />)}
|
||||
</Header>
|
||||
<div class="popover-hint">
|
||||
{beforeBody.map(BodyComponent => <BodyComponent {...componentData} />)}
|
||||
<div class="page-header">
|
||||
<Header {...componentData} >
|
||||
{header.map(HeaderComponent => <HeaderComponent {...componentData} />)}
|
||||
</Header>
|
||||
<div class="popover-hint">
|
||||
{beforeBody.map(BodyComponent => <BodyComponent {...componentData} />)}
|
||||
</div>
|
||||
</div>
|
||||
<Body {...componentData}>
|
||||
<div class="left">
|
||||
{left.map(BodyComponent => <BodyComponent {...componentData} />)}
|
||||
</div>
|
||||
<div class="center popover-hint">
|
||||
{LeftComponent}
|
||||
<div class="center">
|
||||
<Content {...componentData} />
|
||||
<Footer {...componentData} />
|
||||
</div>
|
||||
<div class="right">
|
||||
{right.map(BodyComponent => <BodyComponent {...componentData} />)}
|
||||
</div>
|
||||
{RightComponent}
|
||||
</Body>
|
||||
<Footer {...componentData} />
|
||||
</div>
|
||||
</body>
|
||||
{pageResources.js.filter(resource => resource.loadTime === "afterDOMReady").map(res => JSResourceToScriptElement(res))}
|
||||
|
@ -2,7 +2,7 @@ const userPref = window.matchMedia('(prefers-color-scheme: light)').matches ? 'l
|
||||
const currentTheme = localStorage.getItem('theme') ?? userPref
|
||||
document.documentElement.setAttribute('saved-theme', currentTheme)
|
||||
|
||||
window.addEventListener('DOMContentLoaded', () => {
|
||||
document.addEventListener("nav", () => {
|
||||
const switchTheme = (e: any) => {
|
||||
if (e.target.checked) {
|
||||
document.documentElement.setAttribute('saved-theme', 'dark')
|
||||
@ -16,7 +16,8 @@ window.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
// Darkmode toggle
|
||||
const toggleSwitch = document.querySelector('#darkmode-toggle') as HTMLInputElement
|
||||
toggleSwitch.addEventListener('change', switchTheme, false)
|
||||
toggleSwitch.removeEventListener('change', switchTheme)
|
||||
toggleSwitch.addEventListener('change', switchTheme)
|
||||
if (currentTheme === 'dark') {
|
||||
toggleSwitch.checked = true
|
||||
}
|
||||
|
@ -266,9 +266,9 @@ async function renderGraph(container: string, slug: string) {
|
||||
})
|
||||
}
|
||||
|
||||
function renderGlobalGraph() {
|
||||
async function renderGlobalGraph() {
|
||||
const slug = document.body.dataset["slug"]!
|
||||
renderGraph("global-graph-container", slug)
|
||||
await renderGraph("global-graph-container", slug)
|
||||
const container = document.getElementById("global-graph-outer")
|
||||
container?.classList.add("active")
|
||||
|
||||
@ -293,7 +293,14 @@ document.addEventListener("nav", async (e: unknown) => {
|
||||
containerIcon?.addEventListener("click", renderGlobalGraph)
|
||||
})
|
||||
|
||||
window.addEventListener('resize', async () => {
|
||||
const slug = document.body.dataset["slug"]!
|
||||
await renderGraph("graph-container", slug)
|
||||
let resizeEventDebounce: number | undefined = undefined
|
||||
window.addEventListener('resize', () => {
|
||||
if (resizeEventDebounce) {
|
||||
clearTimeout(resizeEventDebounce)
|
||||
}
|
||||
|
||||
resizeEventDebounce = window.setTimeout(async () => {
|
||||
const slug = document.body.dataset["slug"]!
|
||||
await renderGraph("graph-container", slug)
|
||||
}, 50)
|
||||
})
|
||||
|
3
quartz/components/scripts/plausible.inline.ts
Normal file
3
quartz/components/scripts/plausible.inline.ts
Normal file
@ -0,0 +1,3 @@
|
||||
import Plausible from 'plausible-tracker'
|
||||
const { trackPageview } = Plausible()
|
||||
document.addEventListener("nav", () => trackPageview())
|
@ -1,5 +1,24 @@
|
||||
import { computePosition, flip, inline, shift } from "@floating-ui/dom"
|
||||
|
||||
// from micromorph/src/utils.ts
|
||||
// https://github.com/natemoo-re/micromorph/blob/main/src/utils.ts#L5
|
||||
export function normalizeRelativeURLs(
|
||||
el: Element | Document,
|
||||
base: string | URL
|
||||
) {
|
||||
const update = (el: Element, attr: string, base: string | URL) => {
|
||||
el.setAttribute(attr, new URL(el.getAttribute(attr)!, base).pathname)
|
||||
}
|
||||
|
||||
el.querySelectorAll('[href^="./"], [href^="../"]').forEach((item) =>
|
||||
update(item, 'href', base)
|
||||
)
|
||||
|
||||
el.querySelectorAll('[src^="./"], [src^="../"]').forEach((item) =>
|
||||
update(item, 'src', base)
|
||||
)
|
||||
}
|
||||
|
||||
document.addEventListener("nav", () => {
|
||||
const links = [...document.getElementsByClassName("internal")] as HTMLLinkElement[]
|
||||
const p = new DOMParser()
|
||||
@ -41,6 +60,7 @@ document.addEventListener("nav", () => {
|
||||
|
||||
if (!contents) return
|
||||
const html = p.parseFromString(contents, "text/html")
|
||||
normalizeRelativeURLs(html, targetUrl)
|
||||
const elts = [...html.getElementsByClassName("popover-hint")]
|
||||
if (elts.length === 0) return
|
||||
|
||||
@ -54,11 +74,13 @@ document.addEventListener("nav", () => {
|
||||
setPosition(popoverElement)
|
||||
link.appendChild(popoverElement)
|
||||
link.dataset.fetchedPopover = "true"
|
||||
|
||||
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' })
|
||||
|
||||
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' })
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -7,13 +7,9 @@
|
||||
& > ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
margin: 0.5rem 0;
|
||||
|
||||
& > li {
|
||||
margin: 0.5rem 0;
|
||||
padding: 0.25rem 1rem;
|
||||
border: var(--lightgray) 1px solid;
|
||||
border-radius: 5px;
|
||||
& > a {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
@ -1,6 +1,8 @@
|
||||
footer {
|
||||
text-align: left;
|
||||
opacity: 0.8;
|
||||
margin-bottom: 4rem;
|
||||
|
||||
& ul {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
|
@ -11,6 +11,7 @@
|
||||
height: 250px;
|
||||
margin: 0.5em 0;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
& > #global-graph-icon {
|
||||
color: var(--dark);
|
||||
@ -30,10 +31,6 @@
|
||||
background-color: var(--lightgray);
|
||||
}
|
||||
}
|
||||
|
||||
& > #graph-container > svg {
|
||||
margin-bottom: -5px;
|
||||
}
|
||||
}
|
||||
|
||||
& > #global-graph-outer {
|
||||
|
@ -8,29 +8,36 @@ li.section-li {
|
||||
margin-bottom: 1em;
|
||||
|
||||
& > .section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
display: grid;
|
||||
grid-template-columns: 6em 3fr 1fr;
|
||||
|
||||
@media all and (max-width: 600px) {
|
||||
& .tags {
|
||||
& > .tags {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
& h3 > a {
|
||||
font-weight: 700;
|
||||
margin: 0;
|
||||
background-color: transparent;
|
||||
& > .tags {
|
||||
justify-self: end;
|
||||
margin-left: 1rem;
|
||||
}
|
||||
|
||||
& p {
|
||||
& > .desc a {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
& > .meta {
|
||||
margin: 0;
|
||||
padding-right: 1em;
|
||||
flex-basis: 6em;
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
|
||||
& .meta {
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
|
||||
// modifications in popover context
|
||||
.popover .section {
|
||||
grid-template-columns: 6em 1fr !important;
|
||||
& > .tags {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
@ -24,7 +24,7 @@
|
||||
height: 20rem;
|
||||
padding: 0 1rem 1rem 1rem;
|
||||
font-weight: initial;
|
||||
line-height: initial;
|
||||
line-height: normal;
|
||||
font-size: initial;
|
||||
font-family: var(--bodyFont);
|
||||
border: 1px solid var(--gray);
|
||||
|
@ -1,8 +1,7 @@
|
||||
.search {
|
||||
min-width: 5rem;
|
||||
max-width: 12rem;
|
||||
max-width: 14rem;
|
||||
flex-grow: 0.3;
|
||||
margin: 0 1.5rem;
|
||||
|
||||
& > #search-icon {
|
||||
background-color: var(--lightgray);
|
||||
|
@ -8,7 +8,7 @@ export type QuartzComponentProps = {
|
||||
externalResources: StaticResources
|
||||
fileData: QuartzPluginData
|
||||
cfg: GlobalConfiguration
|
||||
children: QuartzComponent[] | JSX.Element[]
|
||||
children: (QuartzComponent | JSX.Element)[]
|
||||
tree: Node<QuartzPluginData>
|
||||
allFiles: QuartzPluginData[]
|
||||
}
|
||||
|
@ -5,7 +5,17 @@ function slugSegment(s: string): string {
|
||||
return s.replace(/\s/g, '-')
|
||||
}
|
||||
|
||||
// on the client, 'index' isn't ever rendered so we should clean it up
|
||||
export function clientSideSlug(fp: string): string {
|
||||
if (fp.endsWith("index")) {
|
||||
fp = fp.slice(0, -"index".length)
|
||||
}
|
||||
|
||||
return fp
|
||||
}
|
||||
|
||||
export function trimPathSuffix(fp: string): string {
|
||||
fp = clientSideSlug(fp)
|
||||
let [cleanPath, anchor] = fp.split("#", 2)
|
||||
anchor = anchor === undefined ? "" : "#" + anchor
|
||||
|
||||
@ -27,9 +37,6 @@ export function slugify(s: string): string {
|
||||
// resolve /a/b/c to ../../
|
||||
export function resolveToRoot(slug: string): string {
|
||||
let fp = trimPathSuffix(slug)
|
||||
if (fp.endsWith("index")) {
|
||||
fp = fp.slice(0, -"index".length)
|
||||
}
|
||||
|
||||
if (fp === "") {
|
||||
return "."
|
||||
|
@ -36,7 +36,6 @@ function generateRSSFeed(cfg: GlobalConfiguration, idx: ContentIndex): string {
|
||||
const base = cfg.canonicalUrl ?? ""
|
||||
const root = `https://${base}`
|
||||
|
||||
// TODO: ogimage
|
||||
const createURLEntry = (slug: string, content: ContentDetails): string => `<items>
|
||||
<title>${content.title}</title>
|
||||
<link>${root}/${slug}</link>
|
||||
|
@ -1,29 +1,17 @@
|
||||
import { GlobalConfiguration } from '../cfg'
|
||||
import { QuartzComponent } from '../components/types'
|
||||
import { StaticResources } from '../resources'
|
||||
import { googleFontHref, joinStyles } from '../theme'
|
||||
import { joinStyles } from '../theme'
|
||||
import { EmitCallback, PluginTypes } from './types'
|
||||
import styles from '../styles/base.scss'
|
||||
|
||||
// @ts-ignore
|
||||
import spaRouterScript from '../components/scripts/spa.inline'
|
||||
// @ts-ignore
|
||||
import popoverScript from '../components/scripts/popover.inline'
|
||||
import popoverStyle from '../components/styles/popover.scss'
|
||||
|
||||
export type ComponentResources = {
|
||||
css: string[],
|
||||
beforeDOMLoaded: string[],
|
||||
afterDOMLoaded: string[]
|
||||
}
|
||||
|
||||
function joinScripts(scripts: string[]): string {
|
||||
// wrap with iife to prevent scope collision
|
||||
return scripts.map(script => `(function () {${script}})();`).join("\n")
|
||||
}
|
||||
|
||||
export function emitComponentResources(cfg: GlobalConfiguration, resources: StaticResources, plugins: PluginTypes, emit: EmitCallback) {
|
||||
const fps: string[] = []
|
||||
export function getComponentResources(plugins: PluginTypes): ComponentResources {
|
||||
const allComponents: Set<QuartzComponent> = new Set()
|
||||
for (const emitter of plugins.emitters) {
|
||||
const components = emitter.getQuartzComponents()
|
||||
@ -50,41 +38,35 @@ export function emitComponentResources(cfg: GlobalConfiguration, resources: Stat
|
||||
componentResources.afterDOMLoaded.push(afterDOMLoaded)
|
||||
}
|
||||
}
|
||||
|
||||
if (cfg.enablePopovers) {
|
||||
componentResources.afterDOMLoaded.push(popoverScript)
|
||||
componentResources.css.push(popoverStyle)
|
||||
}
|
||||
|
||||
if (cfg.enableSPA) {
|
||||
componentResources.afterDOMLoaded.push(spaRouterScript)
|
||||
} else {
|
||||
componentResources.afterDOMLoaded.push(`
|
||||
window.spaNavigate = (url, _) => window.location.assign(url)
|
||||
const event = new CustomEvent("nav", { detail: { slug: document.body.dataset.slug } })
|
||||
document.dispatchEvent(event)`
|
||||
)
|
||||
}
|
||||
return componentResources
|
||||
}
|
||||
|
||||
emit({
|
||||
slug: "index",
|
||||
ext: ".css",
|
||||
content: joinStyles(cfg.theme, styles, ...componentResources.css)
|
||||
})
|
||||
emit({
|
||||
slug: "prescript",
|
||||
ext: ".js",
|
||||
content: joinScripts(componentResources.beforeDOMLoaded)
|
||||
})
|
||||
emit({
|
||||
slug: "postscript",
|
||||
ext: ".js",
|
||||
content: joinScripts(componentResources.afterDOMLoaded)
|
||||
})
|
||||
function joinScripts(scripts: string[]): string {
|
||||
// wrap with iife to prevent scope collision
|
||||
return scripts.map(script => `(function () {${script}})();`).join("\n")
|
||||
}
|
||||
|
||||
fps.push("index.css", "prescript.js", "postscript.js")
|
||||
resources.css.push(googleFontHref(cfg.theme))
|
||||
export async function emitComponentResources(cfg: GlobalConfiguration, res: ComponentResources, emit: EmitCallback): Promise<string[]> {
|
||||
const fps = await Promise.all([
|
||||
emit({
|
||||
slug: "index",
|
||||
ext: ".css",
|
||||
content: joinStyles(cfg.theme, styles, ...res.css)
|
||||
}),
|
||||
emit({
|
||||
slug: "prescript",
|
||||
ext: ".js",
|
||||
content: joinScripts(res.beforeDOMLoaded)
|
||||
}),
|
||||
emit({
|
||||
slug: "postscript",
|
||||
ext: ".js",
|
||||
content: joinScripts(res.afterDOMLoaded)
|
||||
})
|
||||
])
|
||||
return fps
|
||||
|
||||
}
|
||||
|
||||
export function getStaticResourcesFromPlugins(plugins: PluginTypes) {
|
||||
|
@ -1,4 +1,3 @@
|
||||
import { PluggableList } from "unified"
|
||||
import remarkGfm from "remark-gfm"
|
||||
import smartypants from 'remark-smartypants'
|
||||
import { QuartzTransformerPlugin } from "../types"
|
||||
@ -20,14 +19,14 @@ export const GitHubFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options> |
|
||||
return {
|
||||
name: "GitHubFlavoredMarkdown",
|
||||
markdownPlugins() {
|
||||
return opts.enableSmartyPants ? [remarkGfm] : [remarkGfm, smartypants]
|
||||
return opts.enableSmartyPants ? [remarkGfm, smartypants] : [remarkGfm]
|
||||
},
|
||||
htmlPlugins() {
|
||||
if (opts.linkHeadings) {
|
||||
return [rehypeSlug, [rehypeAutolinkHeadings, {
|
||||
behavior: 'append', content: {
|
||||
type: 'text',
|
||||
value: ' §'
|
||||
value: ' §',
|
||||
}
|
||||
}]]
|
||||
} else {
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { QuartzTransformerPlugin } from "../types"
|
||||
import { relativeToRoot, slugify, trimPathSuffix } from "../../path"
|
||||
import { clientSideSlug, relativeToRoot, slugify, trimPathSuffix } from "../../path"
|
||||
import path from "path"
|
||||
import { visit } from 'unist-util-visit'
|
||||
import isAbsoluteUrl from "is-absolute-url"
|
||||
@ -27,7 +27,7 @@ export const CrawlLinks: QuartzTransformerPlugin<Partial<Options> | undefined> =
|
||||
htmlPlugins() {
|
||||
return [() => {
|
||||
return (tree, file) => {
|
||||
const curSlug = file.data.slug!
|
||||
const curSlug = clientSideSlug(file.data.slug!)
|
||||
const transformLink = (target: string) => {
|
||||
const targetSlug = slugify(decodeURI(target).trim())
|
||||
if (opts.markdownLinkResolution === 'relative' && !path.isAbsolute(targetSlug)) {
|
||||
@ -49,7 +49,6 @@ export const CrawlLinks: QuartzTransformerPlugin<Partial<Options> | undefined> =
|
||||
let dest = node.properties.href
|
||||
node.properties.className = isAbsoluteUrl(dest) ? "external" : "internal"
|
||||
|
||||
|
||||
// don't process external links or intra-document anchors
|
||||
if (!(isAbsoluteUrl(dest) || dest.startsWith("#"))) {
|
||||
node.properties.href = transformLink(dest)
|
||||
|
@ -1,13 +1,69 @@
|
||||
import path from "path"
|
||||
import fs from "fs"
|
||||
import { QuartzConfig } from "../cfg"
|
||||
import { GlobalConfiguration, QuartzConfig } from "../cfg"
|
||||
import { PerfTimer } from "../perf"
|
||||
import { emitComponentResources, getStaticResourcesFromPlugins } from "../plugins"
|
||||
import { ComponentResources, emitComponentResources, getComponentResources, getStaticResourcesFromPlugins } from "../plugins"
|
||||
import { EmitCallback } from "../plugins/types"
|
||||
import { ProcessedContent } from "../plugins/vfile"
|
||||
import { QUARTZ, slugify } from "../path"
|
||||
import { globbyStream } from "globby"
|
||||
import chalk from "chalk"
|
||||
import { googleFontHref } from '../theme'
|
||||
|
||||
// @ts-ignore
|
||||
import spaRouterScript from '../components/scripts/spa.inline'
|
||||
// @ts-ignore
|
||||
import plausibleScript from '../components/scripts/plausible.inline'
|
||||
// @ts-ignore
|
||||
import popoverScript from '../components/scripts/popover.inline'
|
||||
import popoverStyle from '../components/styles/popover.scss'
|
||||
import { StaticResources } from "../resources"
|
||||
|
||||
function addGlobalPageResources(cfg: GlobalConfiguration, staticResources: StaticResources, componentResources: ComponentResources) {
|
||||
// font and other resources
|
||||
staticResources.css.push(googleFontHref(cfg.theme))
|
||||
|
||||
// popovers
|
||||
if (cfg.enablePopovers) {
|
||||
componentResources.afterDOMLoaded.push(popoverScript)
|
||||
componentResources.css.push(popoverStyle)
|
||||
}
|
||||
|
||||
if (cfg.analytics?.provider === "google") {
|
||||
const tagId = cfg.analytics.tagId
|
||||
staticResources.js.push({
|
||||
src: `https://www.googletagmanager.com/gtag/js?id=${tagId}`,
|
||||
contentType: 'external',
|
||||
loadTime: 'afterDOMReady',
|
||||
})
|
||||
componentResources.afterDOMLoaded.push(`
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
function gtag() { dataLayer.push(arguments); }
|
||||
gtag(\`js\`, new Date());
|
||||
gtag(\`config\`, \`${tagId}\`, { send_page_view: false });
|
||||
|
||||
document.addEventListener(\`nav\`, () => {
|
||||
gtag(\`event\`, \`page_view\`, {
|
||||
page_title: document.title,
|
||||
page_location: location.href,
|
||||
});
|
||||
});`
|
||||
)
|
||||
} else if (cfg.analytics?.provider === "plausible") {
|
||||
componentResources.afterDOMLoaded.push(plausibleScript)
|
||||
}
|
||||
|
||||
// spa
|
||||
if (cfg.enableSPA) {
|
||||
componentResources.afterDOMLoaded.push(spaRouterScript)
|
||||
} else {
|
||||
componentResources.afterDOMLoaded.push(`
|
||||
window.spaNavigate = (url, _) => window.location.assign(url)
|
||||
const event = new CustomEvent("nav", { detail: { slug: document.body.dataset.slug } })
|
||||
document.dispatchEvent(event)`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export async function emitContent(contentFolder: string, output: string, cfg: QuartzConfig, content: ProcessedContent[], verbose: boolean) {
|
||||
const perf = new PerfTimer()
|
||||
@ -19,9 +75,25 @@ export async function emitContent(contentFolder: string, output: string, cfg: Qu
|
||||
return pathToPage
|
||||
}
|
||||
|
||||
// initialize from plugins
|
||||
const staticResources = getStaticResourcesFromPlugins(cfg.plugins)
|
||||
emitComponentResources(cfg.configuration, staticResources, cfg.plugins, emit)
|
||||
|
||||
// component specific scripts and styles
|
||||
const componentResources = getComponentResources(cfg.plugins)
|
||||
// important that this goes *after* component scripts
|
||||
// as the "nav" event gets triggered here and we should make sure
|
||||
// that everyone else had the chance to register a listener for it
|
||||
addGlobalPageResources(cfg.configuration, staticResources, componentResources)
|
||||
|
||||
// emit in one go
|
||||
const emittedResources = await emitComponentResources(cfg.configuration, componentResources, emit)
|
||||
if (verbose) {
|
||||
for (const file of emittedResources) {
|
||||
console.log(`[emit:Resources] ${file}`)
|
||||
}
|
||||
}
|
||||
|
||||
// emitter plugins
|
||||
let emittedFiles = 0
|
||||
for (const emitter of cfg.plugins.emitters) {
|
||||
try {
|
||||
|
@ -3,7 +3,8 @@ import { JSX } from "preact/jsx-runtime"
|
||||
|
||||
export type JSResource = {
|
||||
loadTime: 'beforeDOMReady' | 'afterDOMReady'
|
||||
moduleType?: 'module'
|
||||
moduleType?: 'module',
|
||||
spaPreserve?: boolean
|
||||
} & ({
|
||||
src: string
|
||||
contentType: 'external'
|
||||
@ -14,11 +15,12 @@ export type JSResource = {
|
||||
|
||||
export function JSResourceToScriptElement(resource: JSResource, preserve?: boolean): JSX.Element {
|
||||
const scriptType = resource.moduleType ?? 'application/javascript'
|
||||
const spaPreserve = preserve ?? resource.spaPreserve
|
||||
if (resource.contentType === 'external') {
|
||||
return <script key={resource.src} src={resource.src} type={scriptType} spa-preserve={preserve} />
|
||||
return <script key={resource.src} src={resource.src} type={scriptType} spa-preserve={spaPreserve} />
|
||||
} else {
|
||||
const content = resource.script
|
||||
return <script key={randomUUID()} type={scriptType} spa-preserve={preserve}>{content}</script>
|
||||
return <script key={randomUUID()} type={scriptType} spa-preserve={spaPreserve}>{content}</script>
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -11,6 +11,9 @@ body {
|
||||
box-sizing: border-box;
|
||||
background-color: var(--light);
|
||||
font-family: var(--bodyFont);
|
||||
--pageWidth: 800px;
|
||||
--sidePanelWidth: 400px;
|
||||
--topSpacing: 6rem;
|
||||
}
|
||||
|
||||
.text-highlight {
|
||||
@ -27,7 +30,7 @@ p, ul, text, a, tr, td, li, ol, ul, .katex {
|
||||
a {
|
||||
font-weight: 600;
|
||||
text-decoration: none;
|
||||
transition: all 0.2s ease;
|
||||
transition: color 0.2s ease;
|
||||
color: var(--secondary);
|
||||
|
||||
&:hover {
|
||||
@ -43,34 +46,48 @@ a {
|
||||
}
|
||||
|
||||
.page {
|
||||
margin: 6rem 35vw 6rem 20vw;
|
||||
max-width: 1000px;
|
||||
position: relative;
|
||||
& > .page-header {
|
||||
max-width: var(--pageWidth);
|
||||
margin: var(--topSpacing) auto 0 auto;
|
||||
}
|
||||
|
||||
& .left, & .right {
|
||||
position: fixed;
|
||||
height: 100vh;
|
||||
overflow-y: scroll;
|
||||
box-sizing: border-box;
|
||||
& > #quartz-body {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
top: 0;
|
||||
gap: 2rem;
|
||||
padding: 6rem;
|
||||
}
|
||||
|
||||
& .left {
|
||||
left: 0;
|
||||
padding-left: 10vw;
|
||||
width: 20vw;
|
||||
}
|
||||
|
||||
& .right {
|
||||
right: 0;
|
||||
padding-right: 10vw;
|
||||
width: 35vw;
|
||||
}
|
||||
& .left, & .right {
|
||||
flex: 1;
|
||||
width: calc(calc(100vw - var(--pageWidth)) / 2);
|
||||
}
|
||||
|
||||
& .left-inner, & .right-inner {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2rem;
|
||||
top: 0;
|
||||
width: var(--sidePanelWidth);
|
||||
margin-top: calc(var(--topSpacing));
|
||||
box-sizing: border-box;
|
||||
padding: 0 4rem;
|
||||
position: fixed;
|
||||
}
|
||||
|
||||
& .left-inner {
|
||||
left: calc(calc(100vw - var(--pageWidth)) / 2 - var(--sidePanelWidth));
|
||||
}
|
||||
|
||||
& .right-inner {
|
||||
right: calc(calc(100vw - var(--pageWidth)) / 2 - var(--sidePanelWidth));
|
||||
}
|
||||
|
||||
& .center {
|
||||
width: var(--pageWidth);
|
||||
margin: 0 auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.page {
|
||||
@media all and (max-width: 1200px) {
|
||||
margin: 25px 5vw;
|
||||
& .left, & .right {
|
||||
@ -89,9 +106,26 @@ a {
|
||||
& > h1 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
// darkmode diagrams
|
||||
& svg {
|
||||
stroke: var(--dark);
|
||||
}
|
||||
|
||||
& ul:has(input[type='checkbox']) {
|
||||
list-style-type: none;
|
||||
padding-left: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
input[type="checkbox"] {
|
||||
transform: translateY(2px);
|
||||
color: var(--secondary);
|
||||
border-color: var(--lightgray);
|
||||
background-color: var(--light);
|
||||
}
|
||||
|
||||
blockquote {
|
||||
margin: 1rem 0;
|
||||
border-left: 3px solid var(--secondary);
|
||||
@ -120,7 +154,7 @@ thead {
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
&[id] > a {
|
||||
&[id] > a[href^="#"] {
|
||||
margin: 0 0.5rem;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease;
|
||||
|
Loading…
Reference in New Issue
Block a user