modern toc tweaks
This commit is contained in:
		
							parent
							
								
									9d2024b11c
								
							
						
					
					
						commit
						917d5791ac
					
				
					 17 changed files with 318 additions and 58 deletions
				
			
		
							
								
								
									
										13
									
								
								index.d.ts
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										13
									
								
								index.d.ts
									
										
									
									
										vendored
									
									
								
							| 
						 | 
					@ -2,3 +2,16 @@ declare module '*.scss' {
 | 
				
			||||||
  const content: string
 | 
					  const content: string
 | 
				
			||||||
  export = content
 | 
					  export = content
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// dom custom event
 | 
				
			||||||
 | 
					interface CustomEventMap {
 | 
				
			||||||
 | 
					  "spa_nav": CustomEvent<{ url: string }>;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					declare global {
 | 
				
			||||||
 | 
					  interface Document {
 | 
				
			||||||
 | 
					    addEventListener<K extends keyof CustomEventMap>(type: K,
 | 
				
			||||||
 | 
					      listener: (this: Document, ev: CustomEventMap[K]) => void): void;
 | 
				
			||||||
 | 
					    dispatchEvent<K extends keyof CustomEventMap>(ev: CustomEventMap[K]): void;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -25,7 +25,7 @@ const config: QuartzConfig = {
 | 
				
			||||||
          highlight: 'rgba(143, 159, 169, 0.15)',
 | 
					          highlight: 'rgba(143, 159, 169, 0.15)',
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        darkMode: {
 | 
					        darkMode: {
 | 
				
			||||||
          light: '#1e1e21',
 | 
					          light: '#161618',
 | 
				
			||||||
          lightgray: '#292629',
 | 
					          lightgray: '#292629',
 | 
				
			||||||
          gray: '#343434',
 | 
					          gray: '#343434',
 | 
				
			||||||
          darkgray: '#d4d4d4',
 | 
					          darkgray: '#d4d4d4',
 | 
				
			||||||
| 
						 | 
					@ -41,7 +41,7 @@ const config: QuartzConfig = {
 | 
				
			||||||
    transformers: [
 | 
					    transformers: [
 | 
				
			||||||
      Plugin.FrontMatter(),
 | 
					      Plugin.FrontMatter(),
 | 
				
			||||||
      Plugin.Description(),
 | 
					      Plugin.Description(),
 | 
				
			||||||
      Plugin.TableOfContents({ showByDefault: true }),
 | 
					      Plugin.TableOfContents(),
 | 
				
			||||||
      Plugin.CreatedModifiedDate({
 | 
					      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
 | 
				
			||||||
      }),
 | 
					      }),
 | 
				
			||||||
| 
						 | 
					@ -55,11 +55,23 @@ const config: QuartzConfig = {
 | 
				
			||||||
      Plugin.RemoveDrafts()
 | 
					      Plugin.RemoveDrafts()
 | 
				
			||||||
    ],
 | 
					    ],
 | 
				
			||||||
    emitters: [
 | 
					    emitters: [
 | 
				
			||||||
 | 
					      Plugin.AliasRedirects(),
 | 
				
			||||||
      Plugin.ContentPage({
 | 
					      Plugin.ContentPage({
 | 
				
			||||||
        head: Component.Head(),
 | 
					        head: Component.Head(),
 | 
				
			||||||
        header: [Component.PageTitle(), Component.Spacer(), Component.Darkmode()],
 | 
					        header: [Component.PageTitle(), Component.Spacer(), Component.Darkmode()],
 | 
				
			||||||
        body: [Component.ArticleTitle(), Component.ReadingTime(), Component.TagList(), Component.TableOfContents(), Component.Content()]
 | 
					        body: [
 | 
				
			||||||
      })
 | 
					          Component.ArticleTitle(),
 | 
				
			||||||
 | 
					          Component.ReadingTime(),
 | 
				
			||||||
 | 
					          Component.TagList(),
 | 
				
			||||||
 | 
					          Component.TableOfContents(),
 | 
				
			||||||
 | 
					          Component.Content()
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					        left: [],
 | 
				
			||||||
 | 
					        right: [],
 | 
				
			||||||
 | 
					        footer: []
 | 
				
			||||||
 | 
					      }),
 | 
				
			||||||
 | 
					      Plugin.ContentIndex(), // you can exclude this if you don't plan on using popovers, graph, or backlinks,
 | 
				
			||||||
 | 
					      Plugin.CNAME({ domain: "yoursite.xyz" }) // set this to your final deployed domain
 | 
				
			||||||
    ]
 | 
					    ]
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -57,6 +57,7 @@ export default async function buildQuartz(argv: Argv, version: string) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if (argv.serve) {
 | 
					  if (argv.serve) {
 | 
				
			||||||
    const server = http.createServer(async (req, res) => {
 | 
					    const server = http.createServer(async (req, res) => {
 | 
				
			||||||
 | 
					      console.log(chalk.grey(`[req] ${req.url}`))
 | 
				
			||||||
      return serveHandler(req, res, {
 | 
					      return serveHandler(req, res, {
 | 
				
			||||||
        public: output,
 | 
					        public: output,
 | 
				
			||||||
        directoryListing: false,
 | 
					        directoryListing: false,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,3 +1,4 @@
 | 
				
			||||||
 | 
					// @ts-ignore
 | 
				
			||||||
import clipboardScript from './scripts/clipboard.inline'
 | 
					import clipboardScript from './scripts/clipboard.inline'
 | 
				
			||||||
import clipboardStyle from './styles/clipboard.scss'
 | 
					import clipboardStyle from './styles/clipboard.scss'
 | 
				
			||||||
import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
 | 
					import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,22 +1,17 @@
 | 
				
			||||||
import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
 | 
					import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
 | 
				
			||||||
import style from "./styles/toc.scss"
 | 
					import legacyStyle from "./styles/legacyToc.scss"
 | 
				
			||||||
 | 
					import modernStyle from "./styles/toc.scss"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
interface Options {
 | 
					interface Options {
 | 
				
			||||||
  layout: 'modern' | 'quartz-3'
 | 
					  layout: 'modern' | 'legacy'
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const defaultOptions: Options = {
 | 
					const defaultOptions: Options = {
 | 
				
			||||||
  layout: 'quartz-3'
 | 
					  layout: 'modern'
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default ((opts?: Partial<Options>) => {
 | 
					export default ((opts?: Partial<Options>) => {
 | 
				
			||||||
  const layout = opts?.layout ?? defaultOptions.layout
 | 
					  const layout = opts?.layout ?? defaultOptions.layout
 | 
				
			||||||
  if (layout === "modern") {
 | 
					 | 
				
			||||||
    return function() {
 | 
					 | 
				
			||||||
      return null // TODO (make this look like nextra)
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  } else {
 | 
					 | 
				
			||||||
  function TableOfContents({ fileData }: QuartzComponentProps) {
 | 
					  function TableOfContents({ fileData }: QuartzComponentProps) {
 | 
				
			||||||
    if (!fileData.toc) {
 | 
					    if (!fileData.toc) {
 | 
				
			||||||
      return null
 | 
					      return null
 | 
				
			||||||
| 
						 | 
					@ -26,13 +21,45 @@ export default ((opts?: Partial<Options>) => {
 | 
				
			||||||
      <summary><h3>Table of Contents</h3></summary>
 | 
					      <summary><h3>Table of Contents</h3></summary>
 | 
				
			||||||
      <ul>
 | 
					      <ul>
 | 
				
			||||||
        {fileData.toc.map(tocEntry => <li key={tocEntry.slug} class={`depth-${tocEntry.depth}`}>
 | 
					        {fileData.toc.map(tocEntry => <li key={tocEntry.slug} class={`depth-${tocEntry.depth}`}>
 | 
				
			||||||
            <a href={`#${tocEntry.slug}`}>{tocEntry.text}</a>
 | 
					          <a href={`#${tocEntry.slug}`} data-for={tocEntry.slug}>{tocEntry.text}</a>
 | 
				
			||||||
        </li>)}
 | 
					        </li>)}
 | 
				
			||||||
      </ul>
 | 
					      </ul>
 | 
				
			||||||
    </details>
 | 
					    </details>
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    TableOfContents.css = style
 | 
					  TableOfContents.css = layout === "modern" ? modernStyle : legacyStyle
 | 
				
			||||||
    return TableOfContents
 | 
					
 | 
				
			||||||
 | 
					  if (layout === "modern") {
 | 
				
			||||||
 | 
					    TableOfContents.afterDOMLoaded = `
 | 
				
			||||||
 | 
					const bufferPx = 150
 | 
				
			||||||
 | 
					const observer = new IntersectionObserver(entries => {
 | 
				
			||||||
 | 
					  for (const entry of entries) {
 | 
				
			||||||
 | 
					    const slug = entry.target.id
 | 
				
			||||||
 | 
					    const tocEntryElement = document.querySelector(\`a[data-for="$\{slug\}"]\`)
 | 
				
			||||||
 | 
					    const windowHeight = entry.rootBounds?.height
 | 
				
			||||||
 | 
					    if (windowHeight && tocEntryElement) {
 | 
				
			||||||
 | 
					      if (entry.boundingClientRect.y < windowHeight) {
 | 
				
			||||||
 | 
					        tocEntryElement.classList.add("in-view")
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        tocEntryElement.classList.remove("in-view")
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function init() {
 | 
				
			||||||
 | 
					  const headers = document.querySelectorAll("h1[id], h2[id], h3[id], h4[id], h5[id], h6[id]")
 | 
				
			||||||
 | 
					  headers.forEach(header => observer.observe(header))
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					init()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					document.addEventListener("spa_nav", (e) => {
 | 
				
			||||||
 | 
					  observer.disconnect()
 | 
				
			||||||
 | 
					  init()
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					`
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return TableOfContents
 | 
				
			||||||
}) satisfies QuartzComponentConstructor
 | 
					}) satisfies QuartzComponentConstructor
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,6 +1,3 @@
 | 
				
			||||||
const description = "Initialize copy for codeblocks"
 | 
					 | 
				
			||||||
export default description
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const svgCopy =
 | 
					const svgCopy =
 | 
				
			||||||
  '<svg aria-hidden="true" height="16" viewBox="0 0 16 16" version="1.1" width="16" data-view-component="true"><path fill-rule="evenodd" d="M0 6.75C0 5.784.784 5 1.75 5h1.5a.75.75 0 010 1.5h-1.5a.25.25 0 00-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 00.25-.25v-1.5a.75.75 0 011.5 0v1.5A1.75 1.75 0 019.25 16h-7.5A1.75 1.75 0 010 14.25v-7.5z"></path><path fill-rule="evenodd" d="M5 1.75C5 .784 5.784 0 6.75 0h7.5C15.216 0 16 .784 16 1.75v7.5A1.75 1.75 0 0114.25 11h-7.5A1.75 1.75 0 015 9.25v-7.5zm1.75-.25a.25.25 0 00-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 00.25-.25v-7.5a.25.25 0 00-.25-.25h-7.5z"></path></svg>'
 | 
					  '<svg aria-hidden="true" height="16" viewBox="0 0 16 16" version="1.1" width="16" data-view-component="true"><path fill-rule="evenodd" d="M0 6.75C0 5.784.784 5 1.75 5h1.5a.75.75 0 010 1.5h-1.5a.25.25 0 00-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 00.25-.25v-1.5a.75.75 0 011.5 0v1.5A1.75 1.75 0 019.25 16h-7.5A1.75 1.75 0 010 14.25v-7.5z"></path><path fill-rule="evenodd" d="M5 1.75C5 .784 5.784 0 6.75 0h7.5C15.216 0 16 .784 16 1.75v7.5A1.75 1.75 0 0114.25 11h-7.5A1.75 1.75 0 015 9.25v-7.5zm1.75-.25a.25.25 0 00-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 00.25-.25v-7.5a.25.25 0 00-.25-.25h-7.5z"></path></svg>'
 | 
				
			||||||
const svgCheck =
 | 
					const svgCheck =
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -29,6 +29,11 @@ const getOpts = ({ target }: Event): { url: URL, scroll?: boolean } | undefined
 | 
				
			||||||
  return { url: new URL(href), scroll: 'routerNoscroll' in a.dataset ? false : undefined }
 | 
					  return { url: new URL(href), scroll: 'routerNoscroll' in a.dataset ? false : undefined }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function notifyNav(slug: string) {
 | 
				
			||||||
 | 
					  const event = new CustomEvent("spa_nav", { detail: { slug } })
 | 
				
			||||||
 | 
					  document.dispatchEvent(event)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
let p: DOMParser
 | 
					let p: DOMParser
 | 
				
			||||||
async function navigate(url: URL, isBack: boolean = false) {
 | 
					async function navigate(url: URL, isBack: boolean = false) {
 | 
				
			||||||
  p = p || new DOMParser()
 | 
					  p = p || new DOMParser()
 | 
				
			||||||
| 
						 | 
					@ -64,9 +69,7 @@ async function navigate(url: URL, isBack: boolean = false) {
 | 
				
			||||||
  const elementsToAdd = html.head.querySelectorAll(':not([spa-preserve])')
 | 
					  const elementsToAdd = html.head.querySelectorAll(':not([spa-preserve])')
 | 
				
			||||||
  elementsToAdd.forEach(el => document.head.appendChild(el))
 | 
					  elementsToAdd.forEach(el => document.head.appendChild(el))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if (!document.activeElement?.closest('[data-persist]')) {
 | 
					  notifyNav(document.body.dataset.slug!)
 | 
				
			||||||
    document.body.focus()
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
  delete announcer.dataset.persist
 | 
					  delete announcer.dataset.persist
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										27
									
								
								quartz/components/styles/legacyToc.scss
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								quartz/components/styles/legacyToc.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,24 +2,36 @@ details.toc {
 | 
				
			||||||
  & summary {
 | 
					  & summary {
 | 
				
			||||||
    cursor: pointer;
 | 
					    cursor: pointer;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    &::marker {
 | 
					    list-style: none;
 | 
				
			||||||
      color: var(--dark);
 | 
					    &::marker, &::-webkit-details-marker {
 | 
				
			||||||
 | 
					      display: none;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    & > * {
 | 
					    & > * {
 | 
				
			||||||
      padding-left: 0.25rem;
 | 
					 | 
				
			||||||
      display: inline-block;
 | 
					      display: inline-block;
 | 
				
			||||||
      margin: 0;
 | 
					      margin: 0;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    & > h3 {
 | 
				
			||||||
 | 
					      font-size: 1rem;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
  & ul {
 | 
					  & ul {
 | 
				
			||||||
    list-style: none;
 | 
					    list-style: none;
 | 
				
			||||||
    margin: 0.5rem 1.25rem;
 | 
					    margin: 0.5rem 0;
 | 
				
			||||||
    padding: 0;
 | 
					    padding: 0;
 | 
				
			||||||
 | 
					    & > li > a {
 | 
				
			||||||
 | 
					      color: var(--dark);
 | 
				
			||||||
 | 
					      opacity: 0.35;
 | 
				
			||||||
 | 
					      transition: 0.5s ease opacity;
 | 
				
			||||||
 | 
					      &.in-view {
 | 
				
			||||||
 | 
					        opacity: 0.75;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @for $i from 1 through 6 {
 | 
					  @for $i from 0 through 6 {
 | 
				
			||||||
    & .depth-#{$i} {
 | 
					    & .depth-#{$i} {
 | 
				
			||||||
      padding-left: calc(1rem * #{$i});
 | 
					      padding-left: calc(1rem * #{$i});
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -5,6 +5,21 @@ function slugSegment(s: string): string {
 | 
				
			||||||
  return s.replace(/\s/g, '-')
 | 
					  return s.replace(/\s/g, '-')
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function trimPathSuffix(fp: string): string {
 | 
				
			||||||
 | 
					  let [cleanPath, anchor] = fp.split("#", 2)
 | 
				
			||||||
 | 
					  anchor = anchor === undefined ? "" : "#" + anchor
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (cleanPath.endsWith("index")) {
 | 
				
			||||||
 | 
					    cleanPath = cleanPath.slice(0, -"index".length)
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (cleanPath === "") {
 | 
				
			||||||
 | 
					    cleanPath = "./"
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return cleanPath + anchor
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function slugify(s: string): string {
 | 
					export function slugify(s: string): string {
 | 
				
			||||||
  const [fp, anchor] = s.split("#", 2)
 | 
					  const [fp, anchor] = s.split("#", 2)
 | 
				
			||||||
  const sluggedAnchor = anchor === undefined ? "" : "#" + slugAnchor(anchor)
 | 
					  const sluggedAnchor = anchor === undefined ? "" : "#" + slugAnchor(anchor)
 | 
				
			||||||
| 
						 | 
					@ -19,12 +34,9 @@ export function slugify(s: string): string {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// resolve /a/b/c to ../../
 | 
					// resolve /a/b/c to ../../
 | 
				
			||||||
export function resolveToRoot(slug: string): string {
 | 
					export function resolveToRoot(slug: string): string {
 | 
				
			||||||
  let fp = slug
 | 
					  let fp = trimPathSuffix(slug)
 | 
				
			||||||
  if (fp.endsWith("index")) {
 | 
					 | 
				
			||||||
    fp = fp.slice(0, -"index".length)
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if (fp === "") {
 | 
					  if (fp === "./") {
 | 
				
			||||||
    return "."
 | 
					    return "."
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										53
									
								
								quartz/plugins/emitters/aliases.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								quartz/plugins/emitters/aliases.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,53 @@
 | 
				
			||||||
 | 
					import { relativeToRoot } from "../../path"
 | 
				
			||||||
 | 
					import { QuartzEmitterPlugin } from "../types"
 | 
				
			||||||
 | 
					import path from 'path'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const AliasRedirects: QuartzEmitterPlugin = () => ({
 | 
				
			||||||
 | 
					  name: "AliasRedirects",
 | 
				
			||||||
 | 
					  getQuartzComponents() {
 | 
				
			||||||
 | 
					    return []
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  async emit(contentFolder, _cfg, content, _resources, emit): Promise<string[]> {
 | 
				
			||||||
 | 
					    const fps: string[] = []
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    for (const [_tree, file] of content) {
 | 
				
			||||||
 | 
					      const ogSlug = file.data.slug!
 | 
				
			||||||
 | 
					      const dir = path.relative(contentFolder, file.dirname ?? contentFolder)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      let aliases: string[] = []
 | 
				
			||||||
 | 
					      if (file.data.frontmatter?.aliases) {
 | 
				
			||||||
 | 
					        aliases = file.data.frontmatter?.aliases
 | 
				
			||||||
 | 
					      } else if (file.data.frontmatter?.alias) {
 | 
				
			||||||
 | 
					        aliases = [file.data.frontmatter?.alias]
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      for (const alias of aliases) {
 | 
				
			||||||
 | 
					        const slug = alias.startsWith("/")
 | 
				
			||||||
 | 
					          ? alias
 | 
				
			||||||
 | 
					          : path.posix.join(dir, alias)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const fp = slug + ".html"
 | 
				
			||||||
 | 
					        const redirUrl = relativeToRoot(slug, ogSlug)
 | 
				
			||||||
 | 
					        await emit({
 | 
				
			||||||
 | 
					          content: `
 | 
				
			||||||
 | 
					            <!DOCTYPE html>
 | 
				
			||||||
 | 
					            <html lang="en-us">
 | 
				
			||||||
 | 
					            <head>
 | 
				
			||||||
 | 
					            <title>${ogSlug}</title>
 | 
				
			||||||
 | 
					            <link rel="canonical" href="${redirUrl}">
 | 
				
			||||||
 | 
					            <meta name="robots" content="noindex">
 | 
				
			||||||
 | 
					            <meta charset="utf-8">
 | 
				
			||||||
 | 
					            <meta http-equiv="refresh" content="0; url=${redirUrl}">
 | 
				
			||||||
 | 
					            </head>
 | 
				
			||||||
 | 
					            </html>
 | 
				
			||||||
 | 
					            `,
 | 
				
			||||||
 | 
					          slug,
 | 
				
			||||||
 | 
					          ext: ".html",
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        fps.push(fp)
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return fps
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
							
								
								
									
										25
									
								
								quartz/plugins/emitters/cname.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								quartz/plugins/emitters/cname.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,25 @@
 | 
				
			||||||
 | 
					import { QuartzEmitterPlugin } from "../types"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface Options {
 | 
				
			||||||
 | 
					  domain: string
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const CNAME: QuartzEmitterPlugin<Options> = (opts?: Options) => ({
 | 
				
			||||||
 | 
					  name: "CNAME",
 | 
				
			||||||
 | 
					  getQuartzComponents() {
 | 
				
			||||||
 | 
					    return []
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  async emit(_contentFolder, _cfg, _content, _resources, emit): Promise<string[]> {
 | 
				
			||||||
 | 
					    const slug = "CNAME"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (opts?.domain) {
 | 
				
			||||||
 | 
					      await emit({
 | 
				
			||||||
 | 
					        content: opts?.domain,
 | 
				
			||||||
 | 
					        slug,
 | 
				
			||||||
 | 
					        ext: "",
 | 
				
			||||||
 | 
					      })
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return ["CNAME"]
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
							
								
								
									
										72
									
								
								quartz/plugins/emitters/contentIndex.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										72
									
								
								quartz/plugins/emitters/contentIndex.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,72 @@
 | 
				
			||||||
 | 
					import { visit } from "unist-util-visit"
 | 
				
			||||||
 | 
					import { QuartzEmitterPlugin } from "../types"
 | 
				
			||||||
 | 
					import { Element } from "hast"
 | 
				
			||||||
 | 
					import path from "path"
 | 
				
			||||||
 | 
					import { trimPathSuffix } from "../../path"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface Options {
 | 
				
			||||||
 | 
					  indexAnchorLinks: boolean,
 | 
				
			||||||
 | 
					  indexExternalLinks: boolean,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const defaultOptions: Options = {
 | 
				
			||||||
 | 
					  indexAnchorLinks: false,
 | 
				
			||||||
 | 
					  indexExternalLinks: false,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type ContentIndex = Map<string, {
 | 
				
			||||||
 | 
					  title: string,
 | 
				
			||||||
 | 
					  links?: string[],
 | 
				
			||||||
 | 
					  tags?: string[],
 | 
				
			||||||
 | 
					  content: string,
 | 
				
			||||||
 | 
					}> 
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const ContentIndex: QuartzEmitterPlugin<Options> = (userOpts) => {
 | 
				
			||||||
 | 
					  const opts = { ...userOpts, ...defaultOptions }
 | 
				
			||||||
 | 
					  return {
 | 
				
			||||||
 | 
					    name: "ContentIndex",
 | 
				
			||||||
 | 
					    async emit(_contentDir, _cfg, content, _resources, emit) {
 | 
				
			||||||
 | 
					      const fp = "contentIndex"
 | 
				
			||||||
 | 
					      const linkIndex: ContentIndex = new Map()
 | 
				
			||||||
 | 
					      for (const [tree, file] of content) {
 | 
				
			||||||
 | 
					        let slug = trimPathSuffix(file.data.slug!)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const outgoing: Set<string> = new Set()
 | 
				
			||||||
 | 
					        visit(tree, 'element', (node: Element) => {
 | 
				
			||||||
 | 
					          if (node.tagName === 'a' && node.properties && typeof node.properties.href === 'string') {
 | 
				
			||||||
 | 
					            let dest = node.properties.href
 | 
				
			||||||
 | 
					            if (dest.startsWith(".")) {
 | 
				
			||||||
 | 
					              const normalizedPath = path.normalize(path.join(slug, dest))
 | 
				
			||||||
 | 
					              dest = trimPathSuffix(normalizedPath)
 | 
				
			||||||
 | 
					              outgoing.add(dest)
 | 
				
			||||||
 | 
					            } else if (dest.startsWith("#")) {
 | 
				
			||||||
 | 
					              if (opts.indexAnchorLinks) {
 | 
				
			||||||
 | 
					                outgoing.add(dest)
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					              if (opts.indexExternalLinks) {
 | 
				
			||||||
 | 
					                outgoing.add(dest)
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        linkIndex.set(slug, {
 | 
				
			||||||
 | 
					          title: file.data.frontmatter?.title!,
 | 
				
			||||||
 | 
					          links: [...outgoing],
 | 
				
			||||||
 | 
					          tags: file.data.frontmatter?.tags,
 | 
				
			||||||
 | 
					          content: file.data.text ?? ""
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      await emit({
 | 
				
			||||||
 | 
					        content: JSON.stringify(Object.fromEntries(linkIndex)),
 | 
				
			||||||
 | 
					        slug: fp,
 | 
				
			||||||
 | 
					        ext: ".json",
 | 
				
			||||||
 | 
					      })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      return [`${fp}.json`]
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    getQuartzComponents: () => [],
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -1,8 +1,6 @@
 | 
				
			||||||
import { JSResourceToScriptElement, StaticResources } from "../../resources"
 | 
					import { JSResourceToScriptElement, StaticResources } from "../../resources"
 | 
				
			||||||
import { EmitCallback, QuartzEmitterPlugin } from "../types"
 | 
					import { QuartzEmitterPlugin } from "../types"
 | 
				
			||||||
import { ProcessedContent } from "../vfile"
 | 
					 | 
				
			||||||
import { render } from "preact-render-to-string"
 | 
					import { render } from "preact-render-to-string"
 | 
				
			||||||
import { GlobalConfiguration } from "../../cfg"
 | 
					 | 
				
			||||||
import { QuartzComponent } from "../../components/types"
 | 
					import { QuartzComponent } from "../../components/types"
 | 
				
			||||||
import { resolveToRoot } from "../../path"
 | 
					import { resolveToRoot } from "../../path"
 | 
				
			||||||
import HeaderConstructor from "../../components/Header"
 | 
					import HeaderConstructor from "../../components/Header"
 | 
				
			||||||
| 
						 | 
					@ -12,7 +10,10 @@ import BodyConstructor from "../../components/Body"
 | 
				
			||||||
interface Options {
 | 
					interface Options {
 | 
				
			||||||
  head: QuartzComponent
 | 
					  head: QuartzComponent
 | 
				
			||||||
  header: QuartzComponent[],
 | 
					  header: QuartzComponent[],
 | 
				
			||||||
  body: QuartzComponent[]
 | 
					  body: QuartzComponent[],
 | 
				
			||||||
 | 
					  left: QuartzComponent[],
 | 
				
			||||||
 | 
					  right: QuartzComponent[],
 | 
				
			||||||
 | 
					  footer: QuartzComponent[],
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const ContentPage: QuartzEmitterPlugin<Options> = (opts) => {
 | 
					export const ContentPage: QuartzEmitterPlugin<Options> = (opts) => {
 | 
				
			||||||
| 
						 | 
					@ -29,7 +30,7 @@ export const ContentPage: QuartzEmitterPlugin<Options> = (opts) => {
 | 
				
			||||||
    getQuartzComponents() {
 | 
					    getQuartzComponents() {
 | 
				
			||||||
      return [opts.head, Header, ...opts.header, ...opts.body]
 | 
					      return [opts.head, Header, ...opts.header, ...opts.body]
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    async emit(cfg: GlobalConfiguration, content: ProcessedContent[], resources: StaticResources, emit: EmitCallback): Promise<string[]> {
 | 
					    async emit(_contentDir, cfg, content, resources, emit): Promise<string[]> {
 | 
				
			||||||
      const fps: string[] = []
 | 
					      const fps: string[] = []
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      for (const [tree, file] of content) {
 | 
					      for (const [tree, file] of content) {
 | 
				
			||||||
| 
						 | 
					@ -53,7 +54,7 @@ export const ContentPage: QuartzEmitterPlugin<Options> = (opts) => {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const doc = <html>
 | 
					        const doc = <html>
 | 
				
			||||||
          <Head {...componentData} />
 | 
					          <Head {...componentData} />
 | 
				
			||||||
          <body>
 | 
					          <body data-slug={file.data.slug}>
 | 
				
			||||||
            <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} />)}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1 +1,4 @@
 | 
				
			||||||
export { ContentPage } from './contentPage'
 | 
					export { ContentPage } from './contentPage'
 | 
				
			||||||
 | 
					export { ContentIndex } from './contentIndex'
 | 
				
			||||||
 | 
					export { AliasRedirects } from './aliases'
 | 
				
			||||||
 | 
					export { CNAME } from './cname'
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -28,13 +28,13 @@ export type QuartzFilterPluginInstance = {
 | 
				
			||||||
export type QuartzEmitterPlugin<Options extends OptionType = undefined> = (opts?: Options) => QuartzEmitterPluginInstance 
 | 
					export type QuartzEmitterPlugin<Options extends OptionType = undefined> = (opts?: Options) => QuartzEmitterPluginInstance 
 | 
				
			||||||
export type QuartzEmitterPluginInstance = {
 | 
					export type QuartzEmitterPluginInstance = {
 | 
				
			||||||
  name: string
 | 
					  name: string
 | 
				
			||||||
  emit(cfg: GlobalConfiguration, content: ProcessedContent[], resources: StaticResources, emitCallback: EmitCallback): Promise<string[]>
 | 
					  emit(contentDir: string, cfg: GlobalConfiguration, content: ProcessedContent[], resources: StaticResources, emitCallback: EmitCallback): Promise<string[]>
 | 
				
			||||||
  getQuartzComponents(): QuartzComponent[]
 | 
					  getQuartzComponents(): QuartzComponent[]
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface EmitOptions {
 | 
					export interface EmitOptions {
 | 
				
			||||||
  slug: string
 | 
					  slug: string
 | 
				
			||||||
  ext: `.${string}`
 | 
					  ext: `.${string}` | ""
 | 
				
			||||||
  content: string
 | 
					  content: string
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -25,7 +25,7 @@ export async function emitContent(contentFolder: string, output: string, cfg: Qu
 | 
				
			||||||
  let emittedFiles = 0
 | 
					  let emittedFiles = 0
 | 
				
			||||||
  for (const emitter of cfg.plugins.emitters) {
 | 
					  for (const emitter of cfg.plugins.emitters) {
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      const emitted = await emitter.emit(cfg.configuration, content, staticResources, emit)
 | 
					      const emitted = await emitter.emit(contentFolder, cfg.configuration, content, staticResources, emit)
 | 
				
			||||||
      emittedFiles += emitted.length
 | 
					      emittedFiles += emitted.length
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      if (verbose) {
 | 
					      if (verbose) {
 | 
				
			||||||
| 
						 | 
					@ -42,24 +42,25 @@ export async function emitContent(contentFolder: string, output: string, cfg: Qu
 | 
				
			||||||
  const staticPath = path.join(QUARTZ, "static")
 | 
					  const staticPath = path.join(QUARTZ, "static")
 | 
				
			||||||
  await fs.promises.cp(staticPath, path.join(output, "static"), { recursive: true })
 | 
					  await fs.promises.cp(staticPath, path.join(output, "static"), { recursive: true })
 | 
				
			||||||
  if (verbose) {
 | 
					  if (verbose) {
 | 
				
			||||||
    console.log(`[emit:Static] ${path.join(output, "static", "**")}`)
 | 
					    console.log(`[emit:Static] ${path.join("static", "**")}`)
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // glob all non MD/MDX/HTML files in content folder and copy it over
 | 
					  // glob all non MD/MDX/HTML files in content folder and copy it over
 | 
				
			||||||
  const assetsPath = path.join("public", "assets")
 | 
					  const assetsPath = path.join(output, "assets")
 | 
				
			||||||
  for await (const fp of globbyStream("**", {
 | 
					  for await (const fp of globbyStream("**", {
 | 
				
			||||||
    ignore: ["**/*.md"],
 | 
					    ignore: ["**/*.md"],
 | 
				
			||||||
    cwd: contentFolder,
 | 
					    cwd: contentFolder,
 | 
				
			||||||
  })) {
 | 
					  })) {
 | 
				
			||||||
    const ext = path.extname(fp as string)
 | 
					    const ext = path.extname(fp as string)
 | 
				
			||||||
    const src = path.join(contentFolder, fp as string)
 | 
					    const src = path.join(contentFolder, fp as string)
 | 
				
			||||||
    const dest = path.join(assetsPath, slugify(fp as string) + ext)
 | 
					    const name = slugify(fp as string) + ext
 | 
				
			||||||
 | 
					    const dest = path.join(assetsPath, name)
 | 
				
			||||||
    const dir = path.dirname(dest)
 | 
					    const dir = path.dirname(dest)
 | 
				
			||||||
    await fs.promises.mkdir(dir, { recursive: true }) // ensure dir exists
 | 
					    await fs.promises.mkdir(dir, { recursive: true }) // ensure dir exists
 | 
				
			||||||
    await fs.promises.copyFile(src, dest)
 | 
					    await fs.promises.copyFile(src, dest)
 | 
				
			||||||
    emittedFiles += 1
 | 
					    emittedFiles += 1
 | 
				
			||||||
    if (verbose) {
 | 
					    if (verbose) {
 | 
				
			||||||
      console.log(`[emit:Assets] ${dest}`)
 | 
					      console.log(`[emit:Assets] ${path.join("assets", name)}`)
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue