From 4bdc17d4a11f0ba517c6d9124d296458332c536b Mon Sep 17 00:00:00 2001
From: Jacky Zhao <j.zhao2k19@gmail.com>
Date: Sat, 03 Jun 2023 19:07:19 +0000
Subject: [PATCH] inline scripts

---
 quartz/plugins/types.ts                      |    2 
 quartz/processors/filter.ts                  |   15 ++
 quartz/components/Header.tsx                 |    4 
 quartz/styles/base.scss                      |    2 
 package-lock.json                            |   12 ++
 quartz/components/Head.tsx                   |   14 --
 quartz/path.ts                               |   10 +
 quartz/bootstrap.mjs                         |   34 ++++++
 quartz/processors/parse.ts                   |    5 
 quartz/index.ts                              |    2 
 quartz/plugins/index.ts                      |   70 +++++++++++++
 quartz/components/scripts/darkmode.inline.ts |    3 
 quartz/components/types.ts                   |    8 +
 quartz/plugins/transformers/ofm.ts           |    6 
 package.json                                 |    1 
 quartz/processors/emit.ts                    |   16 +-
 quartz/theme.ts                              |    8 -
 quartz/plugins/emitters/contentPage.tsx      |   43 ++++----
 quartz/resources.ts                          |    1 
 19 files changed, 187 insertions(+), 69 deletions(-)

diff --git a/package-lock.json b/package-lock.json
index 0b292c0..e53a3e5 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 3217520..83ddd54 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 4886e3d..16a0c69 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 @@
       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 868294d..29050a7 100644
--- a/quartz/components/Head.tsx
+++ b/quartz/components/Head.tsx
@@ -8,10 +8,9 @@
   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 <head>
@@ -28,16 +27,7 @@
     <meta name="generator" content="Quartz" />
     <link rel="preconnect" href="https://fonts.googleapis.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" />)}
-    {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>
 }
-
-export function beforeDOMLoaded() {
-
-}
-
-export function onDOMLoaded() {
-
-}
diff --git a/quartz/components/Header.tsx b/quartz/components/Header.tsx
index 7da4a8e..229e210 100644
--- a/quartz/components/Header.tsx
+++ b/quartz/components/Header.tsx
@@ -5,10 +5,10 @@
   slug: string
 }
 
-export default function({ title, slug }: HeaderProps) {
+export function Component({ title, slug }: HeaderProps) {
   const baseDir = resolveToRoot(slug)
   return <header>
     <h1><a href={baseDir}>{title}</a></h1>
   </header>
-
 }
+
diff --git a/quartz/components/scripts/darkmode.inline.ts b/quartz/components/scripts/darkmode.inline.ts
new file mode 100644
index 0000000..c17013a
--- /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 0000000..e1bdebc
--- /dev/null
+++ b/quartz/components/types.ts
@@ -0,0 +1,8 @@
+import { ComponentType } from "preact"
+
+export type QuartzComponent<Props> = {
+  Component: ComponentType<Props>
+  css?: string,
+  beforeDOMLoaded?: string,
+  afterDOMLoaded?: string,
+}
diff --git a/quartz/index.ts b/quartz/index.ts
index a65597a..c64f012 100644
--- a/quartz/index.ts
+++ b/quartz/index.ts
@@ -60,7 +60,7 @@
     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 4f3bfa7..aa3870b 100644
--- a/quartz/path.ts
+++ b/quartz/path.ts
@@ -22,11 +22,15 @@
 // 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 fa17a7e..05c4c4b 100644
--- a/quartz/plugins/emitters/contentPage.tsx
+++ b/quartz/plugins/emitters/contentPage.tsx
@@ -4,17 +4,15 @@
 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<HeadProps>
-  Header: ComponentType<HeaderProps>
+  Head: QuartzComponent<HeadProps>
+  Header: QuartzComponent<HeaderProps>
 }
 
 export class ContentPage extends QuartzEmitterPlugin {
@@ -26,40 +24,45 @@
     this.opts = opts
   }
 
+  getQuartzComponents(): QuartzComponent<any>[] {
+    return [...Object.values(this.opts)]
+  }
+
   async emit(cfg: GlobalConfiguration, content: ProcessedContent[], resources: StaticResources, emit: EmitCallback): Promise<string[]> {
     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 = <html>
-        <Head
+        <Head.Component
           title={title ?? "Untitled"}
           description={file.data.description ?? "No description provided"}
           slug={file.data.slug!}
-          externalResources={resources} />
+          externalResources={pageResources} />
         <body>
           <div id="quartz-root" class="page">
-            <Header title={cfg.siteTitle} slug={file.data.slug!} />
+            <Header.Component title={cfg.siteTitle} slug={file.data.slug!} />
             <article>
               {file.data.slug !== "index" && <h1>{title}</h1>}
               {content}
             </article>
           </div>
         </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>
 
       const fp = file.data.slug + ".html"
diff --git a/quartz/plugins/index.ts b/quartz/plugins/index.ts
index 4d0b600..30f77e8 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<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) {
   const staticResources: StaticResources = {
@@ -7,8 +71,8 @@
     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 7569797..1dad0b4 100644
--- a/quartz/plugins/transformers/ofm.ts
+++ b/quartz/plugins/transformers/ofm.ts
@@ -12,7 +12,7 @@
 
 const defaultOptions: Options = {
   highlight: true,
-  wikilinks: true
+  wikilinks: true,
 }
 
 export class ObsidianFlavoredMarkdown extends QuartzTransformerPlugin {
@@ -39,10 +39,10 @@
         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 5239d0c..80d6cee 100644
--- a/quartz/plugins/types.ts
+++ b/quartz/plugins/types.ts
@@ -2,6 +2,7 @@
 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 abstract class QuartzEmitterPlugin {
   abstract name: string
   abstract emit(cfg: GlobalConfiguration, content: ProcessedContent[], resources: StaticResources, emitCallback: EmitCallback): Promise<string[]>
+  abstract getQuartzComponents(): QuartzComponent<any>[]
 }
 
 export interface PluginTypes {
diff --git a/quartz/processors/emit.ts b/quartz/processors/emit.ts
index 5f4b46c..f915574 100644
--- a/quartz/processors/emit.ts
+++ b/quartz/processors/emit.ts
@@ -2,7 +2,7 @@
 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 @@
 
 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 @@
     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 @@
 
   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 @@
     }
   }
 
-  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 31662bc..3152a04 100644
--- a/quartz/processors/filter.ts
+++ b/quartz/processors/filter.ts
@@ -6,11 +6,18 @@
   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 b7ccdd2..83a05d4 100644
--- a/quartz/processors/parse.ts
+++ b/quartz/processors/parse.ts
@@ -50,9 +50,6 @@
     }
   }
 
-  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 9263042..9de0b19 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 5f56b0f..5b28639 100644
--- a/quartz/styles/base.scss
+++ b/quartz/styles/base.scss
@@ -98,6 +98,8 @@
     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 7677b25..318f5cc 100644
--- a/quartz/theme.ts
+++ b/quartz/theme.ts
@@ -26,9 +26,8 @@
   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 @@
   --highlight: ${theme.colors.darkMode.highlight};
 }
 
-${stylesheet}
-`
+${stylesheet.join("\n\n")}`
 }

--
Gitblit v1.10.0