diff --git a/package-lock.json b/package-lock.json
index 0b292c07..e53a3e58 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -13,6 +13,7 @@
"@napi-rs/simple-git": "^0.1.8",
"chalk": "^4.1.2",
"cli-spinner": "^0.2.10",
+ "env-paths": "^3.0.0",
"esbuild-sass-plugin": "^2.9.0",
"github-slugger": "^2.0.0",
"globby": "^13.1.4",
@@ -1346,6 +1347,17 @@
"url": "https://github.com/fb55/entities?sponsor=1"
}
},
+ "node_modules/env-paths": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-3.0.0.tgz",
+ "integrity": "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==",
+ "engines": {
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/esbuild": {
"version": "0.17.19",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.17.19.tgz",
diff --git a/package.json b/package.json
index 32175204..83ddd542 100644
--- a/package.json
+++ b/package.json
@@ -28,6 +28,7 @@
"@napi-rs/simple-git": "^0.1.8",
"chalk": "^4.1.2",
"cli-spinner": "^0.2.10",
+ "env-paths": "^3.0.0",
"esbuild-sass-plugin": "^2.9.0",
"github-slugger": "^2.0.0",
"globby": "^13.1.4",
diff --git a/quartz/bootstrap.mjs b/quartz/bootstrap.mjs
index 4886e3da..16a0c695 100755
--- a/quartz/bootstrap.mjs
+++ b/quartz/bootstrap.mjs
@@ -1,6 +1,7 @@
#!/usr/bin/env node
-import { readFileSync } from 'fs'
+import { promises, readFileSync } from 'fs'
import yargs from 'yargs'
+import path from 'path'
import { hideBin } from 'yargs/helpers'
import esbuild from 'esbuild'
import chalk from 'chalk'
@@ -61,9 +62,34 @@ yargs(hideBin(process.argv))
jsx: "automatic",
jsxImportSource: "preact",
external: ["@napi-rs/simple-git", "shiki"],
- plugins: [sassPlugin({
- type: 'css-text'
- })]
+ plugins: [
+ sassPlugin({
+ type: 'css-text'
+ }),
+ {
+ name: 'inline-script-loader',
+ setup(build) {
+ build.onLoad({ filter: /\.inline\.(ts|js)$/ }, async (args) => {
+ let text = await promises.readFile(args.path, 'utf8')
+ const transpiled = await esbuild.build({
+ stdin: {
+ contents: text,
+ sourcefile: path.relative(path.resolve('.'), args.path),
+ },
+ write: false,
+ bundle: true,
+ platform: "browser",
+ format: "esm",
+ })
+ const rawMod = transpiled.outputFiles[0].text
+ return {
+ contents: rawMod,
+ loader: 'text',
+ }
+ })
+ }
+ }
+ ]
}).catch(err => {
console.error(`${chalk.red("Couldn't parse Quartz configuration:")} ${fp}`)
console.log(`Reason: ${chalk.grey(err)}`)
diff --git a/quartz/components/Head.tsx b/quartz/components/Head.tsx
index 868294dd..29050a79 100644
--- a/quartz/components/Head.tsx
+++ b/quartz/components/Head.tsx
@@ -8,10 +8,9 @@ export interface HeadProps {
externalResources: StaticResources
}
-export default function({ title, description, slug, externalResources }: HeadProps) {
+export function Component({ title, description, slug, externalResources }: HeadProps) {
const { css, js } = externalResources
const baseDir = resolveToRoot(slug)
- const stylePath = baseDir + "/index.css"
const iconPath = baseDir + "/static/icon.png"
const ogImagePath = baseDir + "/static/og-image.png"
return
@@ -28,16 +27,7 @@ export default function({ title, description, slug, externalResources }: HeadPro
-
{css.map(href => )}
- {js.filter(resource => resource.loadTime === "beforeDOMReady").map(resource => )}
+ {js.filter(resource => resource.loadTime === "beforeDOMReady").map(resource => )}
}
-
-export function beforeDOMLoaded() {
-
-}
-
-export function onDOMLoaded() {
-
-}
diff --git a/quartz/components/Header.tsx b/quartz/components/Header.tsx
index 7da4a8e7..229e210e 100644
--- a/quartz/components/Header.tsx
+++ b/quartz/components/Header.tsx
@@ -5,10 +5,10 @@ export interface HeaderProps {
slug: string
}
-export default function({ title, slug }: HeaderProps) {
+export function Component({ title, slug }: HeaderProps) {
const baseDir = resolveToRoot(slug)
return
-
}
+
diff --git a/quartz/components/scripts/darkmode.inline.ts b/quartz/components/scripts/darkmode.inline.ts
new file mode 100644
index 00000000..c17013aa
--- /dev/null
+++ b/quartz/components/scripts/darkmode.inline.ts
@@ -0,0 +1,3 @@
+export default "Darkmode"
+
+console.log("HELLOOOO FROM CONSOLE")
diff --git a/quartz/components/types.ts b/quartz/components/types.ts
new file mode 100644
index 00000000..e1bdebc3
--- /dev/null
+++ b/quartz/components/types.ts
@@ -0,0 +1,8 @@
+import { ComponentType } from "preact"
+
+export type QuartzComponent = {
+ Component: ComponentType
+ css?: string,
+ beforeDOMLoaded?: string,
+ afterDOMLoaded?: string,
+}
diff --git a/quartz/index.ts b/quartz/index.ts
index a65597a4..c64f0127 100644
--- a/quartz/index.ts
+++ b/quartz/index.ts
@@ -60,7 +60,7 @@ export function buildQuartz(cfg: QuartzConfig) {
const parsedFiles = await parseMarkdown(processor, argv.directory, filePaths, argv.verbose)
const filteredContent = filterContent(cfg.plugins.filters, parsedFiles, argv.verbose)
await emitContent(argv.directory, output, cfg, filteredContent, argv.verbose)
- console.log(chalk.green(`Done in ${perf.timeSince()}`))
+ console.log(chalk.green(`Done processing ${fps.length} files in ${perf.timeSince()}`))
if (argv.serve) {
const server = http.createServer(async (req, res) => {
diff --git a/quartz/path.ts b/quartz/path.ts
index 4f3bfa7f..aa3870b9 100644
--- a/quartz/path.ts
+++ b/quartz/path.ts
@@ -22,11 +22,15 @@ export function slugify(s: string): string {
// resolve /a/b/c to ../../
export function resolveToRoot(slug: string): string {
let fp = slug
- if (fp.endsWith("/index")) {
- fp = fp.slice(0, -"/index".length)
+ if (fp.endsWith("index")) {
+ fp = fp.slice(0, -"index".length)
}
- return fp
+ if (fp === "") {
+ return "."
+ }
+
+ return "./" + fp
.split('/')
.filter(x => x !== '')
.map(_ => '..')
diff --git a/quartz/plugins/emitters/contentPage.tsx b/quartz/plugins/emitters/contentPage.tsx
index fa17a7e5..05c4c4b4 100644
--- a/quartz/plugins/emitters/contentPage.tsx
+++ b/quartz/plugins/emitters/contentPage.tsx
@@ -4,17 +4,15 @@ import { EmitCallback, QuartzEmitterPlugin } from "../types"
import { ProcessedContent } from "../vfile"
import { Fragment, jsx, jsxs } from 'preact/jsx-runtime'
import { render } from "preact-render-to-string"
-import { ComponentType } from "preact"
import { HeadProps } from "../../components/Head"
-import { googleFontHref, templateThemeStyles } from "../../theme"
import { GlobalConfiguration } from "../../cfg"
import { HeaderProps } from "../../components/Header"
-
-import styles from '../../styles/base.scss'
+import { QuartzComponent } from "../../components/types"
+import { resolveToRoot } from "../../path"
interface Options {
- Head: ComponentType
- Header: ComponentType
+ Head: QuartzComponent
+ Header: QuartzComponent
}
export class ContentPage extends QuartzEmitterPlugin {
@@ -26,40 +24,45 @@ export class ContentPage extends QuartzEmitterPlugin {
this.opts = opts
}
+ getQuartzComponents(): QuartzComponent[] {
+ return [...Object.values(this.opts)]
+ }
+
async emit(cfg: GlobalConfiguration, content: ProcessedContent[], resources: StaticResources, emit: EmitCallback): Promise {
const fps: string[] = []
- // emit styles
- emit({
- slug: "index",
- ext: ".css",
- content: templateThemeStyles(cfg.theme, styles)
- })
- fps.push("index.css")
- resources.css.push(googleFontHref(cfg.theme))
-
+ const { Head, Header } = this.opts
for (const [tree, file] of content) {
// @ts-ignore (preact makes it angry)
const content = toJsxRuntime(tree, { Fragment, jsx, jsxs, elementAttributeNameCase: 'html' })
+ const baseDir = resolveToRoot(file.data.slug!)
+ const pageResources: StaticResources = {
+ css: [baseDir + "/index.css", ...resources.css,],
+ js: [
+ { src: baseDir + "/prescript.js", loadTime: "beforeDOMReady", type: 'module' },
+ ...resources.js,
+ { src: baseDir + "/postscript.js", loadTime: "afterDOMReady", type: 'module' }
+ ]
+ }
+
const title = file.data.frontmatter?.title
- const { Head, Header } = this.opts
const doc =
-
+ externalResources={pageResources} />
-
+
{file.data.slug !== "index" && {title}
}
{content}
- {resources.js.filter(resource => resource.loadTime === "afterDOMReady").map(resource => )}
+ {pageResources.js.filter(resource => resource.loadTime === "afterDOMReady").map(resource => )}
const fp = file.data.slug + ".html"
diff --git a/quartz/plugins/index.ts b/quartz/plugins/index.ts
index 4d0b600a..30f77e8e 100644
--- a/quartz/plugins/index.ts
+++ b/quartz/plugins/index.ts
@@ -1,5 +1,69 @@
+import { GlobalConfiguration } from '../cfg'
+import { QuartzComponent } from '../components/types'
import { StaticResources } from '../resources'
-import { PluginTypes } from './types'
+import { googleFontHref, joinStyles } from '../theme'
+import { EmitCallback, PluginTypes } from './types'
+import styles from '../styles/base.scss'
+
+export type ComponentResources = {
+ css: string[],
+ beforeDOMLoaded: string[],
+ afterDOMLoaded: string[]
+}
+
+function joinScripts(scripts: string[]): string {
+ return scripts.join("\n")
+}
+
+export function emitComponentResources(cfg: GlobalConfiguration, resources: StaticResources, plugins: PluginTypes, emit: EmitCallback) {
+ const fps: string[] = []
+ const allComponents: Set> = new Set()
+ for (const emitter of plugins.emitters) {
+ const components = emitter.getQuartzComponents()
+ for (const component of components) {
+ allComponents.add(component)
+ }
+ }
+
+ const componentResources: ComponentResources = {
+ css: [],
+ beforeDOMLoaded: [],
+ afterDOMLoaded: []
+ }
+
+ for (const component of allComponents) {
+ const { css, beforeDOMLoaded, afterDOMLoaded } = component
+ if (css) {
+ componentResources.css.push(css)
+ }
+ if (beforeDOMLoaded) {
+ componentResources.beforeDOMLoaded.push(beforeDOMLoaded)
+ }
+ if (afterDOMLoaded) {
+ componentResources.beforeDOMLoaded.push(afterDOMLoaded)
+ }
+ }
+
+ 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)
+ })
+
+ fps.push("index.css", "prescript.js", "postscript.js")
+ resources.css.push(googleFontHref(cfg.theme))
+ return fps
+}
export function getStaticResourcesFromPlugins(plugins: PluginTypes) {
const staticResources: StaticResources = {
@@ -7,8 +71,8 @@ export function getStaticResourcesFromPlugins(plugins: PluginTypes) {
js: [],
}
- for (const plugin of plugins.transformers) {
- const res = plugin.externalResources
+ for (const transformer of plugins.transformers) {
+ const res = transformer.externalResources
if (res?.js) {
staticResources.js = staticResources.js.concat(res.js)
}
diff --git a/quartz/plugins/transformers/ofm.ts b/quartz/plugins/transformers/ofm.ts
index 7569797b..1dad0b4c 100644
--- a/quartz/plugins/transformers/ofm.ts
+++ b/quartz/plugins/transformers/ofm.ts
@@ -12,7 +12,7 @@ export interface Options {
const defaultOptions: Options = {
highlight: true,
- wikilinks: true
+ wikilinks: true,
}
export class ObsidianFlavoredMarkdown extends QuartzTransformerPlugin {
@@ -39,10 +39,10 @@ export class ObsidianFlavoredMarkdown extends QuartzTransformerPlugin {
return (tree: Root, _file) => {
findAndReplace(tree, backlinkRegex, (value: string, ...capture: string[]) => {
if (value.startsWith("!")) {
-
+ // TODO: handle embeds
} else {
const [path, rawHeader, rawAlias] = capture
- const anchor = rawHeader?.slice(1).trim() ?? ""
+ const anchor = rawHeader?.trim() ?? ""
const alias = rawAlias?.slice(1).trim() ?? path
const url = slugify(path.trim() + anchor)
return {
diff --git a/quartz/plugins/types.ts b/quartz/plugins/types.ts
index 5239d0c4..80d6ceeb 100644
--- a/quartz/plugins/types.ts
+++ b/quartz/plugins/types.ts
@@ -2,6 +2,7 @@ import { PluggableList } from "unified"
import { StaticResources } from "../resources"
import { ProcessedContent } from "./vfile"
import { GlobalConfiguration } from "../cfg"
+import { QuartzComponent } from "../components/types"
export abstract class QuartzTransformerPlugin {
abstract name: string
@@ -25,6 +26,7 @@ export type EmitCallback = (data: EmitOptions) => Promise
export abstract class QuartzEmitterPlugin {
abstract name: string
abstract emit(cfg: GlobalConfiguration, content: ProcessedContent[], resources: StaticResources, emitCallback: EmitCallback): Promise
+ abstract getQuartzComponents(): QuartzComponent[]
}
export interface PluginTypes {
diff --git a/quartz/processors/emit.ts b/quartz/processors/emit.ts
index 5f4b46cc..f9155749 100644
--- a/quartz/processors/emit.ts
+++ b/quartz/processors/emit.ts
@@ -2,7 +2,7 @@ import path from "path"
import fs from "fs"
import { QuartzConfig } from "../cfg"
import { PerfTimer } from "../perf"
-import { getStaticResourcesFromPlugins } from "../plugins"
+import { emitComponentResources, getStaticResourcesFromPlugins } from "../plugins"
import { EmitCallback } from "../plugins/types"
import { ProcessedContent } from "../plugins/vfile"
import { QUARTZ, slugify } from "../path"
@@ -10,9 +10,6 @@ import { globbyStream } from "globby"
export async function emitContent(contentFolder: string, output: string, cfg: QuartzConfig, content: ProcessedContent[], verbose: boolean) {
const perf = new PerfTimer()
-
-
- const staticResources = getStaticResourcesFromPlugins(cfg.plugins)
const emit: EmitCallback = async ({ slug, ext, content }) => {
const pathToPage = path.join(output, slug + ext)
const dir = path.dirname(pathToPage)
@@ -21,6 +18,9 @@ export async function emitContent(contentFolder: string, output: string, cfg: Qu
return pathToPage
}
+ const staticResources = getStaticResourcesFromPlugins(cfg.plugins)
+ emitComponentResources(cfg.configuration, staticResources, cfg.plugins, emit)
+
let emittedFiles = 0
for (const emitter of cfg.plugins.emitters) {
const emitted = await emitter.emit(cfg.configuration, content, staticResources, emit)
@@ -35,6 +35,9 @@ export async function emitContent(contentFolder: string, output: string, cfg: Qu
const staticPath = path.join(QUARTZ, "static")
await fs.promises.cp(staticPath, path.join(output, "static"), { recursive: true })
+ if (verbose) {
+ console.log(`[emit:Static] ${path.join(output, "static", "**")}`)
+ }
// glob all non MD/MDX/HTML files in content folder and copy it over
const assetsPath = path.join("public", "assets")
@@ -54,8 +57,5 @@ export async function emitContent(contentFolder: string, output: string, cfg: Qu
}
}
- if (verbose) {
- console.log(`[emit:Static] ${path.join(output, "static", "**")}`)
- console.log(`Emitted ${emittedFiles} files to \`${output}\` in ${perf.timeSince()}`)
- }
+ console.log(`Emitted ${emittedFiles} files to \`${output}\` in ${perf.timeSince()}`)
}
diff --git a/quartz/processors/filter.ts b/quartz/processors/filter.ts
index 31662bcb..3152a04f 100644
--- a/quartz/processors/filter.ts
+++ b/quartz/processors/filter.ts
@@ -6,11 +6,18 @@ export function filterContent(plugins: QuartzFilterPlugin[], content: ProcessedC
const perf = new PerfTimer()
const initialLength = content.length
for (const plugin of plugins) {
- content = content.filter(plugin.shouldPublish)
+ const updatedContent = content.filter(plugin.shouldPublish)
+
+ if (verbose) {
+ const diff = content.filter(x => !updatedContent.includes(x))
+ for (const file of diff) {
+ console.log(`[filter:${plugin.name}] ${file[1].data.slug}`)
+ }
+ }
+
+ content = updatedContent
}
- if (verbose) {
- console.log(`Filtered out ${initialLength - content.length} files in ${perf.timeSince()}`)
- }
+ console.log(`Filtered out ${initialLength - content.length} files in ${perf.timeSince()}`)
return content
}
diff --git a/quartz/processors/parse.ts b/quartz/processors/parse.ts
index b7ccdd2c..83a05d46 100644
--- a/quartz/processors/parse.ts
+++ b/quartz/processors/parse.ts
@@ -50,9 +50,6 @@ export async function parseMarkdown(processor: QuartzProcessor, baseDir: string,
}
}
- if (verbose) {
- console.log(`Parsed and transformed ${res.length} Markdown files in ${perf.timeSince()}`)
- }
-
+ console.log(`Parsed and transformed ${res.length} Markdown files in ${perf.timeSince()}`)
return res
}
diff --git a/quartz/resources.ts b/quartz/resources.ts
index 9263042a..9de0b194 100644
--- a/quartz/resources.ts
+++ b/quartz/resources.ts
@@ -1,6 +1,7 @@
export interface JSResource {
src: string
loadTime: 'beforeDOMReady' | 'afterDOMReady'
+ type?: 'module'
}
export interface StaticResources {
diff --git a/quartz/styles/base.scss b/quartz/styles/base.scss
index 5f56b0f4..5b286393 100644
--- a/quartz/styles/base.scss
+++ b/quartz/styles/base.scss
@@ -98,6 +98,8 @@ h1, h2, h3, h4, h5, h6 {
margin: 0 0.5rem;
opacity: 0;
transition: opacity 0.2s ease;
+ transform: translateY(-0.1rem);
+ display: inline-block;
font-family: var(--codeFont);
user-select: none;
}
diff --git a/quartz/theme.ts b/quartz/theme.ts
index 7677b253..318f5cc2 100644
--- a/quartz/theme.ts
+++ b/quartz/theme.ts
@@ -26,9 +26,8 @@ export function googleFontHref(theme: Theme) {
return `https://fonts.googleapis.com/css2?family=${code}&family=${header}:wght@400;700&family=${body}:ital,wght@0,400;0,600;1,400;1,600&display=swap`
}
-export function templateThemeStyles(theme: Theme, stylesheet: string) {
- return `
-:root {
+export function joinStyles(theme: Theme, ...stylesheet: string[]) {
+ return `:root {
--light: ${theme.colors.lightMode.light};
--lightgray: ${theme.colors.lightMode.lightgray};
--gray: ${theme.colors.lightMode.gray};
@@ -54,6 +53,5 @@ export function templateThemeStyles(theme: Theme, stylesheet: string) {
--highlight: ${theme.colors.darkMode.highlight};
}
-${stylesheet}
-`
+${stylesheet.join("\n\n")}`
}