From 42d3a7de1711bbd40a2b6857e3bf7ff17685f5d9 Mon Sep 17 00:00:00 2001
From: Jacky Zhao <j.zhao2k19@gmail.com>
Date: Thu, 1 Jun 2023 17:35:31 -0400
Subject: [PATCH] scss support

---
 index.d.ts                              |   4 +
 package-lock.json                       | 213 +++++++++++++++++++++---
 package.json                            |   1 +
 quartz.config.ts                        |  62 ++++---
 quartz/bootstrap.mjs                    |   6 +-
 quartz/cfg.ts                           |  36 +---
 quartz/components/Head.tsx              |   4 +
 quartz/plugins/emitters/contentPage.tsx |  19 ++-
 quartz/plugins/transformers/index.ts    |   2 +
 quartz/plugins/transformers/ofm.ts      |  26 +--
 quartz/plugins/types.ts                 |   3 +-
 quartz/processors/emit.ts               |   2 +-
 quartz/styles/base.scss                 | 207 +++++++++++++++++++++++
 quartz/styles/syntax.scss               |  29 ++++
 quartz/theme.ts                         |  59 +++++++
 15 files changed, 574 insertions(+), 99 deletions(-)
 create mode 100644 index.d.ts
 create mode 100644 quartz/styles/base.scss
 create mode 100644 quartz/styles/syntax.scss
 create mode 100644 quartz/theme.ts

diff --git a/index.d.ts b/index.d.ts
new file mode 100644
index 0000000..a74b5f5
--- /dev/null
+++ b/index.d.ts
@@ -0,0 +1,4 @@
+declare module '*.scss' {
+  const content: string 
+  export = content
+}
diff --git a/package-lock.json b/package-lock.json
index 76d74ce..cf60c68 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",
+        "esbuild-sass-plugin": "^2.9.0",
         "globby": "^13.1.4",
         "gray-matter": "^4.0.3",
         "hast-util-to-jsx-runtime": "^1.2.0",
@@ -62,7 +63,6 @@
       "cpu": [
         "arm"
       ],
-      "dev": true,
       "optional": true,
       "os": [
         "android"
@@ -78,7 +78,6 @@
       "cpu": [
         "arm64"
       ],
-      "dev": true,
       "optional": true,
       "os": [
         "android"
@@ -94,7 +93,6 @@
       "cpu": [
         "x64"
       ],
-      "dev": true,
       "optional": true,
       "os": [
         "android"
@@ -110,7 +108,6 @@
       "cpu": [
         "arm64"
       ],
-      "dev": true,
       "optional": true,
       "os": [
         "darwin"
@@ -126,7 +123,6 @@
       "cpu": [
         "x64"
       ],
-      "dev": true,
       "optional": true,
       "os": [
         "darwin"
@@ -142,7 +138,6 @@
       "cpu": [
         "arm64"
       ],
-      "dev": true,
       "optional": true,
       "os": [
         "freebsd"
@@ -158,7 +153,6 @@
       "cpu": [
         "x64"
       ],
-      "dev": true,
       "optional": true,
       "os": [
         "freebsd"
@@ -174,7 +168,6 @@
       "cpu": [
         "arm"
       ],
-      "dev": true,
       "optional": true,
       "os": [
         "linux"
@@ -190,7 +183,6 @@
       "cpu": [
         "arm64"
       ],
-      "dev": true,
       "optional": true,
       "os": [
         "linux"
@@ -206,7 +198,6 @@
       "cpu": [
         "ia32"
       ],
-      "dev": true,
       "optional": true,
       "os": [
         "linux"
@@ -222,7 +213,6 @@
       "cpu": [
         "loong64"
       ],
-      "dev": true,
       "optional": true,
       "os": [
         "linux"
@@ -238,7 +228,6 @@
       "cpu": [
         "mips64el"
       ],
-      "dev": true,
       "optional": true,
       "os": [
         "linux"
@@ -254,7 +243,6 @@
       "cpu": [
         "ppc64"
       ],
-      "dev": true,
       "optional": true,
       "os": [
         "linux"
@@ -270,7 +258,6 @@
       "cpu": [
         "riscv64"
       ],
-      "dev": true,
       "optional": true,
       "os": [
         "linux"
@@ -286,7 +273,6 @@
       "cpu": [
         "s390x"
       ],
-      "dev": true,
       "optional": true,
       "os": [
         "linux"
@@ -302,7 +288,6 @@
       "cpu": [
         "x64"
       ],
-      "dev": true,
       "optional": true,
       "os": [
         "linux"
@@ -318,7 +303,6 @@
       "cpu": [
         "x64"
       ],
-      "dev": true,
       "optional": true,
       "os": [
         "netbsd"
@@ -334,7 +318,6 @@
       "cpu": [
         "x64"
       ],
-      "dev": true,
       "optional": true,
       "os": [
         "openbsd"
@@ -350,7 +333,6 @@
       "cpu": [
         "x64"
       ],
-      "dev": true,
       "optional": true,
       "os": [
         "sunos"
@@ -366,7 +348,6 @@
       "cpu": [
         "arm64"
       ],
-      "dev": true,
       "optional": true,
       "os": [
         "win32"
@@ -382,7 +363,6 @@
       "cpu": [
         "ia32"
       ],
-      "dev": true,
       "optional": true,
       "os": [
         "win32"
@@ -398,7 +378,6 @@
       "cpu": [
         "x64"
       ],
-      "dev": true,
       "optional": true,
       "os": [
         "win32"
@@ -1024,6 +1003,18 @@
         "url": "https://github.com/chalk/ansi-styles?sponsor=1"
       }
     },
+    "node_modules/anymatch": {
+      "version": "3.1.3",
+      "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
+      "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
+      "dependencies": {
+        "normalize-path": "^3.0.0",
+        "picomatch": "^2.0.4"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
     "node_modules/argparse": {
       "version": "1.0.10",
       "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
@@ -1055,6 +1046,14 @@
       "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
       "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
     },
+    "node_modules/binary-extensions": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
+      "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==",
+      "engines": {
+        "node": ">=8"
+      }
+    },
     "node_modules/brace-expansion": {
       "version": "2.0.1",
       "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
@@ -1120,6 +1119,32 @@
       "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz",
       "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA=="
     },
+    "node_modules/chokidar": {
+      "version": "3.5.3",
+      "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz",
+      "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==",
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://paulmillr.com/funding/"
+        }
+      ],
+      "dependencies": {
+        "anymatch": "~3.1.2",
+        "braces": "~3.0.2",
+        "glob-parent": "~5.1.2",
+        "is-binary-path": "~2.1.0",
+        "is-glob": "~4.0.1",
+        "normalize-path": "~3.0.0",
+        "readdirp": "~3.6.0"
+      },
+      "engines": {
+        "node": ">= 8.10.0"
+      },
+      "optionalDependencies": {
+        "fsevents": "~2.3.2"
+      }
+    },
     "node_modules/cli-spinner": {
       "version": "0.2.10",
       "resolved": "https://registry.npmjs.org/cli-spinner/-/cli-spinner-0.2.10.tgz",
@@ -1315,7 +1340,6 @@
       "version": "0.17.19",
       "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.17.19.tgz",
       "integrity": "sha512-XQ0jAPFkK/u3LcVRcvVHQcTIqD6E2H1fvZMA5dQPSOWb3suUbWbfbRf94pjc0bNzRYLfIrDRQXr7X+LHIm5oHw==",
-      "dev": true,
       "hasInstallScript": true,
       "bin": {
         "esbuild": "bin/esbuild"
@@ -1348,6 +1372,18 @@
         "@esbuild/win32-x64": "0.17.19"
       }
     },
+    "node_modules/esbuild-sass-plugin": {
+      "version": "2.9.0",
+      "resolved": "https://registry.npmjs.org/esbuild-sass-plugin/-/esbuild-sass-plugin-2.9.0.tgz",
+      "integrity": "sha512-D7c8Ub+C4RfaqhOoo9EEWprYmP5ZCmpmcodmYDtpvCfPZLgsSbLaLHPXdul4TUWUMH5eJ6POw+aes/s42ffwrA==",
+      "dependencies": {
+        "resolve": "^1.22.2",
+        "sass": "^1.62.0"
+      },
+      "peerDependencies": {
+        "esbuild": "^0.17.17"
+      }
+    },
     "node_modules/escalade": {
       "version": "3.1.1",
       "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz",
@@ -1496,6 +1532,24 @@
         "node": ">=0.4.x"
       }
     },
+    "node_modules/fsevents": {
+      "version": "2.3.2",
+      "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
+      "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
+      "hasInstallScript": true,
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+      }
+    },
+    "node_modules/function-bind": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
+      "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A=="
+    },
     "node_modules/get-caller-file": {
       "version": "2.0.5",
       "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
@@ -1568,6 +1622,17 @@
         "node": ">=6.0"
       }
     },
+    "node_modules/has": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
+      "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
+      "dependencies": {
+        "function-bind": "^1.1.1"
+      },
+      "engines": {
+        "node": ">= 0.4.0"
+      }
+    },
     "node_modules/has-flag": {
       "version": "4.0.0",
       "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
@@ -1807,6 +1872,11 @@
         "node": ">= 4"
       }
     },
+    "node_modules/immutable": {
+      "version": "4.3.0",
+      "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.0.tgz",
+      "integrity": "sha512-0AOCmOip+xgJwEVTQj1EfiDDOkPmuyllDuTuEX+DDXUgapLAsBIfkg3sxCYyCEA8mQqZrrxPUGjcOQ2JS3WLkg=="
+    },
     "node_modules/inline-style-parser": {
       "version": "0.1.1",
       "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.1.1.tgz",
@@ -1823,6 +1893,17 @@
         "url": "https://github.com/sponsors/sindresorhus"
       }
     },
+    "node_modules/is-binary-path": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
+      "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
+      "dependencies": {
+        "binary-extensions": "^2.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
     "node_modules/is-buffer": {
       "version": "2.0.5",
       "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz",
@@ -1845,6 +1926,17 @@
         "node": ">=4"
       }
     },
+    "node_modules/is-core-module": {
+      "version": "2.12.1",
+      "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.12.1.tgz",
+      "integrity": "sha512-Q4ZuBAe2FUsKtyQJoQHlvP8OvBERxO3jEmy1I7hcRXcJBGGHFh/aJBswbXuS9sgrDH2QUO8ilkwNPHvHMd8clg==",
+      "dependencies": {
+        "has": "^1.0.3"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
     "node_modules/is-extendable": {
       "version": "0.1.1",
       "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz",
@@ -2899,6 +2991,14 @@
         "url": "https://opencollective.com/unified"
       }
     },
+    "node_modules/normalize-path": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
+      "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
     "node_modules/os-tmpdir": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz",
@@ -2945,6 +3045,11 @@
         "node": ">=8"
       }
     },
+    "node_modules/path-parse": {
+      "version": "1.0.7",
+      "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
+      "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="
+    },
     "node_modules/path-scurry": {
       "version": "1.9.2",
       "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.9.2.tgz",
@@ -3058,6 +3163,17 @@
         "node": ">= 0.6"
       }
     },
+    "node_modules/readdirp": {
+      "version": "3.6.0",
+      "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
+      "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
+      "dependencies": {
+        "picomatch": "^2.2.1"
+      },
+      "engines": {
+        "node": ">=8.10.0"
+      }
+    },
     "node_modules/rehype-katex": {
       "version": "6.0.3",
       "resolved": "https://registry.npmjs.org/rehype-katex/-/rehype-katex-6.0.3.tgz",
@@ -3221,6 +3337,22 @@
         "node": ">=0.10.0"
       }
     },
+    "node_modules/resolve": {
+      "version": "1.22.2",
+      "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.2.tgz",
+      "integrity": "sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g==",
+      "dependencies": {
+        "is-core-module": "^2.11.0",
+        "path-parse": "^1.0.7",
+        "supports-preserve-symlinks-flag": "^1.0.0"
+      },
+      "bin": {
+        "resolve": "bin/resolve"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
     "node_modules/retext": {
       "version": "8.1.0",
       "resolved": "https://registry.npmjs.org/retext/-/retext-8.1.0.tgz",
@@ -3352,6 +3484,22 @@
       "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
       "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
     },
+    "node_modules/sass": {
+      "version": "1.62.1",
+      "resolved": "https://registry.npmjs.org/sass/-/sass-1.62.1.tgz",
+      "integrity": "sha512-NHpxIzN29MXvWiuswfc1W3I0N8SXBd8UR26WntmDlRYf0bSADnwnOjsyMZ3lMezSlArD33Vs3YFhp7dWvL770A==",
+      "dependencies": {
+        "chokidar": ">=3.0.0 <4.0.0",
+        "immutable": "^4.0.0",
+        "source-map-js": ">=0.6.2 <2.0.0"
+      },
+      "bin": {
+        "sass": "sass.js"
+      },
+      "engines": {
+        "node": ">=14.0.0"
+      }
+    },
     "node_modules/section-matter": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/section-matter/-/section-matter-1.0.0.tgz",
@@ -3440,6 +3588,14 @@
         "url": "https://github.com/sponsors/sindresorhus"
       }
     },
+    "node_modules/source-map-js": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz",
+      "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
     "node_modules/space-separated-tokens": {
       "version": "2.0.2",
       "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz",
@@ -3531,6 +3687,17 @@
         "node": ">=8"
       }
     },
+    "node_modules/supports-preserve-symlinks-flag": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
+      "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
     "node_modules/tmp": {
       "version": "0.0.33",
       "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz",
diff --git a/package.json b/package.json
index 155f00d..8212ce4 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",
+    "esbuild-sass-plugin": "^2.9.0",
     "globby": "^13.1.4",
     "gray-matter": "^4.0.3",
     "hast-util-to-jsx-runtime": "^1.2.0",
diff --git a/quartz.config.ts b/quartz.config.ts
index b36b0aa..1208e4d 100644
--- a/quartz.config.ts
+++ b/quartz.config.ts
@@ -1,14 +1,41 @@
 import { buildQuartz } from "./quartz"
 import Head from "./quartz/components/Head"
-import { ContentPage, CreatedModifiedDate, Description, FrontMatter, GitHubFlavoredMarkdown, Katex, RemoveDrafts } from "./quartz/plugins"
-import { ResolveLinks } from "./quartz/plugins/transformers/links"
-import { ObsidianFlavoredMarkdown } from "./quartz/plugins/transformers/ofm"
+import { ContentPage, CreatedModifiedDate, Description, FrontMatter, GitHubFlavoredMarkdown, Katex, ObsidianFlavoredMarkdown, RemoveDrafts, ResolveLinks } from "./quartz/plugins"
 
 export default buildQuartz({
   configuration: {
     siteTitle: "🪴 Quartz 4.0",
     enableSPA: true,
     ignorePatterns: ["private", "templates"],
+    theme: {
+      typography: { // loaded from Google Fonts
+        header: "Schibsted Grotesk",
+        body: "Source Sans Pro",
+        code: "IBM Plex Mono",
+      },
+      colors: {
+        lightMode: {
+          light: '#faf8f8',
+          lightgray: '#e8e8e8',
+          gray: '#dadada',
+          darkgray: '#4e4e4e',
+          dark: '#141021',
+          secondary: '#284b63',
+          tertiary: '#84a59d',
+          highlight: 'rgba(143, 159, 169, 0.15)',
+        },
+        darkMode: {
+          light: '#1e1e21',
+          lightgray: '#292629',
+          gray: '#343434',
+          darkgray: '#d4d4d4',
+          dark: '#fbfffe',
+          secondary: '#7b97aa',
+          tertiary: '#84a59d',
+          highlight: 'rgba(143, 159, 169, 0.15)',
+        },
+      }
+    }
   },
   plugins: {
     transformers: [
@@ -31,33 +58,4 @@ export default buildQuartz({
       })
     ]
   },
-  theme: {
-    typography: { // loaded from Google Fonts
-      header: "Schibsted Grotesk",
-      body: "Source Sans Pro",
-      code: "IBM Plex Mono",
-    },
-    colors: {
-      lightMode: {
-        light: '#faf8f8',
-        lightgray: '#e8e8e8',
-        gray: '#dadada',
-        darkgray: '#4e4e4e',
-        dark: '#141021',
-        secondary: '#284b63',
-        tertiary: '#84a59d',
-        highlight: 'rgba(143, 159, 169, 0.15)',
-      },
-      darkMode: {
-        light: '#1e1e21',
-        lightgray: '#292629',
-        gray: '#343434',
-        darkgray: '#d4d4d4',
-        dark: '#fbfffe',
-        secondary: '#7b97aa',
-        tertiary: '#84a59d',
-        highlight: 'rgba(143, 159, 169, 0.15)',
-      },
-    }
-  }
 })
diff --git a/quartz/bootstrap.mjs b/quartz/bootstrap.mjs
index 1fa4538..8b429ca 100755
--- a/quartz/bootstrap.mjs
+++ b/quartz/bootstrap.mjs
@@ -5,6 +5,7 @@ import { hideBin } from 'yargs/helpers'
 import esbuild from 'esbuild'
 import chalk from 'chalk'
 import requireFromString from 'require-from-string'
+import { sassPlugin } from 'esbuild-sass-plugin'
 
 const fp = "./quartz.config.ts"
 const { version } = JSON.parse(readFileSync("./package.json").toString())
@@ -59,7 +60,10 @@ yargs(hideBin(process.argv))
       format: "cjs",
       jsx: "automatic",
       jsxImportSource: "preact",
-      external: ["@napi-rs/simple-git"]
+      external: ["@napi-rs/simple-git"],
+      plugins: [sassPlugin({
+        type: 'css-text'
+      })]
     }).catch(err => {
       console.error(`${chalk.red("Couldn't parse Quartz configuration:")} ${fp}`)
       console.log(`Reason: ${chalk.grey(err)}`)
diff --git a/quartz/cfg.ts b/quartz/cfg.ts
index 2ed5867..1406402 100644
--- a/quartz/cfg.ts
+++ b/quartz/cfg.ts
@@ -1,34 +1,16 @@
 import { PluginTypes } from "./plugins/types"
+import { Theme } from "./theme"
 
-export interface ColorScheme {
-  light: string,
-  lightgray: string,
-  gray: string,
-  darkgray: string,
-  dark: string,
-  secondary: string,
-  tertiary: string,
-  highlight: string
+export interface GlobalConfiguration {
+  siteTitle: string,
+  /** Whether to enable single-page-app style rendering. this prevents flashes of unstyled content and improves smoothness of Quartz */
+  enableSPA: boolean,
+  /** Glob patterns to not search */
+  ignorePatterns: string[],
+  theme: Theme
 }
 
 export interface QuartzConfig {
-  configuration: {
-    siteTitle: string,
-    /** Whether to enable single-page-app style rendering. this prevents flashes of unstyled content and improves smoothness of Quartz */
-    enableSPA: boolean,
-    /** Glob patterns to not search */
-    ignorePatterns: string[],
-  },
+  configuration: GlobalConfiguration,
   plugins: PluginTypes,
-  theme: {
-    typography: {
-      header: string,
-      body: string,
-      code: string
-    },
-    colors: {
-      lightMode: ColorScheme,
-      darkMode: ColorScheme
-    }
-  }
 }
diff --git a/quartz/components/Head.tsx b/quartz/components/Head.tsx
index 82c4347..c4deb8b 100644
--- a/quartz/components/Head.tsx
+++ b/quartz/components/Head.tsx
@@ -11,6 +11,7 @@ export interface HeadProps {
 export default function({ 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>
@@ -26,6 +27,9 @@ export default function({ title, description, slug, externalResources }: HeadPro
     <meta name="description" content={description} />
     <meta name="generator" content="Quartz" />
     <base href={slug} />
+    <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} />)}
   </head>
diff --git a/quartz/plugins/emitters/contentPage.tsx b/quartz/plugins/emitters/contentPage.tsx
index 56a2a15..08d989b 100644
--- a/quartz/plugins/emitters/contentPage.tsx
+++ b/quartz/plugins/emitters/contentPage.tsx
@@ -1,5 +1,4 @@
 import { toJsxRuntime } from "hast-util-to-jsx-runtime"
-import { resolveToRoot } from "../../path"
 import { StaticResources } from "../../resources"
 import { EmitCallback, QuartzEmitterPlugin } from "../types"
 import { ProcessedContent } from "../vfile"
@@ -8,6 +7,10 @@ import { render } from "preact-render-to-string"
 import { ComponentType } from "preact"
 import { HeadProps } from "../../components/Head"
 
+import styles from '../../styles/base.scss'
+import { googleFontHref, templateThemeStyles } from "../../theme"
+import { GlobalConfiguration } from "../../cfg"
+
 interface Options {
   Head: ComponentType<HeadProps>
 }
@@ -21,8 +24,18 @@ export class ContentPage extends QuartzEmitterPlugin {
     this.opts = opts
   }
 
-  async emit(content: ProcessedContent[], resources: StaticResources, emit: EmitCallback): Promise<string[]> {
+  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))
+
     for (const [tree, file] of content) {
 
       // @ts-ignore (preact makes it angry)
@@ -36,7 +49,7 @@ export class ContentPage extends QuartzEmitterPlugin {
           slug={file.data.slug!}
           externalResources={resources} />
         <body>
-          <div id="quartz-root">
+          <div id="quartz-root" class="page">
             <header>
               <h1>{file.data.frontmatter?.title}</h1>
             </header>
diff --git a/quartz/plugins/transformers/index.ts b/quartz/plugins/transformers/index.ts
index 9ef6f7d..fb3fcf4 100644
--- a/quartz/plugins/transformers/index.ts
+++ b/quartz/plugins/transformers/index.ts
@@ -3,3 +3,5 @@ export { GitHubFlavoredMarkdown } from './gfm'
 export { CreatedModifiedDate } from './lastmod'
 export { Katex } from './latex'
 export { Description } from './description'
+export { ResolveLinks } from './links'
+export { ObsidianFlavoredMarkdown } from './ofm'
diff --git a/quartz/plugins/transformers/ofm.ts b/quartz/plugins/transformers/ofm.ts
index c089e02..9215e2c 100644
--- a/quartz/plugins/transformers/ofm.ts
+++ b/quartz/plugins/transformers/ofm.ts
@@ -38,17 +38,21 @@ export class ObsidianFlavoredMarkdown extends QuartzTransformerPlugin {
         const backlinkRegex = new RegExp(/!?\[\[([^\[\]\|\#]+)(#[^\[\]\|\#]+)?(\|[^\[\]\|\#]+)?\]\]/, "g")
         return (tree: Root, _file) => {
           findAndReplace(tree, backlinkRegex, (value: string, ...capture: string[]) => {
-            const [path, rawHeader, rawAlias] = capture
-            const header = rawHeader?.slice(1).trim() ?? ""
-            const alias = rawAlias?.slice(1).trim() ?? value 
-            const url = slugify(path.trim() + header)
-            return {
-              type: 'link',
-              url,
-              children: [{
-                type: 'text',
-                value: alias
-              }]
+            if (value.startsWith("!")) {
+
+            } else {
+              const [path, rawHeader, rawAlias] = capture
+              const header = rawHeader?.slice(1).trim() ?? ""
+              const alias = rawAlias?.slice(1).trim() ?? path
+              const url = slugify(path.trim() + header)
+              return {
+                type: 'link',
+                url,
+                children: [{
+                  type: 'text',
+                  value: alias
+                }]
+              }
             }
           })
         }
diff --git a/quartz/plugins/types.ts b/quartz/plugins/types.ts
index 80c70cb..5239d0c 100644
--- a/quartz/plugins/types.ts
+++ b/quartz/plugins/types.ts
@@ -1,6 +1,7 @@
 import { PluggableList } from "unified"
 import { StaticResources } from "../resources"
 import { ProcessedContent } from "./vfile"
+import { GlobalConfiguration } from "../cfg"
 
 export abstract class QuartzTransformerPlugin {
   abstract name: string
@@ -23,7 +24,7 @@ export interface EmitOptions {
 export type EmitCallback = (data: EmitOptions) => Promise<string>
 export abstract class QuartzEmitterPlugin {
   abstract name: string
-  abstract emit(content: ProcessedContent[], resources: StaticResources, emitCallback: EmitCallback): Promise<string[]>
+  abstract emit(cfg: GlobalConfiguration, content: ProcessedContent[], resources: StaticResources, emitCallback: EmitCallback): Promise<string[]>
 }
 
 export interface PluginTypes {
diff --git a/quartz/processors/emit.ts b/quartz/processors/emit.ts
index ec465ba..5f4b46c 100644
--- a/quartz/processors/emit.ts
+++ b/quartz/processors/emit.ts
@@ -23,7 +23,7 @@ export async function emitContent(contentFolder: string, output: string, cfg: Qu
 
   let emittedFiles = 0
   for (const emitter of cfg.plugins.emitters) {
-    const emitted = await emitter.emit(content, staticResources, emit)
+    const emitted = await emitter.emit(cfg.configuration, content, staticResources, emit)
     emittedFiles += emitted.length
 
     if (verbose) {
diff --git a/quartz/styles/base.scss b/quartz/styles/base.scss
new file mode 100644
index 0000000..bf89e5b
--- /dev/null
+++ b/quartz/styles/base.scss
@@ -0,0 +1,207 @@
+@import "./syntax.scss";
+
+html {
+  scroll-behavior: smooth;
+  & footer > p {
+    text-align: center !important;
+  }
+}
+
+body {
+  margin: 0;
+  height: 100vh;
+  width: 100vw;
+  max-width: 100%;
+  box-sizing: border-box;
+  background-color: var(--light);
+  font-family: var(--bodyFont);
+}
+
+.text-highlight {
+  background-color: #fff236aa;
+  padding: 0 0.1rem;
+  border-radius: 5px;
+}
+
+p, ul, text, a, tr, td, li, ol, ul {
+  color: var(--darkgray);
+  fill: var(--darkgray);
+}
+
+a {
+  font-weight: 600;
+  text-decoration: none;
+  transition: all 0.2s ease;
+  color: var(--secondary);
+
+  &:hover {
+    color: var(--tertiary) !important;
+  }
+
+  &.internal {
+    text-decoration: none;
+    background-color: var(--highlight);
+    padding: 0 0.1rem;
+    border-radius: 5px;
+  }
+}
+
+.page {
+  padding: 4rem 30vw;
+  margin: 0 auto;
+  max-width: 1000px;
+  @media all and (max-width: 1200px) {
+    padding: 25px 5vw;
+  }
+
+  & p {
+    overflow-wrap: anywhere;
+  }
+
+  & article {
+    & > h1 {
+      font-size: 2rem;
+    }
+  }
+}
+
+blockquote {
+  font-style: italic;
+  margin-left: 0;
+  border-left: 3px solid var(--secondary);
+  padding-left: 1rem;
+  transition: border-color 0.2s ease;
+}
+
+h1,
+h2,
+h3,
+h4,
+h5,
+h6,
+thead {
+  font-family: var(--headerFont);
+  color: var(--dark);
+  font-weight: revert;
+  margin: 2rem 0 0;
+
+  &:hover > .hanchor {
+    color: var(--secondary);
+  }
+
+  article > & > a {
+    color: var(--dark)
+  }
+}
+
+div[data-rehype-pretty-code-fragment] {
+  line-height: 1.5rem;
+  position: relative;
+
+  & > div[data-rehype-pretty-code-title] {
+    font-family: var(--codeFont);
+    font-size: 0.9rem;
+    padding: 0.1rem 0.8rem;
+    border: 1px solid var(--lightgray);
+    width: max-content;
+    border-radius: 5px;
+    margin-bottom: -0.8rem;
+    color: var(--darkgray);
+  }
+
+  & pre {
+    font-family: var(--codeFont);
+    padding: 0.5rem 0;
+    border-radius: 5px;
+    overflow-x: scroll;
+    border: 1px solid var(--lightgray);
+
+    & > code {
+      background: none;
+      padding: 0;
+      font-size: 0.9rem;
+      counter-reset: line;
+      counter-increment: line 0;
+      display: grid;
+
+      & .line {
+        padding: 0 0.75rem;
+        box-sizing: border-box;
+        border-left: 3px solid transparent;
+
+        &.highlighted {
+          background-color: var(--highlight);
+          border-left: 3px solid var(--secondary);
+        }
+
+        &::before {
+          content: counter(line);
+          counter-increment: line;
+          width: 1rem;
+          margin-right: 1rem;
+          display: inline-block;
+          text-align: right;
+          color: rgba(115, 138, 148, 0.4);
+        }
+      }
+    }
+  }
+}
+
+code {
+  font-size: 0.9em;
+  font-family: var(--codeFont);
+  border-radius: 5px;
+  padding: 0.1rem 0.2rem;
+  background: var(--lightgray);
+}
+
+tbody, li, p {
+  line-height: 1.5rem;
+}
+
+table {
+  border: 2px solid var(--gray);
+  width: 100%;
+  padding: 1.5rem;
+  border-collapse: collapse;
+}
+
+td, th {
+  padding: 0.2rem 1rem;
+  border: 2px solid var(--gray);
+}
+
+img {
+  max-width: 100%;
+  border-radius: 5px;
+  margin: 1rem 0;
+}
+
+p > img + em {
+  display: block;
+  transform: translateY(-1rem);
+}
+
+hr {
+  width: 100%;
+  margin: 2rem auto;
+  height: 1px;
+  border: none;
+  background-color: var(--lightgray);
+}
+
+section {
+  margin: 2rem auto;
+  border-top: 1px solid var(--lightgray);
+
+  & > #footnote-label {
+    & > a {
+      color: var(--dark);
+    }
+  }
+  
+  & ol, & ul {
+    padding: 0 1em
+  }
+}
diff --git a/quartz/styles/syntax.scss b/quartz/styles/syntax.scss
new file mode 100644
index 0000000..ca5aee5
--- /dev/null
+++ b/quartz/styles/syntax.scss
@@ -0,0 +1,29 @@
+// npx convert-sh-theme https://raw.githubusercontent.com/shikijs/shiki/main/packages/shiki/themes/github-light.json
+body {
+  --shiki-color-text: #24292e;
+  --shiki-color-background: #f8f8f8;
+  --shiki-token-constant: #005cc5;
+  --shiki-token-string: #032f62;
+  --shiki-token-comment: #6a737d;
+  --shiki-token-keyword: #d73a49;
+  --shiki-token-parameter: #24292e;
+  --shiki-token-function: #24292e;
+  --shiki-token-string-expression: #22863a;
+  --shiki-token-punctuation: #24292e;
+  --shiki-token-link: #24292e;
+}
+
+// npx convert-sh-theme https://raw.githubusercontent.com/shikijs/shiki/main/packages/shiki/themes/github-dark.json
+:root[saved-theme="dark"] body {
+  --shiki-color-text: #e1e4e8 !important;
+  --shiki-color-background: #24292e !important;
+  --shiki-token-constant: #79b8ff !important;
+  --shiki-token-string: #9ecbff !important;
+  --shiki-token-comment: #6a737d !important;
+  --shiki-token-keyword: #f97583 !important;
+  --shiki-token-parameter: #e1e4e8 !important;
+  --shiki-token-function: #e1e4e8 !important;
+  --shiki-token-string-expression: #85e89d !important;
+  --shiki-token-punctuation: #e1e4e8 !important;
+  --shiki-token-link: #e1e4e8 !important;
+}
diff --git a/quartz/theme.ts b/quartz/theme.ts
new file mode 100644
index 0000000..7677b25
--- /dev/null
+++ b/quartz/theme.ts
@@ -0,0 +1,59 @@
+export interface ColorScheme {
+  light: string,
+  lightgray: string,
+  gray: string,
+  darkgray: string,
+  dark: string,
+  secondary: string,
+  tertiary: string,
+  highlight: string
+}
+
+export interface Theme {
+  typography: {
+    header: string,
+    body: string,
+    code: string
+  },
+  colors: {
+    lightMode: ColorScheme,
+    darkMode: ColorScheme
+  }
+}
+
+export function googleFontHref(theme: Theme) {
+  const { code, header, body } = theme.typography
+  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 {
+  --light: ${theme.colors.lightMode.light};
+  --lightgray: ${theme.colors.lightMode.lightgray};
+  --gray: ${theme.colors.lightMode.gray};
+  --darkgray: ${theme.colors.lightMode.darkgray};
+  --dark: ${theme.colors.lightMode.dark};
+  --secondary: ${theme.colors.lightMode.secondary};
+  --tertiary: ${theme.colors.lightMode.tertiary};
+  --highlight: ${theme.colors.lightMode.highlight};
+
+  --headerFont: ${theme.typography.header};
+  --bodyFont: ${theme.typography.body};
+  --codeFont: ${theme.typography.code};
+}
+
+:root[saved-theme="dark"] {
+  --light: ${theme.colors.darkMode.light};
+  --lightgray: ${theme.colors.darkMode.lightgray};
+  --gray: ${theme.colors.darkMode.gray};
+  --darkgray: ${theme.colors.darkMode.darkgray};
+  --dark: ${theme.colors.darkMode.dark};
+  --secondary: ${theme.colors.darkMode.secondary};
+  --tertiary: ${theme.colors.darkMode.tertiary};
+  --highlight: ${theme.colors.darkMode.highlight};
+}
+
+${stylesheet}
+`
+}