diff --git a/package.json b/package.json index 950ea18..1b258a5 100644 --- a/package.json +++ b/package.json @@ -47,6 +47,7 @@ "esbuild-sass-plugin": "^3.6.0", "github-slugger": "^2.0.0", "globby": "^16.1.0", + "hast-util-from-html": "^2.0.0", "hast-util-to-jsx-runtime": "^2.3.6", "isomorphic-git": "^1.36.3", "lightningcss": "^1.31.1", diff --git a/quartz.config.yaml b/quartz.config.yaml new file mode 100644 index 0000000..a087b99 --- /dev/null +++ b/quartz.config.yaml @@ -0,0 +1,279 @@ +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: ../created-modified-date + enabled: true + options: + defaultDateType: modified + priority: + - frontmatter + - git + - filesystem + order: 10 + - source: ../syntax-highlighting + enabled: true + options: + theme: + light: github-light + dark: github-dark + keepBackground: false + order: 20 + - source: ../obsidian-flavored-markdown + enabled: true + options: + enableInHtmlEmbed: false + enableCheckbox: true + order: 30 + - source: ../github-flavored-markdown + enabled: true + order: 40 + - source: ../table-of-contents + enabled: true + order: 50 + layout: + position: right + priority: 30 + - source: ../crawl-links + enabled: true + options: + markdownLinkResolution: shortest + order: 60 + - source: ../description + enabled: true + order: 70 + - source: ../latex + enabled: true + options: + renderEngine: katex + order: 80 + - source: ../citations + enabled: false + order: 85 + - source: ../hard-line-breaks + enabled: false + order: 90 + - source: ../ox-hugo + enabled: false + order: 91 + - source: ../roam + enabled: false + order: 92 + - source: ../fonts + enabled: true + - source: ../remove-draft + enabled: true + - source: ../explicit-publish + enabled: false + - source: ../alias-redirects + enabled: true + - source: ../content-index + enabled: true + options: + enableSiteMap: true + enableRSS: true + - source: ../favicon + enabled: true + - source: ../og-image + enabled: true + - source: ../cname + enabled: true + - source: ../canvas-page + enabled: true + - source: ../content-page + enabled: true + - source: ../folder-page + enabled: true + - source: ../tag-page + enabled: true + - source: ../explorer + enabled: true + layout: + position: left + priority: 50 + - source: ../graph + enabled: true + layout: + position: right + priority: 10 + - source: ../search + enabled: true + layout: + position: left + priority: 20 + group: toolbar + groupOptions: + grow: true + - source: ../backlinks + enabled: true + layout: + position: right + priority: 50 + - source: ../article-title + enabled: true + layout: + position: beforeBody + priority: 10 + - source: ../content-meta + enabled: true + layout: + position: beforeBody + priority: 20 + - source: ../tag-list + enabled: false + layout: + position: beforeBody + priority: 30 + - source: ../page-title + enabled: true + layout: + position: left + priority: 10 + - source: ../darkmode + enabled: true + layout: + position: left + priority: 30 + group: toolbar + - source: ../reader-mode + enabled: true + layout: + position: left + priority: 35 + group: toolbar + - source: ../breadcrumbs + enabled: true + layout: + position: beforeBody + priority: 5 + condition: not-index + - source: ../comments + enabled: false + options: + provider: giscus + options: {} + layout: + position: afterBody + priority: 10 + - source: ../footer + enabled: true + options: + links: + GitHub: https://github.com/jackyzha0/quartz + Discord Community: https://discord.gg/cRFFHYye7t + - source: ../recent-notes + enabled: false + - source: ../spacer + enabled: true + options: {} + order: 25 + layout: + position: left + priority: 25 + display: mobile-only + - source: ../bases-page + enabled: true + options: {} + order: 50 + - source: ../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: ../unlisted-pages + enabled: true + options: {} + order: 45 + - source: ../encrypted-pages + enabled: true + options: + iterations: 600000 + passwordField: password + unlistWhenEncrypted: false + outputPath: static/encryptedContentIndex.json + order: 900 + layout: + position: body + priority: 100 + display: all + - source: ../stacked-pages + enabled: false + layout: + position: afterBody + priority: 50 + 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: {} diff --git a/quartz/cli/plugin-data.js b/quartz/cli/plugin-data.js index c8cd6b4..d19d3b1 100644 --- a/quartz/cli/plugin-data.js +++ b/quartz/cli/plugin-data.js @@ -190,6 +190,11 @@ export function parseGitSource(source) { typeof source === "object" && source.name ? source.name : path.basename(parsed, ".git") return { name, url: parsed, ref, subdir } } + // Handle npm scoped packages + if (typeof url === "string" && url.startsWith("@") && url.includes("/") && !url.includes(":")) { + const name = typeof source === "object" && source.name ? source.name : url + return { name, url: "", npmPackage: true, subdir } + } throw new Error(`Cannot parse plugin source: ${formatSource(source)}`) } diff --git a/quartz/cli/plugin-git-handlers.js b/quartz/cli/plugin-git-handlers.js index fb475cd..81d1ef0 100644 --- a/quartz/cli/plugin-git-handlers.js +++ b/quartz/cli/plugin-git-handlers.js @@ -1,7 +1,7 @@ import fs from "fs" import path from "path" import os from "os" -import { exec as execCb } from "child_process" +import { exec as execCb, execSync } from "child_process" import { styleText, promisify } from "util" import { readPluginsJson, @@ -1190,6 +1190,15 @@ export async function handlePluginAdd( for (const source of sources) { try { const parsed = parseGitSource(source) + if (parsed.npmPackage) { + const name = nameOverride ?? parsed.name + console.log(styleText("cyan", `→ Installing ${name} from npm...`)) + execSync(`npm install ${parsed.name}`, { cwd: process.cwd(), stdio: "inherit" }) + const configSource = nameOverride ? { repo: parsed.name, name: nameOverride } : parsed.name + const pluginDir = path.join(process.cwd(), "node_modules", ...parsed.name.split("/")) + addedPlugins.push({ name, pluginDir, source: parsed.name, configSource }) + continue + } const name = nameOverride ?? parsed.name const url = parsed.url const ref = parsed.ref diff --git a/quartz/plugins/loader/config-loader.ts b/quartz/plugins/loader/config-loader.ts index 4480a0a..a6a65a9 100644 --- a/quartz/plugins/loader/config-loader.ts +++ b/quartz/plugins/loader/config-loader.ts @@ -2,6 +2,7 @@ import fs from "fs" import path from "path" import YAML from "yaml" import { styleText } from "util" +import { createRequire } from "node:module" import { QuartzConfig, GlobalConfiguration, FullPageLayout } from "../../cfg" import { QuartzComponent, QuartzComponentConstructor } from "../../components/types" import { PluginTypes } from "../types" @@ -200,8 +201,14 @@ async function resolvePluginManifest(source: PluginSource): Promise { try { const gitSpec = parsePluginSource(source) - const pluginDir = path.join(process.cwd(), ".quartz", "plugins", gitSpec.name) - const pkgPath = path.join(pluginDir, "package.json") + const require = createRequire(import.meta.url) + let pkgPath: string + if (gitSpec.npmPackage) { + pkgPath = require.resolve(`${gitSpec.name}/package.json`, { paths: [process.cwd()] }) + } else { + const pluginDir = path.join(process.cwd(), ".quartz", "plugins", gitSpec.name) + pkgPath = path.join(pluginDir, "package.json") + } if (!fs.existsSync(pkgPath)) return null const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8")) @@ -259,6 +266,9 @@ export async function loadQuartzConfig( for (const entry of enabledEntries) { try { const gitSpec = parsePluginSource(entry.source) + if (gitSpec.npmPackage) { + continue + } const result = await installPlugin(gitSpec, { verbose: false }) if (result.nativeDeps.size > 0) { allNativeDeps.set(gitSpec.name, result.nativeDeps) diff --git a/quartz/plugins/loader/gitLoader.ts b/quartz/plugins/loader/gitLoader.ts index 20fbb43..fff95ce 100644 --- a/quartz/plugins/loader/gitLoader.ts +++ b/quartz/plugins/loader/gitLoader.ts @@ -31,6 +31,8 @@ export interface GitPluginSpec { subdir?: string /** Whether this is a local path source */ local?: boolean + /** Whether this is an npm package (installed in node_modules) */ + npmPackage?: boolean } export type PluginInstallSource = string | GitPluginSpec @@ -87,6 +89,7 @@ export function parsePluginSource(source: PluginSource): GitPluginSpec { ref: ref || expanded.ref || undefined, subdir, local: expanded.local, + npmPackage: expanded.npmPackage, } } @@ -129,6 +132,11 @@ export function parsePluginSource(source: PluginSource): GitPluginSpec { return { name, repo: url, ref: ref || undefined } } + // Handle npm scoped packages (e.g. @quartz-community/syntax-highlighting) + if (typeof source === "string" && source.startsWith("@") && source.includes("/") && !source.includes(":")) { + return { name: source, repo: "", npmPackage: true } + } + // Assume it's a plain repo name and try github const parts = source.split("/") if (parts.length === 2) { diff --git a/quartz/plugins/loader/install-plugins.ts b/quartz/plugins/loader/install-plugins.ts index 7b554a5..1ab15e3 100644 --- a/quartz/plugins/loader/install-plugins.ts +++ b/quartz/plugins/loader/install-plugins.ts @@ -1,19 +1,68 @@ #!/usr/bin/env node +import fs from "fs" +import path from "path" +import YAML from "yaml" import { installPlugins, parsePluginSource } from "./gitLoader.js" -import config from "../../../quartz.js" +import type { PluginSource, QuartzPluginsJson } from "./types.js" + +function resolveConfigPath(): string { + const configYamlPath = path.join(process.cwd(), "quartz.config.yaml") + const defaultConfigYamlPath = path.join(process.cwd(), "quartz.config.default.yaml") + const legacyPluginsJsonPath = path.join(process.cwd(), "quartz.plugins.json") + const legacyDefaultPluginsJsonPath = path.join(process.cwd(), "quartz.plugins.default.json") + + if (fs.existsSync(configYamlPath)) return configYamlPath + if (fs.existsSync(legacyPluginsJsonPath)) return legacyPluginsJsonPath + if (fs.existsSync(defaultConfigYamlPath)) return defaultConfigYamlPath + if (fs.existsSync(legacyDefaultPluginsJsonPath)) return legacyDefaultPluginsJsonPath + return configYamlPath +} + +function readPluginsJson(): QuartzPluginsJson | null { + const configPath = resolveConfigPath() + if (!fs.existsSync(configPath)) return null + const raw = fs.readFileSync(configPath, "utf-8") + if (configPath.endsWith(".yaml") || configPath.endsWith(".yml")) { + return YAML.parse(raw) + } + return JSON.parse(raw) +} + +async function getExternalPluginSources(): Promise { + try { + const module = await import("../../../quartz.js") + const config = module.default ?? module + const externalPlugins = config.externalPlugins + if (Array.isArray(externalPlugins) && externalPlugins.length > 0) { + return externalPlugins as PluginSource[] + } + } catch { + // fall back to config yaml parsing + } + + const pluginsJson = readPluginsJson() + const entries = pluginsJson?.plugins ?? [] + return entries.filter((entry) => entry.enabled !== false).map((entry) => entry.source) +} async function main() { - const quartzConfig: any = config - const externalPlugins = quartzConfig.externalPlugins || [] + const externalPlugins = await getExternalPluginSources() if (externalPlugins.length === 0) { console.log("No external plugins to install.") return } - console.log(`Installing ${externalPlugins.length} plugin(s) from Git...`) + const specs = externalPlugins + .map((source: PluginSource) => parsePluginSource(source)) + .filter((spec) => !spec.npmPackage) - const specs = externalPlugins.map((source: string) => parsePluginSource(source)) + if (specs.length === 0) { + console.log("No external Git plugins to install.") + return + } + + console.log(`Installing ${specs.length} plugin(s) from Git...`) const installed = await installPlugins(specs, { verbose: true }) if (installed.size === externalPlugins.length) {