toc
This commit is contained in:
parent
b3cbcaf0f5
commit
8c5dd2287a
@ -1,21 +1,6 @@
|
||||
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: {
|
||||
@ -54,25 +39,26 @@ const config: QuartzConfig = {
|
||||
},
|
||||
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]
|
||||
})
|
||||
]
|
||||
},
|
||||
|
11
quartz/components/ArticleTitle.tsx
Normal file
11
quartz/components/ArticleTitle.tsx
Normal file
@ -0,0 +1,11 @@
|
||||
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
|
||||
}
|
||||
}
|
@ -2,13 +2,8 @@ import clipboardScript from './scripts/clipboard.inline'
|
||||
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>
|
||||
}
|
||||
|
9
quartz/components/Content.tsx
Normal file
9
quartz/components/Content.tsx
Normal file
@ -0,0 +1,9 @@
|
||||
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
|
||||
}
|
@ -1,4 +1,3 @@
|
||||
import style from './styles/header.scss'
|
||||
import { QuartzComponentProps } from "./types"
|
||||
|
||||
export default function Header({ children }: QuartzComponentProps) {
|
||||
@ -7,4 +6,18 @@ 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;
|
||||
}
|
||||
`
|
||||
|
20
quartz/components/ReadingTime.tsx
Normal file
20
quartz/components/ReadingTime.tsx
Normal file
@ -0,0 +1,20 @@
|
||||
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;
|
||||
}
|
||||
`
|
24
quartz/components/TableOfContents.tsx
Normal file
24
quartz/components/TableOfContents.tsx
Normal file
@ -0,0 +1,24 @@
|
||||
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
|
19
quartz/components/index.ts
Normal file
19
quartz/components/index.ts
Normal file
@ -0,0 +1,19 @@
|
||||
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
|
||||
}
|
@ -1,10 +0,0 @@
|
||||
header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
margin: 1em 0 2em 0;
|
||||
& > h1 {
|
||||
margin: 0;
|
||||
flex: auto;
|
||||
}
|
||||
}
|
27
quartz/components/styles/toc.scss
Normal file
27
quartz/components/styles/toc.scss
Normal file
@ -0,0 +1,27 @@
|
||||
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});
|
||||
}
|
||||
}
|
||||
}
|
@ -2,12 +2,15 @@ import { ComponentType, JSX } from "preact"
|
||||
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> & {
|
||||
|
@ -1,7 +1,7 @@
|
||||
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, '-')
|
||||
|
@ -1,19 +1,18 @@
|
||||
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 {
|
||||
@ -26,17 +25,14 @@ 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],
|
||||
@ -51,7 +47,8 @@ export class ContentPage extends QuartzEmitterPlugin {
|
||||
fileData: file.data,
|
||||
externalResources: pageResources,
|
||||
cfg,
|
||||
children: [content]
|
||||
children: [],
|
||||
tree
|
||||
}
|
||||
|
||||
const doc = <html>
|
||||
@ -59,10 +56,10 @@ export class ContentPage extends QuartzEmitterPlugin {
|
||||
<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>
|
||||
|
@ -15,7 +15,7 @@ export class Description extends QuartzTransformerPlugin {
|
||||
name = "Description"
|
||||
opts: Options
|
||||
|
||||
constructor(opts?: Options) {
|
||||
constructor(opts?: Partial<Options>) {
|
||||
super()
|
||||
this.opts = { ...defaultOptions, ...opts }
|
||||
}
|
||||
|
@ -17,7 +17,7 @@ export class FrontMatter extends QuartzTransformerPlugin {
|
||||
name = "FrontMatter"
|
||||
opts: Options
|
||||
|
||||
constructor(opts?: Options) {
|
||||
constructor(opts?: Partial<Options>) {
|
||||
super()
|
||||
this.opts = { ...defaultOptions, ...opts }
|
||||
}
|
||||
|
@ -19,7 +19,7 @@ export class GitHubFlavoredMarkdown extends QuartzTransformerPlugin {
|
||||
name = "GitHubFlavoredMarkdown"
|
||||
opts: Options
|
||||
|
||||
constructor(opts?: Options) {
|
||||
constructor(opts?: Partial<Options>) {
|
||||
super()
|
||||
this.opts = { ...defaultOptions, ...opts }
|
||||
}
|
||||
|
@ -6,3 +6,4 @@ export { Description } from './description'
|
||||
export { ResolveLinks } from './links'
|
||||
export { ObsidianFlavoredMarkdown } from './ofm'
|
||||
export { SyntaxHighlighting } from './syntax'
|
||||
export { TableOfContents } from './toc'
|
||||
|
@ -16,7 +16,7 @@ export class CreatedModifiedDate extends QuartzTransformerPlugin {
|
||||
name = "CreatedModifiedDate"
|
||||
opts: Options
|
||||
|
||||
constructor(opts?: Options) {
|
||||
constructor(opts?: Partial<Options>) {
|
||||
super()
|
||||
this.opts = {
|
||||
...defaultOptions,
|
||||
|
@ -21,7 +21,7 @@ export class ResolveLinks extends QuartzTransformerPlugin {
|
||||
name = "LinkProcessing"
|
||||
opts: Options
|
||||
|
||||
constructor(opts?: Options) {
|
||||
constructor(opts?: Partial<Options>) {
|
||||
super()
|
||||
this.opts = { ...defaultOptions, ...opts }
|
||||
}
|
||||
|
@ -93,7 +93,7 @@ export class ObsidianFlavoredMarkdown extends QuartzTransformerPlugin {
|
||||
name = "ObsidianFlavoredMarkdown"
|
||||
opts: Options
|
||||
|
||||
constructor(opts?: Options) {
|
||||
constructor(opts?: Partial<Options>) {
|
||||
super()
|
||||
this.opts = { ...defaultOptions, ...opts }
|
||||
}
|
||||
|
72
quartz/plugins/transformers/toc.ts
Normal file
72
quartz/plugins/transformers/toc.ts
Normal file
@ -0,0 +1,72 @@
|
||||
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[]
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user