Merge branch 'jackyzha0:v4' into v4

This commit is contained in:
DongDong Chen 2023-11-12 19:39:23 +08:00 committed by GitHub
commit 998fa89027
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
33 changed files with 255 additions and 87 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

@ -228,7 +228,7 @@ export type QuartzEmitterPluginInstance = {
An emitter plugin must define a `name` field an `emit` function and a `getQuartzComponents` function. `emit` is responsible for looking at all the parsed and filtered content and then appropriately creating files and returning a list of paths to files the plugin created. An emitter plugin must define a `name` field an `emit` function and a `getQuartzComponents` function. `emit` is responsible for looking at all the parsed and filtered content and then appropriately creating files and returning a list of paths to files the plugin created.
Creating new files can be done via regular Node [fs module](https://nodejs.org/api/fs.html) (i.e. `fs.cp` or `fs.writeFile`) or via the `emitCallback` if you are creating files that contain text. The `emitCallback` function is the 4th argument of the emit function. It's interface looks something like this: Creating new files can be done via regular Node [fs module](https://nodejs.org/api/fs.html) (i.e. `fs.cp` or `fs.writeFile`) or via the `emitCallback` if you are creating files that contain text. The `emitCallback` function is the 4th argument of the emit function. Its interface looks something like this:
```ts ```ts
export type EmitCallback = (data: { export type EmitCallback = (data: {
@ -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

@ -16,10 +16,10 @@ For example, here's what the default configuration looks like:
```typescript title="quartz.layout.ts" ```typescript title="quartz.layout.ts"
Component.Breadcrumbs({ Component.Breadcrumbs({
spacerSymbol: ">", // symbol between crumbs spacerSymbol: "", // symbol between crumbs
rootName: "Home", // name of first/root element rootName: "Home", // name of first/root element
resolveFrontmatterTitle: false, // wether to resolve folder names through frontmatter titles (more computationally expensive) resolveFrontmatterTitle: true, // whether to resolve folder names through frontmatter titles
hideOnRoot: true, // wether to hide breadcrumbs on root `index.md` page hideOnRoot: true, // whether to hide breadcrumbs on root `index.md` page
}) })
``` ```

View File

@ -33,7 +33,7 @@ See [documentation on supported types and syntax here](https://help.obsidian.md
> [!question]+ Can callouts be nested? > [!question]+ Can callouts be nested?
> >
> > [!todo]- Yes!, they can. > > [!todo]- Yes!, they can. And collapsed!
> > > >
> > > [!example] You can even use multiple layers of nesting. > > > [!example] You can even use multiple layers of nesting.

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

@ -8,7 +8,7 @@ tags:
Quartz can automatically generate a table of contents from a list of headings on each page. It will also show you your current scroll position on the site by marking headings you've scrolled through with a different colour. Quartz can automatically generate a table of contents from a list of headings on each page. It will also show you your current scroll position on the site by marking headings you've scrolled through with a different colour.
By default, it will show all headers from H1 (`# Title`) all the way to H3 (`### Title`) and will only show the table of contents if there is more than 1 header on the page. By default, it will show all headers from H1 (`# Title`) all the way to H3 (`### Title`) and will only show the table of contents if there is more than 1 header on the page.
You can also hide the table of contents on a page by adding `showToc: false` to the frontmatter for that page. You can also hide the table of contents on a page by adding `enableToc: false` to the frontmatter for that page.
> [!info] > [!info]
> This feature requires both `Plugin.TableOfContents` in your `quartz.config.ts` and `Component.TableOfContents` in your `quartz.layout.ts` to function correctly. > This feature requires both `Plugin.TableOfContents` in your `quartz.config.ts` and `Component.TableOfContents` in your `quartz.layout.ts` to function correctly.
@ -18,6 +18,7 @@ You can also hide the table of contents on a page by adding `showToc: false` to
- Removing table of contents: remove all instances of `Plugin.TableOfContents()` from `quartz.config.ts`. and `Component.TableOfContents()` from `quartz.layout.ts` - Removing table of contents: remove all instances of `Plugin.TableOfContents()` from `quartz.config.ts`. and `Component.TableOfContents()` from `quartz.layout.ts`
- Changing the max depth: pass in a parameter to `Plugin.TableOfContents({ maxDepth: 4 })` - Changing the max depth: pass in a parameter to `Plugin.TableOfContents({ maxDepth: 4 })`
- Changing the minimum number of entries in the Table of Contents before it renders: pass in a parameter to `Plugin.TableOfContents({ minEntries: 3 })` - Changing the minimum number of entries in the Table of Contents before it renders: pass in a parameter to `Plugin.TableOfContents({ minEntries: 3 })`
- Collapse the table of content by default: pass in a parameter to `Plugin.TableOfContents({ collapseByDefault: true })`
- Component: `quartz/components/TableOfContents.tsx` - Component: `quartz/components/TableOfContents.tsx`
- Style: - Style:
- Modern (default): `quartz/components/styles/toc.scss` - Modern (default): `quartz/components/styles/toc.scss`

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

@ -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,7 @@ 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/)
- [Mau Camargo's Notkesto](https://notes.camargomau.com/)
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

@ -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

@ -28,9 +28,9 @@ interface BreadcrumbOptions {
} }
const defaultOptions: BreadcrumbOptions = { const defaultOptions: BreadcrumbOptions = {
spacerSymbol: ">", spacerSymbol: "",
rootName: "Home", rootName: "Home",
resolveFrontmatterTitle: false, resolveFrontmatterTitle: true,
hideOnRoot: true, hideOnRoot: true,
} }
@ -41,25 +41,13 @@ function formatCrumb(displayName: string, baseSlug: FullSlug, currentSlug: Simpl
} }
} }
// given a folderName (e.g. "features"), search for the corresponding `index.md` file
function findCurrentFile(allFiles: QuartzPluginData[], folderName: string) {
return allFiles.find((file) => {
if (file.slug?.endsWith("index")) {
const folderParts = file.filePath?.split("/")
if (folderParts) {
const name = folderParts[folderParts?.length - 2]
if (name === folderName) {
return true
}
}
}
})
}
export default ((opts?: Partial<BreadcrumbOptions>) => { export default ((opts?: Partial<BreadcrumbOptions>) => {
// Merge options with defaults // Merge options with defaults
const options: BreadcrumbOptions = { ...defaultOptions, ...opts } const options: BreadcrumbOptions = { ...defaultOptions, ...opts }
// computed index of folder name to its associated file data
let folderIndex: Map<string, QuartzPluginData> | undefined
function Breadcrumbs({ fileData, allFiles, displayClass }: 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") {
@ -70,28 +58,39 @@ export default ((opts?: Partial<BreadcrumbOptions>) => {
const firstEntry = formatCrumb(options.rootName, fileData.slug!, "/" as SimpleSlug) const firstEntry = formatCrumb(options.rootName, fileData.slug!, "/" as SimpleSlug)
const crumbs: CrumbData[] = [firstEntry] const crumbs: CrumbData[] = [firstEntry]
if (!folderIndex && options.resolveFrontmatterTitle) {
folderIndex = new Map()
// construct the index for the first time
for (const file of allFiles) {
if (file.slug?.endsWith("index")) {
const folderParts = file.filePath?.split("/")
if (folderParts) {
const folderName = folderParts[folderParts?.length - 2]
folderIndex.set(folderName, file)
}
}
}
}
// Split slug into hierarchy/parts // Split slug into hierarchy/parts
const slugParts = fileData.slug?.split("/") const slugParts = fileData.slug?.split("/")
if (slugParts) { if (slugParts) {
// full path until current part // full path until current part
let currentPath = "" let currentPath = ""
for (let i = 0; i < slugParts.length - 1; i++) { for (let i = 0; i < slugParts.length - 1; i++) {
let currentTitle = slugParts[i] let curPathSegment = slugParts[i]
// TODO: performance optimizations/memoizing
// Try to resolve frontmatter folder title // Try to resolve frontmatter folder title
if (options?.resolveFrontmatterTitle) { const currentFile = folderIndex?.get(curPathSegment)
// try to find file for current path
const currentFile = findCurrentFile(allFiles, currentTitle)
if (currentFile) { if (currentFile) {
currentTitle = currentFile.frontmatter!.title curPathSegment = currentFile.frontmatter!.title
}
} }
// Add current slug to full path // Add current slug to full path
currentPath += slugParts[i] + "/" currentPath += slugParts[i] + "/"
// Format and add current crumb // Format and add current crumb
const crumb = formatCrumb(currentTitle, fileData.slug!, currentPath as SimpleSlug) const crumb = formatCrumb(curPathSegment, fileData.slug!, currentPath as SimpleSlug)
crumbs.push(crumb) crumbs.push(crumb)
} }

View File

@ -20,7 +20,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" class={fileData.collapseToc ? "collapsed" : ""}>
<h3>Table of Contents</h3> <h3>Table of Contents</h3>
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
@ -60,7 +60,7 @@ function LegacyTableOfContents({ fileData }: QuartzComponentProps) {
} }
return ( return (
<details id="toc" open> <details id="toc" open={!fileData.collapseToc}>
<summary> <summary>
<h3>Table of Contents</h3> <h3>Table of Contents</h3>
</summary> </summary>

View File

@ -28,11 +28,16 @@ function TagList({ fileData, displayClass }: 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 ?? []))]
@ -55,7 +53,7 @@ function TagContent(props: QuartzComponentProps) {
return ( return (
<div> <div>
<h2> <h2>
<a class="internal tag-link" href={`./${tag}`}> <a class="internal tag-link" href={`../tags/${tag}`}>
#{tag} #{tag}
</a> </a>
</h2> </h2>

View File

@ -28,8 +28,11 @@ async function mouseEnterHandler(
}) })
} }
const hasAlreadyBeenFetched = () =>
[...link.children].some((child) => child.classList.contains("popover"))
// dont refetch if there's already a popover // dont refetch if there's already a popover
if ([...link.children].some((child) => child.classList.contains("popover"))) { if (hasAlreadyBeenFetched()) {
return setPosition(link.lastChild as HTMLElement) return setPosition(link.lastChild as HTMLElement)
} }
@ -49,6 +52,11 @@ async function mouseEnterHandler(
console.error(err) console.error(err)
}) })
// bailout if another popover exists
if (hasAlreadyBeenFetched()) {
return
}
if (!contents) return if (!contents) return
const html = p.parseFromString(contents, "text/html") const html = p.parseFromString(contents, "text/html")
normalizeRelativeURLs(html, targetUrl) normalizeRelativeURLs(html, targetUrl)

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

@ -1,5 +1,6 @@
import micromorph from "micromorph" import micromorph from "micromorph"
import { FullSlug, RelativeURL, getFullSlug } from "../../util/path" import { FullSlug, RelativeURL, getFullSlug } from "../../util/path"
import { normalizeRelativeURLs } from "./popover.inline"
// adapted from `micromorph` // adapted from `micromorph`
// https://github.com/natemoo-re/micromorph // https://github.com/natemoo-re/micromorph
@ -18,8 +19,15 @@ const isLocalUrl = (href: string) => {
return false return false
} }
const isSamePage = (url: URL): boolean => {
const sameOrigin = url.origin === window.location.origin
const samePath = url.pathname === window.location.pathname
return sameOrigin && samePath
}
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
@ -45,6 +53,8 @@ async function navigate(url: URL, isBack: boolean = false) {
if (!contents) return if (!contents) return
const html = p.parseFromString(contents, "text/html") const html = p.parseFromString(contents, "text/html")
normalizeRelativeURLs(html, url)
let title = html.querySelector("title")?.textContent let title = html.querySelector("title")?.textContent
if (title) { if (title) {
document.title = title document.title = title
@ -92,8 +102,16 @@ 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 // dont hijack behaviour, just let browser act normally
if (!url || event.ctrlKey || event.metaKey) return
event.preventDefault() event.preventDefault()
if (isSamePage(url) && url.hash) {
const el = document.getElementById(decodeURIComponent(url.hash.substring(1)))
el?.scrollIntoView()
return
}
try { try {
navigate(url, false) navigate(url, false)
} catch (e) { } catch (e) {
@ -139,6 +157,7 @@ if (!customElements.get("route-announcer")) {
style: style:
"position: absolute; left: 0; top: 0; clip: rect(0 0 0 0); clip-path: inset(50%); overflow: hidden; white-space: nowrap; width: 1px; height: 1px", "position: absolute; left: 0; top: 0; clip: rect(0 0 0 0); clip-path: inset(50%); overflow: hidden; white-space: nowrap; width: 1px; height: 1px",
} }
customElements.define( customElements.define(
"route-announcer", "route-announcer",
class RouteAnnouncer extends HTMLElement { class RouteAnnouncer extends HTMLElement {

View File

@ -24,8 +24,9 @@ function toggleToc(this: HTMLElement) {
function setupToc() { function setupToc() {
const toc = document.getElementById("toc") const toc = document.getElementById("toc")
if (toc) { if (toc) {
const collapsed = toc.classList.contains("collapsed")
const content = toc.nextElementSibling as HTMLElement const content = toc.nextElementSibling as HTMLElement
content.style.maxHeight = content.scrollHeight + "px" content.style.maxHeight = collapsed ? "0px" : content.scrollHeight + "px"
toc.removeEventListener("click", toggleToc) toc.removeEventListener("click", toggleToc)
toc.addEventListener("click", toggleToc) toc.addEventListener("click", toggleToc)
} }

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

@ -1,4 +1,4 @@
import { FilePath, FullSlug, resolveRelative, simplifySlug } from "../../util/path" import { FilePath, FullSlug, joinSegments, resolveRelative, simplifySlug } from "../../util/path"
import { QuartzEmitterPlugin } from "../types" import { QuartzEmitterPlugin } from "../types"
import path from "path" import path from "path"
@ -25,7 +25,12 @@ export const AliasRedirects: QuartzEmitterPlugin = () => ({
slugs.push(permalink as FullSlug) slugs.push(permalink as FullSlug)
} }
for (const slug of slugs) { for (let slug of slugs) {
// fix any slugs that have trailing slash
if (slug.endsWith("/")) {
slug = joinSegments(slug, "index") as FullSlug
}
const redirUrl = resolveRelative(slug, file.data.slug!) const redirUrl = resolveRelative(slug, file.data.slug!)
const fp = await emit({ const fp = await emit({
content: ` content: `

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

@ -59,6 +59,17 @@ function generateRSSFeed(cfg: GlobalConfiguration, idx: ContentIndex, limit?: nu
</item>` </item>`
const items = Array.from(idx) const items = Array.from(idx)
.sort(([_, f1], [__, f2]) => {
if (f1.date && f2.date) {
return f2.date.getTime() - f1.date.getTime()
} else if (f1.date && !f2.date) {
return -1
} else if (!f1.date && f2.date) {
return 1
}
return f1.title.localeCompare(f2.title)
})
.map(([slug, content]) => createURLEntry(simplifySlug(slug), content)) .map(([slug, content]) => createURLEntry(simplifySlug(slug), content))
.slice(0, limit ?? idx.size) .slice(0, limit ?? idx.size)
.join("") .join("")

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

@ -8,12 +8,14 @@ export interface Options {
maxDepth: 1 | 2 | 3 | 4 | 5 | 6 maxDepth: 1 | 2 | 3 | 4 | 5 | 6
minEntries: 1 minEntries: 1
showByDefault: boolean showByDefault: boolean
collapseByDefault: boolean
} }
const defaultOptions: Options = { const defaultOptions: Options = {
maxDepth: 3, maxDepth: 3,
minEntries: 1, minEntries: 1,
showByDefault: true, showByDefault: true,
collapseByDefault: false,
} }
interface TocEntry { interface TocEntry {
@ -54,6 +56,7 @@ export const TableOfContents: QuartzTransformerPlugin<Partial<Options> | undefin
...entry, ...entry,
depth: entry.depth - highestDepth, depth: entry.depth - highestDepth,
})) }))
file.data.collapseToc = opts.collapseByDefault
} }
} }
} }
@ -66,5 +69,6 @@ export const TableOfContents: QuartzTransformerPlugin<Partial<Options> | undefin
declare module "vfile" { declare module "vfile" {
interface DataMap { interface DataMap {
toc: TocEntry[] toc: TocEntry[]
collapseToc: boolean
} }
} }

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;
@ -65,7 +64,7 @@ a {
color: var(--tertiary) !important; color: var(--tertiary) !important;
} }
&.internal { &.internal:not(:has(img)) {
text-decoration: none; text-decoration: none;
background-color: var(--highlight); background-color: var(--highlight);
padding: 0 0.1rem; padding: 0 0.1rem;
@ -391,23 +390,33 @@ p {
line-height: 1.6rem; line-height: 1.6rem;
} }
table { .table-container {
overflow-x: auto;
& > table {
margin: 1rem; margin: 1rem;
padding: 1.5rem; padding: 1.5rem;
border-collapse: collapse; border-collapse: collapse;
th,
td {
min-width: 75px;
}
& > * { & > * {
line-height: 2rem; line-height: 2rem;
} }
}
} }
th { th {
text-align: left; text-align: left;
padding: 0.4rem 1rem; padding: 0.4rem 0.7rem;
border-bottom: 2px solid var(--gray); border-bottom: 2px solid var(--gray);
} }
td { td {
padding: 0.2rem 1rem; padding: 0.2rem 0.7rem;
} }
tr { tr {

View File

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

28
quartz/util/jsx.tsx Normal file
View File

@ -0,0 +1,28 @@
import { Components, Jsx, 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"
const customComponents: Components = {
table: (props) => (
<div class="table-container">
<table {...props} />
</div>
),
}
export function htmlToJsx(fp: FilePath, tree: Node<QuartzPluginData>) {
try {
return toJsxRuntime(tree as Root, {
Fragment,
jsx: jsx as Jsx,
jsxs: jsxs as Jsx,
elementAttributeNameCase: "html",
components: customComponents,
})
} 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) {