bump to v4
This commit is contained in:
		@@ -25,6 +25,10 @@ interface BreadcrumbOptions {
 | 
			
		||||
   * Wether to display breadcrumbs on root `index.md`
 | 
			
		||||
   */
 | 
			
		||||
  hideOnRoot: boolean
 | 
			
		||||
  /**
 | 
			
		||||
   * Wether to display the current page in the breadcrumbs.
 | 
			
		||||
   */
 | 
			
		||||
  showCurrentPage: boolean
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const defaultOptions: BreadcrumbOptions = {
 | 
			
		||||
@@ -32,6 +36,7 @@ const defaultOptions: BreadcrumbOptions = {
 | 
			
		||||
  rootName: "Home",
 | 
			
		||||
  resolveFrontmatterTitle: true,
 | 
			
		||||
  hideOnRoot: true,
 | 
			
		||||
  showCurrentPage: true,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function formatCrumb(displayName: string, baseSlug: FullSlug, currentSlug: SimpleSlug): CrumbData {
 | 
			
		||||
@@ -63,8 +68,9 @@ export default ((opts?: Partial<BreadcrumbOptions>) => {
 | 
			
		||||
      // construct the index for the first time
 | 
			
		||||
      for (const file of allFiles) {
 | 
			
		||||
        if (file.slug?.endsWith("index")) {
 | 
			
		||||
          const folderParts = file.filePath?.split("/")
 | 
			
		||||
          const folderParts = file.slug?.split("/")
 | 
			
		||||
          if (folderParts) {
 | 
			
		||||
            // 2nd last to exclude the /index
 | 
			
		||||
            const folderName = folderParts[folderParts?.length - 2]
 | 
			
		||||
            folderIndex.set(folderName, file)
 | 
			
		||||
          }
 | 
			
		||||
@@ -83,7 +89,10 @@ export default ((opts?: Partial<BreadcrumbOptions>) => {
 | 
			
		||||
        // Try to resolve frontmatter folder title
 | 
			
		||||
        const currentFile = folderIndex?.get(curPathSegment)
 | 
			
		||||
        if (currentFile) {
 | 
			
		||||
          curPathSegment = currentFile.frontmatter!.title
 | 
			
		||||
          const title = currentFile.frontmatter!.title
 | 
			
		||||
          if (title !== "index") {
 | 
			
		||||
            curPathSegment = title
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Add current slug to full path
 | 
			
		||||
@@ -95,10 +104,12 @@ export default ((opts?: Partial<BreadcrumbOptions>) => {
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      // Add current file to crumb (can directly use frontmatter title)
 | 
			
		||||
      crumbs.push({
 | 
			
		||||
        displayName: fileData.frontmatter!.title,
 | 
			
		||||
        path: "",
 | 
			
		||||
      })
 | 
			
		||||
      if (options.showCurrentPage) {
 | 
			
		||||
        crumbs.push({
 | 
			
		||||
          displayName: fileData.frontmatter!.title,
 | 
			
		||||
          path: "",
 | 
			
		||||
        })
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    return (
 | 
			
		||||
      <nav class={`breadcrumb-container ${displayClass ?? ""}`} aria-label="breadcrumbs">
 | 
			
		||||
 
 | 
			
		||||
@@ -18,7 +18,7 @@ function Darkmode({ displayClass }: QuartzComponentProps) {
 | 
			
		||||
          x="0px"
 | 
			
		||||
          y="0px"
 | 
			
		||||
          viewBox="0 0 35 35"
 | 
			
		||||
          style="enable-background:new 0 0 35 35;"
 | 
			
		||||
          style="enable-background:new 0 0 35 35"
 | 
			
		||||
          xmlSpace="preserve"
 | 
			
		||||
        >
 | 
			
		||||
          <title>Light mode</title>
 | 
			
		||||
@@ -34,7 +34,7 @@ function Darkmode({ displayClass }: QuartzComponentProps) {
 | 
			
		||||
          x="0px"
 | 
			
		||||
          y="0px"
 | 
			
		||||
          viewBox="0 0 100 100"
 | 
			
		||||
          style="enable-background='new 0 0 100 100'"
 | 
			
		||||
          style="enable-background:new 0 0 100 100"
 | 
			
		||||
          xmlSpace="preserve"
 | 
			
		||||
        >
 | 
			
		||||
          <title>Dark mode</title>
 | 
			
		||||
 
 | 
			
		||||
@@ -12,6 +12,9 @@ const defaultOptions = {
 | 
			
		||||
  folderClickBehavior: "collapse",
 | 
			
		||||
  folderDefaultState: "collapsed",
 | 
			
		||||
  useSavedState: true,
 | 
			
		||||
  mapFn: (node) => {
 | 
			
		||||
    return node
 | 
			
		||||
  },
 | 
			
		||||
  sortFn: (a, b) => {
 | 
			
		||||
    // Sort order: folders first, then files. Sort folders and files alphabetically
 | 
			
		||||
    if ((!a.file && !b.file) || (a.file && b.file)) {
 | 
			
		||||
@@ -22,6 +25,7 @@ const defaultOptions = {
 | 
			
		||||
        sensitivity: "base",
 | 
			
		||||
      })
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (a.file && !b.file) {
 | 
			
		||||
      return 1
 | 
			
		||||
    } else {
 | 
			
		||||
@@ -41,46 +45,34 @@ export default ((userOpts?: Partial<Options>) => {
 | 
			
		||||
  let jsonTree: string
 | 
			
		||||
 | 
			
		||||
  function constructFileTree(allFiles: QuartzPluginData[]) {
 | 
			
		||||
    if (!fileTree) {
 | 
			
		||||
      // Construct tree from allFiles
 | 
			
		||||
      fileTree = new FileNode("")
 | 
			
		||||
      allFiles.forEach((file) => fileTree.add(file, 1))
 | 
			
		||||
    if (fileTree) {
 | 
			
		||||
      return
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
      /**
 | 
			
		||||
       * Keys of this object must match corresponding function name of `FileNode`,
 | 
			
		||||
       * while values must be the argument that will be passed to the function.
 | 
			
		||||
       *
 | 
			
		||||
       * e.g. entry for FileNode.sort: `sort: opts.sortFn` (value is sort function from options)
 | 
			
		||||
       */
 | 
			
		||||
      const functions = {
 | 
			
		||||
        map: opts.mapFn,
 | 
			
		||||
        sort: opts.sortFn,
 | 
			
		||||
        filter: opts.filterFn,
 | 
			
		||||
      }
 | 
			
		||||
    // Construct tree from allFiles
 | 
			
		||||
    fileTree = new FileNode("")
 | 
			
		||||
    allFiles.forEach((file) => fileTree.add(file))
 | 
			
		||||
 | 
			
		||||
      // Execute all functions (sort, filter, map) that were provided (if none were provided, only default "sort" is applied)
 | 
			
		||||
      if (opts.order) {
 | 
			
		||||
        // Order is important, use loop with index instead of order.map()
 | 
			
		||||
        for (let i = 0; i < opts.order.length; i++) {
 | 
			
		||||
          const functionName = opts.order[i]
 | 
			
		||||
          if (functions[functionName]) {
 | 
			
		||||
            // for every entry in order, call matching function in FileNode and pass matching argument
 | 
			
		||||
            // e.g. i = 0; functionName = "filter"
 | 
			
		||||
            // converted to: (if opts.filterFn) => fileTree.filter(opts.filterFn)
 | 
			
		||||
 | 
			
		||||
            // @ts-ignore
 | 
			
		||||
            // typescript cant statically check these dynamic references, so manually make sure reference is valid and ignore warning
 | 
			
		||||
            fileTree[functionName].call(fileTree, functions[functionName])
 | 
			
		||||
          }
 | 
			
		||||
    // Execute all functions (sort, filter, map) that were provided (if none were provided, only default "sort" is applied)
 | 
			
		||||
    if (opts.order) {
 | 
			
		||||
      // Order is important, use loop with index instead of order.map()
 | 
			
		||||
      for (let i = 0; i < opts.order.length; i++) {
 | 
			
		||||
        const functionName = opts.order[i]
 | 
			
		||||
        if (functionName === "map") {
 | 
			
		||||
          fileTree.map(opts.mapFn)
 | 
			
		||||
        } else if (functionName === "sort") {
 | 
			
		||||
          fileTree.sort(opts.sortFn)
 | 
			
		||||
        } else if (functionName === "filter") {
 | 
			
		||||
          fileTree.filter(opts.filterFn)
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      // Get all folders of tree. Initialize with collapsed state
 | 
			
		||||
      const folders = fileTree.getFolderPaths(opts.folderDefaultState === "collapsed")
 | 
			
		||||
 | 
			
		||||
      // Stringify to pass json tree as data attribute ([data-tree])
 | 
			
		||||
      jsonTree = JSON.stringify(folders)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Get all folders of tree. Initialize with collapsed state
 | 
			
		||||
    const folders = fileTree.getFolderPaths(opts.folderDefaultState === "collapsed")
 | 
			
		||||
 | 
			
		||||
    // Stringify to pass json tree as data attribute ([data-tree])
 | 
			
		||||
    jsonTree = JSON.stringify(folders)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  function Explorer({ allFiles, displayClass, fileData }: QuartzComponentProps) {
 | 
			
		||||
@@ -120,6 +112,7 @@ export default ((userOpts?: Partial<Options>) => {
 | 
			
		||||
      </div>
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Explorer.css = explorerStyle
 | 
			
		||||
  Explorer.afterDOMLoaded = script
 | 
			
		||||
  return Explorer
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,13 @@
 | 
			
		||||
// @ts-ignore
 | 
			
		||||
import { QuartzPluginData } from "../plugins/vfile"
 | 
			
		||||
import { resolveRelative } from "../util/path"
 | 
			
		||||
import {
 | 
			
		||||
  joinSegments,
 | 
			
		||||
  resolveRelative,
 | 
			
		||||
  clone,
 | 
			
		||||
  simplifySlug,
 | 
			
		||||
  SimpleSlug,
 | 
			
		||||
  FilePath,
 | 
			
		||||
} from "../util/path"
 | 
			
		||||
 | 
			
		||||
type OrderEntries = "sort" | "filter" | "map"
 | 
			
		||||
 | 
			
		||||
@@ -10,9 +17,9 @@ export interface Options {
 | 
			
		||||
  folderClickBehavior: "collapse" | "link"
 | 
			
		||||
  useSavedState: boolean
 | 
			
		||||
  sortFn: (a: FileNode, b: FileNode) => number
 | 
			
		||||
  filterFn?: (node: FileNode) => boolean
 | 
			
		||||
  mapFn?: (node: FileNode) => void
 | 
			
		||||
  order?: OrderEntries[]
 | 
			
		||||
  filterFn: (node: FileNode) => boolean
 | 
			
		||||
  mapFn: (node: FileNode) => void
 | 
			
		||||
  order: OrderEntries[]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type DataWrapper = {
 | 
			
		||||
@@ -25,59 +32,74 @@ export type FolderState = {
 | 
			
		||||
  collapsed: boolean
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function getPathSegment(fp: FilePath | undefined, idx: number): string | undefined {
 | 
			
		||||
  if (!fp) {
 | 
			
		||||
    return undefined
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return fp.split("/").at(idx)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Structure to add all files into a tree
 | 
			
		||||
export class FileNode {
 | 
			
		||||
  children: FileNode[]
 | 
			
		||||
  name: string
 | 
			
		||||
  children: Array<FileNode>
 | 
			
		||||
  name: string // this is the slug segment
 | 
			
		||||
  displayName: string
 | 
			
		||||
  file: QuartzPluginData | null
 | 
			
		||||
  depth: number
 | 
			
		||||
 | 
			
		||||
  constructor(name: string, file?: QuartzPluginData, depth?: number) {
 | 
			
		||||
  constructor(slugSegment: string, displayName?: string, file?: QuartzPluginData, depth?: number) {
 | 
			
		||||
    this.children = []
 | 
			
		||||
    this.name = name
 | 
			
		||||
    this.displayName = name
 | 
			
		||||
    this.file = file ? structuredClone(file) : null
 | 
			
		||||
    this.name = slugSegment
 | 
			
		||||
    this.displayName = displayName ?? file?.frontmatter?.title ?? slugSegment
 | 
			
		||||
    this.file = file ? clone(file) : null
 | 
			
		||||
    this.depth = depth ?? 0
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private insert(file: DataWrapper) {
 | 
			
		||||
    if (file.path.length === 1) {
 | 
			
		||||
      if (file.path[0] !== "index.md") {
 | 
			
		||||
        this.children.push(new FileNode(file.file.frontmatter!.title, file.file, this.depth + 1))
 | 
			
		||||
      } else {
 | 
			
		||||
        const title = file.file.frontmatter?.title
 | 
			
		||||
        if (title && title !== "index" && file.path[0] === "index.md") {
 | 
			
		||||
  private insert(fileData: DataWrapper) {
 | 
			
		||||
    if (fileData.path.length === 0) {
 | 
			
		||||
      return
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const nextSegment = fileData.path[0]
 | 
			
		||||
 | 
			
		||||
    // base case, insert here
 | 
			
		||||
    if (fileData.path.length === 1) {
 | 
			
		||||
      if (nextSegment === "") {
 | 
			
		||||
        // index case (we are the root and we just found index.md), set our data appropriately
 | 
			
		||||
        const title = fileData.file.frontmatter?.title
 | 
			
		||||
        if (title && title !== "index") {
 | 
			
		||||
          this.displayName = title
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    } else {
 | 
			
		||||
      const next = file.path[0]
 | 
			
		||||
      file.path = file.path.splice(1)
 | 
			
		||||
      for (const child of this.children) {
 | 
			
		||||
        if (child.name === next) {
 | 
			
		||||
          child.insert(file)
 | 
			
		||||
          return
 | 
			
		||||
        }
 | 
			
		||||
      } else {
 | 
			
		||||
        // direct child
 | 
			
		||||
        this.children.push(new FileNode(nextSegment, undefined, fileData.file, this.depth + 1))
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const newChild = new FileNode(next, undefined, this.depth + 1)
 | 
			
		||||
      newChild.insert(file)
 | 
			
		||||
      this.children.push(newChild)
 | 
			
		||||
      return
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // find the right child to insert into
 | 
			
		||||
    fileData.path = fileData.path.splice(1)
 | 
			
		||||
    const child = this.children.find((c) => c.name === nextSegment)
 | 
			
		||||
    if (child) {
 | 
			
		||||
      child.insert(fileData)
 | 
			
		||||
      return
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const newChild = new FileNode(
 | 
			
		||||
      nextSegment,
 | 
			
		||||
      getPathSegment(fileData.file.relativePath, this.depth),
 | 
			
		||||
      undefined,
 | 
			
		||||
      this.depth + 1,
 | 
			
		||||
    )
 | 
			
		||||
    newChild.insert(fileData)
 | 
			
		||||
    this.children.push(newChild)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Add new file to tree
 | 
			
		||||
  add(file: QuartzPluginData, splice: number = 0) {
 | 
			
		||||
    this.insert({ file, path: file.filePath!.split("/").splice(splice) })
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Print tree structure (for debugging)
 | 
			
		||||
  print(depth: number = 0) {
 | 
			
		||||
    let folderChar = ""
 | 
			
		||||
    if (!this.file) folderChar = "|"
 | 
			
		||||
    console.log("-".repeat(depth), folderChar, this.name, this.depth)
 | 
			
		||||
    this.children.forEach((e) => e.print(depth + 1))
 | 
			
		||||
  add(file: QuartzPluginData) {
 | 
			
		||||
    this.insert({ file: file, path: simplifySlug(file.slug!).split("/") })
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
@@ -95,7 +117,6 @@ export class FileNode {
 | 
			
		||||
   */
 | 
			
		||||
  map(mapFn: (node: FileNode) => void) {
 | 
			
		||||
    mapFn(this)
 | 
			
		||||
 | 
			
		||||
    this.children.forEach((child) => child.map(mapFn))
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@@ -110,16 +131,16 @@ export class FileNode {
 | 
			
		||||
 | 
			
		||||
    const traverse = (node: FileNode, currentPath: string) => {
 | 
			
		||||
      if (!node.file) {
 | 
			
		||||
        const folderPath = currentPath + (currentPath ? "/" : "") + node.name
 | 
			
		||||
        const folderPath = joinSegments(currentPath, node.name)
 | 
			
		||||
        if (folderPath !== "") {
 | 
			
		||||
          folderPaths.push({ path: folderPath, collapsed })
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        node.children.forEach((child) => traverse(child, folderPath))
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    traverse(this, "")
 | 
			
		||||
 | 
			
		||||
    return folderPaths
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@@ -147,14 +168,13 @@ export function ExplorerNode({ node, opts, fullPath, fileData }: ExplorerNodePro
 | 
			
		||||
  const isDefaultOpen = opts.folderDefaultState === "open"
 | 
			
		||||
 | 
			
		||||
  // Calculate current folderPath
 | 
			
		||||
  let pathOld = fullPath ? fullPath : ""
 | 
			
		||||
  let folderPath = ""
 | 
			
		||||
  if (node.name !== "") {
 | 
			
		||||
    folderPath = `${pathOld}/${node.name}`
 | 
			
		||||
    folderPath = joinSegments(fullPath ?? "", node.name)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <li>
 | 
			
		||||
    <>
 | 
			
		||||
      {node.file ? (
 | 
			
		||||
        // Single file node
 | 
			
		||||
        <li key={node.file.slug}>
 | 
			
		||||
@@ -163,7 +183,7 @@ export function ExplorerNode({ node, opts, fullPath, fileData }: ExplorerNodePro
 | 
			
		||||
          </a>
 | 
			
		||||
        </li>
 | 
			
		||||
      ) : (
 | 
			
		||||
        <div>
 | 
			
		||||
        <li>
 | 
			
		||||
          {node.name !== "" && (
 | 
			
		||||
            // Node with entire folder
 | 
			
		||||
            // Render svg button + folder name, then children
 | 
			
		||||
@@ -185,12 +205,16 @@ export function ExplorerNode({ node, opts, fullPath, fileData }: ExplorerNodePro
 | 
			
		||||
              {/* render <a> tag if folderBehavior is "link", otherwise render <button> with collapse click event */}
 | 
			
		||||
              <div key={node.name} data-folderpath={folderPath}>
 | 
			
		||||
                {folderBehavior === "link" ? (
 | 
			
		||||
                  <a href={`${folderPath}`} data-for={node.name} class="folder-title">
 | 
			
		||||
                  <a
 | 
			
		||||
                    href={resolveRelative(fileData.slug!, folderPath as SimpleSlug)}
 | 
			
		||||
                    data-for={node.name}
 | 
			
		||||
                    class="folder-title"
 | 
			
		||||
                  >
 | 
			
		||||
                    {node.displayName}
 | 
			
		||||
                  </a>
 | 
			
		||||
                ) : (
 | 
			
		||||
                  <button class="folder-button">
 | 
			
		||||
                    <p class="folder-title">{node.displayName}</p>
 | 
			
		||||
                    <span class="folder-title">{node.displayName}</span>
 | 
			
		||||
                  </button>
 | 
			
		||||
                )}
 | 
			
		||||
              </div>
 | 
			
		||||
@@ -217,8 +241,8 @@ export function ExplorerNode({ node, opts, fullPath, fileData }: ExplorerNodePro
 | 
			
		||||
              ))}
 | 
			
		||||
            </ul>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
        </li>
 | 
			
		||||
      )}
 | 
			
		||||
    </li>
 | 
			
		||||
    </>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -27,8 +27,12 @@ function TagContent(props: QuartzComponentProps) {
 | 
			
		||||
      ? fileData.description
 | 
			
		||||
      : htmlToJsx(fileData.filePath!, tree)
 | 
			
		||||
 | 
			
		||||
  if (tag === "") {
 | 
			
		||||
    const tags = [...new Set(allFiles.flatMap((data) => data.frontmatter?.tags ?? []))]
 | 
			
		||||
  if (tag === "/") {
 | 
			
		||||
    const tags = [
 | 
			
		||||
      ...new Set(
 | 
			
		||||
        allFiles.flatMap((data) => data.frontmatter?.tags ?? []).flatMap(getAllSegmentPrefixes),
 | 
			
		||||
      ),
 | 
			
		||||
    ].sort((a, b) => a.localeCompare(b))
 | 
			
		||||
    const tagItemMap: Map<string, QuartzPluginData[]> = new Map()
 | 
			
		||||
    for (const tag of tags) {
 | 
			
		||||
      tagItemMap.set(tag, allPagesWithTag(tag))
 | 
			
		||||
 
 | 
			
		||||
@@ -3,9 +3,10 @@ import { QuartzComponent, QuartzComponentProps } from "./types"
 | 
			
		||||
import HeaderConstructor from "./Header"
 | 
			
		||||
import BodyConstructor from "./Body"
 | 
			
		||||
import { JSResourceToScriptElement, StaticResources } from "../util/resources"
 | 
			
		||||
import { FullSlug, RelativeURL, joinSegments } from "../util/path"
 | 
			
		||||
import { FullSlug, RelativeURL, joinSegments, normalizeHastElement } from "../util/path"
 | 
			
		||||
import { visit } from "unist-util-visit"
 | 
			
		||||
import { Root, Element, ElementContent } from "hast"
 | 
			
		||||
import { QuartzPluginData } from "../plugins/vfile"
 | 
			
		||||
 | 
			
		||||
interface RenderComponents {
 | 
			
		||||
  head: QuartzComponent
 | 
			
		||||
@@ -22,7 +23,7 @@ export function pageResources(
 | 
			
		||||
  staticResources: StaticResources,
 | 
			
		||||
): StaticResources {
 | 
			
		||||
  const contentIndexPath = joinSegments(baseDir, "static/contentIndex.json")
 | 
			
		||||
  const contentIndexScript = `const fetchData = fetch(\`${contentIndexPath}\`).then(data => data.json())`
 | 
			
		||||
  const contentIndexScript = `const fetchData = fetch("${contentIndexPath}").then(data => data.json())`
 | 
			
		||||
 | 
			
		||||
  return {
 | 
			
		||||
    css: [joinSegments(baseDir, "index.css"), ...staticResources.css],
 | 
			
		||||
@@ -49,6 +50,18 @@ export function pageResources(
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
let pageIndex: Map<FullSlug, QuartzPluginData> | undefined = undefined
 | 
			
		||||
function getOrComputeFileIndex(allFiles: QuartzPluginData[]): Map<FullSlug, QuartzPluginData> {
 | 
			
		||||
  if (!pageIndex) {
 | 
			
		||||
    pageIndex = new Map()
 | 
			
		||||
    for (const file of allFiles) {
 | 
			
		||||
      pageIndex.set(file.slug!, file)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return pageIndex
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function renderPage(
 | 
			
		||||
  slug: FullSlug,
 | 
			
		||||
  componentData: QuartzComponentProps,
 | 
			
		||||
@@ -61,30 +74,29 @@ export function renderPage(
 | 
			
		||||
      const classNames = (node.properties?.className ?? []) as string[]
 | 
			
		||||
      if (classNames.includes("transclude")) {
 | 
			
		||||
        const inner = node.children[0] as Element
 | 
			
		||||
        const transcludeTarget = inner.properties?.["data-slug"] as FullSlug
 | 
			
		||||
 | 
			
		||||
        // TODO: avoid this expensive find operation and construct an index ahead of time
 | 
			
		||||
        const page = componentData.allFiles.find((f) => f.slug === transcludeTarget)
 | 
			
		||||
        const transcludeTarget = inner.properties["data-slug"] as FullSlug
 | 
			
		||||
        const page = getOrComputeFileIndex(componentData.allFiles).get(transcludeTarget)
 | 
			
		||||
        if (!page) {
 | 
			
		||||
          return
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let blockRef = node.properties?.dataBlock as string | undefined
 | 
			
		||||
        if (blockRef?.startsWith("^")) {
 | 
			
		||||
        let blockRef = node.properties.dataBlock as string | undefined
 | 
			
		||||
        if (blockRef?.startsWith("#^")) {
 | 
			
		||||
          // block transclude
 | 
			
		||||
          blockRef = blockRef.slice(1)
 | 
			
		||||
          blockRef = blockRef.slice("#^".length)
 | 
			
		||||
          let blockNode = page.blocks?.[blockRef]
 | 
			
		||||
          if (blockNode) {
 | 
			
		||||
            if (blockNode.tagName === "li") {
 | 
			
		||||
              blockNode = {
 | 
			
		||||
                type: "element",
 | 
			
		||||
                tagName: "ul",
 | 
			
		||||
                properties: {},
 | 
			
		||||
                children: [blockNode],
 | 
			
		||||
              }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            node.children = [
 | 
			
		||||
              blockNode,
 | 
			
		||||
              normalizeHastElement(blockNode, slug, transcludeTarget),
 | 
			
		||||
              {
 | 
			
		||||
                type: "element",
 | 
			
		||||
                tagName: "a",
 | 
			
		||||
@@ -104,7 +116,7 @@ export function renderPage(
 | 
			
		||||
                break
 | 
			
		||||
              }
 | 
			
		||||
 | 
			
		||||
              if (startIdx) {
 | 
			
		||||
              if (startIdx !== undefined) {
 | 
			
		||||
                endIdx = i
 | 
			
		||||
              } else if (el.properties?.id === blockRef) {
 | 
			
		||||
                startIdx = i
 | 
			
		||||
@@ -112,12 +124,14 @@ export function renderPage(
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          if (!startIdx) {
 | 
			
		||||
          if (startIdx === undefined) {
 | 
			
		||||
            return
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          node.children = [
 | 
			
		||||
            ...(page.htmlAst.children.slice(startIdx, endIdx) as ElementContent[]),
 | 
			
		||||
            ...(page.htmlAst.children.slice(startIdx, endIdx) as ElementContent[]).map((child) =>
 | 
			
		||||
              normalizeHastElement(child as Element, slug, transcludeTarget),
 | 
			
		||||
            ),
 | 
			
		||||
            {
 | 
			
		||||
              type: "element",
 | 
			
		||||
              tagName: "a",
 | 
			
		||||
@@ -131,11 +145,14 @@ export function renderPage(
 | 
			
		||||
            {
 | 
			
		||||
              type: "element",
 | 
			
		||||
              tagName: "h1",
 | 
			
		||||
              properties: {},
 | 
			
		||||
              children: [
 | 
			
		||||
                { type: "text", value: page.frontmatter?.title ?? `Transclude of ${page.slug}` },
 | 
			
		||||
              ],
 | 
			
		||||
            },
 | 
			
		||||
            ...(page.htmlAst.children as ElementContent[]),
 | 
			
		||||
            ...(page.htmlAst.children as ElementContent[]).map((child) =>
 | 
			
		||||
              normalizeHastElement(child as Element, slug, transcludeTarget),
 | 
			
		||||
            ),
 | 
			
		||||
            {
 | 
			
		||||
              type: "element",
 | 
			
		||||
              tagName: "a",
 | 
			
		||||
 
 | 
			
		||||
@@ -59,8 +59,7 @@ function toggleFolder(evt: MouseEvent) {
 | 
			
		||||
  // Save folder state to localStorage
 | 
			
		||||
  const clickFolderPath = currentFolderParent.dataset.folderpath as string
 | 
			
		||||
 | 
			
		||||
  // Remove leading "/"
 | 
			
		||||
  const fullFolderPath = clickFolderPath.substring(1)
 | 
			
		||||
  const fullFolderPath = clickFolderPath
 | 
			
		||||
  toggleCollapsedByPath(explorerState, fullFolderPath)
 | 
			
		||||
 | 
			
		||||
  const stringifiedFileTree = JSON.stringify(explorerState)
 | 
			
		||||
@@ -108,9 +107,7 @@ function setupExplorer() {
 | 
			
		||||
    explorerState = JSON.parse(storageTree)
 | 
			
		||||
    explorerState.map((folderUl) => {
 | 
			
		||||
      // grab <li> element for matching folder path
 | 
			
		||||
      const folderLi = document.querySelector(
 | 
			
		||||
        `[data-folderpath='/${folderUl.path}']`,
 | 
			
		||||
      ) as HTMLElement
 | 
			
		||||
      const folderLi = document.querySelector(`[data-folderpath='${folderUl.path}']`) as HTMLElement
 | 
			
		||||
 | 
			
		||||
      // Get corresponding content <ul> tag and set state
 | 
			
		||||
      if (folderLi) {
 | 
			
		||||
@@ -120,9 +117,9 @@ function setupExplorer() {
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    })
 | 
			
		||||
  } else {
 | 
			
		||||
  } else if (explorer?.dataset.tree) {
 | 
			
		||||
    // If tree is not in localStorage or config is disabled, use tree passed from Explorer as dataset
 | 
			
		||||
    explorerState = JSON.parse(explorer?.dataset.tree as string)
 | 
			
		||||
    explorerState = JSON.parse(explorer.dataset.tree)
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -130,12 +127,13 @@ window.addEventListener("resize", setupExplorer)
 | 
			
		||||
document.addEventListener("nav", () => {
 | 
			
		||||
  setupExplorer()
 | 
			
		||||
 | 
			
		||||
  const explorerContent = document.getElementById("explorer-ul")
 | 
			
		||||
  observer.disconnect()
 | 
			
		||||
 | 
			
		||||
  // select pseudo element at end of list
 | 
			
		||||
  const lastItem = document.getElementById("explorer-end")
 | 
			
		||||
 | 
			
		||||
  observer.disconnect()
 | 
			
		||||
  observer.observe(lastItem as Element)
 | 
			
		||||
  if (lastItem) {
 | 
			
		||||
    observer.observe(lastItem)
 | 
			
		||||
  }
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
import type { ContentDetails } from "../../plugins/emitters/contentIndex"
 | 
			
		||||
import type { ContentDetails, ContentIndex } from "../../plugins/emitters/contentIndex"
 | 
			
		||||
import * as d3 from "d3"
 | 
			
		||||
import { registerEscapeHandler, removeAllChildren } from "./util"
 | 
			
		||||
import { FullSlug, SimpleSlug, getFullSlug, resolveRelative, simplifySlug } from "../../util/path"
 | 
			
		||||
@@ -46,20 +46,22 @@ async function renderGraph(container: string, fullSlug: FullSlug) {
 | 
			
		||||
    showTags,
 | 
			
		||||
  } = JSON.parse(graph.dataset["cfg"]!)
 | 
			
		||||
 | 
			
		||||
  const data = await fetchData
 | 
			
		||||
 | 
			
		||||
  const data: Map<SimpleSlug, ContentDetails> = new Map(
 | 
			
		||||
    Object.entries<ContentDetails>(await fetchData).map(([k, v]) => [
 | 
			
		||||
      simplifySlug(k as FullSlug),
 | 
			
		||||
      v,
 | 
			
		||||
    ]),
 | 
			
		||||
  )
 | 
			
		||||
  const links: LinkData[] = []
 | 
			
		||||
  const tags: SimpleSlug[] = []
 | 
			
		||||
 | 
			
		||||
  const validLinks = new Set(Object.keys(data).map((slug) => simplifySlug(slug as FullSlug)))
 | 
			
		||||
 | 
			
		||||
  for (const [src, details] of Object.entries<ContentDetails>(data)) {
 | 
			
		||||
    const source = simplifySlug(src as FullSlug)
 | 
			
		||||
  const validLinks = new Set(data.keys())
 | 
			
		||||
  for (const [source, details] of data.entries()) {
 | 
			
		||||
    const outgoing = details.links ?? []
 | 
			
		||||
 | 
			
		||||
    for (const dest of outgoing) {
 | 
			
		||||
      if (validLinks.has(dest)) {
 | 
			
		||||
        links.push({ source, target: dest })
 | 
			
		||||
        links.push({ source: source, target: dest })
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -71,7 +73,7 @@ async function renderGraph(container: string, fullSlug: FullSlug) {
 | 
			
		||||
      tags.push(...localTags.filter((tag) => !tags.includes(tag)))
 | 
			
		||||
 | 
			
		||||
      for (const tag of localTags) {
 | 
			
		||||
        links.push({ source, target: tag })
 | 
			
		||||
        links.push({ source: source, target: tag })
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
@@ -93,17 +95,17 @@ async function renderGraph(container: string, fullSlug: FullSlug) {
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  } else {
 | 
			
		||||
    Object.keys(data).forEach((id) => neighbourhood.add(simplifySlug(id as FullSlug)))
 | 
			
		||||
    validLinks.forEach((id) => neighbourhood.add(id))
 | 
			
		||||
    if (showTags) tags.forEach((tag) => neighbourhood.add(tag))
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const graphData: { nodes: NodeData[]; links: LinkData[] } = {
 | 
			
		||||
    nodes: [...neighbourhood].map((url) => {
 | 
			
		||||
      const text = url.startsWith("tags/") ? "#" + url.substring(5) : data[url]?.title ?? url
 | 
			
		||||
      const text = url.startsWith("tags/") ? "#" + url.substring(5) : data.get(url)?.title ?? url
 | 
			
		||||
      return {
 | 
			
		||||
        id: url,
 | 
			
		||||
        text: text,
 | 
			
		||||
        tags: data[url]?.tags ?? [],
 | 
			
		||||
        tags: data.get(url)?.tags ?? [],
 | 
			
		||||
      }
 | 
			
		||||
    }),
 | 
			
		||||
    links: links.filter((l) => neighbourhood.has(l.source) && neighbourhood.has(l.target)),
 | 
			
		||||
@@ -200,7 +202,7 @@ async function renderGraph(container: string, fullSlug: FullSlug) {
 | 
			
		||||
      window.spaNavigate(new URL(targ, window.location.toString()))
 | 
			
		||||
    })
 | 
			
		||||
    .on("mouseover", function (_, d) {
 | 
			
		||||
      const neighbours: SimpleSlug[] = data[fullSlug].links ?? []
 | 
			
		||||
      const neighbours: SimpleSlug[] = data.get(slug)?.links ?? []
 | 
			
		||||
      const neighbourNodes = d3
 | 
			
		||||
        .selectAll<HTMLElement, NodeData>(".node")
 | 
			
		||||
        .filter((d) => neighbours.includes(d.id))
 | 
			
		||||
 
 | 
			
		||||
@@ -1,3 +0,0 @@
 | 
			
		||||
import Plausible from "plausible-tracker"
 | 
			
		||||
const { trackPageview } = Plausible()
 | 
			
		||||
document.addEventListener("nav", () => trackPageview())
 | 
			
		||||
@@ -1,16 +1,5 @@
 | 
			
		||||
import { computePosition, flip, inline, shift } from "@floating-ui/dom"
 | 
			
		||||
 | 
			
		||||
// from micromorph/src/utils.ts
 | 
			
		||||
// https://github.com/natemoo-re/micromorph/blob/main/src/utils.ts#L5
 | 
			
		||||
export function normalizeRelativeURLs(el: Element | Document, base: string | URL) {
 | 
			
		||||
  const update = (el: Element, attr: string, base: string | URL) => {
 | 
			
		||||
    el.setAttribute(attr, new URL(el.getAttribute(attr)!, base).pathname)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  el.querySelectorAll('[href^="./"], [href^="../"]').forEach((item) => update(item, "href", base))
 | 
			
		||||
 | 
			
		||||
  el.querySelectorAll('[src^="./"], [src^="../"]').forEach((item) => update(item, "src", base))
 | 
			
		||||
}
 | 
			
		||||
import { normalizeRelativeURLs } from "../../util/path"
 | 
			
		||||
 | 
			
		||||
const p = new DOMParser()
 | 
			
		||||
async function mouseEnterHandler(
 | 
			
		||||
@@ -18,6 +7,10 @@ async function mouseEnterHandler(
 | 
			
		||||
  { clientX, clientY }: { clientX: number; clientY: number },
 | 
			
		||||
) {
 | 
			
		||||
  const link = this
 | 
			
		||||
  if (link.dataset.noPopover === "true") {
 | 
			
		||||
    return
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async function setPosition(popoverElement: HTMLElement) {
 | 
			
		||||
    const { x, y } = await computePosition(link, popoverElement, {
 | 
			
		||||
      middleware: [inline({ x: clientX, y: clientY }), shift(), flip()],
 | 
			
		||||
@@ -43,8 +36,6 @@ async function mouseEnterHandler(
 | 
			
		||||
  const hash = targetUrl.hash
 | 
			
		||||
  targetUrl.hash = ""
 | 
			
		||||
  targetUrl.search = ""
 | 
			
		||||
  // prevent hover of the same page
 | 
			
		||||
  if (thisUrl.toString() === targetUrl.toString()) return
 | 
			
		||||
 | 
			
		||||
  const contents = await fetch(`${targetUrl}`)
 | 
			
		||||
    .then((res) => res.text())
 | 
			
		||||
 
 | 
			
		||||
@@ -1,10 +1,8 @@
 | 
			
		||||
import micromorph from "micromorph"
 | 
			
		||||
import { FullSlug, RelativeURL, getFullSlug } from "../../util/path"
 | 
			
		||||
import { normalizeRelativeURLs } from "./popover.inline"
 | 
			
		||||
import { FullSlug, RelativeURL, getFullSlug, normalizeRelativeURLs } from "../../util/path"
 | 
			
		||||
 | 
			
		||||
// adapted from `micromorph`
 | 
			
		||||
// https://github.com/natemoo-re/micromorph
 | 
			
		||||
 | 
			
		||||
const NODE_TYPE_ELEMENT = 1
 | 
			
		||||
let announcer = document.createElement("route-announcer")
 | 
			
		||||
const isElement = (target: EventTarget | null): target is Element =>
 | 
			
		||||
@@ -45,7 +43,14 @@ let p: DOMParser
 | 
			
		||||
async function navigate(url: URL, isBack: boolean = false) {
 | 
			
		||||
  p = p || new DOMParser()
 | 
			
		||||
  const contents = await fetch(`${url}`)
 | 
			
		||||
    .then((res) => res.text())
 | 
			
		||||
    .then((res) => {
 | 
			
		||||
      const contentType = res.headers.get("content-type")
 | 
			
		||||
      if (contentType?.startsWith("text/html")) {
 | 
			
		||||
        return res.text()
 | 
			
		||||
      } else {
 | 
			
		||||
        window.location.assign(url)
 | 
			
		||||
      }
 | 
			
		||||
    })
 | 
			
		||||
    .catch(() => {
 | 
			
		||||
      window.location.assign(url)
 | 
			
		||||
    })
 | 
			
		||||
@@ -109,6 +114,7 @@ function createRouter() {
 | 
			
		||||
      if (isSamePage(url) && url.hash) {
 | 
			
		||||
        const el = document.getElementById(decodeURIComponent(url.hash.substring(1)))
 | 
			
		||||
        el?.scrollIntoView()
 | 
			
		||||
        history.pushState({}, "", url)
 | 
			
		||||
        return
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,7 @@
 | 
			
		||||
  float: right;
 | 
			
		||||
  right: 0;
 | 
			
		||||
  padding: 0.4rem;
 | 
			
		||||
  margin: -0.2rem 0.3rem;
 | 
			
		||||
  margin: 0.3rem;
 | 
			
		||||
  color: var(--gray);
 | 
			
		||||
  border-color: var(--dark);
 | 
			
		||||
  background-color: var(--light);
 | 
			
		||||
 
 | 
			
		||||
@@ -106,7 +106,7 @@ svg {
 | 
			
		||||
    align-items: center;
 | 
			
		||||
    font-family: var(--headerFont);
 | 
			
		||||
 | 
			
		||||
    & p {
 | 
			
		||||
    & span {
 | 
			
		||||
      font-size: 0.95rem;
 | 
			
		||||
      display: inline-block;
 | 
			
		||||
      color: var(--secondary);
 | 
			
		||||
 
 | 
			
		||||
@@ -30,6 +30,7 @@ button#toc {
 | 
			
		||||
  overflow: hidden;
 | 
			
		||||
  max-height: none;
 | 
			
		||||
  transition: max-height 0.5s ease;
 | 
			
		||||
  position: relative;
 | 
			
		||||
 | 
			
		||||
  &.collapsed > .overflow::after {
 | 
			
		||||
    opacity: 0;
 | 
			
		||||
 
 | 
			
		||||
@@ -9,7 +9,7 @@ export type QuartzComponentProps = {
 | 
			
		||||
  fileData: QuartzPluginData
 | 
			
		||||
  cfg: GlobalConfiguration
 | 
			
		||||
  children: (QuartzComponent | JSX.Element)[]
 | 
			
		||||
  tree: Node<QuartzPluginData>
 | 
			
		||||
  tree: Node
 | 
			
		||||
  allFiles: QuartzPluginData[]
 | 
			
		||||
  displayClass?: "mobile-only" | "desktop-only"
 | 
			
		||||
} & JSX.IntrinsicAttributes & {
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user