inline scripts

This commit is contained in:
Jacky Zhao 2023-06-03 15:07:19 -04:00
parent 894c7ff4e7
commit 7b5df46f1c
19 changed files with 187 additions and 69 deletions

12
package-lock.json generated
View File

@ -13,6 +13,7 @@
"@napi-rs/simple-git": "^0.1.8", "@napi-rs/simple-git": "^0.1.8",
"chalk": "^4.1.2", "chalk": "^4.1.2",
"cli-spinner": "^0.2.10", "cli-spinner": "^0.2.10",
"env-paths": "^3.0.0",
"esbuild-sass-plugin": "^2.9.0", "esbuild-sass-plugin": "^2.9.0",
"github-slugger": "^2.0.0", "github-slugger": "^2.0.0",
"globby": "^13.1.4", "globby": "^13.1.4",
@ -1346,6 +1347,17 @@
"url": "https://github.com/fb55/entities?sponsor=1" "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": { "node_modules/esbuild": {
"version": "0.17.19", "version": "0.17.19",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.17.19.tgz", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.17.19.tgz",

View File

@ -28,6 +28,7 @@
"@napi-rs/simple-git": "^0.1.8", "@napi-rs/simple-git": "^0.1.8",
"chalk": "^4.1.2", "chalk": "^4.1.2",
"cli-spinner": "^0.2.10", "cli-spinner": "^0.2.10",
"env-paths": "^3.0.0",
"esbuild-sass-plugin": "^2.9.0", "esbuild-sass-plugin": "^2.9.0",
"github-slugger": "^2.0.0", "github-slugger": "^2.0.0",
"globby": "^13.1.4", "globby": "^13.1.4",

View File

@ -1,6 +1,7 @@
#!/usr/bin/env node #!/usr/bin/env node
import { readFileSync } from 'fs' import { promises, readFileSync } from 'fs'
import yargs from 'yargs' import yargs from 'yargs'
import path from 'path'
import { hideBin } from 'yargs/helpers' import { hideBin } from 'yargs/helpers'
import esbuild from 'esbuild' import esbuild from 'esbuild'
import chalk from 'chalk' import chalk from 'chalk'
@ -61,9 +62,34 @@ yargs(hideBin(process.argv))
jsx: "automatic", jsx: "automatic",
jsxImportSource: "preact", jsxImportSource: "preact",
external: ["@napi-rs/simple-git", "shiki"], external: ["@napi-rs/simple-git", "shiki"],
plugins: [sassPlugin({ plugins: [
sassPlugin({
type: 'css-text' 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 => { }).catch(err => {
console.error(`${chalk.red("Couldn't parse Quartz configuration:")} ${fp}`) console.error(`${chalk.red("Couldn't parse Quartz configuration:")} ${fp}`)
console.log(`Reason: ${chalk.grey(err)}`) console.log(`Reason: ${chalk.grey(err)}`)

View File

@ -8,10 +8,9 @@ export interface HeadProps {
externalResources: StaticResources externalResources: StaticResources
} }
export default function({ title, description, slug, externalResources }: HeadProps) { export function Component({ title, description, slug, externalResources }: HeadProps) {
const { css, js } = externalResources const { css, js } = externalResources
const baseDir = resolveToRoot(slug) const baseDir = resolveToRoot(slug)
const stylePath = baseDir + "/index.css"
const iconPath = baseDir + "/static/icon.png" const iconPath = baseDir + "/static/icon.png"
const ogImagePath = baseDir + "/static/og-image.png" const ogImagePath = baseDir + "/static/og-image.png"
return <head> return <head>
@ -28,16 +27,7 @@ export default function({ title, description, slug, externalResources }: HeadPro
<meta name="generator" content="Quartz" /> <meta name="generator" content="Quartz" />
<link rel="preconnect" href="https://fonts.googleapis.com" /> <link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" /> <link rel="preconnect" href="https://fonts.gstatic.com" />
<link rel="stylesheet" type="text/css" href={stylePath} />
{css.map(href => <link key={href} href={href} rel="stylesheet" type="text/css" />)} {css.map(href => <link key={href} href={href} rel="stylesheet" type="text/css" />)}
{js.filter(resource => resource.loadTime === "beforeDOMReady").map(resource => <script key={resource.src} src={resource.src} />)} {js.filter(resource => resource.loadTime === "beforeDOMReady").map(resource => <script key={resource.src} {...resource} />)}
</head> </head>
} }
export function beforeDOMLoaded() {
}
export function onDOMLoaded() {
}

View File

@ -5,10 +5,10 @@ export interface HeaderProps {
slug: string slug: string
} }
export default function({ title, slug }: HeaderProps) { export function Component({ title, slug }: HeaderProps) {
const baseDir = resolveToRoot(slug) const baseDir = resolveToRoot(slug)
return <header> return <header>
<h1><a href={baseDir}>{title}</a></h1> <h1><a href={baseDir}>{title}</a></h1>
</header> </header>
} }

View File

@ -0,0 +1,3 @@
export default "Darkmode"
console.log("HELLOOOO FROM CONSOLE")

View File

@ -0,0 +1,8 @@
import { ComponentType } from "preact"
export type QuartzComponent<Props> = {
Component: ComponentType<Props>
css?: string,
beforeDOMLoaded?: string,
afterDOMLoaded?: string,
}

View File

@ -60,7 +60,7 @@ export function buildQuartz(cfg: QuartzConfig) {
const parsedFiles = await parseMarkdown(processor, argv.directory, filePaths, argv.verbose) const parsedFiles = await parseMarkdown(processor, argv.directory, filePaths, argv.verbose)
const filteredContent = filterContent(cfg.plugins.filters, parsedFiles, argv.verbose) const filteredContent = filterContent(cfg.plugins.filters, parsedFiles, argv.verbose)
await emitContent(argv.directory, output, cfg, filteredContent, 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) { if (argv.serve) {
const server = http.createServer(async (req, res) => { const server = http.createServer(async (req, res) => {

View File

@ -22,11 +22,15 @@ export function slugify(s: string): string {
// resolve /a/b/c to ../../ // resolve /a/b/c to ../../
export function resolveToRoot(slug: string): string { export function resolveToRoot(slug: string): string {
let fp = slug let fp = slug
if (fp.endsWith("/index")) { if (fp.endsWith("index")) {
fp = fp.slice(0, -"/index".length) fp = fp.slice(0, -"index".length)
} }
return fp if (fp === "") {
return "."
}
return "./" + fp
.split('/') .split('/')
.filter(x => x !== '') .filter(x => x !== '')
.map(_ => '..') .map(_ => '..')

View File

@ -4,17 +4,15 @@ import { EmitCallback, QuartzEmitterPlugin } from "../types"
import { ProcessedContent } from "../vfile" import { ProcessedContent } from "../vfile"
import { Fragment, jsx, jsxs } from 'preact/jsx-runtime' import { Fragment, jsx, jsxs } from 'preact/jsx-runtime'
import { render } from "preact-render-to-string" import { render } from "preact-render-to-string"
import { ComponentType } from "preact"
import { HeadProps } from "../../components/Head" import { HeadProps } from "../../components/Head"
import { googleFontHref, templateThemeStyles } from "../../theme"
import { GlobalConfiguration } from "../../cfg" import { GlobalConfiguration } from "../../cfg"
import { HeaderProps } from "../../components/Header" import { HeaderProps } from "../../components/Header"
import { QuartzComponent } from "../../components/types"
import styles from '../../styles/base.scss' import { resolveToRoot } from "../../path"
interface Options { interface Options {
Head: ComponentType<HeadProps> Head: QuartzComponent<HeadProps>
Header: ComponentType<HeaderProps> Header: QuartzComponent<HeaderProps>
} }
export class ContentPage extends QuartzEmitterPlugin { export class ContentPage extends QuartzEmitterPlugin {
@ -26,40 +24,45 @@ export class ContentPage extends QuartzEmitterPlugin {
this.opts = opts this.opts = opts
} }
getQuartzComponents(): QuartzComponent<any>[] {
return [...Object.values(this.opts)]
}
async emit(cfg: GlobalConfiguration, content: ProcessedContent[], resources: StaticResources, emit: EmitCallback): Promise<string[]> { async emit(cfg: GlobalConfiguration, content: ProcessedContent[], resources: StaticResources, emit: EmitCallback): Promise<string[]> {
const fps: string[] = [] const fps: string[] = []
// emit styles const { Head, Header } = this.opts
emit({
slug: "index",
ext: ".css",
content: templateThemeStyles(cfg.theme, styles)
})
fps.push("index.css")
resources.css.push(googleFontHref(cfg.theme))
for (const [tree, file] of content) { for (const [tree, file] of content) {
// @ts-ignore (preact makes it angry) // @ts-ignore (preact makes it angry)
const content = toJsxRuntime(tree, { Fragment, jsx, jsxs, elementAttributeNameCase: 'html' }) 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 title = file.data.frontmatter?.title
const { Head, Header } = this.opts
const doc = <html> const doc = <html>
<Head <Head.Component
title={title ?? "Untitled"} title={title ?? "Untitled"}
description={file.data.description ?? "No description provided"} description={file.data.description ?? "No description provided"}
slug={file.data.slug!} slug={file.data.slug!}
externalResources={resources} /> externalResources={pageResources} />
<body> <body>
<div id="quartz-root" class="page"> <div id="quartz-root" class="page">
<Header title={cfg.siteTitle} slug={file.data.slug!} /> <Header.Component title={cfg.siteTitle} slug={file.data.slug!} />
<article> <article>
{file.data.slug !== "index" && <h1>{title}</h1>} {file.data.slug !== "index" && <h1>{title}</h1>}
{content} {content}
</article> </article>
</div> </div>
</body> </body>
{resources.js.filter(resource => resource.loadTime === "afterDOMReady").map(resource => <script key={resource.src} src={resource.src} />)} {pageResources.js.filter(resource => resource.loadTime === "afterDOMReady").map(resource => <script key={resource.src} {...resource} />)}
</html> </html>
const fp = file.data.slug + ".html" const fp = file.data.slug + ".html"

View File

@ -1,5 +1,69 @@
import { GlobalConfiguration } from '../cfg'
import { QuartzComponent } from '../components/types'
import { StaticResources } from '../resources' 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<QuartzComponent<any>> = 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) { export function getStaticResourcesFromPlugins(plugins: PluginTypes) {
const staticResources: StaticResources = { const staticResources: StaticResources = {
@ -7,8 +71,8 @@ export function getStaticResourcesFromPlugins(plugins: PluginTypes) {
js: [], js: [],
} }
for (const plugin of plugins.transformers) { for (const transformer of plugins.transformers) {
const res = plugin.externalResources const res = transformer.externalResources
if (res?.js) { if (res?.js) {
staticResources.js = staticResources.js.concat(res.js) staticResources.js = staticResources.js.concat(res.js)
} }

View File

@ -12,7 +12,7 @@ export interface Options {
const defaultOptions: Options = { const defaultOptions: Options = {
highlight: true, highlight: true,
wikilinks: true wikilinks: true,
} }
export class ObsidianFlavoredMarkdown extends QuartzTransformerPlugin { export class ObsidianFlavoredMarkdown extends QuartzTransformerPlugin {
@ -39,10 +39,10 @@ export class ObsidianFlavoredMarkdown extends QuartzTransformerPlugin {
return (tree: Root, _file) => { return (tree: Root, _file) => {
findAndReplace(tree, backlinkRegex, (value: string, ...capture: string[]) => { findAndReplace(tree, backlinkRegex, (value: string, ...capture: string[]) => {
if (value.startsWith("!")) { if (value.startsWith("!")) {
// TODO: handle embeds
} else { } else {
const [path, rawHeader, rawAlias] = capture const [path, rawHeader, rawAlias] = capture
const anchor = rawHeader?.slice(1).trim() ?? "" const anchor = rawHeader?.trim() ?? ""
const alias = rawAlias?.slice(1).trim() ?? path const alias = rawAlias?.slice(1).trim() ?? path
const url = slugify(path.trim() + anchor) const url = slugify(path.trim() + anchor)
return { return {

View File

@ -2,6 +2,7 @@ import { PluggableList } from "unified"
import { StaticResources } from "../resources" import { StaticResources } from "../resources"
import { ProcessedContent } from "./vfile" import { ProcessedContent } from "./vfile"
import { GlobalConfiguration } from "../cfg" import { GlobalConfiguration } from "../cfg"
import { QuartzComponent } from "../components/types"
export abstract class QuartzTransformerPlugin { export abstract class QuartzTransformerPlugin {
abstract name: string abstract name: string
@ -25,6 +26,7 @@ export type EmitCallback = (data: EmitOptions) => Promise<string>
export abstract class QuartzEmitterPlugin { export abstract class QuartzEmitterPlugin {
abstract name: string abstract name: string
abstract emit(cfg: GlobalConfiguration, content: ProcessedContent[], resources: StaticResources, emitCallback: EmitCallback): Promise<string[]> abstract emit(cfg: GlobalConfiguration, content: ProcessedContent[], resources: StaticResources, emitCallback: EmitCallback): Promise<string[]>
abstract getQuartzComponents(): QuartzComponent<any>[]
} }
export interface PluginTypes { export interface PluginTypes {

View File

@ -2,7 +2,7 @@ import path from "path"
import fs from "fs" import fs from "fs"
import { QuartzConfig } from "../cfg" import { QuartzConfig } from "../cfg"
import { PerfTimer } from "../perf" import { PerfTimer } from "../perf"
import { getStaticResourcesFromPlugins } from "../plugins" import { emitComponentResources, getStaticResourcesFromPlugins } from "../plugins"
import { EmitCallback } from "../plugins/types" import { EmitCallback } from "../plugins/types"
import { ProcessedContent } from "../plugins/vfile" import { ProcessedContent } from "../plugins/vfile"
import { QUARTZ, slugify } from "../path" 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) { export async function emitContent(contentFolder: string, output: string, cfg: QuartzConfig, content: ProcessedContent[], verbose: boolean) {
const perf = new PerfTimer() const perf = new PerfTimer()
const staticResources = getStaticResourcesFromPlugins(cfg.plugins)
const emit: EmitCallback = async ({ slug, ext, content }) => { const emit: EmitCallback = async ({ slug, ext, content }) => {
const pathToPage = path.join(output, slug + ext) const pathToPage = path.join(output, slug + ext)
const dir = path.dirname(pathToPage) const dir = path.dirname(pathToPage)
@ -21,6 +18,9 @@ export async function emitContent(contentFolder: string, output: string, cfg: Qu
return pathToPage return pathToPage
} }
const staticResources = getStaticResourcesFromPlugins(cfg.plugins)
emitComponentResources(cfg.configuration, staticResources, cfg.plugins, emit)
let emittedFiles = 0 let emittedFiles = 0
for (const emitter of cfg.plugins.emitters) { for (const emitter of cfg.plugins.emitters) {
const emitted = await emitter.emit(cfg.configuration, content, staticResources, emit) 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") const staticPath = path.join(QUARTZ, "static")
await fs.promises.cp(staticPath, path.join(output, "static"), { recursive: true }) 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 // glob all non MD/MDX/HTML files in content folder and copy it over
const assetsPath = path.join("public", "assets") 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()}`)
}
} }

View File

@ -6,11 +6,18 @@ export function filterContent(plugins: QuartzFilterPlugin[], content: ProcessedC
const perf = new PerfTimer() const perf = new PerfTimer()
const initialLength = content.length const initialLength = content.length
for (const plugin of plugins) { for (const plugin of plugins) {
content = content.filter(plugin.shouldPublish) const updatedContent = content.filter(plugin.shouldPublish)
}
if (verbose) { if (verbose) {
console.log(`Filtered out ${initialLength - content.length} files in ${perf.timeSince()}`) const diff = content.filter(x => !updatedContent.includes(x))
for (const file of diff) {
console.log(`[filter:${plugin.name}] ${file[1].data.slug}`)
} }
}
content = updatedContent
}
console.log(`Filtered out ${initialLength - content.length} files in ${perf.timeSince()}`)
return content return content
} }

View File

@ -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 return res
} }

View File

@ -1,6 +1,7 @@
export interface JSResource { export interface JSResource {
src: string src: string
loadTime: 'beforeDOMReady' | 'afterDOMReady' loadTime: 'beforeDOMReady' | 'afterDOMReady'
type?: 'module'
} }
export interface StaticResources { export interface StaticResources {

View File

@ -98,6 +98,8 @@ h1, h2, h3, h4, h5, h6 {
margin: 0 0.5rem; margin: 0 0.5rem;
opacity: 0; opacity: 0;
transition: opacity 0.2s ease; transition: opacity 0.2s ease;
transform: translateY(-0.1rem);
display: inline-block;
font-family: var(--codeFont); font-family: var(--codeFont);
user-select: none; user-select: none;
} }

View File

@ -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` 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) { export function joinStyles(theme: Theme, ...stylesheet: string[]) {
return ` return `:root {
:root {
--light: ${theme.colors.lightMode.light}; --light: ${theme.colors.lightMode.light};
--lightgray: ${theme.colors.lightMode.lightgray}; --lightgray: ${theme.colors.lightMode.lightgray};
--gray: ${theme.colors.lightMode.gray}; --gray: ${theme.colors.lightMode.gray};
@ -54,6 +53,5 @@ export function templateThemeStyles(theme: Theme, stylesheet: string) {
--highlight: ${theme.colors.darkMode.highlight}; --highlight: ${theme.colors.darkMode.highlight};
} }
${stylesheet} ${stylesheet.join("\n\n")}`
`
} }