Initial import
This commit is contained in:
@@ -0,0 +1,120 @@
|
||||
export const CommonArgv = {
|
||||
directory: {
|
||||
string: true,
|
||||
alias: ["d"],
|
||||
default: "content",
|
||||
describe: "directory to look for content files",
|
||||
},
|
||||
verbose: {
|
||||
boolean: true,
|
||||
alias: ["v"],
|
||||
default: false,
|
||||
describe: "print out extra logging information",
|
||||
},
|
||||
concurrency: {
|
||||
number: true,
|
||||
alias: ["c"],
|
||||
describe: "max parallel operations (default: number of CPU cores)",
|
||||
},
|
||||
}
|
||||
|
||||
export const CreateArgv = {
|
||||
...CommonArgv,
|
||||
template: {
|
||||
string: true,
|
||||
alias: ["t"],
|
||||
choices: ["default", "obsidian", "ttrpg", "blog"],
|
||||
describe: "template to use for initial configuration",
|
||||
},
|
||||
source: {
|
||||
string: true,
|
||||
alias: ["s"],
|
||||
describe: "source directory to copy/create symlink from",
|
||||
},
|
||||
strategy: {
|
||||
string: true,
|
||||
alias: ["X"],
|
||||
choices: ["new", "copy", "symlink"],
|
||||
describe: "strategy for content folder setup",
|
||||
},
|
||||
baseUrl: {
|
||||
string: true,
|
||||
alias: ["b"],
|
||||
describe: "base URL for your Quartz site (e.g. mysite.github.io/quartz)",
|
||||
},
|
||||
links: {
|
||||
string: true,
|
||||
alias: ["l"],
|
||||
choices: ["absolute", "shortest", "relative"],
|
||||
describe: "strategy to resolve links",
|
||||
},
|
||||
}
|
||||
|
||||
export const SyncArgv = {
|
||||
...CommonArgv,
|
||||
commit: {
|
||||
boolean: true,
|
||||
default: true,
|
||||
describe: "create a git commit for your unsaved changes",
|
||||
},
|
||||
message: {
|
||||
string: true,
|
||||
alias: ["m"],
|
||||
describe: "option to override the default Quartz commit message",
|
||||
},
|
||||
push: {
|
||||
boolean: true,
|
||||
default: true,
|
||||
describe: "push updates to your Quartz fork",
|
||||
},
|
||||
pull: {
|
||||
boolean: true,
|
||||
default: true,
|
||||
describe: "pull updates from your Quartz fork",
|
||||
},
|
||||
}
|
||||
|
||||
export const BuildArgv = {
|
||||
...CommonArgv,
|
||||
output: {
|
||||
string: true,
|
||||
alias: ["o"],
|
||||
default: "public",
|
||||
describe: "output folder for files",
|
||||
},
|
||||
serve: {
|
||||
boolean: true,
|
||||
default: false,
|
||||
describe: "run a local server to live-preview your Quartz",
|
||||
},
|
||||
watch: {
|
||||
boolean: true,
|
||||
default: false,
|
||||
describe: "watch for changes and rebuild automatically",
|
||||
},
|
||||
baseDir: {
|
||||
string: true,
|
||||
default: "",
|
||||
describe: "base path to serve your local server on",
|
||||
},
|
||||
port: {
|
||||
number: true,
|
||||
default: 8080,
|
||||
describe: "port to serve Quartz on",
|
||||
},
|
||||
wsPort: {
|
||||
number: true,
|
||||
default: 3001,
|
||||
describe: "port to use for WebSocket-based hot-reload notifications",
|
||||
},
|
||||
remoteDevHost: {
|
||||
string: true,
|
||||
default: "",
|
||||
describe: "A URL override for the websocket connection if you are not developing on localhost",
|
||||
},
|
||||
bundleInfo: {
|
||||
boolean: true,
|
||||
default: false,
|
||||
describe: "show detailed bundle information",
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import path from "path"
|
||||
import { readFileSync } from "fs"
|
||||
|
||||
/**
|
||||
* All constants relating to helpers or handlers
|
||||
*/
|
||||
export const ORIGIN_NAME = "origin"
|
||||
export const UPSTREAM_NAME = "upstream"
|
||||
export const QUARTZ_SOURCE_BRANCH = "v5"
|
||||
export const QUARTZ_SOURCE_REPO = "https://github.com/jackyzha0/quartz.git"
|
||||
export const cwd = process.cwd()
|
||||
export const cacheDir = path.join(cwd, ".quartz-cache")
|
||||
export const cacheFile = "./quartz/.quartz-cache/transpiled-build.mjs"
|
||||
export const fp = "./quartz/build.ts"
|
||||
export const { version } = JSON.parse(readFileSync("./package.json").toString())
|
||||
export const contentCacheFolder = path.join(cacheDir, "content-cache")
|
||||
@@ -0,0 +1,801 @@
|
||||
import { promises } from "fs"
|
||||
import path from "path"
|
||||
import esbuild from "esbuild"
|
||||
import { styleText } from "util"
|
||||
import { sassPlugin } from "esbuild-sass-plugin"
|
||||
import fs from "fs"
|
||||
import { intro, outro, select, text } from "@clack/prompts"
|
||||
import { rm } from "fs/promises"
|
||||
import chokidar from "chokidar"
|
||||
import prettyBytes from "pretty-bytes"
|
||||
import { execSync, spawnSync } from "child_process"
|
||||
import http from "http"
|
||||
import serveHandler from "serve-handler"
|
||||
import { WebSocketServer } from "ws"
|
||||
import { randomUUID } from "crypto"
|
||||
import { Mutex } from "async-mutex"
|
||||
import { CreateArgv } from "./args.js"
|
||||
import { globby } from "globby"
|
||||
import {
|
||||
exitIfCancel,
|
||||
escapePath,
|
||||
gitPull,
|
||||
popContentFolder,
|
||||
stashContentFolder,
|
||||
symlinkOrCopy,
|
||||
} from "./helpers.js"
|
||||
import {
|
||||
handlePluginRestore,
|
||||
handlePluginCheck,
|
||||
handlePluginResolve,
|
||||
} from "./plugin-git-handlers.js"
|
||||
import {
|
||||
configExists,
|
||||
createConfigFromDefault,
|
||||
createConfigFromTemplate,
|
||||
readPluginsJson,
|
||||
writePluginsJson,
|
||||
extractPluginName,
|
||||
updateGlobalConfig,
|
||||
LOCKFILE_PATH,
|
||||
} from "./plugin-data.js"
|
||||
import {
|
||||
UPSTREAM_NAME,
|
||||
QUARTZ_SOURCE_BRANCH,
|
||||
QUARTZ_SOURCE_REPO,
|
||||
ORIGIN_NAME,
|
||||
version,
|
||||
fp,
|
||||
cacheFile,
|
||||
cwd,
|
||||
} from "./constants.js"
|
||||
|
||||
/**
|
||||
* Resolve content directory path
|
||||
* @param contentPath path to resolve
|
||||
*/
|
||||
function resolveContentPath(contentPath) {
|
||||
if (path.isAbsolute(contentPath)) return path.relative(cwd, contentPath)
|
||||
return path.join(cwd, contentPath)
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles `npx quartz create`
|
||||
* @param {*} argv arguments for `create`
|
||||
*/
|
||||
export async function handleCreate(argv) {
|
||||
console.log()
|
||||
intro(styleText(["bgGreen", "black"], ` Quartz v${version} `))
|
||||
const contentFolder = resolveContentPath(argv.directory)
|
||||
let setupStrategy = argv.strategy?.toLowerCase()
|
||||
let linkResolutionStrategy = argv.links?.toLowerCase()
|
||||
const sourceDirectory = argv.source
|
||||
let template = argv.template?.toLowerCase()
|
||||
let baseUrl = argv.baseUrl
|
||||
|
||||
// If all cmd arguments were provided, check if they're valid
|
||||
if (setupStrategy && linkResolutionStrategy) {
|
||||
// If setup isn't, "new", source argument is required
|
||||
if (setupStrategy !== "new") {
|
||||
// Error handling
|
||||
if (!sourceDirectory) {
|
||||
outro(
|
||||
styleText(
|
||||
"red",
|
||||
`Setup strategies (arg '${styleText(
|
||||
"yellow",
|
||||
`-${CreateArgv.strategy.alias[0]}`,
|
||||
)}') other than '${styleText(
|
||||
"yellow",
|
||||
"new",
|
||||
)}' require content folder argument ('${styleText(
|
||||
"yellow",
|
||||
`-${CreateArgv.source.alias[0]}`,
|
||||
)}') to be set`,
|
||||
),
|
||||
)
|
||||
process.exit(1)
|
||||
} else {
|
||||
if (!fs.existsSync(sourceDirectory)) {
|
||||
outro(
|
||||
styleText(
|
||||
"red",
|
||||
`Input directory to copy/symlink 'content' from not found ('${styleText(
|
||||
"yellow",
|
||||
sourceDirectory,
|
||||
)}', invalid argument "${styleText("yellow", `-${CreateArgv.source.alias[0]}`)})`,
|
||||
),
|
||||
)
|
||||
process.exit(1)
|
||||
} else if (!fs.lstatSync(sourceDirectory).isDirectory()) {
|
||||
outro(
|
||||
styleText(
|
||||
"red",
|
||||
`Source directory to copy/symlink 'content' from is not a directory (found file at '${styleText(
|
||||
"yellow",
|
||||
sourceDirectory,
|
||||
)}', invalid argument ${styleText("yellow", `-${CreateArgv.source.alias[0]}`)}")`,
|
||||
),
|
||||
)
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Template selection
|
||||
if (!template) {
|
||||
template = exitIfCancel(
|
||||
await select({
|
||||
message: "Choose a template for your Quartz configuration",
|
||||
options: [
|
||||
{ value: "default", label: "Default", hint: "clean Quartz setup with sensible defaults" },
|
||||
{
|
||||
value: "obsidian",
|
||||
label: "Obsidian",
|
||||
hint: "optimized for Obsidian vaults with full OFM support",
|
||||
},
|
||||
{
|
||||
value: "ttrpg",
|
||||
label: "TTRPG",
|
||||
hint: "Obsidian + map plugin + ITS Theme for D&D/TTRPG wikis",
|
||||
},
|
||||
{
|
||||
value: "blog",
|
||||
label: "Blog",
|
||||
hint: "recent notes and comments enabled for blogging",
|
||||
},
|
||||
],
|
||||
}),
|
||||
)
|
||||
}
|
||||
// Use cli process if cmd args werent provided
|
||||
if (!setupStrategy) {
|
||||
setupStrategy = exitIfCancel(
|
||||
await select({
|
||||
message: `Choose how to initialize the content in \`${contentFolder}\``,
|
||||
options: [
|
||||
{ value: "new", label: "Empty Quartz" },
|
||||
{ value: "copy", label: "Copy an existing folder", hint: "overwrites `content`" },
|
||||
{
|
||||
value: "symlink",
|
||||
label: "Symlink an existing folder",
|
||||
hint: "don't select this unless you know what you are doing!",
|
||||
},
|
||||
],
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
async function rmContentFolder() {
|
||||
const contentStat = await fs.promises.lstat(contentFolder)
|
||||
if (contentStat.isSymbolicLink()) {
|
||||
await fs.promises.unlink(contentFolder)
|
||||
} else {
|
||||
await rm(contentFolder, { recursive: true, force: true })
|
||||
}
|
||||
}
|
||||
|
||||
const gitkeepPath = path.join(contentFolder, ".gitkeep")
|
||||
if (fs.existsSync(gitkeepPath)) {
|
||||
await fs.promises.unlink(gitkeepPath)
|
||||
}
|
||||
if (setupStrategy === "copy" || setupStrategy === "symlink") {
|
||||
let originalFolder = sourceDirectory
|
||||
|
||||
// If input directory was not passed, use cli
|
||||
if (!sourceDirectory) {
|
||||
originalFolder = escapePath(
|
||||
exitIfCancel(
|
||||
await text({
|
||||
message: "Enter the full path to existing content folder",
|
||||
placeholder:
|
||||
"On most terminal emulators, you can drag and drop a folder into the window and it will paste the full path",
|
||||
validate(fp) {
|
||||
const fullPath = escapePath(fp)
|
||||
if (!fs.existsSync(fullPath)) {
|
||||
return "The given path doesn't exist"
|
||||
} else if (!fs.lstatSync(fullPath).isDirectory()) {
|
||||
return "The given path is not a folder"
|
||||
}
|
||||
},
|
||||
}),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
await rmContentFolder()
|
||||
if (setupStrategy === "copy") {
|
||||
await fs.promises.cp(originalFolder, contentFolder, {
|
||||
recursive: true,
|
||||
preserveTimestamps: true,
|
||||
})
|
||||
} else if (setupStrategy === "symlink") {
|
||||
await symlinkOrCopy(originalFolder, contentFolder)
|
||||
}
|
||||
} else if (setupStrategy === "new") {
|
||||
await fs.promises.writeFile(
|
||||
path.join(contentFolder, "index.md"),
|
||||
`---
|
||||
title: Welcome to Quartz
|
||||
---
|
||||
|
||||
This is a blank Quartz installation.
|
||||
See the [documentation](https://quartz.jzhao.xyz) for how to get started.
|
||||
`,
|
||||
)
|
||||
}
|
||||
|
||||
// Obsidian and TTRPG templates auto-set link resolution to "shortest"
|
||||
const skipLinkPrompt = template === "obsidian" || template === "ttrpg"
|
||||
if (skipLinkPrompt) {
|
||||
linkResolutionStrategy = "shortest"
|
||||
}
|
||||
|
||||
// Use cli process if cmd args werent provided
|
||||
if (!linkResolutionStrategy) {
|
||||
// get a preferred link resolution strategy
|
||||
linkResolutionStrategy = exitIfCancel(
|
||||
await select({
|
||||
message: `Choose how Quartz should resolve links in your content. This should match Obsidian's link format. You can change this later in \`quartz.config.yaml\`.`,
|
||||
options: [
|
||||
{
|
||||
value: "shortest",
|
||||
label: "Treat links as shortest path",
|
||||
hint: "(default)",
|
||||
},
|
||||
{
|
||||
value: "absolute",
|
||||
label: "Treat links as absolute path",
|
||||
},
|
||||
{
|
||||
value: "relative",
|
||||
label: "Treat links as relative paths",
|
||||
},
|
||||
],
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
// Base URL prompt
|
||||
if (!baseUrl) {
|
||||
baseUrl = exitIfCancel(
|
||||
await text({
|
||||
message: "Enter the base URL for your Quartz site (e.g. mysite.github.io/quartz)",
|
||||
placeholder: "mysite.github.io",
|
||||
validate(value) {
|
||||
if (!value || value.trim().length === 0) {
|
||||
return "Base URL cannot be empty"
|
||||
}
|
||||
},
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
// Strip protocol prefix if user included it
|
||||
baseUrl = baseUrl.replace(/^https?:\/\//, "").replace(/\/+$/, "")
|
||||
|
||||
if (template && template !== "default") {
|
||||
createConfigFromTemplate(template)
|
||||
console.log(styleText("green", `Created quartz.config.yaml from '${template}' template`))
|
||||
} else {
|
||||
createConfigFromTemplate("default")
|
||||
console.log(styleText("green", "Created quartz.config.yaml from defaults"))
|
||||
}
|
||||
|
||||
// Update markdownLinkResolution in the crawl-links plugin options via YAML config
|
||||
const json = readPluginsJson()
|
||||
if (json?.plugins) {
|
||||
const crawlLinksIndex = json.plugins.findIndex(
|
||||
(p) => extractPluginName(p.source) === "crawl-links",
|
||||
)
|
||||
if (crawlLinksIndex !== -1) {
|
||||
json.plugins[crawlLinksIndex].options = {
|
||||
...json.plugins[crawlLinksIndex].options,
|
||||
markdownLinkResolution: linkResolutionStrategy,
|
||||
}
|
||||
writePluginsJson(json)
|
||||
}
|
||||
}
|
||||
|
||||
// Update baseUrl in configuration
|
||||
updateGlobalConfig({ baseUrl })
|
||||
|
||||
// install plugins referenced in the template config
|
||||
await handlePluginResolve()
|
||||
|
||||
// setup remote
|
||||
execSync(`git remote show upstream || git remote add upstream ${QUARTZ_SOURCE_REPO}`, {
|
||||
stdio: "ignore",
|
||||
})
|
||||
|
||||
outro(`You're all set! Not sure what to do next? Try:
|
||||
• Customizing Quartz a bit more by editing \`quartz.config.yaml\`
|
||||
• Running \`npx quartz build --serve\` to preview your Quartz locally
|
||||
• Hosting your Quartz online (see: https://quartz.jzhao.xyz/hosting)
|
||||
`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles `npx quartz build`
|
||||
* @param {*} argv arguments for `build`
|
||||
*/
|
||||
export async function handleBuild(argv) {
|
||||
if (argv.concurrency !== undefined && argv.concurrency < 1) {
|
||||
console.error("Concurrency must be at least 1")
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
if (argv.serve) {
|
||||
argv.watch = true
|
||||
}
|
||||
|
||||
console.log(`\n${styleText(["bgGreen", "black"], ` Quartz v${version} `)} \n`)
|
||||
const ctx = await esbuild.context({
|
||||
entryPoints: [fp],
|
||||
outfile: cacheFile,
|
||||
bundle: true,
|
||||
keepNames: true,
|
||||
minifyWhitespace: true,
|
||||
minifySyntax: true,
|
||||
platform: "node",
|
||||
format: "esm",
|
||||
jsx: "automatic",
|
||||
jsxImportSource: "preact",
|
||||
packages: "external",
|
||||
metafile: true,
|
||||
sourcemap: true,
|
||||
sourcesContent: false,
|
||||
logOverride: {
|
||||
"direct-eval": "silent",
|
||||
"equals-negative-zero": "silent",
|
||||
"duplicate-object-key": "silent",
|
||||
},
|
||||
plugins: [
|
||||
sassPlugin({
|
||||
type: "css-text",
|
||||
cssImports: true,
|
||||
}),
|
||||
sassPlugin({
|
||||
filter: /\.inline\.scss$/,
|
||||
type: "css",
|
||||
cssImports: true,
|
||||
}),
|
||||
{
|
||||
name: "inline-script-loader",
|
||||
setup(build) {
|
||||
build.onLoad({ filter: /\.inline\.(ts|js)$/ }, async (args) => {
|
||||
let text = await promises.readFile(args.path, "utf8")
|
||||
|
||||
// remove default exports that we manually inserted
|
||||
text = text.replace("export default", "")
|
||||
text = text.replace("export", "")
|
||||
|
||||
const sourcefile = path.relative(path.resolve("."), args.path)
|
||||
const resolveDir = path.dirname(sourcefile)
|
||||
const transpiled = await esbuild.build({
|
||||
stdin: {
|
||||
contents: text,
|
||||
loader: "ts",
|
||||
resolveDir,
|
||||
sourcefile,
|
||||
},
|
||||
write: false,
|
||||
bundle: true,
|
||||
minify: true,
|
||||
platform: "browser",
|
||||
format: "esm",
|
||||
})
|
||||
const rawMod = transpiled.outputFiles[0].text
|
||||
return {
|
||||
contents: rawMod,
|
||||
loader: "text",
|
||||
}
|
||||
})
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
const buildMutex = new Mutex()
|
||||
let lastBuildMs = 0
|
||||
let cleanupBuild = null
|
||||
const build = async (clientRefresh) => {
|
||||
const buildStart = new Date().getTime()
|
||||
lastBuildMs = buildStart
|
||||
const release = await buildMutex.acquire()
|
||||
if (lastBuildMs > buildStart) {
|
||||
release()
|
||||
return
|
||||
}
|
||||
|
||||
if (cleanupBuild) {
|
||||
console.log(styleText("yellow", "Detected a source code change, doing a hard rebuild..."))
|
||||
await cleanupBuild()
|
||||
}
|
||||
|
||||
const result = await ctx.rebuild().catch((err) => {
|
||||
console.error(
|
||||
`${styleText("red", "Failed to build Quartz.")} Check for syntax errors in your configuration or plugins.`,
|
||||
)
|
||||
console.log(`Reason: ${styleText("gray", err.message ?? String(err))}`)
|
||||
process.exit(1)
|
||||
})
|
||||
release()
|
||||
|
||||
if (argv.bundleInfo) {
|
||||
const outputFileName = "quartz/.quartz-cache/transpiled-build.mjs"
|
||||
const meta = result.metafile.outputs[outputFileName]
|
||||
console.log(
|
||||
`Successfully transpiled ${Object.keys(meta.inputs).length} files (${prettyBytes(
|
||||
meta.bytes,
|
||||
)})`,
|
||||
)
|
||||
console.log(await esbuild.analyzeMetafile(result.metafile, { color: true }))
|
||||
}
|
||||
|
||||
// bypass module cache
|
||||
// https://github.com/nodejs/modules/issues/307
|
||||
const { default: buildQuartz } = await import(`../../${cacheFile}?update=${randomUUID()}`)
|
||||
// ^ this import is relative, so base "cacheFile" path can't be used
|
||||
|
||||
cleanupBuild = await buildQuartz(argv, buildMutex, clientRefresh)
|
||||
clientRefresh()
|
||||
}
|
||||
|
||||
let clientRefresh = () => {}
|
||||
if (argv.serve) {
|
||||
const connections = []
|
||||
clientRefresh = () => connections.forEach((conn) => conn.send("rebuild"))
|
||||
|
||||
if (argv.baseDir !== "" && !argv.baseDir.startsWith("/")) {
|
||||
argv.baseDir = "/" + argv.baseDir
|
||||
}
|
||||
|
||||
await build(clientRefresh)
|
||||
const server = http.createServer(async (req, res) => {
|
||||
if (argv.baseDir && !req.url?.startsWith(argv.baseDir)) {
|
||||
console.log(
|
||||
styleText(
|
||||
"red",
|
||||
`[404] ${req.url} (warning: link outside of site, this is likely a Quartz bug)`,
|
||||
),
|
||||
)
|
||||
res.writeHead(404)
|
||||
res.end()
|
||||
return
|
||||
}
|
||||
|
||||
// strip baseDir prefix
|
||||
req.url = req.url?.slice(argv.baseDir.length)
|
||||
|
||||
const serve = async () => {
|
||||
const release = await buildMutex.acquire()
|
||||
await serveHandler(req, res, {
|
||||
public: argv.output,
|
||||
directoryListing: false,
|
||||
headers: [
|
||||
{
|
||||
source: "**/*.*",
|
||||
headers: [{ key: "Content-Disposition", value: "inline" }],
|
||||
},
|
||||
{
|
||||
source: "**/*.webp",
|
||||
headers: [{ key: "Content-Type", value: "image/webp" }],
|
||||
},
|
||||
// fixes bug where avif images are displayed as text instead of images (future proof)
|
||||
{
|
||||
source: "**/*.avif",
|
||||
headers: [{ key: "Content-Type", value: "image/avif" }],
|
||||
},
|
||||
],
|
||||
})
|
||||
const status = res.statusCode
|
||||
const statusString =
|
||||
status >= 200 && status < 300
|
||||
? styleText("green", `[${status}]`)
|
||||
: styleText("red", `[${status}]`)
|
||||
console.log(statusString + styleText("gray", ` ${argv.baseDir}${req.url}`))
|
||||
release()
|
||||
}
|
||||
|
||||
const redirect = (newFp) => {
|
||||
newFp = argv.baseDir + newFp
|
||||
res.writeHead(302, {
|
||||
Location: newFp,
|
||||
})
|
||||
console.log(
|
||||
styleText("yellow", "[302]") +
|
||||
styleText("gray", ` ${argv.baseDir}${req.url} -> ${newFp}`),
|
||||
)
|
||||
res.end()
|
||||
}
|
||||
|
||||
let fp = req.url?.split("?")[0] ?? "/"
|
||||
|
||||
// handle redirects
|
||||
if (fp.endsWith("/")) {
|
||||
// /trailing/
|
||||
// does /trailing/index.html exist? if so, serve it
|
||||
const indexFp = path.posix.join(fp, "index.html")
|
||||
if (fs.existsSync(path.posix.join(argv.output, indexFp))) {
|
||||
req.url = fp
|
||||
return serve()
|
||||
}
|
||||
|
||||
// does /trailing.html exist? if so, redirect to /trailing
|
||||
let base = fp.slice(0, -1)
|
||||
if (path.extname(base) === "") {
|
||||
base += ".html"
|
||||
}
|
||||
if (fs.existsSync(path.posix.join(argv.output, base))) {
|
||||
return redirect(fp.slice(0, -1))
|
||||
}
|
||||
} else {
|
||||
// /regular
|
||||
// does /regular.html exist? if so, serve it
|
||||
let base = fp
|
||||
if (path.extname(base) === "") {
|
||||
base += ".html"
|
||||
}
|
||||
if (fs.existsSync(path.posix.join(argv.output, base))) {
|
||||
req.url = fp
|
||||
return serve()
|
||||
}
|
||||
|
||||
// does /regular/index.html exist? if so, redirect to /regular/
|
||||
let indexFp = path.posix.join(fp, "index.html")
|
||||
if (fs.existsSync(path.posix.join(argv.output, indexFp))) {
|
||||
return redirect(fp + "/")
|
||||
}
|
||||
}
|
||||
|
||||
return serve()
|
||||
})
|
||||
|
||||
server.on("error", (err) => {
|
||||
if (err.code === "EADDRINUSE") {
|
||||
console.error(
|
||||
`Port ${argv.port} is already in use. Try a different port with --port <number>`,
|
||||
)
|
||||
process.exit(1)
|
||||
}
|
||||
throw err
|
||||
})
|
||||
server.listen(argv.port)
|
||||
const wss = new WebSocketServer({ port: argv.wsPort })
|
||||
wss.on("error", (err) => {
|
||||
if (err.code === "EADDRINUSE") {
|
||||
console.error(
|
||||
`WebSocket port ${argv.wsPort} is already in use. Try a different port with --wsPort <number>`,
|
||||
)
|
||||
process.exit(1)
|
||||
}
|
||||
throw err
|
||||
})
|
||||
wss.on("connection", (ws) => connections.push(ws))
|
||||
console.log(
|
||||
styleText(
|
||||
"cyan",
|
||||
`Started a Quartz server listening at http://localhost:${argv.port}${argv.baseDir}`,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
await build(clientRefresh)
|
||||
ctx.dispose()
|
||||
}
|
||||
|
||||
if (argv.watch) {
|
||||
const paths = await globby([
|
||||
"**/*.ts",
|
||||
"quartz/cli/*.js",
|
||||
"quartz/static/**/*",
|
||||
"**/*.tsx",
|
||||
"**/*.scss",
|
||||
"package.json",
|
||||
"quartz.config.yaml",
|
||||
"quartz.config.default.yaml",
|
||||
])
|
||||
chokidar
|
||||
.watch(paths, { ignoreInitial: true })
|
||||
.on("add", () => build(clientRefresh))
|
||||
.on("change", () => build(clientRefresh))
|
||||
.on("unlink", () => build(clientRefresh))
|
||||
|
||||
console.log(styleText("gray", "hint: exit with ctrl+c"))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles `npx quartz upgrade`
|
||||
* Upgrades the Quartz framework itself by pulling latest changes from upstream.
|
||||
* @param {*} argv arguments for `upgrade`
|
||||
*/
|
||||
export async function handleUpgrade(argv) {
|
||||
const contentFolder = resolveContentPath(argv.directory)
|
||||
console.log(`\n${styleText(["bgGreen", "black"], ` Quartz v${version} `)} \n`)
|
||||
console.log("Backing up your content")
|
||||
execSync(`git remote show upstream || git remote add upstream ${QUARTZ_SOURCE_REPO}`)
|
||||
await stashContentFolder(contentFolder)
|
||||
|
||||
const lockfileBackup = LOCKFILE_PATH + ".bak"
|
||||
const hasLockfile = fs.existsSync(LOCKFILE_PATH)
|
||||
if (hasLockfile) {
|
||||
fs.copyFileSync(LOCKFILE_PATH, lockfileBackup)
|
||||
}
|
||||
|
||||
console.log(
|
||||
"Pulling updates... you may need to resolve some `git` conflicts if you've made changes to components or plugins.",
|
||||
)
|
||||
|
||||
let pullOk = false
|
||||
try {
|
||||
gitPull(UPSTREAM_NAME, QUARTZ_SOURCE_BRANCH)
|
||||
pullOk = true
|
||||
} catch {
|
||||
if (hasLockfile) {
|
||||
try {
|
||||
fs.copyFileSync(lockfileBackup, LOCKFILE_PATH)
|
||||
execSync(`git add ${LOCKFILE_PATH}`)
|
||||
const remaining = execSync("git diff --name-only --diff-filter=U", {
|
||||
encoding: "utf-8",
|
||||
}).trim()
|
||||
if (remaining.length === 0) {
|
||||
execSync("git commit --no-edit")
|
||||
pullOk = true
|
||||
console.log(styleText("cyan", "Resolved quartz.lock.json merge conflict automatically."))
|
||||
}
|
||||
} catch {
|
||||
// Could not auto-resolve, fall through to manual resolution
|
||||
}
|
||||
}
|
||||
|
||||
if (!pullOk) {
|
||||
console.log(
|
||||
styleText("red", "An error occurred while pulling updates.") +
|
||||
"\nCheck your network connection and git credentials. If you see merge conflicts, resolve them manually and run `npx quartz sync --no-pull`.",
|
||||
)
|
||||
await popContentFolder(contentFolder)
|
||||
if (fs.existsSync(lockfileBackup)) fs.unlinkSync(lockfileBackup)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (hasLockfile && fs.existsSync(lockfileBackup)) {
|
||||
fs.copyFileSync(lockfileBackup, LOCKFILE_PATH)
|
||||
fs.unlinkSync(lockfileBackup)
|
||||
}
|
||||
|
||||
await popContentFolder(contentFolder)
|
||||
|
||||
// Read the new version after pulling
|
||||
const newPkg = JSON.parse(fs.readFileSync("./package.json").toString())
|
||||
const newVersion = newPkg.version
|
||||
if (newVersion !== version) {
|
||||
console.log(styleText("cyan", `Upgraded Quartz: v${version} → v${newVersion}`))
|
||||
} else {
|
||||
console.log(styleText("gray", `Quartz is already up to date (v${version})`))
|
||||
}
|
||||
|
||||
console.log("Ensuring dependencies are up to date")
|
||||
|
||||
/*
|
||||
On Windows, if the command `npm` is really `npm.cmd', this call fails
|
||||
as it will be unable to find `npm`. This is often the case on systems
|
||||
where `npm` is installed via a package manager.
|
||||
|
||||
This means `npx quartz upgrade` will not actually update dependencies
|
||||
on Windows, without a manual `npm i` from the caller.
|
||||
|
||||
However, by spawning a shell, we are able to call `npm.cmd`.
|
||||
See: https://nodejs.org/api/child_process.html#spawning-bat-and-cmd-files-on-windows
|
||||
*/
|
||||
|
||||
const opts = { stdio: "inherit" }
|
||||
if (process.platform === "win32") {
|
||||
opts.shell = true
|
||||
}
|
||||
|
||||
const res = spawnSync("npm", ["i"], opts)
|
||||
if (res.status === 0) {
|
||||
console.log(styleText("green", "Dependencies updated!"))
|
||||
} else {
|
||||
console.log(
|
||||
styleText("red", "An error occurred while installing dependencies.") +
|
||||
"\nTry running `npm install` manually to see detailed errors.",
|
||||
)
|
||||
}
|
||||
|
||||
console.log("Restoring plugins from lockfile...")
|
||||
await handlePluginRestore()
|
||||
|
||||
console.log("Checking plugin compatibility...")
|
||||
await handlePluginCheck()
|
||||
|
||||
console.log(styleText("green", "Done!"))
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles `npx quartz restore`
|
||||
* @param {*} argv arguments for `restore`
|
||||
*/
|
||||
export async function handleRestore(argv) {
|
||||
const contentFolder = resolveContentPath(argv.directory)
|
||||
await popContentFolder(contentFolder)
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles `npx quartz sync`
|
||||
* @param {*} argv arguments for `sync`
|
||||
*/
|
||||
export async function handleSync(argv) {
|
||||
const contentFolder = resolveContentPath(argv.directory)
|
||||
console.log(`\n${styleText(["bgGreen", "black"], ` Quartz v${version} `)}\n`)
|
||||
console.log("Backing up your content")
|
||||
|
||||
if (argv.commit) {
|
||||
const contentStat = await fs.promises.lstat(contentFolder)
|
||||
if (contentStat.isSymbolicLink()) {
|
||||
const linkTarg = await fs.promises.readlink(contentFolder)
|
||||
console.log(styleText("yellow", "Detected symlink, trying to dereference before committing"))
|
||||
|
||||
// stash symlink file
|
||||
await stashContentFolder(contentFolder)
|
||||
|
||||
// follow symlink and copy content
|
||||
await fs.promises.cp(linkTarg, contentFolder, {
|
||||
recursive: true,
|
||||
preserveTimestamps: true,
|
||||
})
|
||||
}
|
||||
|
||||
const currentTimestamp = new Date().toLocaleString("en-US", {
|
||||
dateStyle: "medium",
|
||||
timeStyle: "short",
|
||||
})
|
||||
const commitMessage = argv.message ?? `Quartz sync: ${currentTimestamp}`
|
||||
spawnSync("git", ["add", "."], { stdio: "inherit" })
|
||||
spawnSync("git", ["commit", "-m", commitMessage], { stdio: "inherit" })
|
||||
|
||||
if (contentStat.isSymbolicLink()) {
|
||||
// put symlink back
|
||||
await popContentFolder(contentFolder)
|
||||
}
|
||||
}
|
||||
|
||||
await stashContentFolder(contentFolder)
|
||||
|
||||
if (argv.pull) {
|
||||
console.log(
|
||||
"Pulling updates from your repository. You may need to resolve some `git` conflicts if you've made changes to components or plugins.",
|
||||
)
|
||||
try {
|
||||
gitPull(ORIGIN_NAME, QUARTZ_SOURCE_BRANCH)
|
||||
} catch {
|
||||
console.log(
|
||||
styleText("red", "An error occurred while pulling updates from your repository.") +
|
||||
"\nCheck your network connection and git credentials.",
|
||||
)
|
||||
await popContentFolder(contentFolder)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
await popContentFolder(contentFolder)
|
||||
if (argv.push) {
|
||||
console.log("Pushing your changes")
|
||||
const currentBranch = execSync("git rev-parse --abbrev-ref HEAD").toString().trim()
|
||||
const res = spawnSync("git", ["push", "-uf", ORIGIN_NAME, currentBranch], {
|
||||
stdio: "inherit",
|
||||
})
|
||||
if (res.status !== 0) {
|
||||
console.log(
|
||||
styleText("red", `An error occurred while pushing to remote ${ORIGIN_NAME}.`) +
|
||||
"\nCheck that you have push access to the remote repository.",
|
||||
)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
console.log(styleText("green", "Done!"))
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
import { isCancel, outro } from "@clack/prompts"
|
||||
import { styleText } from "util"
|
||||
import { contentCacheFolder } from "./constants.js"
|
||||
import { spawnSync } from "child_process"
|
||||
import fs from "fs"
|
||||
import path from "path"
|
||||
|
||||
export function escapePath(fp) {
|
||||
return fp
|
||||
.replace(/\\ /g, " ") // unescape spaces
|
||||
.replace(/^"(.*)"$/, "$1")
|
||||
.replace(/^'(.*)'$/, "$1")
|
||||
.trim()
|
||||
}
|
||||
|
||||
export function exitIfCancel(val) {
|
||||
if (isCancel(val)) {
|
||||
outro(styleText("red", "Exiting"))
|
||||
process.exit(0)
|
||||
} else {
|
||||
return val
|
||||
}
|
||||
}
|
||||
|
||||
export async function stashContentFolder(contentFolder) {
|
||||
await fs.promises.rm(contentCacheFolder, { force: true, recursive: true })
|
||||
await fs.promises.cp(contentFolder, contentCacheFolder, {
|
||||
force: true,
|
||||
recursive: true,
|
||||
verbatimSymlinks: true,
|
||||
preserveTimestamps: true,
|
||||
})
|
||||
await fs.promises.rm(contentFolder, { force: true, recursive: true })
|
||||
}
|
||||
|
||||
export function gitPull(origin, branch) {
|
||||
const flags = ["--no-rebase", "--autostash", "--no-edit", "--allow-unrelated-histories"]
|
||||
const out = spawnSync("git", ["pull", ...flags, origin, branch], { stdio: "inherit" })
|
||||
if (out.stderr) {
|
||||
throw new Error(styleText("red", `Error while pulling updates: ${out.stderr}`))
|
||||
} else if (out.status !== 0) {
|
||||
throw new Error(styleText("red", "Error while pulling updates"))
|
||||
}
|
||||
}
|
||||
|
||||
export async function popContentFolder(contentFolder) {
|
||||
await fs.promises.rm(contentFolder, { force: true, recursive: true })
|
||||
await fs.promises.cp(contentCacheFolder, contentFolder, {
|
||||
force: true,
|
||||
recursive: true,
|
||||
verbatimSymlinks: true,
|
||||
preserveTimestamps: true,
|
||||
})
|
||||
await fs.promises.rm(contentCacheFolder, { force: true, recursive: true })
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a directory symlink with Windows fallback.
|
||||
*
|
||||
* On Windows, creating symlinks requires Developer Mode or admin privileges.
|
||||
* When that fails (EPERM), we try a junction first (no elevation needed),
|
||||
* then fall back to a recursive copy as a last resort.
|
||||
*
|
||||
* @param {string} target Symlink target (may be relative)
|
||||
* @param {string} linkPath Path where the link is created
|
||||
*/
|
||||
export function symlinkOrCopySync(target, linkPath) {
|
||||
try {
|
||||
fs.symlinkSync(target, linkPath, "dir")
|
||||
} catch (err) {
|
||||
if (err.code === "EEXIST") return
|
||||
if (err.code === "EPERM" && process.platform === "win32") {
|
||||
try {
|
||||
fs.symlinkSync(target, linkPath, "junction")
|
||||
return
|
||||
} catch {
|
||||
const resolvedTarget = path.resolve(path.dirname(linkPath), target)
|
||||
fs.cpSync(resolvedTarget, linkPath, { recursive: true })
|
||||
return
|
||||
}
|
||||
}
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Async version of {@link symlinkOrCopySync}.
|
||||
*
|
||||
* @param {string} target Symlink target (may be relative)
|
||||
* @param {string} linkPath Path where the link is created
|
||||
*/
|
||||
export async function symlinkOrCopy(target, linkPath) {
|
||||
try {
|
||||
await fs.promises.symlink(target, linkPath, "dir")
|
||||
} catch (err) {
|
||||
if (err.code === "EEXIST") return
|
||||
if (err.code === "EPERM" && process.platform === "win32") {
|
||||
try {
|
||||
await fs.promises.symlink(target, linkPath, "junction")
|
||||
return
|
||||
} catch {
|
||||
const resolvedTarget = path.resolve(path.dirname(linkPath), target)
|
||||
await fs.promises.cp(resolvedTarget, linkPath, { recursive: true })
|
||||
return
|
||||
}
|
||||
}
|
||||
throw err
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,237 @@
|
||||
import test, { describe, beforeEach, afterEach, mock } from "node:test"
|
||||
import assert from "node:assert"
|
||||
import fs from "fs"
|
||||
import path from "path"
|
||||
import os from "os"
|
||||
import { symlinkOrCopySync, symlinkOrCopy } from "./helpers.js"
|
||||
|
||||
function makeTmpDir() {
|
||||
return fs.mkdtempSync(path.join(os.tmpdir(), "quartz-symlink-test-"))
|
||||
}
|
||||
|
||||
function makeTarget(tmpDir) {
|
||||
const target = path.join(tmpDir, "target")
|
||||
fs.mkdirSync(target)
|
||||
fs.writeFileSync(path.join(target, "marker.txt"), "hello")
|
||||
return target
|
||||
}
|
||||
|
||||
describe("symlinkOrCopySync", () => {
|
||||
let tmpDir
|
||||
|
||||
beforeEach(() => {
|
||||
tmpDir = makeTmpDir()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
test("creates a symlink on success", () => {
|
||||
const target = makeTarget(tmpDir)
|
||||
const linkPath = path.join(tmpDir, "link")
|
||||
|
||||
symlinkOrCopySync(target, linkPath)
|
||||
|
||||
const stat = fs.lstatSync(linkPath)
|
||||
assert.ok(stat.isSymbolicLink())
|
||||
assert.ok(fs.existsSync(path.join(linkPath, "marker.txt")))
|
||||
})
|
||||
|
||||
test("silently succeeds when link already exists (EEXIST)", () => {
|
||||
const target = makeTarget(tmpDir)
|
||||
const linkPath = path.join(tmpDir, "link")
|
||||
|
||||
symlinkOrCopySync(target, linkPath)
|
||||
assert.doesNotThrow(() => symlinkOrCopySync(target, linkPath))
|
||||
})
|
||||
|
||||
test("re-throws non-EPERM errors", () => {
|
||||
const target = makeTarget(tmpDir)
|
||||
const linkPath = path.join(tmpDir, "nonexistent-parent", "link")
|
||||
|
||||
assert.throws(
|
||||
() => symlinkOrCopySync(target, linkPath),
|
||||
(err) => {
|
||||
return err.code === "ENOENT"
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
test("falls back to junction on Windows EPERM", () => {
|
||||
const target = makeTarget(tmpDir)
|
||||
const linkPath = path.join(tmpDir, "link")
|
||||
|
||||
const originalPlatform = Object.getOwnPropertyDescriptor(process, "platform")
|
||||
Object.defineProperty(process, "platform", { value: "win32", configurable: true })
|
||||
|
||||
let callCount = 0
|
||||
const originalSymlinkSync = fs.symlinkSync
|
||||
mock.method(fs, "symlinkSync", (t, lp, type) => {
|
||||
callCount++
|
||||
if (callCount === 1 && type === "dir") {
|
||||
const err = new Error("EPERM: operation not permitted, symlink")
|
||||
err.code = "EPERM"
|
||||
err.errno = -4048
|
||||
err.syscall = "symlink"
|
||||
throw err
|
||||
}
|
||||
return originalSymlinkSync(t, lp, type)
|
||||
})
|
||||
|
||||
try {
|
||||
symlinkOrCopySync(target, linkPath)
|
||||
assert.ok(fs.existsSync(path.join(linkPath, "marker.txt")))
|
||||
assert.strictEqual(callCount, 2)
|
||||
} finally {
|
||||
fs.symlinkSync.mock.restore()
|
||||
if (originalPlatform) {
|
||||
Object.defineProperty(process, "platform", originalPlatform)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
test("falls back to copy when both symlink and junction fail on Windows", () => {
|
||||
const target = makeTarget(tmpDir)
|
||||
const linkPath = path.join(tmpDir, "link")
|
||||
|
||||
const originalPlatform = Object.getOwnPropertyDescriptor(process, "platform")
|
||||
Object.defineProperty(process, "platform", { value: "win32", configurable: true })
|
||||
|
||||
const originalSymlinkSync = fs.symlinkSync
|
||||
mock.method(fs, "symlinkSync", (_t, _lp, _type) => {
|
||||
const err = new Error("EPERM: operation not permitted, symlink")
|
||||
err.code = "EPERM"
|
||||
err.errno = -4048
|
||||
err.syscall = "symlink"
|
||||
throw err
|
||||
})
|
||||
|
||||
try {
|
||||
symlinkOrCopySync(target, linkPath)
|
||||
|
||||
const stat = fs.lstatSync(linkPath)
|
||||
assert.ok(stat.isDirectory(), "fallback should produce a real directory, not a symlink")
|
||||
assert.strictEqual(fs.readFileSync(path.join(linkPath, "marker.txt"), "utf-8"), "hello")
|
||||
} finally {
|
||||
fs.symlinkSync.mock.restore()
|
||||
if (originalPlatform) {
|
||||
Object.defineProperty(process, "platform", originalPlatform)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
test("does not fall back on EPERM when not on Windows", () => {
|
||||
const target = makeTarget(tmpDir)
|
||||
const linkPath = path.join(tmpDir, "link")
|
||||
|
||||
const originalPlatform = Object.getOwnPropertyDescriptor(process, "platform")
|
||||
Object.defineProperty(process, "platform", { value: "linux", configurable: true })
|
||||
|
||||
const originalSymlinkSync = fs.symlinkSync
|
||||
mock.method(fs, "symlinkSync", (_t, _lp, _type) => {
|
||||
const err = new Error("EPERM: operation not permitted, symlink")
|
||||
err.code = "EPERM"
|
||||
throw err
|
||||
})
|
||||
|
||||
try {
|
||||
assert.throws(
|
||||
() => symlinkOrCopySync(target, linkPath),
|
||||
(err) => err.code === "EPERM",
|
||||
)
|
||||
} finally {
|
||||
fs.symlinkSync.mock.restore()
|
||||
if (originalPlatform) {
|
||||
Object.defineProperty(process, "platform", originalPlatform)
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe("symlinkOrCopy", () => {
|
||||
let tmpDir
|
||||
|
||||
beforeEach(() => {
|
||||
tmpDir = makeTmpDir()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
test("creates a symlink on success", async () => {
|
||||
const target = makeTarget(tmpDir)
|
||||
const linkPath = path.join(tmpDir, "link")
|
||||
|
||||
await symlinkOrCopy(target, linkPath)
|
||||
|
||||
const stat = fs.lstatSync(linkPath)
|
||||
assert.ok(stat.isSymbolicLink())
|
||||
assert.ok(fs.existsSync(path.join(linkPath, "marker.txt")))
|
||||
})
|
||||
|
||||
test("silently succeeds when link already exists (EEXIST)", async () => {
|
||||
const target = makeTarget(tmpDir)
|
||||
const linkPath = path.join(tmpDir, "link")
|
||||
|
||||
await symlinkOrCopy(target, linkPath)
|
||||
await assert.doesNotReject(() => symlinkOrCopy(target, linkPath))
|
||||
})
|
||||
|
||||
test("falls back to copy when both symlink and junction fail on Windows", async () => {
|
||||
const target = makeTarget(tmpDir)
|
||||
const linkPath = path.join(tmpDir, "link")
|
||||
|
||||
const originalPlatform = Object.getOwnPropertyDescriptor(process, "platform")
|
||||
Object.defineProperty(process, "platform", { value: "win32", configurable: true })
|
||||
|
||||
const originalSymlink = fs.promises.symlink
|
||||
mock.method(fs.promises, "symlink", async (_t, _lp, _type) => {
|
||||
const err = new Error("EPERM: operation not permitted, symlink")
|
||||
err.code = "EPERM"
|
||||
err.errno = -4048
|
||||
err.syscall = "symlink"
|
||||
throw err
|
||||
})
|
||||
|
||||
try {
|
||||
await symlinkOrCopy(target, linkPath)
|
||||
|
||||
const stat = fs.lstatSync(linkPath)
|
||||
assert.ok(stat.isDirectory(), "fallback should produce a real directory, not a symlink")
|
||||
assert.strictEqual(fs.readFileSync(path.join(linkPath, "marker.txt"), "utf-8"), "hello")
|
||||
} finally {
|
||||
fs.promises.symlink.mock.restore()
|
||||
if (originalPlatform) {
|
||||
Object.defineProperty(process, "platform", originalPlatform)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
test("does not fall back on EPERM when not on Windows", async () => {
|
||||
const target = makeTarget(tmpDir)
|
||||
const linkPath = path.join(tmpDir, "link")
|
||||
|
||||
const originalPlatform = Object.getOwnPropertyDescriptor(process, "platform")
|
||||
Object.defineProperty(process, "platform", { value: "linux", configurable: true })
|
||||
|
||||
mock.method(fs.promises, "symlink", async (_t, _lp, _type) => {
|
||||
const err = new Error("EPERM: operation not permitted, symlink")
|
||||
err.code = "EPERM"
|
||||
throw err
|
||||
})
|
||||
|
||||
try {
|
||||
await assert.rejects(
|
||||
() => symlinkOrCopy(target, linkPath),
|
||||
(err) => err.code === "EPERM",
|
||||
)
|
||||
} finally {
|
||||
fs.promises.symlink.mock.restore()
|
||||
if (originalPlatform) {
|
||||
Object.defineProperty(process, "platform", originalPlatform)
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,367 @@
|
||||
import fs from "fs"
|
||||
import path from "path"
|
||||
import { execSync } from "child_process"
|
||||
import YAML from "yaml"
|
||||
|
||||
const LOCKFILE_PATH = path.join(process.cwd(), "quartz.lock.json")
|
||||
const PLUGINS_DIR = path.join(process.cwd(), ".quartz", "plugins")
|
||||
const CONFIG_YAML_PATH = path.join(process.cwd(), "quartz.config.yaml")
|
||||
const DEFAULT_CONFIG_YAML_PATH = path.join(process.cwd(), "quartz.config.default.yaml")
|
||||
const TEMPLATES_DIR = path.join(process.cwd(), "quartz", "cli", "templates")
|
||||
|
||||
const LEGACY_PLUGINS_JSON_PATH = path.join(process.cwd(), "quartz.plugins.json")
|
||||
const LEGACY_DEFAULT_PLUGINS_JSON_PATH = path.join(process.cwd(), "quartz.plugins.default.json")
|
||||
|
||||
function resolveConfigPath() {
|
||||
if (fs.existsSync(CONFIG_YAML_PATH)) return CONFIG_YAML_PATH
|
||||
if (fs.existsSync(LEGACY_PLUGINS_JSON_PATH)) return LEGACY_PLUGINS_JSON_PATH
|
||||
if (fs.existsSync(DEFAULT_CONFIG_YAML_PATH)) return DEFAULT_CONFIG_YAML_PATH
|
||||
if (fs.existsSync(LEGACY_DEFAULT_PLUGINS_JSON_PATH)) return LEGACY_DEFAULT_PLUGINS_JSON_PATH
|
||||
return CONFIG_YAML_PATH
|
||||
}
|
||||
|
||||
function resolveDefaultConfigPath() {
|
||||
if (fs.existsSync(DEFAULT_CONFIG_YAML_PATH)) return DEFAULT_CONFIG_YAML_PATH
|
||||
if (fs.existsSync(LEGACY_DEFAULT_PLUGINS_JSON_PATH)) return LEGACY_DEFAULT_PLUGINS_JSON_PATH
|
||||
return DEFAULT_CONFIG_YAML_PATH
|
||||
}
|
||||
|
||||
function readFileAsData(filePath) {
|
||||
if (!fs.existsSync(filePath)) return null
|
||||
try {
|
||||
const raw = fs.readFileSync(filePath, "utf-8")
|
||||
if (filePath.endsWith(".yaml") || filePath.endsWith(".yml")) {
|
||||
return YAML.parse(raw)
|
||||
}
|
||||
return JSON.parse(raw)
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function writeDataToFile(filePath, data) {
|
||||
if (filePath.endsWith(".yaml") || filePath.endsWith(".yml")) {
|
||||
const header = "# yaml-language-server: $schema=./quartz/plugins/quartz-plugins.schema.json\n"
|
||||
fs.writeFileSync(filePath, header + YAML.stringify(data, { lineWidth: 120 }))
|
||||
} else {
|
||||
fs.writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n")
|
||||
}
|
||||
}
|
||||
|
||||
export function readPluginsJson() {
|
||||
const configPath = resolveConfigPath()
|
||||
return readFileAsData(configPath)
|
||||
}
|
||||
|
||||
export function writePluginsJson(data) {
|
||||
const { $schema, ...rest } = data
|
||||
writeDataToFile(CONFIG_YAML_PATH, rest)
|
||||
}
|
||||
|
||||
function readDefaultPluginsJson() {
|
||||
const defaultPath = resolveDefaultConfigPath()
|
||||
return readFileAsData(defaultPath)
|
||||
}
|
||||
|
||||
export function readLockfile() {
|
||||
if (!fs.existsSync(LOCKFILE_PATH)) return null
|
||||
try {
|
||||
return JSON.parse(fs.readFileSync(LOCKFILE_PATH, "utf-8"))
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export function writeLockfile(lockfile) {
|
||||
if (lockfile.plugins) {
|
||||
const sorted = {}
|
||||
for (const key of Object.keys(lockfile.plugins).sort()) {
|
||||
sorted[key] = lockfile.plugins[key]
|
||||
}
|
||||
lockfile = { ...lockfile, plugins: sorted }
|
||||
}
|
||||
fs.writeFileSync(LOCKFILE_PATH, JSON.stringify(lockfile, null, 2) + "\n")
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes a source value to a URL string.
|
||||
* Source can be a plain string (e.g. "github:owner/repo") or an object
|
||||
* with { name?, repo, subdir? } for installing from a subdirectory of a repo.
|
||||
*/
|
||||
export function getSourceUrl(source) {
|
||||
if (typeof source === "string") return source
|
||||
if (typeof source === "object" && source !== null && typeof source.repo === "string") {
|
||||
return source.repo
|
||||
}
|
||||
throw new Error(`Invalid plugin source: ${JSON.stringify(source)}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the subdir from an object source, or undefined for string sources.
|
||||
*/
|
||||
function getSourceSubdir(source) {
|
||||
if (typeof source === "object" && source !== null && typeof source.subdir === "string") {
|
||||
return source.subdir
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a display-friendly string for a source value.
|
||||
*/
|
||||
export function formatSource(source) {
|
||||
if (typeof source === "string") return source
|
||||
if (typeof source === "object" && source !== null) {
|
||||
const parts = [source.repo]
|
||||
if (source.subdir) parts.push(`(subdir: ${source.subdir})`)
|
||||
return parts.join(" ")
|
||||
}
|
||||
return String(source)
|
||||
}
|
||||
|
||||
export function isLocalSource(source) {
|
||||
const url = getSourceUrl(source)
|
||||
if (url.startsWith("./") || url.startsWith("../") || url.startsWith("/")) {
|
||||
return true
|
||||
}
|
||||
// Windows absolute paths (e.g. C:\ or D:/)
|
||||
if (/^[A-Za-z]:[\\/]/.test(url)) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
export function extractPluginName(source) {
|
||||
if (typeof source === "object" && source !== null && typeof source.name === "string") {
|
||||
return source.name
|
||||
}
|
||||
const url = getSourceUrl(source)
|
||||
if (isLocalSource(url)) {
|
||||
return path.basename(url.replace(/[\/]+$/, ""))
|
||||
}
|
||||
if (url.startsWith("github:")) {
|
||||
const withoutPrefix = url.replace("github:", "")
|
||||
const [repoPath] = withoutPrefix.split("#")
|
||||
const parts = repoPath.split("/")
|
||||
return parts[parts.length - 1]
|
||||
}
|
||||
if (url.startsWith("git+") || url.startsWith("https://")) {
|
||||
const cleaned = url.replace("git+", "")
|
||||
const match = cleaned.match(/\/([^/]+?)(?:\.git)?(?:#|$)/)
|
||||
return match?.[1] ?? url
|
||||
}
|
||||
return url
|
||||
}
|
||||
|
||||
export function readManifestFromPackageJson(pluginDir) {
|
||||
const pkgPath = path.join(pluginDir, "package.json")
|
||||
if (!fs.existsSync(pkgPath)) return null
|
||||
try {
|
||||
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"))
|
||||
return pkg.quartz ?? null
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export function parseGitSource(source) {
|
||||
const url = getSourceUrl(source)
|
||||
const subdir = getSourceSubdir(source)
|
||||
if (isLocalSource(url)) {
|
||||
const resolved = path.resolve(url)
|
||||
const name = typeof source === "object" && source.name ? source.name : path.basename(resolved)
|
||||
return { name, url: resolved, ref: undefined, local: true, subdir }
|
||||
}
|
||||
if (url.startsWith("github:")) {
|
||||
const [repoPath, ref] = url.replace("github:", "").split("#")
|
||||
const [owner, repo] = repoPath.split("/")
|
||||
const name = typeof source === "object" && source.name ? source.name : repo
|
||||
return { name, url: `https://github.com/${owner}/${repo}.git`, ref, subdir }
|
||||
}
|
||||
if (url.startsWith("git+")) {
|
||||
const raw = url.replace("git+", "")
|
||||
const [parsed, ref] = raw.split("#")
|
||||
const name =
|
||||
typeof source === "object" && source.name ? source.name : path.basename(parsed, ".git")
|
||||
return { name, url: parsed, ref, subdir }
|
||||
}
|
||||
if (url.startsWith("https://")) {
|
||||
const [parsed, ref] = url.split("#")
|
||||
const name =
|
||||
typeof source === "object" && source.name ? source.name : path.basename(parsed, ".git")
|
||||
return { name, url: parsed, ref, subdir }
|
||||
}
|
||||
throw new Error(`Cannot parse plugin source: ${formatSource(source)}`)
|
||||
}
|
||||
|
||||
export function getGitCommit(pluginDir) {
|
||||
try {
|
||||
return execSync("git rev-parse HEAD", { cwd: pluginDir, encoding: "utf-8" }).trim()
|
||||
} catch {
|
||||
return "unknown"
|
||||
}
|
||||
}
|
||||
|
||||
export function updateGlobalConfig(updates) {
|
||||
const json = readPluginsJson()
|
||||
if (!json) return false
|
||||
json.configuration = { ...json.configuration, ...updates }
|
||||
writePluginsJson(json)
|
||||
return true
|
||||
}
|
||||
|
||||
export function configExists() {
|
||||
return fs.existsSync(CONFIG_YAML_PATH) || fs.existsSync(LEGACY_PLUGINS_JSON_PATH)
|
||||
}
|
||||
|
||||
export function createConfigFromDefault() {
|
||||
const defaultData = readDefaultPluginsJson()
|
||||
if (!defaultData) {
|
||||
// No default available — create minimal config
|
||||
const minimal = {
|
||||
configuration: {
|
||||
pageTitle: "Quartz",
|
||||
enableSPA: true,
|
||||
enablePopovers: true,
|
||||
analytics: { provider: "plausible" },
|
||||
locale: "en-US",
|
||||
baseUrl: "quartz.jzhao.xyz",
|
||||
ignorePatterns: ["private", "templates", ".obsidian"],
|
||||
theme: {
|
||||
cdnCaching: true,
|
||||
typography: {
|
||||
header: "Schibsted Grotesk",
|
||||
body: "Source Sans Pro",
|
||||
code: "IBM Plex Mono",
|
||||
},
|
||||
colors: {
|
||||
lightMode: {
|
||||
light: "#faf8f8",
|
||||
lightgray: "#e5e5e5",
|
||||
gray: "#b8b8b8",
|
||||
darkgray: "#4e4e4e",
|
||||
dark: "#2b2b2b",
|
||||
secondary: "#284b63",
|
||||
tertiary: "#84a59d",
|
||||
highlight: "rgba(143, 159, 169, 0.15)",
|
||||
textHighlight: "#fff23688",
|
||||
},
|
||||
darkMode: {
|
||||
light: "#161618",
|
||||
lightgray: "#393639",
|
||||
gray: "#646464",
|
||||
darkgray: "#d4d4d4",
|
||||
dark: "#ebebec",
|
||||
secondary: "#7b97aa",
|
||||
tertiary: "#84a59d",
|
||||
highlight: "rgba(143, 159, 169, 0.15)",
|
||||
textHighlight: "#fff23688",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
layout: { groups: {}, byPageType: {} },
|
||||
}
|
||||
writePluginsJson(minimal)
|
||||
return minimal
|
||||
}
|
||||
|
||||
const { $schema, ...rest } = defaultData
|
||||
writePluginsJson(rest)
|
||||
return rest
|
||||
}
|
||||
|
||||
const VALID_TEMPLATES = ["default", "obsidian", "ttrpg", "blog"]
|
||||
|
||||
export function createConfigFromTemplate(templateName) {
|
||||
if (!VALID_TEMPLATES.includes(templateName)) {
|
||||
throw new Error(
|
||||
`Unknown template: ${templateName}. Valid templates: ${VALID_TEMPLATES.join(", ")}`,
|
||||
)
|
||||
}
|
||||
|
||||
const templatePath = path.join(TEMPLATES_DIR, `${templateName}.yaml`)
|
||||
const templateData = readFileAsData(templatePath)
|
||||
if (!templateData) {
|
||||
// Template file missing — fall back to default config creation
|
||||
return createConfigFromDefault()
|
||||
}
|
||||
|
||||
const { $schema, ...rest } = templateData
|
||||
writePluginsJson(rest)
|
||||
return rest
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves a user-facing plugin name (which may be an overridden name from config)
|
||||
* to the corresponding lockfile key (the original name at install time).
|
||||
*
|
||||
* This bridges the naming identity split between config YAML (which supports
|
||||
* source.name overrides) and the lockfile/disk (which are keyed by the original name).
|
||||
*
|
||||
* @param {string} name - The name the user provided (may be overridden or original)
|
||||
* @param {object|null} lockfile - The parsed lockfile
|
||||
* @param {object|null} pluginsJson - The parsed config YAML
|
||||
* @returns {string} The lockfile key that corresponds to this plugin
|
||||
*/
|
||||
export function resolveLockfileName(name, lockfile, pluginsJson) {
|
||||
// Direct match — no resolution needed
|
||||
if (lockfile?.plugins?.[name]) return name
|
||||
|
||||
// Check if any config entry with this overridden name maps to a different lockfile key
|
||||
if (pluginsJson?.plugins) {
|
||||
const configEntry = pluginsJson.plugins.find(
|
||||
(e) => extractPluginName(e.source) === name || formatSource(e.source) === name,
|
||||
)
|
||||
if (configEntry) {
|
||||
const url = getSourceUrl(configEntry.source)
|
||||
for (const [key, lock] of Object.entries(lockfile?.plugins ?? {})) {
|
||||
if (
|
||||
lock.source === url ||
|
||||
lock.source === formatSource(configEntry.source) ||
|
||||
lock.resolved === url
|
||||
) {
|
||||
return key
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return name
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a map from lockfile keys to their overridden display names from config.
|
||||
* Returns entries only where the overridden name differs from the lockfile key.
|
||||
*
|
||||
* @param {object|null} lockfile - The parsed lockfile
|
||||
* @param {object|null} pluginsJson - The parsed config YAML
|
||||
* @returns {Map<string, string>} Map of lockfileKey → overriddenName
|
||||
*/
|
||||
export function getNameOverrides(lockfile, pluginsJson) {
|
||||
const overrides = new Map()
|
||||
if (!lockfile?.plugins || !pluginsJson?.plugins) return overrides
|
||||
|
||||
for (const entry of pluginsJson.plugins) {
|
||||
const configName = extractPluginName(entry.source)
|
||||
const url = getSourceUrl(entry.source)
|
||||
|
||||
for (const [lockKey, lock] of Object.entries(lockfile.plugins)) {
|
||||
if (lockKey === configName) break // no override, names match
|
||||
if (
|
||||
lock.source === url ||
|
||||
lock.source === formatSource(entry.source) ||
|
||||
lock.resolved === url
|
||||
) {
|
||||
overrides.set(lockKey, configName)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return overrides
|
||||
}
|
||||
|
||||
export const PLUGINS_JSON_PATH = CONFIG_YAML_PATH
|
||||
export const DEFAULT_PLUGINS_JSON_PATH = DEFAULT_CONFIG_YAML_PATH
|
||||
export { LOCKFILE_PATH, PLUGINS_DIR }
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,291 @@
|
||||
# yaml-language-server: $schema=../../plugins/quartz-plugins.schema.json
|
||||
# Template: blog
|
||||
# A blog-focused setup with recent notes and comments enabled.
|
||||
configuration:
|
||||
pageTitle: Quartz 5
|
||||
pageTitleSuffix: ""
|
||||
enableSPA: true
|
||||
enablePopovers: true
|
||||
analytics:
|
||||
provider: plausible
|
||||
locale: en-US
|
||||
baseUrl: quartz.jzhao.xyz
|
||||
ignorePatterns:
|
||||
- private
|
||||
- templates
|
||||
- .obsidian
|
||||
theme:
|
||||
fontOrigin: googleFonts
|
||||
cdnCaching: true
|
||||
typography:
|
||||
header: Schibsted Grotesk
|
||||
body: Source Sans Pro
|
||||
code: IBM Plex Mono
|
||||
colors:
|
||||
lightMode:
|
||||
light: "#faf8f8"
|
||||
lightgray: "#e5e5e5"
|
||||
gray: "#b8b8b8"
|
||||
darkgray: "#4e4e4e"
|
||||
dark: "#2b2b2b"
|
||||
secondary: "#284b63"
|
||||
tertiary: "#84a59d"
|
||||
highlight: rgba(143, 159, 169, 0.15)
|
||||
textHighlight: "#fff23688"
|
||||
darkMode:
|
||||
light: "#161618"
|
||||
lightgray: "#393639"
|
||||
gray: "#646464"
|
||||
darkgray: "#d4d4d4"
|
||||
dark: "#ebebec"
|
||||
secondary: "#7b97aa"
|
||||
tertiary: "#84a59d"
|
||||
highlight: rgba(143, 159, 169, 0.15)
|
||||
textHighlight: "#b3aa0288"
|
||||
plugins:
|
||||
- source: github:quartz-community/created-modified-date
|
||||
enabled: true
|
||||
options:
|
||||
defaultDateType: modified
|
||||
priority:
|
||||
- frontmatter
|
||||
- git
|
||||
- filesystem
|
||||
order: 10
|
||||
- source: github:quartz-community/syntax-highlighting
|
||||
enabled: true
|
||||
options:
|
||||
theme:
|
||||
light: github-light
|
||||
dark: github-dark
|
||||
keepBackground: false
|
||||
order: 20
|
||||
- source: github:quartz-community/obsidian-flavored-markdown
|
||||
enabled: true
|
||||
options:
|
||||
enableInHtmlEmbed: false
|
||||
enableCheckbox: true
|
||||
order: 30
|
||||
- source: github:quartz-community/github-flavored-markdown
|
||||
enabled: true
|
||||
order: 40
|
||||
- source: github:quartz-community/table-of-contents
|
||||
enabled: true
|
||||
order: 50
|
||||
layout:
|
||||
position: right
|
||||
priority: 30
|
||||
- source: github:quartz-community/crawl-links
|
||||
enabled: true
|
||||
options:
|
||||
markdownLinkResolution: shortest
|
||||
order: 60
|
||||
- source: github:quartz-community/description
|
||||
enabled: true
|
||||
order: 70
|
||||
- source: github:quartz-community/latex
|
||||
enabled: true
|
||||
options:
|
||||
renderEngine: katex
|
||||
order: 80
|
||||
- source: github:quartz-community/citations
|
||||
enabled: false
|
||||
order: 85
|
||||
- source: github:quartz-community/hard-line-breaks
|
||||
enabled: false
|
||||
order: 90
|
||||
- source: github:quartz-community/ox-hugo
|
||||
enabled: false
|
||||
order: 91
|
||||
- source: github:quartz-community/roam
|
||||
enabled: false
|
||||
order: 92
|
||||
- source: github:quartz-community/fonts
|
||||
enabled: true
|
||||
- source: github:quartz-community/remove-draft
|
||||
enabled: true
|
||||
- source: github:quartz-community/explicit-publish
|
||||
enabled: false
|
||||
- source: github:quartz-community/unlisted-pages
|
||||
enabled: true
|
||||
options: {}
|
||||
order: 45
|
||||
- source: github:quartz-community/encrypted-pages
|
||||
enabled: false
|
||||
- source: github:quartz-community/stacked-pages
|
||||
enabled: false
|
||||
layout:
|
||||
position: afterBody
|
||||
priority: 50
|
||||
display: all
|
||||
- source: github:quartz-community/alias-redirects
|
||||
enabled: true
|
||||
- source: github:quartz-community/content-index
|
||||
enabled: true
|
||||
options:
|
||||
enableSiteMap: true
|
||||
enableRSS: true
|
||||
- source: github:quartz-community/favicon
|
||||
enabled: true
|
||||
- source: github:quartz-community/og-image
|
||||
enabled: true
|
||||
- source: github:quartz-community/cname
|
||||
enabled: true
|
||||
- source: github:quartz-community/canvas-page
|
||||
enabled: true
|
||||
- source: github:quartz-community/content-page
|
||||
enabled: true
|
||||
- source: github:quartz-community/folder-page
|
||||
enabled: true
|
||||
- source: github:quartz-community/tag-page
|
||||
enabled: true
|
||||
- source: github:quartz-community/explorer
|
||||
enabled: true
|
||||
layout:
|
||||
position: left
|
||||
priority: 50
|
||||
- source: github:quartz-community/graph
|
||||
enabled: true
|
||||
layout:
|
||||
position: right
|
||||
priority: 10
|
||||
- source: github:quartz-community/search
|
||||
enabled: true
|
||||
layout:
|
||||
position: left
|
||||
priority: 20
|
||||
group: toolbar
|
||||
groupOptions:
|
||||
grow: true
|
||||
- source: github:quartz-community/backlinks
|
||||
enabled: true
|
||||
layout:
|
||||
position: right
|
||||
priority: 50
|
||||
- source: github:quartz-community/article-title
|
||||
enabled: true
|
||||
layout:
|
||||
position: beforeBody
|
||||
priority: 10
|
||||
- source: github:quartz-community/content-meta
|
||||
enabled: true
|
||||
layout:
|
||||
position: beforeBody
|
||||
priority: 20
|
||||
- source: github:quartz-community/tag-list
|
||||
enabled: false
|
||||
layout:
|
||||
position: beforeBody
|
||||
priority: 30
|
||||
- source: github:quartz-community/page-title
|
||||
enabled: true
|
||||
layout:
|
||||
position: left
|
||||
priority: 10
|
||||
- source: github:quartz-community/darkmode
|
||||
enabled: true
|
||||
layout:
|
||||
position: left
|
||||
priority: 30
|
||||
group: toolbar
|
||||
- source: github:quartz-community/reader-mode
|
||||
enabled: true
|
||||
layout:
|
||||
position: left
|
||||
priority: 35
|
||||
group: toolbar
|
||||
- source: github:quartz-community/breadcrumbs
|
||||
enabled: true
|
||||
layout:
|
||||
position: beforeBody
|
||||
priority: 5
|
||||
condition: not-index
|
||||
- source: github:quartz-community/comments
|
||||
enabled: true
|
||||
options:
|
||||
provider: giscus
|
||||
options:
|
||||
repo: "TODO:username/repo-name"
|
||||
repoId: "TODO:your-repo-id"
|
||||
category: Announcements
|
||||
categoryId: "TODO:your-category-id"
|
||||
mapping: url
|
||||
strict: true
|
||||
reactionsEnabled: true
|
||||
inputPosition: bottom
|
||||
lightTheme: light
|
||||
darkTheme: dark
|
||||
lang: en
|
||||
layout:
|
||||
position: afterBody
|
||||
priority: 10
|
||||
- source: github:quartz-community/footer
|
||||
enabled: true
|
||||
options:
|
||||
links:
|
||||
GitHub: https://github.com/jackyzha0/quartz
|
||||
Discord Community: https://discord.gg/cRFFHYye7t
|
||||
- source: github:quartz-community/recent-notes
|
||||
enabled: true
|
||||
options:
|
||||
title: Recent Notes
|
||||
limit: 5
|
||||
linkToMore: false
|
||||
showTags: true
|
||||
layout:
|
||||
position: left
|
||||
priority: 25
|
||||
- source: github:quartz-community/spacer
|
||||
enabled: true
|
||||
options: {}
|
||||
order: 25
|
||||
layout:
|
||||
position: left
|
||||
priority: 25
|
||||
display: mobile-only
|
||||
- source: github:quartz-community/bases-page
|
||||
enabled: true
|
||||
options: {}
|
||||
order: 50
|
||||
- source: github:quartz-community/note-properties
|
||||
enabled: true
|
||||
options:
|
||||
includeAll: false
|
||||
includedProperties:
|
||||
- description
|
||||
- tags
|
||||
- aliases
|
||||
excludedProperties: []
|
||||
hidePropertiesView: false
|
||||
delimiters: "---"
|
||||
language: yaml
|
||||
order: 5
|
||||
layout:
|
||||
position: beforeBody
|
||||
priority: 15
|
||||
display: all
|
||||
layout:
|
||||
groups:
|
||||
toolbar:
|
||||
priority: 35
|
||||
direction: row
|
||||
gap: 0.5rem
|
||||
byPageType:
|
||||
"404":
|
||||
positions:
|
||||
beforeBody: []
|
||||
left: []
|
||||
right: []
|
||||
content: {}
|
||||
folder:
|
||||
exclude:
|
||||
- reader-mode
|
||||
positions:
|
||||
right: []
|
||||
tag:
|
||||
exclude:
|
||||
- reader-mode
|
||||
positions:
|
||||
right: []
|
||||
canvas: {}
|
||||
bases: {}
|
||||
@@ -0,0 +1,277 @@
|
||||
# yaml-language-server: $schema=../../plugins/quartz-plugins.schema.json
|
||||
# Template: default
|
||||
# A clean Quartz setup with sensible defaults.
|
||||
configuration:
|
||||
pageTitle: Quartz 5
|
||||
pageTitleSuffix: ""
|
||||
enableSPA: true
|
||||
enablePopovers: true
|
||||
analytics:
|
||||
provider: plausible
|
||||
locale: en-US
|
||||
baseUrl: quartz.jzhao.xyz
|
||||
ignorePatterns:
|
||||
- private
|
||||
- templates
|
||||
- .obsidian
|
||||
theme:
|
||||
fontOrigin: googleFonts
|
||||
cdnCaching: true
|
||||
typography:
|
||||
header: Schibsted Grotesk
|
||||
body: Source Sans Pro
|
||||
code: IBM Plex Mono
|
||||
colors:
|
||||
lightMode:
|
||||
light: "#faf8f8"
|
||||
lightgray: "#e5e5e5"
|
||||
gray: "#b8b8b8"
|
||||
darkgray: "#4e4e4e"
|
||||
dark: "#2b2b2b"
|
||||
secondary: "#284b63"
|
||||
tertiary: "#84a59d"
|
||||
highlight: rgba(143, 159, 169, 0.15)
|
||||
textHighlight: "#fff23688"
|
||||
darkMode:
|
||||
light: "#161618"
|
||||
lightgray: "#393639"
|
||||
gray: "#646464"
|
||||
darkgray: "#d4d4d4"
|
||||
dark: "#ebebec"
|
||||
secondary: "#7b97aa"
|
||||
tertiary: "#84a59d"
|
||||
highlight: rgba(143, 159, 169, 0.15)
|
||||
textHighlight: "#b3aa0288"
|
||||
plugins:
|
||||
- source: github:quartz-community/created-modified-date
|
||||
enabled: true
|
||||
options:
|
||||
defaultDateType: modified
|
||||
priority:
|
||||
- frontmatter
|
||||
- git
|
||||
- filesystem
|
||||
order: 10
|
||||
- source: github:quartz-community/syntax-highlighting
|
||||
enabled: true
|
||||
options:
|
||||
theme:
|
||||
light: github-light
|
||||
dark: github-dark
|
||||
keepBackground: false
|
||||
order: 20
|
||||
- source: github:quartz-community/obsidian-flavored-markdown
|
||||
enabled: true
|
||||
options:
|
||||
enableInHtmlEmbed: false
|
||||
enableCheckbox: true
|
||||
order: 30
|
||||
- source: github:quartz-community/github-flavored-markdown
|
||||
enabled: true
|
||||
order: 40
|
||||
- source: github:quartz-community/table-of-contents
|
||||
enabled: true
|
||||
order: 50
|
||||
layout:
|
||||
position: right
|
||||
priority: 30
|
||||
- source: github:quartz-community/crawl-links
|
||||
enabled: true
|
||||
options:
|
||||
markdownLinkResolution: shortest
|
||||
order: 60
|
||||
- source: github:quartz-community/description
|
||||
enabled: true
|
||||
order: 70
|
||||
- source: github:quartz-community/latex
|
||||
enabled: true
|
||||
options:
|
||||
renderEngine: katex
|
||||
order: 80
|
||||
- source: github:quartz-community/citations
|
||||
enabled: false
|
||||
order: 85
|
||||
- source: github:quartz-community/hard-line-breaks
|
||||
enabled: false
|
||||
order: 90
|
||||
- source: github:quartz-community/ox-hugo
|
||||
enabled: false
|
||||
order: 91
|
||||
- source: github:quartz-community/roam
|
||||
enabled: false
|
||||
order: 92
|
||||
- source: github:quartz-community/fonts
|
||||
enabled: true
|
||||
- source: github:quartz-community/remove-draft
|
||||
enabled: true
|
||||
- source: github:quartz-community/explicit-publish
|
||||
enabled: false
|
||||
- source: github:quartz-community/unlisted-pages
|
||||
enabled: true
|
||||
options: {}
|
||||
order: 45
|
||||
- source: github:quartz-community/encrypted-pages
|
||||
enabled: true
|
||||
options:
|
||||
iterations: 600000
|
||||
passwordField: password
|
||||
unlistWhenEncrypted: false
|
||||
outputPath: static/encryptedContentIndex.json
|
||||
- source: github:quartz-community/stacked-pages
|
||||
enabled: false
|
||||
layout:
|
||||
position: afterBody
|
||||
priority: 50
|
||||
display: all
|
||||
- source: github:quartz-community/alias-redirects
|
||||
enabled: true
|
||||
- source: github:quartz-community/content-index
|
||||
enabled: true
|
||||
options:
|
||||
enableSiteMap: true
|
||||
enableRSS: true
|
||||
- source: github:quartz-community/favicon
|
||||
enabled: true
|
||||
- source: github:quartz-community/og-image
|
||||
enabled: true
|
||||
- source: github:quartz-community/cname
|
||||
enabled: true
|
||||
- source: github:quartz-community/canvas-page
|
||||
enabled: true
|
||||
- source: github:quartz-community/content-page
|
||||
enabled: true
|
||||
- source: github:quartz-community/folder-page
|
||||
enabled: true
|
||||
- source: github:quartz-community/tag-page
|
||||
enabled: true
|
||||
- source: github:quartz-community/explorer
|
||||
enabled: true
|
||||
layout:
|
||||
position: left
|
||||
priority: 50
|
||||
- source: github:quartz-community/graph
|
||||
enabled: true
|
||||
layout:
|
||||
position: right
|
||||
priority: 10
|
||||
- source: github:quartz-community/search
|
||||
enabled: true
|
||||
layout:
|
||||
position: left
|
||||
priority: 20
|
||||
group: toolbar
|
||||
groupOptions:
|
||||
grow: true
|
||||
- source: github:quartz-community/backlinks
|
||||
enabled: true
|
||||
layout:
|
||||
position: right
|
||||
priority: 50
|
||||
- source: github:quartz-community/article-title
|
||||
enabled: true
|
||||
layout:
|
||||
position: beforeBody
|
||||
priority: 10
|
||||
- source: github:quartz-community/content-meta
|
||||
enabled: true
|
||||
layout:
|
||||
position: beforeBody
|
||||
priority: 20
|
||||
- source: github:quartz-community/tag-list
|
||||
enabled: false
|
||||
layout:
|
||||
position: beforeBody
|
||||
priority: 30
|
||||
- source: github:quartz-community/page-title
|
||||
enabled: true
|
||||
layout:
|
||||
position: left
|
||||
priority: 10
|
||||
- source: github:quartz-community/darkmode
|
||||
enabled: true
|
||||
layout:
|
||||
position: left
|
||||
priority: 30
|
||||
group: toolbar
|
||||
- source: github:quartz-community/reader-mode
|
||||
enabled: true
|
||||
layout:
|
||||
position: left
|
||||
priority: 35
|
||||
group: toolbar
|
||||
- source: github:quartz-community/breadcrumbs
|
||||
enabled: true
|
||||
layout:
|
||||
position: beforeBody
|
||||
priority: 5
|
||||
condition: not-index
|
||||
- source: github:quartz-community/comments
|
||||
enabled: false
|
||||
options:
|
||||
provider: giscus
|
||||
options: {}
|
||||
layout:
|
||||
position: afterBody
|
||||
priority: 10
|
||||
- source: github:quartz-community/footer
|
||||
enabled: true
|
||||
options:
|
||||
links:
|
||||
GitHub: https://github.com/jackyzha0/quartz
|
||||
Discord Community: https://discord.gg/cRFFHYye7t
|
||||
- source: github:quartz-community/recent-notes
|
||||
enabled: false
|
||||
- source: github:quartz-community/spacer
|
||||
enabled: true
|
||||
options: {}
|
||||
order: 25
|
||||
layout:
|
||||
position: left
|
||||
priority: 25
|
||||
display: mobile-only
|
||||
- source: github:quartz-community/bases-page
|
||||
enabled: true
|
||||
options: {}
|
||||
order: 50
|
||||
- source: github:quartz-community/note-properties
|
||||
enabled: true
|
||||
options:
|
||||
includeAll: false
|
||||
includedProperties:
|
||||
- description
|
||||
- tags
|
||||
- aliases
|
||||
excludedProperties: []
|
||||
hidePropertiesView: false
|
||||
delimiters: "---"
|
||||
language: yaml
|
||||
order: 5
|
||||
layout:
|
||||
position: beforeBody
|
||||
priority: 15
|
||||
display: all
|
||||
layout:
|
||||
groups:
|
||||
toolbar:
|
||||
priority: 35
|
||||
direction: row
|
||||
gap: 0.5rem
|
||||
byPageType:
|
||||
"404":
|
||||
positions:
|
||||
beforeBody: []
|
||||
left: []
|
||||
right: []
|
||||
content: {}
|
||||
folder:
|
||||
exclude:
|
||||
- reader-mode
|
||||
positions:
|
||||
right: []
|
||||
tag:
|
||||
exclude:
|
||||
- reader-mode
|
||||
positions:
|
||||
right: []
|
||||
canvas: {}
|
||||
bases: {}
|
||||
@@ -0,0 +1,302 @@
|
||||
# yaml-language-server: $schema=../../plugins/quartz-plugins.schema.json
|
||||
# Template: obsidian
|
||||
# Optimized for Obsidian vaults with full OFM support and shortest link resolution.
|
||||
configuration:
|
||||
pageTitle: Quartz 5
|
||||
pageTitleSuffix: ""
|
||||
enableSPA: true
|
||||
enablePopovers: true
|
||||
analytics:
|
||||
provider: plausible
|
||||
locale: en-US
|
||||
baseUrl: quartz.jzhao.xyz
|
||||
ignorePatterns:
|
||||
- private
|
||||
- templates
|
||||
- .obsidian
|
||||
theme:
|
||||
fontOrigin: googleFonts
|
||||
cdnCaching: true
|
||||
typography:
|
||||
header: Schibsted Grotesk
|
||||
body: Source Sans Pro
|
||||
code: IBM Plex Mono
|
||||
colors:
|
||||
lightMode:
|
||||
light: "#faf8f8"
|
||||
lightgray: "#e5e5e5"
|
||||
gray: "#b8b8b8"
|
||||
darkgray: "#4e4e4e"
|
||||
dark: "#2b2b2b"
|
||||
secondary: "#284b63"
|
||||
tertiary: "#84a59d"
|
||||
highlight: rgba(143, 159, 169, 0.15)
|
||||
textHighlight: "#fff23688"
|
||||
darkMode:
|
||||
light: "#161618"
|
||||
lightgray: "#393639"
|
||||
gray: "#646464"
|
||||
darkgray: "#d4d4d4"
|
||||
dark: "#ebebec"
|
||||
secondary: "#7b97aa"
|
||||
tertiary: "#84a59d"
|
||||
highlight: rgba(143, 159, 169, 0.15)
|
||||
textHighlight: "#b3aa0288"
|
||||
plugins:
|
||||
- source: github:quartz-community/created-modified-date
|
||||
enabled: true
|
||||
options:
|
||||
defaultDateType: modified
|
||||
priority:
|
||||
- frontmatter
|
||||
- git
|
||||
- filesystem
|
||||
order: 10
|
||||
- source: github:quartz-community/syntax-highlighting
|
||||
enabled: true
|
||||
options:
|
||||
theme:
|
||||
light: github-light
|
||||
dark: github-dark
|
||||
keepBackground: false
|
||||
order: 20
|
||||
- source: github:quartz-community/obsidian-flavored-markdown
|
||||
enabled: true
|
||||
options:
|
||||
comments: true
|
||||
highlight: true
|
||||
wikilinks: true
|
||||
callouts: true
|
||||
mermaid: true
|
||||
parseTags: true
|
||||
parseArrows: true
|
||||
parseBlockReferences: true
|
||||
enableInHtmlEmbed: false
|
||||
enableYouTubeEmbed: true
|
||||
enableVideoEmbed: true
|
||||
enableCheckbox: true
|
||||
order: 30
|
||||
- source: github:quartz-community/github-flavored-markdown
|
||||
enabled: true
|
||||
order: 40
|
||||
- source: github:quartz-community/table-of-contents
|
||||
enabled: true
|
||||
order: 50
|
||||
layout:
|
||||
position: right
|
||||
priority: 30
|
||||
- source: github:quartz-community/crawl-links
|
||||
enabled: true
|
||||
options:
|
||||
markdownLinkResolution: shortest
|
||||
# disableBrokenWikilinks: false # Set true to add a "broken" CSS class to internal links whose target is not in ctx.allSlugs.
|
||||
order: 60
|
||||
- source: github:quartz-community/description
|
||||
enabled: true
|
||||
order: 70
|
||||
- source: github:quartz-community/latex
|
||||
enabled: true
|
||||
options:
|
||||
renderEngine: katex
|
||||
order: 80
|
||||
- source: github:quartz-community/citations
|
||||
enabled: false
|
||||
order: 85
|
||||
- source: github:quartz-community/hard-line-breaks
|
||||
enabled: true
|
||||
order: 90
|
||||
- source: github:quartz-community/ox-hugo
|
||||
enabled: false
|
||||
order: 91
|
||||
- source: github:quartz-community/roam
|
||||
enabled: false
|
||||
order: 92
|
||||
- source: github:quartz-community/fonts
|
||||
enabled: true
|
||||
- source: github:quartz-community/remove-draft
|
||||
enabled: true
|
||||
- source: github:quartz-community/explicit-publish
|
||||
enabled: false
|
||||
- source: github:quartz-community/unlisted-pages
|
||||
enabled: true
|
||||
options: {}
|
||||
order: 45
|
||||
- source: github:quartz-community/encrypted-pages
|
||||
enabled: true
|
||||
options:
|
||||
iterations: 600000
|
||||
passwordField: password
|
||||
unlistWhenEncrypted: false
|
||||
outputPath: static/encryptedContentIndex.json
|
||||
- source: github:quartz-community/stacked-pages
|
||||
enabled: false
|
||||
layout:
|
||||
position: afterBody
|
||||
priority: 50
|
||||
display: all
|
||||
- source: github:quartz-community/alias-redirects
|
||||
enabled: true
|
||||
- source: github:quartz-community/content-index
|
||||
enabled: true
|
||||
options:
|
||||
enableSiteMap: true
|
||||
enableRSS: true
|
||||
- source: github:quartz-community/favicon
|
||||
enabled: true
|
||||
- source: github:quartz-community/og-image
|
||||
enabled: true
|
||||
- source: github:quartz-community/cname
|
||||
enabled: true
|
||||
- source: github:quartz-community/canvas-page
|
||||
enabled: true
|
||||
- source: github:quartz-community/content-page
|
||||
enabled: true
|
||||
- source: github:quartz-community/folder-page
|
||||
enabled: true
|
||||
- source: github:quartz-community/tag-page
|
||||
enabled: true
|
||||
- source: github:quartz-community/explorer
|
||||
enabled: true
|
||||
layout:
|
||||
position: left
|
||||
priority: 50
|
||||
- source: github:quartz-community/graph
|
||||
enabled: true
|
||||
layout:
|
||||
position: right
|
||||
priority: 10
|
||||
- source: github:quartz-community/search
|
||||
enabled: true
|
||||
layout:
|
||||
position: left
|
||||
priority: 20
|
||||
group: toolbar
|
||||
groupOptions:
|
||||
grow: true
|
||||
- source: github:quartz-community/backlinks
|
||||
enabled: true
|
||||
layout:
|
||||
position: right
|
||||
priority: 50
|
||||
- source: github:quartz-community/article-title
|
||||
enabled: true
|
||||
layout:
|
||||
position: beforeBody
|
||||
priority: 10
|
||||
- source: github:quartz-community/content-meta
|
||||
enabled: true
|
||||
layout:
|
||||
position: beforeBody
|
||||
priority: 20
|
||||
- source: github:quartz-community/tag-list
|
||||
enabled: false
|
||||
layout:
|
||||
position: beforeBody
|
||||
priority: 30
|
||||
- source: github:quartz-community/page-title
|
||||
enabled: true
|
||||
layout:
|
||||
position: left
|
||||
priority: 10
|
||||
- source: github:quartz-community/darkmode
|
||||
enabled: true
|
||||
layout:
|
||||
position: left
|
||||
priority: 30
|
||||
group: toolbar
|
||||
- source: github:quartz-community/reader-mode
|
||||
enabled: true
|
||||
layout:
|
||||
position: left
|
||||
priority: 35
|
||||
group: toolbar
|
||||
- source: github:quartz-community/breadcrumbs
|
||||
enabled: true
|
||||
layout:
|
||||
position: beforeBody
|
||||
priority: 5
|
||||
condition: not-index
|
||||
- source: github:quartz-community/comments
|
||||
enabled: false
|
||||
options:
|
||||
provider: giscus
|
||||
options: {}
|
||||
layout:
|
||||
position: afterBody
|
||||
priority: 10
|
||||
- source: github:quartz-community/footer
|
||||
enabled: true
|
||||
options:
|
||||
links:
|
||||
GitHub: https://github.com/jackyzha0/quartz
|
||||
Discord Community: https://discord.gg/cRFFHYye7t
|
||||
- source: github:quartz-community/recent-notes
|
||||
enabled: false
|
||||
- source: github:quartz-community/spacer
|
||||
enabled: true
|
||||
options: {}
|
||||
order: 25
|
||||
layout:
|
||||
position: left
|
||||
priority: 25
|
||||
display: mobile-only
|
||||
- source: github:quartz-community/bases-page
|
||||
enabled: true
|
||||
options: {}
|
||||
order: 50
|
||||
- source: github:quartz-community/note-properties
|
||||
enabled: true
|
||||
options:
|
||||
includeAll: false
|
||||
includedProperties:
|
||||
- description
|
||||
- tags
|
||||
- aliases
|
||||
excludedProperties: []
|
||||
hidePropertiesView: false
|
||||
delimiters: "---"
|
||||
language: yaml
|
||||
order: 5
|
||||
layout:
|
||||
position: beforeBody
|
||||
priority: 15
|
||||
display: all
|
||||
- source:
|
||||
name: quartz-themes
|
||||
repo: github:saberzero1/quartz-themes
|
||||
subdir: plugin
|
||||
enabled: true
|
||||
options:
|
||||
theme: default
|
||||
- source: github:quartz-community/obsidian-plugin-excalidraw
|
||||
enabled: true
|
||||
options:
|
||||
enableInteraction: true
|
||||
darkMode: auto
|
||||
exportPadding: 20
|
||||
order: 50
|
||||
layout:
|
||||
groups:
|
||||
toolbar:
|
||||
priority: 35
|
||||
direction: row
|
||||
gap: 0.5rem
|
||||
byPageType:
|
||||
"404":
|
||||
positions:
|
||||
beforeBody: []
|
||||
left: []
|
||||
right: []
|
||||
content: {}
|
||||
folder:
|
||||
exclude:
|
||||
- reader-mode
|
||||
positions:
|
||||
right: []
|
||||
tag:
|
||||
exclude:
|
||||
- reader-mode
|
||||
positions:
|
||||
right: []
|
||||
canvas: {}
|
||||
bases: {}
|
||||
@@ -0,0 +1,308 @@
|
||||
# yaml-language-server: $schema=../../plugins/quartz-plugins.schema.json
|
||||
# Template: ttrpg
|
||||
# Obsidian-based setup with map plugin and ITS Theme for TTRPG/D&D wikis.
|
||||
configuration:
|
||||
pageTitle: Quartz 5
|
||||
pageTitleSuffix: ""
|
||||
enableSPA: true
|
||||
enablePopovers: true
|
||||
analytics:
|
||||
provider: plausible
|
||||
locale: en-US
|
||||
baseUrl: quartz.jzhao.xyz
|
||||
ignorePatterns:
|
||||
- private
|
||||
- templates
|
||||
- .obsidian
|
||||
theme:
|
||||
fontOrigin: googleFonts
|
||||
cdnCaching: true
|
||||
typography:
|
||||
header: Schibsted Grotesk
|
||||
body: Source Sans Pro
|
||||
code: IBM Plex Mono
|
||||
colors:
|
||||
lightMode:
|
||||
light: "#faf8f8"
|
||||
lightgray: "#e5e5e5"
|
||||
gray: "#b8b8b8"
|
||||
darkgray: "#4e4e4e"
|
||||
dark: "#2b2b2b"
|
||||
secondary: "#284b63"
|
||||
tertiary: "#84a59d"
|
||||
highlight: rgba(143, 159, 169, 0.15)
|
||||
textHighlight: "#fff23688"
|
||||
darkMode:
|
||||
light: "#161618"
|
||||
lightgray: "#393639"
|
||||
gray: "#646464"
|
||||
darkgray: "#d4d4d4"
|
||||
dark: "#ebebec"
|
||||
secondary: "#7b97aa"
|
||||
tertiary: "#84a59d"
|
||||
highlight: rgba(143, 159, 169, 0.15)
|
||||
textHighlight: "#b3aa0288"
|
||||
plugins:
|
||||
- source: github:quartz-community/created-modified-date
|
||||
enabled: true
|
||||
options:
|
||||
defaultDateType: modified
|
||||
priority:
|
||||
- frontmatter
|
||||
- git
|
||||
- filesystem
|
||||
order: 10
|
||||
- source: github:quartz-community/syntax-highlighting
|
||||
enabled: true
|
||||
options:
|
||||
theme:
|
||||
light: github-light
|
||||
dark: github-dark
|
||||
keepBackground: false
|
||||
order: 20
|
||||
- source: github:quartz-community/obsidian-flavored-markdown
|
||||
enabled: true
|
||||
options:
|
||||
comments: true
|
||||
highlight: true
|
||||
wikilinks: true
|
||||
callouts: true
|
||||
mermaid: true
|
||||
parseTags: true
|
||||
parseArrows: true
|
||||
parseBlockReferences: true
|
||||
enableInHtmlEmbed: false
|
||||
enableYouTubeEmbed: true
|
||||
enableVideoEmbed: true
|
||||
enableCheckbox: true
|
||||
order: 30
|
||||
- source: github:quartz-community/github-flavored-markdown
|
||||
enabled: true
|
||||
order: 40
|
||||
- source: github:quartz-community/table-of-contents
|
||||
enabled: true
|
||||
order: 50
|
||||
layout:
|
||||
position: right
|
||||
priority: 30
|
||||
- source: github:quartz-community/crawl-links
|
||||
enabled: true
|
||||
options:
|
||||
markdownLinkResolution: shortest
|
||||
# disableBrokenWikilinks: false # Set true to add a "broken" CSS class to internal links whose target is not in ctx.allSlugs.
|
||||
order: 60
|
||||
- source: github:quartz-community/description
|
||||
enabled: true
|
||||
order: 70
|
||||
- source: github:quartz-community/latex
|
||||
enabled: true
|
||||
options:
|
||||
renderEngine: katex
|
||||
order: 80
|
||||
- source: github:quartz-community/citations
|
||||
enabled: false
|
||||
order: 85
|
||||
- source: github:quartz-community/hard-line-breaks
|
||||
enabled: true
|
||||
order: 90
|
||||
- source: github:quartz-community/ox-hugo
|
||||
enabled: false
|
||||
order: 91
|
||||
- source: github:quartz-community/roam
|
||||
enabled: false
|
||||
order: 92
|
||||
- source: github:quartz-community/fonts
|
||||
enabled: true
|
||||
- source: github:quartz-community/remove-draft
|
||||
enabled: true
|
||||
- source: github:quartz-community/explicit-publish
|
||||
enabled: false
|
||||
- source: github:quartz-community/unlisted-pages
|
||||
enabled: true
|
||||
options: {}
|
||||
order: 45
|
||||
- source: github:quartz-community/encrypted-pages
|
||||
enabled: true
|
||||
options:
|
||||
iterations: 600000
|
||||
passwordField: password
|
||||
unlistWhenEncrypted: false
|
||||
outputPath: static/encryptedContentIndex.json
|
||||
- source: github:quartz-community/stacked-pages
|
||||
enabled: false
|
||||
layout:
|
||||
position: afterBody
|
||||
priority: 50
|
||||
display: all
|
||||
- source: github:quartz-community/alias-redirects
|
||||
enabled: true
|
||||
- source: github:quartz-community/content-index
|
||||
enabled: true
|
||||
options:
|
||||
enableSiteMap: true
|
||||
enableRSS: true
|
||||
- source: github:quartz-community/favicon
|
||||
enabled: true
|
||||
- source: github:quartz-community/og-image
|
||||
enabled: true
|
||||
- source: github:quartz-community/cname
|
||||
enabled: true
|
||||
- source: github:quartz-community/canvas-page
|
||||
enabled: true
|
||||
- source: github:quartz-community/content-page
|
||||
enabled: true
|
||||
- source: github:quartz-community/folder-page
|
||||
enabled: true
|
||||
- source: github:quartz-community/tag-page
|
||||
enabled: true
|
||||
- source: github:quartz-community/explorer
|
||||
enabled: true
|
||||
layout:
|
||||
position: left
|
||||
priority: 50
|
||||
- source: github:quartz-community/graph
|
||||
enabled: true
|
||||
layout:
|
||||
position: right
|
||||
priority: 10
|
||||
- source: github:quartz-community/search
|
||||
enabled: true
|
||||
layout:
|
||||
position: left
|
||||
priority: 20
|
||||
group: toolbar
|
||||
groupOptions:
|
||||
grow: true
|
||||
- source: github:quartz-community/backlinks
|
||||
enabled: true
|
||||
layout:
|
||||
position: right
|
||||
priority: 50
|
||||
- source: github:quartz-community/article-title
|
||||
enabled: true
|
||||
layout:
|
||||
position: beforeBody
|
||||
priority: 10
|
||||
- source: github:quartz-community/content-meta
|
||||
enabled: true
|
||||
layout:
|
||||
position: beforeBody
|
||||
priority: 20
|
||||
- source: github:quartz-community/tag-list
|
||||
enabled: false
|
||||
layout:
|
||||
position: beforeBody
|
||||
priority: 30
|
||||
- source: github:quartz-community/page-title
|
||||
enabled: true
|
||||
layout:
|
||||
position: left
|
||||
priority: 10
|
||||
- source: github:quartz-community/darkmode
|
||||
enabled: true
|
||||
layout:
|
||||
position: left
|
||||
priority: 30
|
||||
group: toolbar
|
||||
- source: github:quartz-community/reader-mode
|
||||
enabled: true
|
||||
layout:
|
||||
position: left
|
||||
priority: 35
|
||||
group: toolbar
|
||||
- source: github:quartz-community/breadcrumbs
|
||||
enabled: true
|
||||
layout:
|
||||
position: beforeBody
|
||||
priority: 5
|
||||
condition: not-index
|
||||
- source: github:quartz-community/comments
|
||||
enabled: false
|
||||
options:
|
||||
provider: giscus
|
||||
options: {}
|
||||
layout:
|
||||
position: afterBody
|
||||
priority: 10
|
||||
- source: github:quartz-community/footer
|
||||
enabled: true
|
||||
options:
|
||||
links:
|
||||
GitHub: https://github.com/jackyzha0/quartz
|
||||
Discord Community: https://discord.gg/cRFFHYye7t
|
||||
- source: github:quartz-community/recent-notes
|
||||
enabled: false
|
||||
- source: github:quartz-community/spacer
|
||||
enabled: true
|
||||
options: {}
|
||||
order: 25
|
||||
layout:
|
||||
position: left
|
||||
priority: 25
|
||||
display: mobile-only
|
||||
- source: github:quartz-community/bases-page
|
||||
enabled: true
|
||||
options: {}
|
||||
order: 50
|
||||
- source: github:quartz-community/note-properties
|
||||
enabled: true
|
||||
options:
|
||||
includeAll: false
|
||||
includedProperties:
|
||||
- description
|
||||
- tags
|
||||
- aliases
|
||||
excludedProperties: []
|
||||
hidePropertiesView: false
|
||||
delimiters: "---"
|
||||
language: yaml
|
||||
order: 5
|
||||
layout:
|
||||
position: beforeBody
|
||||
priority: 15
|
||||
display: all
|
||||
# TTRPG-specific plugins
|
||||
- source: github:Requiae/quartz-leaflet-bases-plugin
|
||||
enabled: true
|
||||
options:
|
||||
enableCopyTool: false
|
||||
- source:
|
||||
name: quartz-themes
|
||||
repo: github:saberzero1/quartz-themes
|
||||
subdir: plugin
|
||||
enabled: true
|
||||
options:
|
||||
theme: its-theme
|
||||
variation: ttrpg-dnd
|
||||
- source: github:quartz-community/obsidian-plugin-excalidraw
|
||||
enabled: true
|
||||
options:
|
||||
enableInteraction: true
|
||||
darkMode: auto
|
||||
exportPadding: 20
|
||||
order: 50
|
||||
layout:
|
||||
groups:
|
||||
toolbar:
|
||||
priority: 35
|
||||
direction: row
|
||||
gap: 0.5rem
|
||||
byPageType:
|
||||
"404":
|
||||
positions:
|
||||
beforeBody: []
|
||||
left: []
|
||||
right: []
|
||||
content: {}
|
||||
folder:
|
||||
exclude:
|
||||
- reader-mode
|
||||
positions:
|
||||
right: []
|
||||
tag:
|
||||
exclude:
|
||||
- reader-mode
|
||||
positions:
|
||||
right: []
|
||||
canvas: {}
|
||||
bases: {}
|
||||
Reference in New Issue
Block a user