1 files deleted
7 files added
13 files modified
| | |
| | | import { QuartzConfig } from "./quartz/cfg" |
| | | import Body from "./quartz/components/Body" |
| | | import Darkmode from "./quartz/components/Darkmode" |
| | | import Head from "./quartz/components/Head" |
| | | import PageTitle from "./quartz/components/PageTitle" |
| | | import Spacer from "./quartz/components/Spacer" |
| | | import { |
| | | ContentPage, |
| | | CreatedModifiedDate, |
| | | Description, |
| | | FrontMatter, |
| | | GitHubFlavoredMarkdown, |
| | | Katex, |
| | | ObsidianFlavoredMarkdown, |
| | | RemoveDrafts, |
| | | ResolveLinks, |
| | | SyntaxHighlighting |
| | | } from "./quartz/plugins" |
| | | import * as Component from "./quartz/components" |
| | | import * as Plugin from "./quartz/plugins" |
| | | |
| | | const config: QuartzConfig = { |
| | | configuration: { |
| | |
| | | }, |
| | | plugins: { |
| | | transformers: [ |
| | | new FrontMatter(), |
| | | new Katex(), |
| | | new Description(), |
| | | new CreatedModifiedDate({ |
| | | new Plugin.FrontMatter(), |
| | | new Plugin.Description(), |
| | | new Plugin.TableOfContents({ showByDefault: true }), |
| | | new Plugin.CreatedModifiedDate({ |
| | | priority: ['frontmatter', 'filesystem'] // you can add 'git' here for last modified from Git but this makes the build slower |
| | | }), |
| | | new SyntaxHighlighting(), |
| | | new GitHubFlavoredMarkdown(), |
| | | new ObsidianFlavoredMarkdown(), |
| | | new ResolveLinks(), |
| | | new Plugin.GitHubFlavoredMarkdown(), |
| | | new Plugin.ObsidianFlavoredMarkdown(), |
| | | new Plugin.ResolveLinks(), |
| | | new Plugin.SyntaxHighlighting(), |
| | | new Plugin.Katex(), |
| | | ], |
| | | filters: [ |
| | | new RemoveDrafts() |
| | | new Plugin.RemoveDrafts() |
| | | ], |
| | | emitters: [ |
| | | new ContentPage({ |
| | | head: Head, |
| | | header: [PageTitle, Spacer, Darkmode], |
| | | body: Body |
| | | new Plugin.ContentPage({ |
| | | head: Component.Head, |
| | | header: [Component.PageTitle, Component.Spacer, Component.Darkmode], |
| | | body: [Component.ArticleTitle, Component.ReadingTime, Component.TableOfContents, Component.Content] |
| | | }) |
| | | ] |
| | | }, |
| New file |
| | |
| | | import { QuartzComponentProps } from "./types" |
| | | |
| | | export default function ArticleTitle({ fileData }: QuartzComponentProps) { |
| | | const title = fileData.frontmatter?.title |
| | | const displayTitle = fileData.slug === "index" ? undefined : title |
| | | if (displayTitle) { |
| | | return <h1>{displayTitle}</h1> |
| | | } else { |
| | | return null |
| | | } |
| | | } |
| | |
| | | import clipboardStyle from './styles/clipboard.scss' |
| | | import { QuartzComponentProps } from "./types" |
| | | |
| | | export default function Body({ fileData, children }: QuartzComponentProps) { |
| | | const title = fileData.frontmatter?.title |
| | | const displayTitle = fileData.slug === "index" ? undefined : title |
| | | export default function Body({ children }: QuartzComponentProps) { |
| | | return <article> |
| | | <div class="top-section"> |
| | | {displayTitle && <h1>{displayTitle}</h1>} |
| | | </div> |
| | | {children} |
| | | </article> |
| | | } |
| New file |
| | |
| | | import { QuartzComponentProps } from "./types" |
| | | import { Fragment, jsx, jsxs } from 'preact/jsx-runtime' |
| | | import { toJsxRuntime } from "hast-util-to-jsx-runtime" |
| | | |
| | | export default function Content({ tree }: QuartzComponentProps) { |
| | | // @ts-ignore (preact makes it angry) |
| | | const content = toJsxRuntime(tree, { Fragment, jsx, jsxs, elementAttributeNameCase: 'html' }) |
| | | return content |
| | | } |
| | |
| | | import style from './styles/header.scss' |
| | | import { QuartzComponentProps } from "./types" |
| | | |
| | | export default function Header({ children }: QuartzComponentProps) { |
| | |
| | | </header> |
| | | } |
| | | |
| | | Header.css = style |
| | | Header.css = ` |
| | | header { |
| | | display: flex; |
| | | flex-direction: row; |
| | | align-items: center; |
| | | margin: 1em 0 2em 0; |
| | | & > h1 { |
| | | } |
| | | } |
| | | |
| | | header > h1 { |
| | | margin: 0; |
| | | flex: auto; |
| | | } |
| | | ` |
| New file |
| | |
| | | import { QuartzComponentProps } from "./types" |
| | | import readingTime from "reading-time" |
| | | |
| | | export default function ReadingTime({ fileData }: QuartzComponentProps) { |
| | | const text = fileData.text |
| | | const isHomePage = fileData.slug === "index" |
| | | if (text && !isHomePage) { |
| | | const { text: timeTaken, words } = readingTime(text) |
| | | return <p class="reading-time">{words} words, {timeTaken}</p> |
| | | } else { |
| | | return null |
| | | } |
| | | } |
| | | |
| | | ReadingTime.css = ` |
| | | .reading-time { |
| | | margin-top: 0; |
| | | opacity: 0.5; |
| | | } |
| | | ` |
| New file |
| | |
| | | import { QuartzComponentProps } from "./types" |
| | | import style from "./styles/toc.scss" |
| | | |
| | | export default function TableOfContents({ fileData, position }: QuartzComponentProps) { |
| | | if (!fileData.toc) { |
| | | return null |
| | | } |
| | | |
| | | if (position === 'body') { |
| | | // TODO: animate this |
| | | return <details className="toc" open> |
| | | <summary><h3>Table of Contents</h3></summary> |
| | | <ul> |
| | | {fileData.toc.map(tocEntry => <li key={tocEntry.slug} className={`depth-${tocEntry.depth}`}> |
| | | <a href={`#${tocEntry.slug}`}>{tocEntry.text}</a> |
| | | </li>)} |
| | | </ul> |
| | | </details> |
| | | } else if (position === 'sidebar') { |
| | | // TODO |
| | | } |
| | | } |
| | | |
| | | TableOfContents.css = style |
| New file |
| | |
| | | import ArticleTitle from "./ArticleTitle" |
| | | import Content from "./Content" |
| | | import Darkmode from "./Darkmode" |
| | | import Head from "./Head" |
| | | import PageTitle from "./PageTitle" |
| | | import ReadingTime from "./ReadingTime" |
| | | import Spacer from "./Spacer" |
| | | import TableOfContents from "./TableOfContents" |
| | | |
| | | export { |
| | | ArticleTitle, |
| | | Content, |
| | | Darkmode, |
| | | Head, |
| | | PageTitle, |
| | | ReadingTime, |
| | | Spacer, |
| | | TableOfContents |
| | | } |
| New file |
| | |
| | | details.toc { |
| | | & summary { |
| | | cursor: pointer; |
| | | |
| | | &::marker { |
| | | color: var(--dark); |
| | | } |
| | | |
| | | & > * { |
| | | padding-left: 0.25rem; |
| | | display: inline-block; |
| | | margin: 0; |
| | | } |
| | | } |
| | | |
| | | & ul { |
| | | list-style: none; |
| | | margin: 0.5rem 1.25rem; |
| | | padding: 0; |
| | | } |
| | | |
| | | @for $i from 1 through 6 { |
| | | & .depth-#{$i} { |
| | | padding-left: calc(1rem * #{$i}); |
| | | } |
| | | } |
| | | } |
| | |
| | | import { StaticResources } from "../resources" |
| | | import { QuartzPluginData } from "../plugins/vfile" |
| | | import { GlobalConfiguration } from "../cfg" |
| | | import { Node } from "hast" |
| | | |
| | | export type QuartzComponentProps = { |
| | | externalResources: StaticResources |
| | | fileData: QuartzPluginData |
| | | cfg: GlobalConfiguration |
| | | children: QuartzComponent[] | JSX.Element[] |
| | | tree: Node<QuartzPluginData> |
| | | position?: 'sidebar' | 'header' | 'body' |
| | | } |
| | | |
| | | export type QuartzComponent = ComponentType<QuartzComponentProps> & { |
| | |
| | | import path from 'path' |
| | | import SlugAnchor from 'github-slugger' |
| | | |
| | | const slugAnchor = new SlugAnchor() |
| | | export const slugAnchor = new SlugAnchor() |
| | | |
| | | function slugSegment(s: string): string { |
| | | return s.replace(/\s/g, '-') |
| | |
| | | import { toJsxRuntime } from "hast-util-to-jsx-runtime" |
| | | import { StaticResources } from "../../resources" |
| | | import { EmitCallback, QuartzEmitterPlugin } from "../types" |
| | | import { ProcessedContent } from "../vfile" |
| | | import { Fragment, jsx, jsxs } from 'preact/jsx-runtime' |
| | | import { render } from "preact-render-to-string" |
| | | import { GlobalConfiguration } from "../../cfg" |
| | | import { QuartzComponent } from "../../components/types" |
| | | import { resolveToRoot } from "../../path" |
| | | import Header from "../../components/Header" |
| | | import { QuartzComponentProps } from "../../components/types" |
| | | import Body from "../../components/Body" |
| | | |
| | | interface Options { |
| | | head: QuartzComponent |
| | | header: QuartzComponent[], |
| | | body: QuartzComponent |
| | | body: QuartzComponent[] |
| | | } |
| | | |
| | | export class ContentPage extends QuartzEmitterPlugin { |
| | |
| | | } |
| | | |
| | | getQuartzComponents(): QuartzComponent[] { |
| | | return [this.opts.head, Header, ...this.opts.header, this.opts.body] |
| | | return [this.opts.head, Header, ...this.opts.header, ...this.opts.body] |
| | | } |
| | | |
| | | async emit(cfg: GlobalConfiguration, content: ProcessedContent[], resources: StaticResources, emit: EmitCallback): Promise<string[]> { |
| | | const fps: string[] = [] |
| | | |
| | | const { head: Head, header, body: Body } = this.opts |
| | | const { head: Head, header, body } = this.opts |
| | | for (const [tree, file] of content) { |
| | | // @ts-ignore (preact makes it angry) |
| | | const content = toJsxRuntime(tree, { Fragment, jsx, jsxs, elementAttributeNameCase: 'html' }) |
| | | |
| | | const baseDir = resolveToRoot(file.data.slug!) |
| | | const pageResources: StaticResources = { |
| | | css: [baseDir + "/index.css", ...resources.css], |
| | |
| | | fileData: file.data, |
| | | externalResources: pageResources, |
| | | cfg, |
| | | children: [content] |
| | | children: [], |
| | | tree |
| | | } |
| | | |
| | | const doc = <html> |
| | |
| | | <body> |
| | | <div id="quartz-root" class="page"> |
| | | <Header {...componentData} > |
| | | {header.map(HeaderComponent => <HeaderComponent {...componentData}/>)} |
| | | {header.map(HeaderComponent => <HeaderComponent {...componentData} position="header" />)} |
| | | </Header> |
| | | <Body {...componentData}> |
| | | {content} |
| | | {body.map(BodyComponent => <BodyComponent {...componentData } position="body" />)} |
| | | </Body> |
| | | </div> |
| | | </body> |
| | |
| | | name = "Description" |
| | | opts: Options |
| | | |
| | | constructor(opts?: Options) { |
| | | constructor(opts?: Partial<Options>) { |
| | | super() |
| | | this.opts = { ...defaultOptions, ...opts } |
| | | } |
| | |
| | | name = "FrontMatter" |
| | | opts: Options |
| | | |
| | | constructor(opts?: Options) { |
| | | constructor(opts?: Partial<Options>) { |
| | | super() |
| | | this.opts = { ...defaultOptions, ...opts } |
| | | } |
| | |
| | | name = "GitHubFlavoredMarkdown" |
| | | opts: Options |
| | | |
| | | constructor(opts?: Options) { |
| | | constructor(opts?: Partial<Options>) { |
| | | super() |
| | | this.opts = { ...defaultOptions, ...opts } |
| | | } |
| | |
| | | export { ResolveLinks } from './links' |
| | | export { ObsidianFlavoredMarkdown } from './ofm' |
| | | export { SyntaxHighlighting } from './syntax' |
| | | export { TableOfContents } from './toc' |
| | |
| | | name = "CreatedModifiedDate" |
| | | opts: Options |
| | | |
| | | constructor(opts?: Options) { |
| | | constructor(opts?: Partial<Options>) { |
| | | super() |
| | | this.opts = { |
| | | ...defaultOptions, |
| | |
| | | name = "LinkProcessing" |
| | | opts: Options |
| | | |
| | | constructor(opts?: Options) { |
| | | constructor(opts?: Partial<Options>) { |
| | | super() |
| | | this.opts = { ...defaultOptions, ...opts } |
| | | } |
| | |
| | | name = "ObsidianFlavoredMarkdown" |
| | | opts: Options |
| | | |
| | | constructor(opts?: Options) { |
| | | constructor(opts?: Partial<Options>) { |
| | | super() |
| | | this.opts = { ...defaultOptions, ...opts } |
| | | } |
| New file |
| | |
| | | import { PluggableList } from "unified" |
| | | import { QuartzTransformerPlugin } from "../types" |
| | | import { Root } from "mdast" |
| | | import { visit } from "unist-util-visit" |
| | | import { toString } from "mdast-util-to-string" |
| | | import { slugAnchor } from "../../path" |
| | | |
| | | export interface Options { |
| | | maxDepth: 1 | 2 | 3 | 4 | 5 | 6, |
| | | minEntries: 1, |
| | | showByDefault: boolean |
| | | } |
| | | |
| | | const defaultOptions: Options = { |
| | | maxDepth: 3, |
| | | minEntries: 1, |
| | | showByDefault: true, |
| | | } |
| | | |
| | | interface TocEntry { |
| | | depth: number, |
| | | text: string, |
| | | slug: string |
| | | } |
| | | |
| | | export class TableOfContents extends QuartzTransformerPlugin { |
| | | name = "TableOfContents" |
| | | opts: Options |
| | | |
| | | constructor(opts?: Partial<Options>) { |
| | | super() |
| | | this.opts = { ...defaultOptions, ...opts } |
| | | } |
| | | |
| | | markdownPlugins(): PluggableList { |
| | | return [() => { |
| | | return async (tree: Root, file) => { |
| | | const display = file.data.frontmatter?.enableToc ?? this.opts.showByDefault |
| | | if (display) { |
| | | const toc: TocEntry[] = [] |
| | | let highestDepth: number = this.opts.maxDepth |
| | | visit(tree, 'heading', (node) => { |
| | | if (node.depth <= this.opts.maxDepth) { |
| | | const text = toString(node) |
| | | highestDepth = Math.min(highestDepth, node.depth) |
| | | toc.push({ |
| | | depth: node.depth, |
| | | text, |
| | | slug: slugAnchor.slug(text) |
| | | }) |
| | | } |
| | | }) |
| | | |
| | | if (toc.length > this.opts.minEntries) { |
| | | file.data.toc = toc.map(entry => ({ ...entry, depth: entry.depth - highestDepth })) |
| | | } |
| | | } |
| | | } |
| | | }] |
| | | } |
| | | |
| | | htmlPlugins(): PluggableList { |
| | | return [] |
| | | } |
| | | } |
| | | |
| | | declare module 'vfile' { |
| | | interface DataMap { |
| | | toc: TocEntry[] |
| | | } |
| | | } |
| | | |