toc
This commit is contained in:
parent
b3cbcaf0f5
commit
8c5dd2287a
@ -1,21 +1,6 @@
|
|||||||
import { QuartzConfig } from "./quartz/cfg"
|
import { QuartzConfig } from "./quartz/cfg"
|
||||||
import Body from "./quartz/components/Body"
|
import * as Component from "./quartz/components"
|
||||||
import Darkmode from "./quartz/components/Darkmode"
|
import * as Plugin from "./quartz/plugins"
|
||||||
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"
|
|
||||||
|
|
||||||
const config: QuartzConfig = {
|
const config: QuartzConfig = {
|
||||||
configuration: {
|
configuration: {
|
||||||
@ -54,25 +39,26 @@ const config: QuartzConfig = {
|
|||||||
},
|
},
|
||||||
plugins: {
|
plugins: {
|
||||||
transformers: [
|
transformers: [
|
||||||
new FrontMatter(),
|
new Plugin.FrontMatter(),
|
||||||
new Katex(),
|
new Plugin.Description(),
|
||||||
new Description(),
|
new Plugin.TableOfContents({ showByDefault: true }),
|
||||||
new CreatedModifiedDate({
|
new Plugin.CreatedModifiedDate({
|
||||||
priority: ['frontmatter', 'filesystem'] // you can add 'git' here for last modified from Git but this makes the build slower
|
priority: ['frontmatter', 'filesystem'] // you can add 'git' here for last modified from Git but this makes the build slower
|
||||||
}),
|
}),
|
||||||
new SyntaxHighlighting(),
|
new Plugin.GitHubFlavoredMarkdown(),
|
||||||
new GitHubFlavoredMarkdown(),
|
new Plugin.ObsidianFlavoredMarkdown(),
|
||||||
new ObsidianFlavoredMarkdown(),
|
new Plugin.ResolveLinks(),
|
||||||
new ResolveLinks(),
|
new Plugin.SyntaxHighlighting(),
|
||||||
|
new Plugin.Katex(),
|
||||||
],
|
],
|
||||||
filters: [
|
filters: [
|
||||||
new RemoveDrafts()
|
new Plugin.RemoveDrafts()
|
||||||
],
|
],
|
||||||
emitters: [
|
emitters: [
|
||||||
new ContentPage({
|
new Plugin.ContentPage({
|
||||||
head: Head,
|
head: Component.Head,
|
||||||
header: [PageTitle, Spacer, Darkmode],
|
header: [Component.PageTitle, Component.Spacer, Component.Darkmode],
|
||||||
body: Body
|
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 clipboardStyle from './styles/clipboard.scss'
|
||||||
import { QuartzComponentProps } from "./types"
|
import { QuartzComponentProps } from "./types"
|
||||||
|
|
||||||
export default function Body({ fileData, children }: QuartzComponentProps) {
|
export default function Body({ children }: QuartzComponentProps) {
|
||||||
const title = fileData.frontmatter?.title
|
|
||||||
const displayTitle = fileData.slug === "index" ? undefined : title
|
|
||||||
return <article>
|
return <article>
|
||||||
<div class="top-section">
|
|
||||||
{displayTitle && <h1>{displayTitle}</h1>}
|
|
||||||
</div>
|
|
||||||
{children}
|
{children}
|
||||||
</article>
|
</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"
|
import { QuartzComponentProps } from "./types"
|
||||||
|
|
||||||
export default function Header({ children }: QuartzComponentProps) {
|
export default function Header({ children }: QuartzComponentProps) {
|
||||||
@ -7,4 +6,18 @@ export default function Header({ children }: QuartzComponentProps) {
|
|||||||
</header>
|
</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 { StaticResources } from "../resources"
|
||||||
import { QuartzPluginData } from "../plugins/vfile"
|
import { QuartzPluginData } from "../plugins/vfile"
|
||||||
import { GlobalConfiguration } from "../cfg"
|
import { GlobalConfiguration } from "../cfg"
|
||||||
|
import { Node } from "hast"
|
||||||
|
|
||||||
export type QuartzComponentProps = {
|
export type QuartzComponentProps = {
|
||||||
externalResources: StaticResources
|
externalResources: StaticResources
|
||||||
fileData: QuartzPluginData
|
fileData: QuartzPluginData
|
||||||
cfg: GlobalConfiguration
|
cfg: GlobalConfiguration
|
||||||
children: QuartzComponent[] | JSX.Element[]
|
children: QuartzComponent[] | JSX.Element[]
|
||||||
|
tree: Node<QuartzPluginData>
|
||||||
|
position?: 'sidebar' | 'header' | 'body'
|
||||||
}
|
}
|
||||||
|
|
||||||
export type QuartzComponent = ComponentType<QuartzComponentProps> & {
|
export type QuartzComponent = ComponentType<QuartzComponentProps> & {
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import path from 'path'
|
import path from 'path'
|
||||||
import SlugAnchor from 'github-slugger'
|
import SlugAnchor from 'github-slugger'
|
||||||
|
|
||||||
const slugAnchor = new SlugAnchor()
|
export const slugAnchor = new SlugAnchor()
|
||||||
|
|
||||||
function slugSegment(s: string): string {
|
function slugSegment(s: string): string {
|
||||||
return s.replace(/\s/g, '-')
|
return s.replace(/\s/g, '-')
|
||||||
|
@ -1,19 +1,18 @@
|
|||||||
import { toJsxRuntime } from "hast-util-to-jsx-runtime"
|
|
||||||
import { StaticResources } from "../../resources"
|
import { StaticResources } from "../../resources"
|
||||||
import { EmitCallback, QuartzEmitterPlugin } from "../types"
|
import { EmitCallback, QuartzEmitterPlugin } from "../types"
|
||||||
import { ProcessedContent } from "../vfile"
|
import { ProcessedContent } from "../vfile"
|
||||||
import { Fragment, jsx, jsxs } from 'preact/jsx-runtime'
|
|
||||||
import { render } from "preact-render-to-string"
|
import { render } from "preact-render-to-string"
|
||||||
import { GlobalConfiguration } from "../../cfg"
|
import { GlobalConfiguration } from "../../cfg"
|
||||||
import { QuartzComponent } from "../../components/types"
|
import { QuartzComponent } from "../../components/types"
|
||||||
import { resolveToRoot } from "../../path"
|
import { resolveToRoot } from "../../path"
|
||||||
import Header from "../../components/Header"
|
import Header from "../../components/Header"
|
||||||
import { QuartzComponentProps } from "../../components/types"
|
import { QuartzComponentProps } from "../../components/types"
|
||||||
|
import Body from "../../components/Body"
|
||||||
|
|
||||||
interface Options {
|
interface Options {
|
||||||
head: QuartzComponent
|
head: QuartzComponent
|
||||||
header: QuartzComponent[],
|
header: QuartzComponent[],
|
||||||
body: QuartzComponent
|
body: QuartzComponent[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ContentPage extends QuartzEmitterPlugin {
|
export class ContentPage extends QuartzEmitterPlugin {
|
||||||
@ -26,17 +25,14 @@ export class ContentPage extends QuartzEmitterPlugin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getQuartzComponents(): QuartzComponent[] {
|
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[]> {
|
async emit(cfg: GlobalConfiguration, content: ProcessedContent[], resources: StaticResources, emit: EmitCallback): Promise<string[]> {
|
||||||
const fps: 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) {
|
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 baseDir = resolveToRoot(file.data.slug!)
|
||||||
const pageResources: StaticResources = {
|
const pageResources: StaticResources = {
|
||||||
css: [baseDir + "/index.css", ...resources.css],
|
css: [baseDir + "/index.css", ...resources.css],
|
||||||
@ -51,7 +47,8 @@ export class ContentPage extends QuartzEmitterPlugin {
|
|||||||
fileData: file.data,
|
fileData: file.data,
|
||||||
externalResources: pageResources,
|
externalResources: pageResources,
|
||||||
cfg,
|
cfg,
|
||||||
children: [content]
|
children: [],
|
||||||
|
tree
|
||||||
}
|
}
|
||||||
|
|
||||||
const doc = <html>
|
const doc = <html>
|
||||||
@ -59,10 +56,10 @@ export class ContentPage extends QuartzEmitterPlugin {
|
|||||||
<body>
|
<body>
|
||||||
<div id="quartz-root" class="page">
|
<div id="quartz-root" class="page">
|
||||||
<Header {...componentData} >
|
<Header {...componentData} >
|
||||||
{header.map(HeaderComponent => <HeaderComponent {...componentData}/>)}
|
{header.map(HeaderComponent => <HeaderComponent {...componentData} position="header" />)}
|
||||||
</Header>
|
</Header>
|
||||||
<Body {...componentData}>
|
<Body {...componentData}>
|
||||||
{content}
|
{body.map(BodyComponent => <BodyComponent {...componentData } position="body" />)}
|
||||||
</Body>
|
</Body>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
|
@ -15,7 +15,7 @@ export class Description extends QuartzTransformerPlugin {
|
|||||||
name = "Description"
|
name = "Description"
|
||||||
opts: Options
|
opts: Options
|
||||||
|
|
||||||
constructor(opts?: Options) {
|
constructor(opts?: Partial<Options>) {
|
||||||
super()
|
super()
|
||||||
this.opts = { ...defaultOptions, ...opts }
|
this.opts = { ...defaultOptions, ...opts }
|
||||||
}
|
}
|
||||||
|
@ -17,7 +17,7 @@ export class FrontMatter extends QuartzTransformerPlugin {
|
|||||||
name = "FrontMatter"
|
name = "FrontMatter"
|
||||||
opts: Options
|
opts: Options
|
||||||
|
|
||||||
constructor(opts?: Options) {
|
constructor(opts?: Partial<Options>) {
|
||||||
super()
|
super()
|
||||||
this.opts = { ...defaultOptions, ...opts }
|
this.opts = { ...defaultOptions, ...opts }
|
||||||
}
|
}
|
||||||
|
@ -19,7 +19,7 @@ export class GitHubFlavoredMarkdown extends QuartzTransformerPlugin {
|
|||||||
name = "GitHubFlavoredMarkdown"
|
name = "GitHubFlavoredMarkdown"
|
||||||
opts: Options
|
opts: Options
|
||||||
|
|
||||||
constructor(opts?: Options) {
|
constructor(opts?: Partial<Options>) {
|
||||||
super()
|
super()
|
||||||
this.opts = { ...defaultOptions, ...opts }
|
this.opts = { ...defaultOptions, ...opts }
|
||||||
}
|
}
|
||||||
|
@ -6,3 +6,4 @@ export { Description } from './description'
|
|||||||
export { ResolveLinks } from './links'
|
export { ResolveLinks } from './links'
|
||||||
export { ObsidianFlavoredMarkdown } from './ofm'
|
export { ObsidianFlavoredMarkdown } from './ofm'
|
||||||
export { SyntaxHighlighting } from './syntax'
|
export { SyntaxHighlighting } from './syntax'
|
||||||
|
export { TableOfContents } from './toc'
|
||||||
|
@ -16,7 +16,7 @@ export class CreatedModifiedDate extends QuartzTransformerPlugin {
|
|||||||
name = "CreatedModifiedDate"
|
name = "CreatedModifiedDate"
|
||||||
opts: Options
|
opts: Options
|
||||||
|
|
||||||
constructor(opts?: Options) {
|
constructor(opts?: Partial<Options>) {
|
||||||
super()
|
super()
|
||||||
this.opts = {
|
this.opts = {
|
||||||
...defaultOptions,
|
...defaultOptions,
|
||||||
|
@ -21,7 +21,7 @@ export class ResolveLinks extends QuartzTransformerPlugin {
|
|||||||
name = "LinkProcessing"
|
name = "LinkProcessing"
|
||||||
opts: Options
|
opts: Options
|
||||||
|
|
||||||
constructor(opts?: Options) {
|
constructor(opts?: Partial<Options>) {
|
||||||
super()
|
super()
|
||||||
this.opts = { ...defaultOptions, ...opts }
|
this.opts = { ...defaultOptions, ...opts }
|
||||||
}
|
}
|
||||||
|
@ -93,7 +93,7 @@ export class ObsidianFlavoredMarkdown extends QuartzTransformerPlugin {
|
|||||||
name = "ObsidianFlavoredMarkdown"
|
name = "ObsidianFlavoredMarkdown"
|
||||||
opts: Options
|
opts: Options
|
||||||
|
|
||||||
constructor(opts?: Options) {
|
constructor(opts?: Partial<Options>) {
|
||||||
super()
|
super()
|
||||||
this.opts = { ...defaultOptions, ...opts }
|
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