This commit is contained in:
Ryan Kes 2024-07-15 16:37:25 +02:00
commit 1783300c87
23 changed files with 237 additions and 167 deletions

View file

@ -260,11 +260,11 @@ export const ContentPage: QuartzEmitterPlugin = () => {
...defaultContentPageLayout, ...defaultContentPageLayout,
pageBody: Content(), pageBody: Content(),
} }
const { head, header, beforeBody, pageBody, left, right, footer } = layout const { head, header, beforeBody, pageBody, afterBody, left, right, footer } = layout
return { return {
name: "ContentPage", name: "ContentPage",
getQuartzComponents() { getQuartzComponents() {
return [head, ...header, ...beforeBody, pageBody, ...left, ...right, footer] return [head, ...header, ...beforeBody, pageBody, ...afterBody, ...left, ...right, footer]
}, },
async emit(ctx, content, resources, emit): Promise<FilePath[]> { async emit(ctx, content, resources, emit): Promise<FilePath[]> {
const cfg = ctx.cfg.configuration const cfg = ctx.cfg.configuration

View file

@ -53,6 +53,7 @@ This part of the configuration concerns anything that can affect the whole site.
- `secondary`: link colour, current [[graph view|graph]] node - `secondary`: link colour, current [[graph view|graph]] node
- `tertiary`: hover states and visited [[graph view|graph]] nodes - `tertiary`: hover states and visited [[graph view|graph]] nodes
- `highlight`: internal link background, highlighted text, [[syntax highlighting|highlighted lines of code]] - `highlight`: internal link background, highlighted text, [[syntax highlighting|highlighted lines of code]]
- `textHighlight`: markdown highlighted text background
## Plugins ## Plugins

View file

@ -30,4 +30,4 @@ As with folder listings, you can also provide a description and title for a tag
## Customization ## Customization
The folder listings are a functionality of the [[FolderPage]] plugin, the tag listings of the [[TagPage]] plugin. See the plugin pages for customization options. Quartz allows you to define a custom sort ordering for content on both page types. The folder listings are a functionality of the [[FolderPage]] plugin, the tag listings of the [[TagPage]] plugin. See the plugin pages for customization options.

View file

@ -2,22 +2,12 @@
draft: true draft: true
--- ---
## high priority backlog
- static dead link detection
- block links: https://help.obsidian.md/Linking+notes+and+files/Internal+links#Link+to+a+block+in+a+note
- note/header/block transcludes: https://help.obsidian.md/Linking+notes+and+files/Embedding+files
- docker support
## misc backlog ## misc backlog
- breadcrumbs component - static dead link detection
- cursor chat extension - cursor chat extension
- https://giscus.app/ extension - https://giscus.app/ extension
- sidenotes? https://github.com/capnfabs/paperesque - sidenotes? https://github.com/capnfabs/paperesque
- direct match in search using double quotes - direct match in search using double quotes
- https://help.obsidian.md/Advanced+topics/Using+Obsidian+URI - https://help.obsidian.md/Advanced+topics/Using+Obsidian+URI
- audio/video embed styling
- Canvas - Canvas
- parse all images in page: use this for page lists if applicable?
- CV mode? with print stylesheet

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

After

Width:  |  Height:  |  Size: 65 KiB

View file

@ -12,6 +12,7 @@ export interface FullPageLayout {
header: QuartzComponent[] // laid out horizontally header: QuartzComponent[] // laid out horizontally
beforeBody: QuartzComponent[] // laid out vertically beforeBody: QuartzComponent[] // laid out vertically
pageBody: QuartzComponent // single component pageBody: QuartzComponent // single component
afterBody: QuartzComponent[] // laid out vertically
left: QuartzComponent[] // vertical on desktop, horizontal on mobile left: QuartzComponent[] // vertical on desktop, horizontal on mobile
right: QuartzComponent[] // vertical on desktop, horizontal on mobile right: QuartzComponent[] // vertical on desktop, horizontal on mobile
footer: QuartzComponent // single component footer: QuartzComponent // single component

View file

@ -11,10 +11,12 @@ Example: [[advanced/|Advanced]]
> [!note] > [!note]
> For information on how to add, remove or configure plugins, see the [[configuration#Plugins|Configuration]] page. > For information on how to add, remove or configure plugins, see the [[configuration#Plugins|Configuration]] page.
This plugin has no configuration options.
The pages are displayed using the `defaultListPageLayout` in `quartz.layouts.ts`. For the content, the `FolderContent` component is used. If you want to modify the layout, you must edit it directly (`quartz/components/pages/FolderContent.tsx`). The pages are displayed using the `defaultListPageLayout` in `quartz.layouts.ts`. For the content, the `FolderContent` component is used. If you want to modify the layout, you must edit it directly (`quartz/components/pages/FolderContent.tsx`).
This plugin accepts the following configuration options:
- `sort`: A function of type `(f1: QuartzPluginData, f2: QuartzPluginData) => number{:ts}` used to sort entries. Defaults to sorting by date and tie-breaking on lexographical order.
## API ## API
- Category: Emitter - Category: Emitter

View file

@ -9,10 +9,12 @@ This plugin emits dedicated pages for each tag used in the content. See [[folder
> [!note] > [!note]
> For information on how to add, remove or configure plugins, see the [[configuration#Plugins|Configuration]] page. > For information on how to add, remove or configure plugins, see the [[configuration#Plugins|Configuration]] page.
This plugin has no configuration options.
The pages are displayed using the `defaultListPageLayout` in `quartz.layouts.ts`. For the content, the `TagContent` component is used. If you want to modify the layout, you must edit it directly (`quartz/components/pages/TagContent.tsx`). The pages are displayed using the `defaultListPageLayout` in `quartz.layouts.ts`. For the content, the `TagContent` component is used. If you want to modify the layout, you must edit it directly (`quartz/components/pages/TagContent.tsx`).
This plugin accepts the following configuration options:
- `sort`: A function of type `(f1: QuartzPluginData, f2: QuartzPluginData) => number{:ts}` used to sort entries. Defaults to sorting by date and tie-breaking on lexographical order.
## API ## API
- Category: Emitter - Category: Emitter

View file

@ -34,6 +34,7 @@ const config: QuartzConfig = {
secondary: "#284b63", secondary: "#284b63",
tertiary: "#84a59d", tertiary: "#84a59d",
highlight: "rgba(143, 159, 169, 0.15)", highlight: "rgba(143, 159, 169, 0.15)",
textHighlight: "#fff23688",
}, },
darkMode: { darkMode: {
light: "#161618", light: "#161618",
@ -44,6 +45,7 @@ const config: QuartzConfig = {
secondary: "#7b97aa", secondary: "#7b97aa",
tertiary: "#84a59d", tertiary: "#84a59d",
highlight: "rgba(143, 159, 169, 0.15)", highlight: "rgba(143, 159, 169, 0.15)",
textHighlight: "#b3aa0288",
}, },
}, },
}, },

View file

@ -5,6 +5,7 @@ import * as Component from "./quartz/components"
export const sharedPageComponents: SharedLayout = { export const sharedPageComponents: SharedLayout = {
head: Component.Head(), head: Component.Head(),
header: [], header: [],
afterBody: [],
footer: Component.Footer({ footer: Component.Footer({
links: { links: {
"Ryan's Namepage": "https://ryankes.eu", "Ryan's Namepage": "https://ryankes.eu",

View file

@ -77,10 +77,11 @@ export interface FullPageLayout {
header: QuartzComponent[] header: QuartzComponent[]
beforeBody: QuartzComponent[] beforeBody: QuartzComponent[]
pageBody: QuartzComponent pageBody: QuartzComponent
afterBody: QuartzComponent[]
left: QuartzComponent[] left: QuartzComponent[]
right: QuartzComponent[] right: QuartzComponent[]
footer: QuartzComponent footer: QuartzComponent
} }
export type PageLayout = Pick<FullPageLayout, "beforeBody" | "left" | "right"> export type PageLayout = Pick<FullPageLayout, "beforeBody" | "left" | "right">
export type SharedLayout = Pick<FullPageLayout, "head" | "header" | "footer"> export type SharedLayout = Pick<FullPageLayout, "head" | "header" | "footer" | "afterBody">

View file

@ -13,7 +13,6 @@ export default ((opts?: Options) => {
const links = opts?.links ?? [] const links = opts?.links ?? []
return ( return (
<footer class={`${displayClass ?? ""}`}> <footer class={`${displayClass ?? ""}`}>
<hr />
<p> <p>
{i18n(cfg.locale).components.footer.createdWith}{" "} {i18n(cfg.locale).components.footer.createdWith}{" "}
<a href="https://quartz.jzhao.xyz/">Quartz v{version}</a> © {year} <a href="https://quartz.jzhao.xyz/">Quartz v{version}</a> © {year}

View file

@ -4,9 +4,9 @@ import { Date, getDate } from "./Date"
import { QuartzComponent, QuartzComponentProps } from "./types" import { QuartzComponent, QuartzComponentProps } from "./types"
import { GlobalConfiguration } from "../cfg" import { GlobalConfiguration } from "../cfg"
export function byDateAndAlphabetical( export type SortFn = (f1: QuartzPluginData, f2: QuartzPluginData) => number
cfg: GlobalConfiguration,
): (f1: QuartzPluginData, f2: QuartzPluginData) => number { export function byDateAndAlphabetical(cfg: GlobalConfiguration): SortFn {
return (f1, f2) => { return (f1, f2) => {
if (f1.dates && f2.dates) { if (f1.dates && f2.dates) {
// sort descending // sort descending
@ -27,10 +27,12 @@ export function byDateAndAlphabetical(
type Props = { type Props = {
limit?: number limit?: number
sort?: SortFn
} & QuartzComponentProps } & QuartzComponentProps
export const PageList: QuartzComponent = ({ cfg, fileData, allFiles, limit }: Props) => { export const PageList: QuartzComponent = ({ cfg, fileData, allFiles, limit, sort }: Props) => {
let list = allFiles.sort(byDateAndAlphabetical(cfg)) const sorter = sort ?? byDateAndAlphabetical(cfg)
let list = allFiles.sort(sorter)
if (limit) { if (limit) {
list = list.slice(0, limit) list = list.slice(0, limit)
} }

View file

@ -2,7 +2,7 @@ import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } fro
import path from "path" import path from "path"
import style from "../styles/listPage.scss" import style from "../styles/listPage.scss"
import { PageList } from "../PageList" import { PageList, SortFn } from "../PageList"
import { stripSlashes, simplifySlug } from "../../util/path" import { stripSlashes, simplifySlug } from "../../util/path"
import { Root } from "hast" import { Root } from "hast"
import { htmlToJsx } from "../../util/jsx" import { htmlToJsx } from "../../util/jsx"
@ -13,6 +13,7 @@ interface FolderContentOptions {
* Whether to display number of folders * Whether to display number of folders
*/ */
showFolderCount: boolean showFolderCount: boolean
sort?: SortFn
} }
const defaultOptions: FolderContentOptions = { const defaultOptions: FolderContentOptions = {
@ -37,6 +38,7 @@ export default ((opts?: Partial<FolderContentOptions>) => {
const classes = ["popover-hint", ...cssClasses].join(" ") const classes = ["popover-hint", ...cssClasses].join(" ")
const listProps = { const listProps = {
...props, ...props,
sort: options.sort,
allFiles: allPagesInFolder, allFiles: allPagesInFolder,
} }

View file

@ -1,113 +1,127 @@
import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "../types" import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "../types"
import style from "../styles/listPage.scss" import style from "../styles/listPage.scss"
import { PageList } from "../PageList" import { PageList, SortFn } from "../PageList"
import { FullSlug, getAllSegmentPrefixes, simplifySlug } from "../../util/path" import { FullSlug, getAllSegmentPrefixes, simplifySlug } from "../../util/path"
import { QuartzPluginData } from "../../plugins/vfile" import { QuartzPluginData } from "../../plugins/vfile"
import { Root } from "hast" import { Root } from "hast"
import { htmlToJsx } from "../../util/jsx" import { htmlToJsx } from "../../util/jsx"
import { i18n } from "../../i18n" import { i18n } from "../../i18n"
const numPages = 10 interface TagContentOptions {
const TagContent: QuartzComponent = (props: QuartzComponentProps) => { sort?: SortFn
const { tree, fileData, allFiles, cfg } = props numPages: number
const slug = fileData.slug
if (!(slug?.startsWith("tags/") || slug === "tags")) {
throw new Error(`Component "TagContent" tried to render a non-tag page: ${slug}`)
}
const tag = simplifySlug(slug.slice("tags/".length) as FullSlug)
const allPagesWithTag = (tag: string) =>
allFiles.filter((file) =>
(file.frontmatter?.tags ?? []).flatMap(getAllSegmentPrefixes).includes(tag),
)
const content =
(tree as Root).children.length === 0
? fileData.description
: htmlToJsx(fileData.filePath!, tree)
const cssClasses: string[] = fileData.frontmatter?.cssclasses ?? []
const classes = ["popover-hint", ...cssClasses].join(" ")
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))
}
return (
<div class={classes}>
<article>
<p>{content}</p>
</article>
<p>{i18n(cfg.locale).pages.tagContent.totalTags({ count: tags.length })}</p>
<div>
{tags.map((tag) => {
const pages = tagItemMap.get(tag)!
const listProps = {
...props,
allFiles: pages,
}
const contentPage = allFiles.filter((file) => file.slug === `tags/${tag}`).at(0)
const root = contentPage?.htmlAst
const content =
!root || root?.children.length === 0
? contentPage?.description
: htmlToJsx(contentPage.filePath!, root)
return (
<div>
<h2>
<a class="internal tag-link" href={`../tags/${tag}`}>
{tag}
</a>
</h2>
{content && <p>{content}</p>}
<div class="page-listing">
<p>
{i18n(cfg.locale).pages.tagContent.itemsUnderTag({ count: pages.length })}
{pages.length > numPages && (
<>
{" "}
<span>
{i18n(cfg.locale).pages.tagContent.showingFirst({ count: numPages })}
</span>
</>
)}
</p>
<PageList limit={numPages} {...listProps} />
</div>
</div>
)
})}
</div>
</div>
)
} else {
const pages = allPagesWithTag(tag)
const listProps = {
...props,
allFiles: pages,
}
return (
<div class={classes}>
<article>{content}</article>
<div class="page-listing">
<p>{i18n(cfg.locale).pages.tagContent.itemsUnderTag({ count: pages.length })}</p>
<div>
<PageList {...listProps} />
</div>
</div>
</div>
)
}
} }
TagContent.css = style + PageList.css const defaultOptions: TagContentOptions = {
export default (() => TagContent) satisfies QuartzComponentConstructor numPages: 10,
}
export default ((opts?: Partial<TagContentOptions>) => {
const options: TagContentOptions = { ...defaultOptions, ...opts }
const TagContent: QuartzComponent = (props: QuartzComponentProps) => {
const { tree, fileData, allFiles, cfg } = props
const slug = fileData.slug
if (!(slug?.startsWith("tags/") || slug === "tags")) {
throw new Error(`Component "TagContent" tried to render a non-tag page: ${slug}`)
}
const tag = simplifySlug(slug.slice("tags/".length) as FullSlug)
const allPagesWithTag = (tag: string) =>
allFiles.filter((file) =>
(file.frontmatter?.tags ?? []).flatMap(getAllSegmentPrefixes).includes(tag),
)
const content =
(tree as Root).children.length === 0
? fileData.description
: htmlToJsx(fileData.filePath!, tree)
const cssClasses: string[] = fileData.frontmatter?.cssclasses ?? []
const classes = ["popover-hint", ...cssClasses].join(" ")
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))
}
return (
<div class={classes}>
<article>
<p>{content}</p>
</article>
<p>{i18n(cfg.locale).pages.tagContent.totalTags({ count: tags.length })}</p>
<div>
{tags.map((tag) => {
const pages = tagItemMap.get(tag)!
const listProps = {
...props,
allFiles: pages,
}
const contentPage = allFiles.filter((file) => file.slug === `tags/${tag}`).at(0)
const root = contentPage?.htmlAst
const content =
!root || root?.children.length === 0
? contentPage?.description
: htmlToJsx(contentPage.filePath!, root)
return (
<div>
<h2>
<a class="internal tag-link" href={`../tags/${tag}`}>
{tag}
</a>
</h2>
{content && <p>{content}</p>}
<div class="page-listing">
<p>
{i18n(cfg.locale).pages.tagContent.itemsUnderTag({ count: pages.length })}
{pages.length > options.numPages && (
<>
{" "}
<span>
{i18n(cfg.locale).pages.tagContent.showingFirst({
count: options.numPages,
})}
</span>
</>
)}
</p>
<PageList limit={options.numPages} {...listProps} sort={opts?.sort} />
</div>
</div>
)
})}
</div>
</div>
)
} else {
const pages = allPagesWithTag(tag)
const listProps = {
...props,
allFiles: pages,
}
return (
<div class={classes}>
<article>{content}</article>
<div class="page-listing">
<p>{i18n(cfg.locale).pages.tagContent.itemsUnderTag({ count: pages.length })}</p>
<div>
<PageList {...listProps} />
</div>
</div>
</div>
)
}
}
TagContent.css = style + PageList.css
return TagContent
}) satisfies QuartzComponentConstructor

View file

@ -14,6 +14,7 @@ interface RenderComponents {
header: QuartzComponent[] header: QuartzComponent[]
beforeBody: QuartzComponent[] beforeBody: QuartzComponent[]
pageBody: QuartzComponent pageBody: QuartzComponent
afterBody: QuartzComponent[]
left: QuartzComponent[] left: QuartzComponent[]
right: QuartzComponent[] right: QuartzComponent[]
footer: QuartzComponent footer: QuartzComponent
@ -187,6 +188,7 @@ export function renderPage(
header, header,
beforeBody, beforeBody,
pageBody: Content, pageBody: Content,
afterBody,
left, left,
right, right,
footer: Footer, footer: Footer,
@ -232,6 +234,12 @@ export function renderPage(
</div> </div>
</div> </div>
<Content {...componentData} /> <Content {...componentData} />
<hr />
<div class="page-footer">
{afterBody.map((BodyComponent) => (
<BodyComponent {...componentData} />
))}
</div>
</div> </div>
{RightComponent} {RightComponent}
</Body> </Body>

View file

@ -3,7 +3,7 @@ import { normalizeRelativeURLs } from "../../util/path"
const p = new DOMParser() const p = new DOMParser()
async function mouseEnterHandler( async function mouseEnterHandler(
this: HTMLLinkElement, this: HTMLAnchorElement,
{ clientX, clientY }: { clientX: number; clientY: number }, { clientX, clientY }: { clientX: number; clientY: number },
) { ) {
const link = this const link = this
@ -33,7 +33,7 @@ async function mouseEnterHandler(
thisUrl.hash = "" thisUrl.hash = ""
thisUrl.search = "" thisUrl.search = ""
const targetUrl = new URL(link.href) const targetUrl = new URL(link.href)
const hash = targetUrl.hash const hash = decodeURIComponent(targetUrl.hash)
targetUrl.hash = "" targetUrl.hash = ""
targetUrl.search = "" targetUrl.search = ""
@ -100,7 +100,7 @@ async function mouseEnterHandler(
} }
document.addEventListener("nav", () => { document.addEventListener("nav", () => {
const links = [...document.getElementsByClassName("internal")] as HTMLLinkElement[] const links = [...document.getElementsByClassName("internal")] as HTMLAnchorElement[]
for (const link of links) { for (const link of links) {
link.addEventListener("mouseenter", mouseEnterHandler) link.addEventListener("mouseenter", mouseEnterHandler)
window.addCleanup(() => link.removeEventListener("mouseenter", mouseEnterHandler)) window.addCleanup(() => link.removeEventListener("mouseenter", mouseEnterHandler))

View file

@ -59,14 +59,25 @@ export const ContentPage: QuartzEmitterPlugin<Partial<FullPageLayout>> = (userOp
...userOpts, ...userOpts,
} }
const { head: Head, header, beforeBody, pageBody, left, right, footer: Footer } = opts const { head: Head, header, beforeBody, pageBody, afterBody, left, right, footer: Footer } = opts
const Header = HeaderConstructor() const Header = HeaderConstructor()
const Body = BodyConstructor() const Body = BodyConstructor()
return { return {
name: "ContentPage", name: "ContentPage",
getQuartzComponents() { getQuartzComponents() {
return [Head, Header, Body, ...header, ...beforeBody, pageBody, ...left, ...right, Footer] return [
Head,
Header,
Body,
...header,
...beforeBody,
pageBody,
...afterBody,
...left,
...right,
Footer,
]
}, },
async getDependencyGraph(ctx, content, _resources) { async getDependencyGraph(ctx, content, _resources) {
const graph = new DepGraph<FilePath>() const graph = new DepGraph<FilePath>()

View file

@ -3,7 +3,7 @@ import { QuartzComponentProps } from "../../components/types"
import HeaderConstructor from "../../components/Header" import HeaderConstructor from "../../components/Header"
import BodyConstructor from "../../components/Body" import BodyConstructor from "../../components/Body"
import { pageResources, renderPage } from "../../components/renderPage" import { pageResources, renderPage } from "../../components/renderPage"
import { ProcessedContent, defaultProcessedContent } from "../vfile" import { ProcessedContent, QuartzPluginData, defaultProcessedContent } from "../vfile"
import { FullPageLayout } from "../../cfg" import { FullPageLayout } from "../../cfg"
import path from "path" import path from "path"
import { import {
@ -21,22 +21,37 @@ import { write } from "./helpers"
import { i18n } from "../../i18n" import { i18n } from "../../i18n"
import DepGraph from "../../depgraph" import DepGraph from "../../depgraph"
export const FolderPage: QuartzEmitterPlugin<Partial<FullPageLayout>> = (userOpts) => { interface FolderPageOptions extends FullPageLayout {
sort?: (f1: QuartzPluginData, f2: QuartzPluginData) => number
}
export const FolderPage: QuartzEmitterPlugin<Partial<FolderPageOptions>> = (userOpts) => {
const opts: FullPageLayout = { const opts: FullPageLayout = {
...sharedPageComponents, ...sharedPageComponents,
...defaultListPageLayout, ...defaultListPageLayout,
pageBody: FolderContent(), pageBody: FolderContent({ sort: userOpts?.sort }),
...userOpts, ...userOpts,
} }
const { head: Head, header, beforeBody, pageBody, left, right, footer: Footer } = opts const { head: Head, header, beforeBody, pageBody, afterBody, left, right, footer: Footer } = opts
const Header = HeaderConstructor() const Header = HeaderConstructor()
const Body = BodyConstructor() const Body = BodyConstructor()
return { return {
name: "FolderPage", name: "FolderPage",
getQuartzComponents() { getQuartzComponents() {
return [Head, Header, Body, ...header, ...beforeBody, pageBody, ...left, ...right, Footer] return [
Head,
Header,
Body,
...header,
...beforeBody,
pageBody,
...afterBody,
...left,
...right,
Footer,
]
}, },
async getDependencyGraph(_ctx, content, _resources) { async getDependencyGraph(_ctx, content, _resources) {
// Example graph: // Example graph:

View file

@ -3,7 +3,7 @@ import { QuartzComponentProps } from "../../components/types"
import HeaderConstructor from "../../components/Header" import HeaderConstructor from "../../components/Header"
import BodyConstructor from "../../components/Body" import BodyConstructor from "../../components/Body"
import { pageResources, renderPage } from "../../components/renderPage" import { pageResources, renderPage } from "../../components/renderPage"
import { ProcessedContent, defaultProcessedContent } from "../vfile" import { ProcessedContent, QuartzPluginData, defaultProcessedContent } from "../vfile"
import { FullPageLayout } from "../../cfg" import { FullPageLayout } from "../../cfg"
import { import {
FilePath, FilePath,
@ -18,22 +18,37 @@ import { write } from "./helpers"
import { i18n } from "../../i18n" import { i18n } from "../../i18n"
import DepGraph from "../../depgraph" import DepGraph from "../../depgraph"
export const TagPage: QuartzEmitterPlugin<Partial<FullPageLayout>> = (userOpts) => { interface TagPageOptions extends FullPageLayout {
sort?: (f1: QuartzPluginData, f2: QuartzPluginData) => number
}
export const TagPage: QuartzEmitterPlugin<Partial<TagPageOptions>> = (userOpts) => {
const opts: FullPageLayout = { const opts: FullPageLayout = {
...sharedPageComponents, ...sharedPageComponents,
...defaultListPageLayout, ...defaultListPageLayout,
pageBody: TagContent(), pageBody: TagContent({ sort: userOpts?.sort }),
...userOpts, ...userOpts,
} }
const { head: Head, header, beforeBody, pageBody, left, right, footer: Footer } = opts const { head: Head, header, beforeBody, pageBody, afterBody, left, right, footer: Footer } = opts
const Header = HeaderConstructor() const Header = HeaderConstructor()
const Body = BodyConstructor() const Body = BodyConstructor()
return { return {
name: "TagPage", name: "TagPage",
getQuartzComponents() { getQuartzComponents() {
return [Head, Header, Body, ...header, ...beforeBody, pageBody, ...left, ...right, Footer] return [
Head,
Header,
Body,
...header,
...beforeBody,
pageBody,
...afterBody,
...left,
...right,
Footer,
]
}, },
async getDependencyGraph(ctx, content, _resources) { async getDependencyGraph(ctx, content, _resources) {
const graph = new DepGraph<FilePath>() const graph = new DepGraph<FilePath>()

View file

@ -2,7 +2,6 @@ import { QuartzTransformerPlugin } from "../types"
import { Root, Html, BlockContent, DefinitionContent, Paragraph, Code } from "mdast" import { Root, Html, BlockContent, DefinitionContent, Paragraph, Code } from "mdast"
import { Element, Literal, Root as HtmlRoot } from "hast" import { Element, Literal, Root as HtmlRoot } from "hast"
import { ReplaceFunction, findAndReplace as mdastFindReplace } from "mdast-util-find-and-replace" import { ReplaceFunction, findAndReplace as mdastFindReplace } from "mdast-util-find-and-replace"
import { slug as slugAnchor } from "github-slugger"
import rehypeRaw from "rehype-raw" import rehypeRaw from "rehype-raw"
import { SKIP, visit } from "unist-util-visit" import { SKIP, visit } from "unist-util-visit"
import path from "path" import path from "path"
@ -98,7 +97,7 @@ function canonicalizeCallout(calloutName: string): keyof typeof calloutMapping {
export const externalLinkRegex = /^https?:\/\//i export const externalLinkRegex = /^https?:\/\//i
export const arrowRegex = new RegExp(/(-{1,2}>|={1,2}>|<-{1,2}|<={1,2})/, "g") export const arrowRegex = new RegExp(/(-{1,2}>|={1,2}>|<-{1,2}|<={1,2})/g)
// !? -> optional embedding // !? -> optional embedding
// \[\[ -> open brace // \[\[ -> open brace
@ -106,35 +105,30 @@ export const arrowRegex = new RegExp(/(-{1,2}>|={1,2}>|<-{1,2}|<={1,2})/, "g")
// (#[^\[\]\|\#]+)? -> # then one or more non-special characters (heading link) // (#[^\[\]\|\#]+)? -> # then one or more non-special characters (heading link)
// (\\?\|[^\[\]\#]+)? -> optional escape \ then | then one or more non-special characters (alias) // (\\?\|[^\[\]\#]+)? -> optional escape \ then | then one or more non-special characters (alias)
export const wikilinkRegex = new RegExp( export const wikilinkRegex = new RegExp(
/!?\[\[([^\[\]\|\#\\]+)?(#+[^\[\]\|\#\\]+)?(\\?\|[^\[\]\#]+)?\]\]/, /!?\[\[([^\[\]\|\#\\]+)?(#+[^\[\]\|\#\\]+)?(\\?\|[^\[\]\#]+)?\]\]/g,
"g",
) )
// ^\|([^\n])+\|\n(\|) -> matches the header row // ^\|([^\n])+\|\n(\|) -> matches the header row
// ( ?:?-{3,}:? ?\|)+ -> matches the header row separator // ( ?:?-{3,}:? ?\|)+ -> matches the header row separator
// (\|([^\n])+\|\n)+ -> matches the body rows // (\|([^\n])+\|\n)+ -> matches the body rows
export const tableRegex = new RegExp( export const tableRegex = new RegExp(/^\|([^\n])+\|\n(\|)( ?:?-{3,}:? ?\|)+\n(\|([^\n])+\|\n?)+/gm)
/^\|([^\n])+\|\n(\|)( ?:?-{3,}:? ?\|)+\n(\|([^\n])+\|\n?)+/,
"gm",
)
// matches any wikilink, only used for escaping wikilinks inside tables // matches any wikilink, only used for escaping wikilinks inside tables
export const tableWikilinkRegex = new RegExp(/(!?\[\[[^\]]*?\]\])/, "g") export const tableWikilinkRegex = new RegExp(/(!?\[\[[^\]]*?\]\])/g)
const highlightRegex = new RegExp(/==([^=]+)==/, "g") const highlightRegex = new RegExp(/==([^=]+)==/g)
const commentRegex = new RegExp(/%%[\s\S]*?%%/, "g") const commentRegex = new RegExp(/%%[\s\S]*?%%/g)
// from https://github.com/escwxyz/remark-obsidian-callout/blob/main/src/index.ts // from https://github.com/escwxyz/remark-obsidian-callout/blob/main/src/index.ts
const calloutRegex = new RegExp(/^\[\!(\w+)\|?(.+?)?\]([+-]?)/) const calloutRegex = new RegExp(/^\[\!(\w+)\|?(.+?)?\]([+-]?)/)
const calloutLineRegex = new RegExp(/^> *\[\!\w+\|?.*?\][+-]?.*$/, "gm") const calloutLineRegex = new RegExp(/^> *\[\!\w+\|?.*?\][+-]?.*$/gm)
// (?:^| ) -> non-capturing group, tag should start be separated by a space or be the start of the line // (?:^| ) -> non-capturing group, tag should start be separated by a space or be the start of the line
// #(...) -> capturing group, tag itself must start with # // #(...) -> capturing group, tag itself must start with #
// (?:[-_\p{L}\d\p{Z}])+ -> non-capturing group, non-empty string of (Unicode-aware) alpha-numeric characters and symbols, hyphens and/or underscores // (?:[-_\p{L}\d\p{Z}])+ -> non-capturing group, non-empty string of (Unicode-aware) alpha-numeric characters and symbols, hyphens and/or underscores
// (?:\/[-_\p{L}\d\p{Z}]+)*) -> non-capturing group, matches an arbitrary number of tag strings separated by "/" // (?:\/[-_\p{L}\d\p{Z}]+)*) -> non-capturing group, matches an arbitrary number of tag strings separated by "/"
const tagRegex = new RegExp( const tagRegex = new RegExp(
/(?:^| )#((?:[-_\p{L}\p{Emoji}\p{M}\d])+(?:\/[-_\p{L}\p{Emoji}\p{M}\d]+)*)/, /(?:^| )#((?:[-_\p{L}\p{Emoji}\p{M}\d])+(?:\/[-_\p{L}\p{Emoji}\p{M}\d]+)*)/gu,
"gu",
) )
const blockReferenceRegex = new RegExp(/\^([-_A-Za-z0-9]+)$/, "g") const blockReferenceRegex = new RegExp(/\^([-_A-Za-z0-9]+)$/g)
const ytLinkRegex = /^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|\&v=)([^#\&\?]*).*/ const ytLinkRegex = /^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|\&v=)([^#\&\?]*).*/
const ytPlaylistLinkRegex = /[?&]list=([^#?&]*)/ const ytPlaylistLinkRegex = /[?&]list=([^#?&]*)/
const videoExtensionRegex = new RegExp(/\.(mp4|webm|ogg|avi|mov|flv|wmv|mkv|mpg|mpeg|3gp|m4v)$/) const videoExtensionRegex = new RegExp(/\.(mp4|webm|ogg|avi|mov|flv|wmv|mkv|mpg|mpeg|3gp|m4v)$/)
@ -185,8 +179,8 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>
// replace all wikilinks inside a table first // replace all wikilinks inside a table first
src = src.replace(tableRegex, (value) => { src = src.replace(tableRegex, (value) => {
// escape all aliases and headers in wikilinks inside a table // escape all aliases and headers in wikilinks inside a table
return value.replace(tableWikilinkRegex, (value, ...capture) => { return value.replace(tableWikilinkRegex, (_value, raw) => {
const [raw]: (string | undefined)[] = capture // const [raw]: (string | undefined)[] = capture
let escaped = raw ?? "" let escaped = raw ?? ""
escaped = escaped.replace("#", "\\#") escaped = escaped.replace("#", "\\#")
// escape pipe characters if they are not already escaped // escape pipe characters if they are not already escaped
@ -201,7 +195,7 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>
const [rawFp, rawHeader, rawAlias]: (string | undefined)[] = capture const [rawFp, rawHeader, rawAlias]: (string | undefined)[] = capture
const [fp, anchor] = splitAnchor(`${rawFp ?? ""}${rawHeader ?? ""}`) const [fp, anchor] = splitAnchor(`${rawFp ?? ""}${rawHeader ?? ""}`)
const blockRef = Boolean(anchor?.startsWith("^")) ? "^" : "" const blockRef = Boolean(rawHeader?.match(/^#?\^/)) ? "^" : ""
const displayAnchor = anchor ? `#${blockRef}${anchor.trim().replace(/^#+/, "")}` : "" const displayAnchor = anchor ? `#${blockRef}${anchor.trim().replace(/^#+/, "")}` : ""
const displayAlias = rawAlias ?? rawHeader?.replace("#", "|") ?? "" const displayAlias = rawAlias ?? rawHeader?.replace("#", "|") ?? ""
const embedDisplay = value.startsWith("!") ? "!" : "" const embedDisplay = value.startsWith("!") ? "!" : ""
@ -276,7 +270,7 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>
return { return {
type: "html", type: "html",
data: { hProperties: { transclude: true } }, data: { hProperties: { transclude: true } },
value: `<blockquote class="transclude" data-url="${url}" data-block="${block}"><a href="${ value: `<blockquote class="transclude" data-url="${url}" data-block="${block}" data-embed-alias="${alias}"><a href="${
url + anchor url + anchor
}" class="transclude-inner">Transclude of ${url}${block}</a></blockquote>`, }" class="transclude-inner">Transclude of ${url}${block}</a></blockquote>`,
} }

View file

@ -20,11 +20,10 @@ section {
} }
.text-highlight { .text-highlight {
background-color: #fff23688; background-color: var(--textHighlight);
padding: 0 0.1rem; padding: 0 0.1rem;
border-radius: 5px; border-radius: 5px;
} }
::selection { ::selection {
background: color-mix(in srgb, var(--tertiary) 60%, rgba(255, 255, 255, 0)); background: color-mix(in srgb, var(--tertiary) 60%, rgba(255, 255, 255, 0));
color: var(--darkgray); color: var(--darkgray);
@ -202,11 +201,19 @@ a {
} }
} }
& .page-header { & .page-header,
& .page-footer {
width: $pageWidth; width: $pageWidth;
margin: $topSpacing auto 0 auto; margin-top: 1rem;
@media all and (max-width: $fullPageWidth) { @media all and (max-width: $fullPageWidth) {
width: initial; width: initial;
}
}
& .page-header {
margin: $topSpacing auto 0 auto;
@media all and (max-width: $fullPageWidth) {
margin-top: 2rem; margin-top: 2rem;
} }
} }

View file

@ -7,6 +7,7 @@ export interface ColorScheme {
secondary: string secondary: string
tertiary: string tertiary: string
highlight: string highlight: string
textHighlight: string
} }
interface Colors { interface Colors {
@ -49,6 +50,7 @@ ${stylesheet.join("\n\n")}
--secondary: ${theme.colors.lightMode.secondary}; --secondary: ${theme.colors.lightMode.secondary};
--tertiary: ${theme.colors.lightMode.tertiary}; --tertiary: ${theme.colors.lightMode.tertiary};
--highlight: ${theme.colors.lightMode.highlight}; --highlight: ${theme.colors.lightMode.highlight};
--textHighlight: ${theme.colors.lightMode.textHighlight};
--headerFont: "${theme.typography.header}", ${DEFAULT_SANS_SERIF}; --headerFont: "${theme.typography.header}", ${DEFAULT_SANS_SERIF};
--bodyFont: "${theme.typography.body}", ${DEFAULT_SANS_SERIF}; --bodyFont: "${theme.typography.body}", ${DEFAULT_SANS_SERIF};
@ -64,6 +66,7 @@ ${stylesheet.join("\n\n")}
--secondary: ${theme.colors.darkMode.secondary}; --secondary: ${theme.colors.darkMode.secondary};
--tertiary: ${theme.colors.darkMode.tertiary}; --tertiary: ${theme.colors.darkMode.tertiary};
--highlight: ${theme.colors.darkMode.highlight}; --highlight: ${theme.colors.darkMode.highlight};
--textHighlight: ${theme.colors.darkMode.textHighlight};
} }
` `
} }