Merge branch 'v4' of github-bfahrenfort:jackyzha0/quartz into v4

This commit is contained in:
bfahrenfort 2023-11-04 12:40:45 -05:00
commit fb20e20122
37 changed files with 186 additions and 90 deletions

View File

@ -45,3 +45,9 @@ jobs:
- name: Ensure Quartz builds, check bundle info - name: Ensure Quartz builds, check bundle info
run: npx quartz build --bundleInfo run: npx quartz build --bundleInfo
- name: Create release tag
uses: Klemensas/action-autotag@stable
with:
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
tag_prefix: "v"

11
Dockerfile Normal file
View File

@ -0,0 +1,11 @@
FROM node:20-slim as builder
WORKDIR /usr/src/app
COPY package.json .
COPY package-lock.json* .
RUN npm ci
FROM node:20-slim
WORKDIR /usr/src/app
COPY --from=builder /usr/src/app/ /usr/src/app/
COPY . .
CMD ["npx", "quartz", "build", "--serve"]

View File

@ -247,7 +247,7 @@ If you are creating an emitter plugin that needs to render components, there are
- Your component should use `getQuartzComponents` to declare a list of `QuartzComponents` that it uses to construct the page. See the page on [[creating components]] for more information. - Your component should use `getQuartzComponents` to declare a list of `QuartzComponents` that it uses to construct the page. See the page on [[creating components]] for more information.
- You can use the `renderPage` function defined in `quartz/components/renderPage.tsx` to render Quartz components into HTML. - You can use the `renderPage` function defined in `quartz/components/renderPage.tsx` to render Quartz components into HTML.
- If you need to render an HTML AST to JSX, you can use the `toJsxRuntime` function from `hast-util-to-jsx-runtime` library. An example of this can be found in `quartz/components/pages/Content.tsx`. - If you need to render an HTML AST to JSX, you can use the `htmlToJsx` function from `quartz/util/jsx.ts`. An example of this can be found in `quartz/components/pages/Content.tsx`.
For example, the following is a simplified version of the content page plugin that renders every single page. For example, the following is a simplified version of the content page plugin that renders every single page.

View File

@ -0,0 +1,7 @@
Quartz comes shipped with a Docker image that will allow you to preview your Quartz locally without installing Node.
You can run the below one-liner to run Quartz in Docker.
```sh
docker run --rm -itp 8080:8080 $(docker build -q .)
```

View File

@ -196,7 +196,7 @@ Component.Explorer({
} }
} }
}, },
}}) })
``` ```
### Putting it all together ### Putting it all together

View File

@ -12,9 +12,17 @@ There may be some notes you want to avoid publishing as a website. Quartz suppor
If you'd like to only publish a select number of notes, you can instead use `Plugin.ExplicitPublish` which will filter out all notes except for any that have `publish: true` in the frontmatter. If you'd like to only publish a select number of notes, you can instead use `Plugin.ExplicitPublish` which will filter out all notes except for any that have `publish: true` in the frontmatter.
> [!warning]
> Regardless of the filter plugin used, **all non-markdown files will be emitted and available publically in the final build.** This includes files such as images, voice recordings, PDFs, etc. One way to prevent this and still be able to embed local images is to create a folder specifically for public media and add the following two patterns to the ignorePatterns array.
>
> `"!(PublicMedia)**/!(*.md)", "!(*.md)"`
## `ignorePatterns` ## `ignorePatterns`
This is a field in `quartz.config.ts` under the main [[configuration]] which allows you to specify a list of patterns to effectively exclude from parsing all together. Any valid [glob](https://github.com/mrmlnc/fast-glob#pattern-syntax) pattern works here. This is a field in `quartz.config.ts` under the main [[configuration]] which allows you to specify a list of patterns to effectively exclude from parsing all together. Any valid [fast-glob](https://github.com/mrmlnc/fast-glob#pattern-syntax) pattern works here.
> [!note]
> Bash's glob syntax is slightly different from fast-glob's and using bash's syntax may lead to unexpected results.
Common examples include: Common examples include:

View File

@ -166,3 +166,56 @@ Using `docs.example.com` is an example of a subdomain. They're a simple way of c
3. Go to the [Vercel Dashboard](https://vercel.com/dashboard) and select your Quartz project. 3. Go to the [Vercel Dashboard](https://vercel.com/dashboard) and select your Quartz project.
4. Go to the Settings tab and then click Domains in the sidebar 4. Go to the Settings tab and then click Domains in the sidebar
5. Enter your subdomain into the field and press Add 5. Enter your subdomain into the field and press Add
## GitLab Pages
You can configure GitLab CI to build and deploy a Quartz 4 project.
In your local Quartz, create a new file `.gitlab-ci.yaml`.
```yaml title=".gitlab-ci.yaml"
stages:
- build
- deploy
variables:
NODE_VERSION: "18.14"
build:
stage: build
rules:
- if: '$CI_COMMIT_REF_NAME == "v4"'
before_script:
- apt-get update -q && apt-get install -y nodejs npm
- npm install -g n
- n $NODE_VERSION
- hash -r
- npm ci
script:
- npx prettier --write .
- npm run check
- npx quartz build
artifacts:
paths:
- public
cache:
paths:
- ~/.npm/
key: "${CI_COMMIT_REF_SLUG}-node-${CI_COMMIT_REF_NAME}"
tags:
- docker
pages:
stage: deploy
rules:
- if: '$CI_COMMIT_REF_NAME == "v4"'
script:
- echo "Deploying to GitLab Pages..."
artifacts:
paths:
- public
```
When `.gitlab-ci.yaml` is commited, GitLab will build and deploy the website as a GitLab Page. You can find the url under `Deploy` -> `Pages` in the sidebar.
By default, the page is private and only visible when logged in to a GitLab account with access to the repository but can be opened in the settings under `Deploy` -> `Pages`.

View File

@ -6,7 +6,7 @@ Quartz is a fast, batteries-included static-site generator that transforms Markd
## 🪴 Get Started ## 🪴 Get Started
Quartz requires **at least [Node](https://nodejs.org/) v18.14** to function correctly. Ensure you have this installed on your machine before continuing. Quartz requires **at least [Node](https://nodejs.org/) v18.14** and `npm` v9.3.1 to function correctly. Ensure you have this installed on your machine before continuing.
Then, in your terminal of choice, enter the following commands line by line: Then, in your terminal of choice, enter the following commands line by line:
@ -30,7 +30,7 @@ This will guide you through initializing your Quartz with content. Once you've d
## 🔧 Features ## 🔧 Features
- [[Obsidian compatibility]], [[full-text search]], [[graph view]], note transclusion, [[wikilinks]], [[backlinks]], [[Latex]], [[syntax highlighting]], [[popover previews]], and [many more](./features) right out of the box - [[Obsidian compatibility]], [[full-text search]], [[graph view]], note transclusion, [[wikilinks]], [[backlinks]], [[Latex]], [[syntax highlighting]], [[popover previews]], [[Docker Support]], and [many more](./features) right out of the box
- Hot-reload for both configuration and content - Hot-reload for both configuration and content
- Simple JSX layouts and [[creating components|page components]] - Simple JSX layouts and [[creating components|page components]]
- [[SPA Routing|Ridiculously fast page loads]] and tiny bundle sizes - [[SPA Routing|Ridiculously fast page loads]] and tiny bundle sizes

View File

@ -9,7 +9,6 @@ Want to see what Quartz can do? Here are some cool community gardens:
- [Brandon Boswell's Garden](https://brandonkboswell.com) - [Brandon Boswell's Garden](https://brandonkboswell.com)
- [Scaling Synthesis - A hypertext research notebook](https://scalingsynthesis.com/) - [Scaling Synthesis - A hypertext research notebook](https://scalingsynthesis.com/)
- [AWAGMI Intern Notes](https://notes.awagmi.xyz/) - [AWAGMI Intern Notes](https://notes.awagmi.xyz/)
- [Course notes for Information Technology Advanced Theory](https://a2itnotes.github.io/quartz/)
- [Data Dictionary 🧠](https://glossary.airbyte.com/) - [Data Dictionary 🧠](https://glossary.airbyte.com/)
- [sspaeti.com's Second Brain](https://brain.sspaeti.com/) - [sspaeti.com's Second Brain](https://brain.sspaeti.com/)
- [oldwinter の数字花园](https://garden.oldwinter.top/) - [oldwinter の数字花园](https://garden.oldwinter.top/)
@ -19,5 +18,6 @@ Want to see what Quartz can do? Here are some cool community gardens:
- [Pelayo Arbues' Notes](https://pelayoarbues.github.io/) - [Pelayo Arbues' Notes](https://pelayoarbues.github.io/)
- [Vince Imbat's Talahardin](https://vinceimbat.com/) - [Vince Imbat's Talahardin](https://vinceimbat.com/)
- [🧠🌳 Chad's Mind Garden](https://www.chadly.net/) - [🧠🌳 Chad's Mind Garden](https://www.chadly.net/)
- [Pedro MC Fernandes's Topo da Mente](https://www.pmcf.xyz/topo-da-mente/)
If you want to see your own on here, submit a [Pull Request adding yourself to this file](https://github.com/jackyzha0/quartz/blob/v4/docs/showcase.md)! If you want to see your own on here, submit a [Pull Request adding yourself to this file](https://github.com/jackyzha0/quartz/blob/v4/docs/showcase.md)!

View File

@ -19,6 +19,7 @@
"profile": "0x -D prof ./quartz/bootstrap-cli.mjs build --concurrency=1" "profile": "0x -D prof ./quartz/bootstrap-cli.mjs build --concurrency=1"
}, },
"engines": { "engines": {
"npm": ">=9.3.1",
"node": ">=18.14" "node": ">=18.14"
}, },
"keywords": [ "keywords": [

View File

@ -1,4 +1,4 @@
import { promises, readFileSync } from "fs" import { promises } from "fs"
import path from "path" import path from "path"
import esbuild from "esbuild" import esbuild from "esbuild"
import chalk from "chalk" import chalk from "chalk"

View File

@ -1,9 +1,9 @@
import { QuartzComponentConstructor, QuartzComponentProps } from "./types" import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
function ArticleTitle({ fileData }: QuartzComponentProps) { function ArticleTitle({ fileData, displayClass }: QuartzComponentProps) {
const title = fileData.frontmatter?.title const title = fileData.frontmatter?.title
if (title) { if (title) {
return <h1 class="article-title">{title}</h1> return <h1 class={`article-title ${displayClass ?? ""}`}>{title}</h1>
} else { } else {
return null return null
} }

View File

@ -2,11 +2,11 @@ import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
import style from "./styles/backlinks.scss" import style from "./styles/backlinks.scss"
import { resolveRelative, simplifySlug } from "../util/path" import { resolveRelative, simplifySlug } from "../util/path"
function Backlinks({ fileData, allFiles }: QuartzComponentProps) { function Backlinks({ fileData, allFiles, displayClass }: QuartzComponentProps) {
const slug = simplifySlug(fileData.slug!) const slug = simplifySlug(fileData.slug!)
const backlinkFiles = allFiles.filter((file) => file.links?.includes(slug)) const backlinkFiles = allFiles.filter((file) => file.links?.includes(slug))
return ( return (
<div class="backlinks"> <div class={`backlinks ${displayClass ?? ""}`}>
<h3>Backlinks</h3> <h3>Backlinks</h3>
<ul class="overflow"> <ul class="overflow">
{backlinkFiles.length > 0 ? ( {backlinkFiles.length > 0 ? (

View File

@ -1,7 +1,6 @@
import { QuartzComponentConstructor, QuartzComponentProps } from "./types" import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
import breadcrumbsStyle from "./styles/breadcrumbs.scss" import breadcrumbsStyle from "./styles/breadcrumbs.scss"
import { FullSlug, SimpleSlug, resolveRelative } from "../util/path" import { FullSlug, SimpleSlug, resolveRelative } from "../util/path"
import { capitalize } from "../util/lang"
import { QuartzPluginData } from "../plugins/vfile" import { QuartzPluginData } from "../plugins/vfile"
type CrumbData = { type CrumbData = {
@ -36,7 +35,10 @@ const defaultOptions: BreadcrumbOptions = {
} }
function formatCrumb(displayName: string, baseSlug: FullSlug, currentSlug: SimpleSlug): CrumbData { function formatCrumb(displayName: string, baseSlug: FullSlug, currentSlug: SimpleSlug): CrumbData {
return { displayName, path: resolveRelative(baseSlug, currentSlug) } return {
displayName: displayName.replaceAll("-", " "),
path: resolveRelative(baseSlug, currentSlug),
}
} }
// given a folderName (e.g. "features"), search for the corresponding `index.md` file // given a folderName (e.g. "features"), search for the corresponding `index.md` file
@ -58,52 +60,49 @@ export default ((opts?: Partial<BreadcrumbOptions>) => {
// Merge options with defaults // Merge options with defaults
const options: BreadcrumbOptions = { ...defaultOptions, ...opts } const options: BreadcrumbOptions = { ...defaultOptions, ...opts }
function Breadcrumbs({ fileData, allFiles }: QuartzComponentProps) { function Breadcrumbs({ fileData, allFiles, displayClass }: QuartzComponentProps) {
// Hide crumbs on root if enabled // Hide crumbs on root if enabled
if (options.hideOnRoot && fileData.slug === "index") { if (options.hideOnRoot && fileData.slug === "index") {
return <></> return <></>
} }
// Format entry for root element // Format entry for root element
const firstEntry = formatCrumb(capitalize(options.rootName), fileData.slug!, "/" as SimpleSlug) const firstEntry = formatCrumb(options.rootName, fileData.slug!, "/" as SimpleSlug)
const crumbs: CrumbData[] = [firstEntry] const crumbs: CrumbData[] = [firstEntry]
// Get parts of filePath (every folder) // Split slug into hierarchy/parts
const parts = fileData.filePath?.split("/")?.splice(1) const slugParts = fileData.slug?.split("/")
if (parts) { if (slugParts) {
// full path until current part // full path until current part
let current = "" let currentPath = ""
for (let i = 0; i < parts.length - 1; i++) { for (let i = 0; i < slugParts.length - 1; i++) {
const folderName = parts[i] let currentTitle = slugParts[i]
let currentTitle = folderName
// TODO: performance optimizations/memoizing // TODO: performance optimizations/memoizing
// Try to resolve frontmatter folder title // Try to resolve frontmatter folder title
if (options?.resolveFrontmatterTitle) { if (options?.resolveFrontmatterTitle) {
// try to find file for current path // try to find file for current path
const currentFile = findCurrentFile(allFiles, folderName) const currentFile = findCurrentFile(allFiles, currentTitle)
if (currentFile) { if (currentFile) {
currentTitle = currentFile.frontmatter!.title currentTitle = currentFile.frontmatter!.title
} }
} }
// Add current path to full path // Add current slug to full path
current += folderName + "/" currentPath += slugParts[i] + "/"
// Format and add current crumb // Format and add current crumb
const crumb = formatCrumb(capitalize(currentTitle), fileData.slug!, current as SimpleSlug) const crumb = formatCrumb(currentTitle, fileData.slug!, currentPath as SimpleSlug)
crumbs.push(crumb) crumbs.push(crumb)
} }
// Add current file to crumb (can directly use frontmatter title) // Add current file to crumb (can directly use frontmatter title)
if (parts.length > 0) { crumbs.push({
crumbs.push({ displayName: fileData.frontmatter!.title,
displayName: capitalize(fileData.frontmatter!.title), path: "",
path: "", })
})
}
} }
return ( return (
<nav class="breadcrumb-container" aria-label="breadcrumbs"> <nav class={`breadcrumb-container ${displayClass ?? ""}`} aria-label="breadcrumbs">
{crumbs.map((crumb, index) => ( {crumbs.map((crumb, index) => (
<div class="breadcrumb-element"> <div class="breadcrumb-element">
<a href={crumb.path}>{crumb.displayName}</a> <a href={crumb.path}>{crumb.displayName}</a>

View File

@ -3,7 +3,7 @@ import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
import readingTime from "reading-time" import readingTime from "reading-time"
export default (() => { export default (() => {
function ContentMetadata({ cfg, fileData }: QuartzComponentProps) { function ContentMetadata({ cfg, fileData, displayClass }: QuartzComponentProps) {
const text = fileData.text const text = fileData.text
if (text) { if (text) {
const segments: string[] = [] const segments: string[] = []
@ -14,7 +14,7 @@ export default (() => {
} }
segments.push(timeTaken) segments.push(timeTaken)
return <p class="content-meta">{segments.join(", ")}</p> return <p class={`content-meta ${displayClass ?? ""}`}>{segments.join(", ")}</p>
} else { } else {
return null return null
} }

View File

@ -3,11 +3,11 @@
// see: https://v8.dev/features/modules#defer // see: https://v8.dev/features/modules#defer
import darkmodeScript from "./scripts/darkmode.inline" import darkmodeScript from "./scripts/darkmode.inline"
import styles from "./styles/darkmode.scss" import styles from "./styles/darkmode.scss"
import { QuartzComponentConstructor } from "./types" import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
function Darkmode() { function Darkmode({ displayClass }: QuartzComponentProps) {
return ( return (
<div class="darkmode"> <div class={`darkmode ${displayClass ?? ""}`}>
<input class="toggle" id="darkmode-toggle" type="checkbox" tabIndex={-1} /> <input class="toggle" id="darkmode-toggle" type="checkbox" tabIndex={-1} />
<label id="toggle-label-light" for="darkmode-toggle" tabIndex={-1}> <label id="toggle-label-light" for="darkmode-toggle" tabIndex={-1}>
<svg <svg

View File

@ -86,7 +86,7 @@ export default ((userOpts?: Partial<Options>) => {
function Explorer({ allFiles, displayClass, fileData }: QuartzComponentProps) { function Explorer({ allFiles, displayClass, fileData }: QuartzComponentProps) {
constructFileTree(allFiles) constructFileTree(allFiles)
return ( return (
<div class={`explorer ${displayClass}`}> <div class={`explorer ${displayClass ?? ""}`}>
<button <button
type="button" type="button"
id="explorer" id="explorer"

View File

@ -14,7 +14,7 @@ export default ((opts?: Optionss) => {
const year = new Date().getFullYear() const year = new Date().getFullYear()
const links = opts?.links ?? [] const links = opts?.links ?? []
return ( return (
<footer> <footer class={`${displayClass ?? ""}`}>
<hr /> <hr />
<p style="margin-bottom:4px;font-weight:bold;font-size:2em;"> <p style="margin-bottom:4px;font-weight:bold;font-size:2em;">
Share your thoughts with <a class="internal" href="/Projects/Obsidian/quartz-comments">Remark42</a> Share your thoughts with <a class="internal" href="/Projects/Obsidian/quartz-comments">Remark42</a>

View File

@ -1,4 +1,4 @@
import { QuartzComponentConstructor } from "./types" import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
// @ts-ignore // @ts-ignore
import script from "./scripts/graph.inline" import script from "./scripts/graph.inline"
import style from "./styles/graph.scss" import style from "./styles/graph.scss"
@ -52,11 +52,11 @@ const defaultOptions: GraphOptions = {
} }
export default ((opts?: GraphOptions) => { export default ((opts?: GraphOptions) => {
function Graph() { function Graph({ displayClass }: QuartzComponentProps) {
const localGraph = { ...defaultOptions.localGraph, ...opts?.localGraph } const localGraph = { ...defaultOptions.localGraph, ...opts?.localGraph }
const globalGraph = { ...defaultOptions.globalGraph, ...opts?.globalGraph } const globalGraph = { ...defaultOptions.globalGraph, ...opts?.globalGraph }
return ( return (
<div class="graph"> <div class={`graph ${displayClass ?? ""}`}>
<h3>Graph View</h3> <h3>Graph View</h3>
<div class="graph-outer"> <div class="graph-outer">
<div id="graph-container" data-cfg={JSON.stringify(localGraph)}></div> <div id="graph-container" data-cfg={JSON.stringify(localGraph)}></div>

View File

@ -1,11 +1,11 @@
import { pathToRoot } from "../util/path" import { pathToRoot } from "../util/path"
import { QuartzComponentConstructor, QuartzComponentProps } from "./types" import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
function PageTitle({ fileData, cfg }: QuartzComponentProps) { function PageTitle({ fileData, cfg, displayClass }: QuartzComponentProps) {
const title = cfg?.pageTitle ?? "Untitled Quartz" const title = cfg?.pageTitle ?? "Untitled Quartz"
const baseDir = pathToRoot(fileData.slug!) const baseDir = pathToRoot(fileData.slug!)
return ( return (
<h1 class="page-title"> <h1 class={`page-title ${displayClass ?? ""}`}>
<a href={baseDir}>{title}</a> <a href={baseDir}>{title}</a>
</h1> </h1>
) )

View File

@ -23,13 +23,12 @@ const defaultOptions = (cfg: GlobalConfiguration): Options => ({
}) })
export default ((userOpts?: Partial<Options>) => { export default ((userOpts?: Partial<Options>) => {
function RecentNotes(props: QuartzComponentProps) { function RecentNotes({ allFiles, fileData, displayClass, cfg }: QuartzComponentProps) {
const { allFiles, fileData, displayClass, cfg } = props
const opts = { ...defaultOptions(cfg), ...userOpts } const opts = { ...defaultOptions(cfg), ...userOpts }
const pages = allFiles.filter(opts.filter).sort(opts.sort) const pages = allFiles.filter(opts.filter).sort(opts.sort)
const remaining = Math.max(0, pages.length - opts.limit) const remaining = Math.max(0, pages.length - opts.limit)
return ( return (
<div class={`recent-notes ${displayClass}`}> <div class={`recent-notes ${displayClass ?? ""}`}>
<h3>{opts.title}</h3> <h3>{opts.title}</h3>
<ul class="recent-ul"> <ul class="recent-ul">
{pages.slice(0, opts.limit).map((page) => { {pages.slice(0, opts.limit).map((page) => {

View File

@ -1,12 +1,12 @@
import { QuartzComponentConstructor } from "./types" import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
import style from "./styles/search.scss" import style from "./styles/search.scss"
// @ts-ignore // @ts-ignore
import script from "./scripts/search.inline" import script from "./scripts/search.inline"
export default (() => { export default (() => {
function Search() { function Search({ displayClass }: QuartzComponentProps) {
return ( return (
<div class="search"> <div class={`search ${displayClass ?? ""}`}>
<div id="search-icon"> <div id="search-icon">
<p>Search</p> <p>Search</p>
<div></div> <div></div>

View File

@ -1,8 +1,7 @@
import { QuartzComponentConstructor, QuartzComponentProps } from "./types" import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
function Spacer({ displayClass }: QuartzComponentProps) { function Spacer({ displayClass }: QuartzComponentProps) {
const className = displayClass ? `spacer ${displayClass}` : "spacer" return <div class={`spacer ${displayClass ?? ""}`}></div>
return <div class={className}></div>
} }
export default (() => Spacer) satisfies QuartzComponentConstructor export default (() => Spacer) satisfies QuartzComponentConstructor

View File

@ -19,7 +19,7 @@ function TableOfContents({ fileData, displayClass }: QuartzComponentProps) {
} }
return ( return (
<div class={`toc ${displayClass}`}> <div class={`toc ${displayClass ?? ""}`}>
<button type="button" id="toc"> <button type="button" id="toc">
<h3>On This Page</h3> <h3>On This Page</h3>
<svg <svg

View File

@ -1,12 +1,12 @@
import { pathToRoot, slugTag } from "../util/path" import { pathToRoot, slugTag } from "../util/path"
import { QuartzComponentConstructor, QuartzComponentProps } from "./types" import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
function TagList({ fileData }: QuartzComponentProps) { function TagList({ fileData, displayClass }: QuartzComponentProps) {
const tags = fileData.frontmatter?.tags const tags = fileData.frontmatter?.tags
const baseDir = pathToRoot(fileData.slug!) const baseDir = pathToRoot(fileData.slug!)
if (tags && tags.length > 0) { if (tags && tags.length > 0) {
return ( return (
<ul class="tags"> <ul class={`tags ${displayClass ?? ""}`}>
{tags.map((tag) => { {tags.map((tag) => {
const display = `#${tag}` const display = `#${tag}`
const linkDest = baseDir + `/tags/${slugTag(tag)}` const linkDest = baseDir + `/tags/${slugTag(tag)}`
@ -28,11 +28,16 @@ function TagList({ fileData }: QuartzComponentProps) {
TagList.css = ` TagList.css = `
.tags { .tags {
list-style: none; list-style: none;
display:flex; display: flex;
flex-wrap: wrap;
padding-left: 0; padding-left: 0;
gap: 0.4rem; gap: 0.4rem;
margin: 1rem 0; margin: 1rem 0;
flex-wrap: wrap;
justify-self: end;
}
.section-li > .section > .tags {
justify-content: flex-end;
} }
.tags > li { .tags > li {
@ -42,7 +47,7 @@ TagList.css = `
overflow-wrap: normal; overflow-wrap: normal;
} }
a.tag-link { a.internal.tag-link {
border-radius: 8px; border-radius: 8px;
background-color: var(--highlight); background-color: var(--highlight);
padding: 0.2rem 0.4rem; padding: 0.2rem 0.4rem;

View File

@ -1,10 +1,8 @@
import { htmlToJsx } from "../../util/jsx"
import { QuartzComponentConstructor, QuartzComponentProps } from "../types" import { QuartzComponentConstructor, QuartzComponentProps } from "../types"
import { Fragment, jsx, jsxs } from "preact/jsx-runtime"
import { toJsxRuntime } from "hast-util-to-jsx-runtime"
function Content({ tree }: QuartzComponentProps) { function Content({ fileData, tree }: QuartzComponentProps) {
// @ts-ignore (preact makes it angry) const content = htmlToJsx(fileData.filePath!, tree)
const content = toJsxRuntime(tree, { Fragment, jsx, jsxs, elementAttributeNameCase: "html" })
return <article class="popover-hint">{content}</article> return <article class="popover-hint">{content}</article>
} }

View File

@ -1,6 +1,4 @@
import { QuartzComponentConstructor, QuartzComponentProps } from "../types" import { QuartzComponentConstructor, QuartzComponentProps } from "../types"
import { Fragment, jsx, jsxs } from "preact/jsx-runtime"
import { toJsxRuntime } from "hast-util-to-jsx-runtime"
import path from "path" import path from "path"
import style from "../styles/listPage.scss" import style from "../styles/listPage.scss"
@ -8,6 +6,7 @@ import { PageList } from "../PageList"
import { _stripSlashes, simplifySlug } from "../../util/path" import { _stripSlashes, simplifySlug } from "../../util/path"
import { Root } from "hast" import { Root } from "hast"
import { pluralize } from "../../util/lang" import { pluralize } from "../../util/lang"
import { htmlToJsx } from "../../util/jsx"
function FolderContent(props: QuartzComponentProps) { function FolderContent(props: QuartzComponentProps) {
const { tree, fileData, allFiles } = props const { tree, fileData, allFiles } = props
@ -29,8 +28,7 @@ function FolderContent(props: QuartzComponentProps) {
const content = const content =
(tree as Root).children.length === 0 (tree as Root).children.length === 0
? fileData.description ? fileData.description
: // @ts-ignore : htmlToJsx(fileData.filePath!, tree)
toJsxRuntime(tree, { Fragment, jsx, jsxs, elementAttributeNameCase: "html" })
return ( return (
<div class="popover-hint"> <div class="popover-hint">

View File

@ -1,12 +1,11 @@
import { QuartzComponentConstructor, QuartzComponentProps } from "../types" import { QuartzComponentConstructor, QuartzComponentProps } from "../types"
import { Fragment, jsx, jsxs } from "preact/jsx-runtime"
import { toJsxRuntime } from "hast-util-to-jsx-runtime"
import style from "../styles/listPage.scss" import style from "../styles/listPage.scss"
import { PageList } from "../PageList" import { PageList } from "../PageList"
import { FullSlug, getAllSegmentPrefixes, simplifySlug } from "../../util/path" import { FullSlug, getAllSegmentPrefixes, simplifySlug } from "../../util/path"
import { QuartzPluginData } from "../../plugins/vfile" import { QuartzPluginData } from "../../plugins/vfile"
import { Root } from "hast" import { Root } from "hast"
import { pluralize } from "../../util/lang" import { pluralize } from "../../util/lang"
import { htmlToJsx } from "../../util/jsx"
const numPages = 10 const numPages = 10
function TagContent(props: QuartzComponentProps) { function TagContent(props: QuartzComponentProps) {
@ -26,8 +25,7 @@ function TagContent(props: QuartzComponentProps) {
const content = const content =
(tree as Root).children.length === 0 (tree as Root).children.length === 0
? fileData.description ? fileData.description
: // @ts-ignore : htmlToJsx(fileData.filePath!, tree)
toJsxRuntime(tree, { Fragment, jsx, jsxs, elementAttributeNameCase: "html" })
if (tag === "") { if (tag === "") {
const tags = [...new Set(allFiles.flatMap((data) => data.frontmatter?.tags ?? []))] const tags = [...new Set(allFiles.flatMap((data) => data.frontmatter?.tags ?? []))]

View File

@ -303,7 +303,6 @@ document.addEventListener("nav", async (e: unknown) => {
// setup index if it hasn't been already // setup index if it hasn't been already
if (!index) { if (!index) {
index = new Document({ index = new Document({
cache: true,
charset: "latin:extra", charset: "latin:extra",
optimize: true, optimize: true,
encode: encoder, encode: encoder,

View File

@ -20,6 +20,7 @@ const isLocalUrl = (href: string) => {
const getOpts = ({ target }: Event): { url: URL; scroll?: boolean } | undefined => { const getOpts = ({ target }: Event): { url: URL; scroll?: boolean } | undefined => {
if (!isElement(target)) return if (!isElement(target)) return
if (target.attributes.getNamedItem("target")?.value === "_blank") return
const a = target.closest("a") const a = target.closest("a")
if (!a) return if (!a) return
if ("routerIgnore" in a.dataset) return if ("routerIgnore" in a.dataset) return
@ -92,7 +93,7 @@ function createRouter() {
if (typeof window !== "undefined") { if (typeof window !== "undefined") {
window.addEventListener("click", async (event) => { window.addEventListener("click", async (event) => {
const { url } = getOpts(event) ?? {} const { url } = getOpts(event) ?? {}
if (!url) return if (!url || event.ctrlKey || event.metaKey) return
event.preventDefault() event.preventDefault()
try { try {
navigate(url, false) navigate(url, false)

View File

@ -19,11 +19,6 @@ li.section-li {
} }
} }
& > .tags {
justify-self: end;
margin-left: 1rem;
}
& > .desc > h3 > a { & > .desc > h3 > a {
background-color: transparent; background-color: transparent;
} }

View File

@ -7,7 +7,7 @@ import spaRouterScript from "../../components/scripts/spa.inline"
import plausibleScript from "../../components/scripts/plausible.inline" import plausibleScript from "../../components/scripts/plausible.inline"
// @ts-ignore // @ts-ignore
import popoverScript from "../../components/scripts/popover.inline" import popoverScript from "../../components/scripts/popover.inline"
import styles from "../../styles/base.scss" import styles from "../../styles/custom.scss"
import popoverStyle from "../../components/styles/popover.scss" import popoverStyle from "../../components/styles/popover.scss"
import { BuildCtx } from "../../util/ctx" import { BuildCtx } from "../../util/ctx"
import { StaticResources } from "../../util/resources" import { StaticResources } from "../../util/resources"
@ -164,7 +164,7 @@ export const ComponentResources: QuartzEmitterPlugin<Options> = (opts?: Partial<
addGlobalPageResources(ctx, resources, componentResources) addGlobalPageResources(ctx, resources, componentResources)
const stylesheet = joinStyles(ctx.cfg.configuration.theme, styles, ...componentResources.css) const stylesheet = joinStyles(ctx.cfg.configuration.theme, ...componentResources.css, styles)
const prescript = joinScripts(componentResources.beforeDOMLoaded) const prescript = joinScripts(componentResources.beforeDOMLoaded)
const postscript = joinScripts(componentResources.afterDOMLoaded) const postscript = joinScripts(componentResources.afterDOMLoaded)
const fps = await Promise.all([ const fps = await Promise.all([

View File

@ -18,11 +18,13 @@ interface Options {
markdownLinkResolution: TransformOptions["strategy"] markdownLinkResolution: TransformOptions["strategy"]
/** Strips folders from a link so that it looks nice */ /** Strips folders from a link so that it looks nice */
prettyLinks: boolean prettyLinks: boolean
openLinksInNewTab: boolean
} }
const defaultOptions: Options = { const defaultOptions: Options = {
markdownLinkResolution: "absolute", markdownLinkResolution: "absolute",
prettyLinks: true, prettyLinks: true,
openLinksInNewTab: false,
} }
export const CrawlLinks: QuartzTransformerPlugin<Partial<Options> | undefined> = (userOpts) => { export const CrawlLinks: QuartzTransformerPlugin<Partial<Options> | undefined> = (userOpts) => {
@ -52,6 +54,10 @@ export const CrawlLinks: QuartzTransformerPlugin<Partial<Options> | undefined> =
node.properties.className ??= [] node.properties.className ??= []
node.properties.className.push(isAbsoluteUrl(dest) ? "external" : "internal") node.properties.className.push(isAbsoluteUrl(dest) ? "external" : "internal")
if (opts.openLinksInNewTab) {
node.properties.target = "_blank"
}
// don't process external links or intra-document anchors // don't process external links or intra-document anchors
const isInternal = !(isAbsoluteUrl(dest) || dest.startsWith("#")) const isInternal = !(isAbsoluteUrl(dest) || dest.startsWith("#"))
if (isInternal) { if (isInternal) {

View File

@ -1,7 +1,6 @@
@use "./custom.scss"; @use "./variables.scss" as *;
@use "./syntax.scss"; @use "./syntax.scss";
@use "./callouts.scss"; @use "./callouts.scss";
@use "./variables.scss" as *;
html { html {
scroll-behavior: smooth; scroll-behavior: smooth;
@ -95,6 +94,8 @@ a {
} }
& article { & article {
position: relative;
& > h1 { & > h1 {
font-size: 2rem; font-size: 2rem;
} }
@ -328,7 +329,6 @@ pre {
&:has(> code.mermaid) { &:has(> code.mermaid) {
border: none; border: none;
position: relative;
} }
& > code { & > code {

View File

@ -1 +1,3 @@
@use "./base.scss";
// put your custom CSS here! // put your custom CSS here!

15
quartz/util/jsx.ts Normal file
View File

@ -0,0 +1,15 @@
import { toJsxRuntime } from "hast-util-to-jsx-runtime"
import { QuartzPluginData } from "../plugins/vfile"
import { Node, Root } from "hast"
import { Fragment, jsx, jsxs } from "preact/jsx-runtime"
import { trace } from "./trace"
import { type FilePath } from "./path"
export function htmlToJsx(fp: FilePath, tree: Node<QuartzPluginData>) {
try {
// @ts-ignore (preact makes it angry)
return toJsxRuntime(tree as Root, { Fragment, jsx, jsxs, elementAttributeNameCase: "html" })
} catch (e) {
trace(`Failed to parse Markdown in \`${fp}\` into JSX`, e as Error)
}
}

View File

@ -4,7 +4,7 @@ import { isMainThread } from "workerpool"
const rootFile = /.*at file:/ const rootFile = /.*at file:/
export function trace(msg: string, err: Error) { export function trace(msg: string, err: Error) {
const stack = err.stack let stack = err.stack ?? ""
const lines: string[] = [] const lines: string[] = []
@ -12,15 +12,11 @@ export function trace(msg: string, err: Error) {
lines.push( lines.push(
"\n" + "\n" +
chalk.bgRed.black.bold(" ERROR ") + chalk.bgRed.black.bold(" ERROR ") +
"\n" + "\n\n" +
chalk.red(` ${msg}`) + chalk.red(` ${msg}`) +
(err.message.length > 0 ? `: ${err.message}` : ""), (err.message.length > 0 ? `: ${err.message}` : ""),
) )
if (!stack) {
return
}
let reachedEndOfLegibleTrace = false let reachedEndOfLegibleTrace = false
for (const line of stack.split("\n").slice(1)) { for (const line of stack.split("\n").slice(1)) {
if (reachedEndOfLegibleTrace) { if (reachedEndOfLegibleTrace) {