Compare commits

...

22 Commits

Author SHA1 Message Date
Jacky Zhao
76f2664277 versioning: bump to v4.1.1 2023-11-13 22:57:05 -08:00
Jacky Zhao
74777118a7 feat: header and full-page transcludes (closes #557) 2023-11-13 22:51:40 -08:00
Jacky Zhao
8223465bda fix: make :has img selector direct 2023-11-12 14:33:19 -08:00
Jacky Zhao
cf6ab9e933 feat: option to specify npx quartz sync message (closes #583) 2023-11-12 14:27:53 -08:00
Jacky Zhao
74c63e448e fix(style): dont internal-link highlight when image (closes #581) 2023-11-11 21:13:10 -08:00
Jacky Zhao
43d638a6de perf: compute mapping of folder name to file data for faster breadcrumbs 2023-11-11 21:06:37 -08:00
Jacky Zhao
d1551872ff fix: check if popover exists after fetching and before inserting 2023-11-11 20:46:57 -08:00
Jacky Zhao
275bea3051 style + cfg: resolve breadcrumb titles by default and change arrow character 2023-11-11 20:46:29 -08:00
Jacky Zhao
bc02791734 fix: .date.getTime() based sort 2023-11-11 20:28:26 -08:00
Jacky Zhao
bf603c49c2 fix: sort rss feed by date 2023-11-11 12:08:54 -08:00
Jacky Zhao
f67356c3d2 lint: format 2023-11-11 12:02:34 -08:00
Jacky Zhao
5d666d1860 fix: normalize relative urls (closes #569) 2023-11-11 11:59:05 -08:00
Jacky Zhao
22b7cf135e types: cast in jsx.tsx to avoid @ts-ignore 2023-11-11 11:41:44 -08:00
Jacky Zhao
50a87d0d86 style: scrollable tables 2023-11-11 11:39:56 -08:00
Jacky Zhao
134b6ed582 fix: anchors links shouldnt cause reload (closes #574) 2023-11-11 10:11:31 -08:00
Jacky Zhao
99e8f5944f fix: trailing slash aliases (closes #577) 2023-11-11 09:56:30 -08:00
Yes365
e9f4e28a2d fix: adapt vercel cleanurls (#487)
Co-authored-by: Harrison <Harrison@fanruan.com>
2023-11-09 19:44:16 -08:00
Niklas Schröder
2a6b9a9ea0 docs: fix property name for ToC toggle (#573) 2023-11-07 09:16:48 -08:00
Mau Camargo
e806c30fa1 docs: Add Mau Camargo's Notkesto to showcase (#570) 2023-11-05 11:30:10 -08:00
Anson Yu
aac7b7e97d docs: Update making plugins.md (#567)
:)
2023-11-04 14:20:16 -07:00
Jacky Zhao
101e9946bd feat: add collapseByDefault option to TableOfContents (closes #566) 2023-11-04 12:11:42 -07:00
Emil Rofors
a62a97c7ab docs: add GitLab pages CI (#549)
* add .gitlab-ci.yml

* move GitLab CI to hosting.md

* remove extra folder name

Co-authored-by: Jacky Zhao <j.zhao2k19@gmail.com>

* remove test from gitlab instructions

* run prettier

---------

Co-authored-by: Jacky Zhao <j.zhao2k19@gmail.com>
2023-11-03 16:40:43 -07:00
23 changed files with 270 additions and 79 deletions

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: {

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

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

@@ -19,5 +19,6 @@ Want to see what Quartz can do? Here are some cool community gardens:
- [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/) - [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

@@ -2,7 +2,7 @@
"name": "@jackyzha0/quartz", "name": "@jackyzha0/quartz",
"description": "🌱 publish your digital garden and notes as a website", "description": "🌱 publish your digital garden and notes as a website",
"private": true, "private": true,
"version": "4.1.0", "version": "4.1.1",
"type": "module", "type": "module",
"author": "jackyzha0 <j.zhao2k19@gmail.com>", "author": "jackyzha0 <j.zhao2k19@gmail.com>",
"license": "MIT", "license": "MIT",

View File

@@ -41,6 +41,11 @@ export const SyncArgv = {
default: true, default: true,
describe: "create a git commit for your unsaved changes", describe: "create a git commit for your unsaved changes",
}, },
message: {
string: true,
alias: ["m"],
describe: "option to override the default Quartz commit message",
},
push: { push: {
boolean: true, boolean: true,
default: true, default: true,

View File

@@ -483,8 +483,9 @@ export async function handleSync(argv) {
dateStyle: "medium", dateStyle: "medium",
timeStyle: "short", timeStyle: "short",
}) })
const commitMessage = argv.message ?? `Quartz sync: ${currentTimestamp}`
spawnSync("git", ["add", "."], { stdio: "inherit" }) spawnSync("git", ["add", "."], { stdio: "inherit" })
spawnSync("git", ["commit", "-m", `Quartz sync: ${currentTimestamp}`], { stdio: "inherit" }) spawnSync("git", ["commit", "-m", commitMessage], { stdio: "inherit" })
if (contentStat.isSymbolicLink()) { if (contentStat.isSymbolicLink()) {
// put symlink back // put symlink back

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 if (currentFile) {
const currentFile = findCurrentFile(allFiles, currentTitle) curPathSegment = currentFile.frontmatter!.title
if (currentFile) {
currentTitle = 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

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

@@ -5,7 +5,7 @@ import BodyConstructor from "./Body"
import { JSResourceToScriptElement, StaticResources } from "../util/resources" import { JSResourceToScriptElement, StaticResources } from "../util/resources"
import { FullSlug, RelativeURL, joinSegments } from "../util/path" import { FullSlug, RelativeURL, joinSegments } from "../util/path"
import { visit } from "unist-util-visit" import { visit } from "unist-util-visit"
import { Root, Element } from "hast" import { Root, Element, ElementContent } from "hast"
interface RenderComponents { interface RenderComponents {
head: QuartzComponent head: QuartzComponent
@@ -61,22 +61,81 @@ export function renderPage(
const classNames = (node.properties?.className ?? []) as string[] const classNames = (node.properties?.className ?? []) as string[]
if (classNames.includes("transclude")) { if (classNames.includes("transclude")) {
const inner = node.children[0] as Element const inner = node.children[0] as Element
const blockSlug = inner.properties?.["data-slug"] as FullSlug const transcludeTarget = inner.properties?.["data-slug"] as FullSlug
const blockRef = node.properties!.dataBlock as string
// TODO: avoid this expensive find operation and construct an index ahead of time // TODO: avoid this expensive find operation and construct an index ahead of time
let blockNode = componentData.allFiles.find((f) => f.slug === blockSlug)?.blocks?.[blockRef] const page = componentData.allFiles.find((f) => f.slug === transcludeTarget)
if (blockNode) { if (!page) {
if (blockNode.tagName === "li") { return
blockNode = { }
type: "element",
tagName: "ul", let blockRef = node.properties?.dataBlock as string | undefined
children: [blockNode], if (blockRef?.startsWith("^")) {
// block transclude
blockRef = blockRef.slice(1)
let blockNode = page.blocks?.[blockRef]
if (blockNode) {
if (blockNode.tagName === "li") {
blockNode = {
type: "element",
tagName: "ul",
children: [blockNode],
}
}
node.children = [
blockNode,
{
type: "element",
tagName: "a",
properties: { href: inner.properties?.href, class: ["internal"] },
children: [{ type: "text", value: `Link to original` }],
},
]
}
} else if (blockRef?.startsWith("#") && page.htmlAst) {
// header transclude
blockRef = blockRef.slice(1)
let startIdx = undefined
let endIdx = undefined
for (const [i, el] of page.htmlAst.children.entries()) {
if (el.type === "element" && el.tagName.match(/h[1-6]/)) {
if (endIdx) {
break
}
if (startIdx) {
endIdx = i
} else if (el.properties?.id === blockRef) {
startIdx = i
}
} }
} }
if (!startIdx) {
return
}
node.children = [ node.children = [
blockNode, ...(page.htmlAst.children.slice(startIdx, endIdx) as ElementContent[]),
{
type: "element",
tagName: "a",
properties: { href: inner.properties?.href, class: ["internal"] },
children: [{ type: "text", value: `Link to original` }],
},
]
} else if (page.htmlAst) {
// page transclude
node.children = [
{
type: "element",
tagName: "h1",
children: [
{ type: "text", value: page.frontmatter?.title ?? `Transclude of ${page.slug}` },
],
},
...(page.htmlAst.children as ElementContent[]),
{ {
type: "element", type: "element",
tagName: "a", tagName: "a",

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

@@ -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,6 +19,12 @@ 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 if (target.attributes.getNamedItem("target")?.value === "_blank") return
@@ -46,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
@@ -93,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) ?? {}
// dont hijack behaviour, just let browser act normally
if (!url || event.ctrlKey || event.metaKey) return 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) {
@@ -140,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

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

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

@@ -1,7 +1,7 @@
import { PluggableList } from "unified" import { PluggableList } from "unified"
import { QuartzTransformerPlugin } from "../types" import { QuartzTransformerPlugin } from "../types"
import { Root, HTML, BlockContent, DefinitionContent, Code, Paragraph } from "mdast" import { Root, HTML, BlockContent, DefinitionContent, Code, Paragraph } from "mdast"
import { Element, Literal } from "hast" import { Element, Literal, Root as HtmlRoot } from "hast"
import { Replace, findAndReplace as mdastFindReplace } from "mdast-util-find-and-replace" import { Replace, findAndReplace as mdastFindReplace } from "mdast-util-find-and-replace"
import { slug as slugAnchor } from "github-slugger" import { slug as slugAnchor } from "github-slugger"
import rehypeRaw from "rehype-raw" import rehypeRaw from "rehype-raw"
@@ -236,13 +236,13 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>
value: `<iframe src="${url}"></iframe>`, value: `<iframe src="${url}"></iframe>`,
} }
} else if (ext === "") { } else if (ext === "") {
const block = anchor.slice(1) const block = anchor
return { return {
type: "html", type: "html",
data: { hProperties: { transclude: true } }, data: { hProperties: { transclude: true } },
value: `<blockquote class="transclude" data-url="${url}" data-block="${block}"><a href="${ value: `<blockquote class="transclude" data-url="${url}" data-block="${block}"><a href="${
url + anchor url + anchor
}" class="transclude-inner">Transclude of block ${block}</a></blockquote>`, }" class="transclude-inner">Transclude of ${url}${block}</a></blockquote>`,
} }
} }
@@ -436,6 +436,7 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>
const blockTagTypes = new Set(["blockquote"]) const blockTagTypes = new Set(["blockquote"])
return (tree, file) => { return (tree, file) => {
file.data.blocks = {} file.data.blocks = {}
file.data.htmlAst = tree
visit(tree, "element", (node, index, parent) => { visit(tree, "element", (node, index, parent) => {
if (blockTagTypes.has(node.tagName)) { if (blockTagTypes.has(node.tagName)) {
@@ -524,5 +525,6 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>
declare module "vfile" { declare module "vfile" {
interface DataMap { interface DataMap {
blocks: Record<string, Element> blocks: Record<string, Element>
htmlAst: HtmlRoot
} }
} }

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

@@ -64,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;
@@ -390,23 +390,33 @@ p {
line-height: 1.6rem; line-height: 1.6rem;
} }
table { .table-container {
margin: 1rem; overflow-x: auto;
padding: 1.5rem;
border-collapse: collapse; & > table {
& > * { margin: 1rem;
line-height: 2rem; padding: 1.5rem;
border-collapse: collapse;
th,
td {
min-width: 75px;
}
& > * {
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,15 +0,0 @@
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)
}
}

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)
}
}