Merge branch 'v4' into develop

This commit is contained in:
Shawn 2024-12-28 21:09:17 +08:00 committed by GitHub
commit 8317e98756
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 99 additions and 32 deletions

View File

@ -9,6 +9,7 @@ A backlink for a note is a link from another note to that note. Links in the bac
## Customization ## Customization
- Removing backlinks: delete all usages of `Component.Backlinks()` from `quartz.layout.ts`. - Removing backlinks: delete all usages of `Component.Backlinks()` from `quartz.layout.ts`.
- Hide when empty: hide `Backlinks` if given page doesn't contain any backlinks (default to `true`). To disable this, use `Component.Backlinks({ hideWhenEmpty: false })`.
- Component: `quartz/components/Backlinks.tsx` - Component: `quartz/components/Backlinks.tsx`
- Style: `quartz/components/styles/backlinks.scss` - Style: `quartz/components/styles/backlinks.scss`
- Script: `quartz/components/scripts/search.inline.ts` - Script: `quartz/components/scripts/search.inline.ts`

View File

@ -4,14 +4,28 @@ import { resolveRelative, simplifySlug } from "../util/path"
import { i18n } from "../i18n" import { i18n } from "../i18n"
import { classNames } from "../util/lang" import { classNames } from "../util/lang"
const Backlinks: QuartzComponent = ({ interface BacklinksOptions {
hideWhenEmpty: boolean
}
const defaultOptions: BacklinksOptions = {
hideWhenEmpty: true,
}
export default ((opts?: Partial<BacklinksOptions>) => {
const options: BacklinksOptions = { ...defaultOptions, ...opts }
const Backlinks: QuartzComponent = ({
fileData, fileData,
allFiles, allFiles,
displayClass, displayClass,
cfg, cfg,
}: QuartzComponentProps) => { }: QuartzComponentProps) => {
const slug = simplifySlug(fileData.slug!) const slug = simplifySlug(fileData.slug!)
const backlinkFiles = allFiles.filter((file) => file.links?.includes(slug)) const backlinkFiles = allFiles.filter((file) => file.links?.includes(slug))
if (options.hideWhenEmpty && backlinkFiles.length == 0) {
return null
}
return ( return (
<div class={classNames(displayClass, "backlinks")}> <div class={classNames(displayClass, "backlinks")}>
<h3>{i18n(cfg.locale).components.backlinks.title}</h3> <h3>{i18n(cfg.locale).components.backlinks.title}</h3>
@ -30,7 +44,9 @@ const Backlinks: QuartzComponent = ({
</ul> </ul>
</div> </div>
) )
} }
Backlinks.css = style Backlinks.css = style
export default (() => Backlinks) satisfies QuartzComponentConstructor
return Backlinks
}) satisfies QuartzComponentConstructor

View File

@ -110,7 +110,7 @@ export default ((opts?: Partial<TagContentOptions>) => {
return ( return (
<div class={classes}> <div class={classes}>
<article>{content}</article> <article class="popover-hint">{content}</article>
<div class="page-listing"> <div class="page-listing">
<p>{i18n(cfg.locale).pages.tagContent.itemsUnderTag({ count: pages.length })}</p> <p>{i18n(cfg.locale).pages.tagContent.itemsUnderTag({ count: pages.length })}</p>
<div> <div>

View File

@ -1,5 +1,6 @@
import { computePosition, flip, inline, shift } from "@floating-ui/dom" import { computePosition, flip, inline, shift } from "@floating-ui/dom"
import { normalizeRelativeURLs } from "../../util/path" import { normalizeRelativeURLs } from "../../util/path"
import { fetchCanonical } from "./util"
const p = new DOMParser() const p = new DOMParser()
async function mouseEnterHandler( async function mouseEnterHandler(
@ -37,7 +38,7 @@ async function mouseEnterHandler(
targetUrl.hash = "" targetUrl.hash = ""
targetUrl.search = "" targetUrl.search = ""
const response = await fetch(`${targetUrl}`).catch((err) => { const response = await fetchCanonical(targetUrl).catch((err) => {
console.error(err) console.error(err)
}) })

View File

@ -1,5 +1,6 @@
import micromorph from "micromorph" import micromorph from "micromorph"
import { FullSlug, RelativeURL, getFullSlug, normalizeRelativeURLs } from "../../util/path" import { FullSlug, RelativeURL, getFullSlug, normalizeRelativeURLs } from "../../util/path"
import { fetchCanonical } from "./util"
// adapted from `micromorph` // adapted from `micromorph`
// https://github.com/natemoo-re/micromorph // https://github.com/natemoo-re/micromorph
@ -42,10 +43,24 @@ function notifyNav(url: FullSlug) {
const cleanupFns: Set<(...args: any[]) => void> = new Set() const cleanupFns: Set<(...args: any[]) => void> = new Set()
window.addCleanup = (fn) => cleanupFns.add(fn) window.addCleanup = (fn) => cleanupFns.add(fn)
function startLoading() {
const loadingBar = document.createElement("div")
loadingBar.className = "navigation-progress"
loadingBar.style.width = "0"
if (!document.body.contains(loadingBar)) {
document.body.appendChild(loadingBar)
}
setTimeout(() => {
loadingBar.style.width = "80%"
}, 100)
}
let p: DOMParser let p: DOMParser
async function navigate(url: URL, isBack: boolean = false) { async function navigate(url: URL, isBack: boolean = false) {
startLoading()
p = p || new DOMParser() p = p || new DOMParser()
const contents = await fetch(`${url}`) const contents = await fetchCanonical(url)
.then((res) => { .then((res) => {
const contentType = res.headers.get("content-type") const contentType = res.headers.get("content-type")
if (contentType?.startsWith("text/html")) { if (contentType?.startsWith("text/html")) {
@ -104,6 +119,7 @@ async function navigate(url: URL, isBack: boolean = false) {
if (!isBack) { if (!isBack) {
history.pushState({}, "", url) history.pushState({}, "", url)
} }
notifyNav(getFullSlug(window)) notifyNav(getFullSlug(window))
delete announcer.dataset.persist delete announcer.dataset.persist
} }

View File

@ -24,3 +24,22 @@ export function removeAllChildren(node: HTMLElement) {
node.removeChild(node.firstChild) node.removeChild(node.firstChild)
} }
} }
// AliasRedirect emits HTML redirects which also have the link[rel="canonical"]
// containing the URL it's redirecting to.
// Extracting it here with regex is _probably_ faster than parsing the entire HTML
// with a DOMParser effectively twice (here and later in the SPA code), even if
// way less robust - we only care about our own generated redirects after all.
const canonicalRegex = /<link rel="canonical" href="([^"]*)">/
export async function fetchCanonical(url: URL): Promise<Response> {
const res = await fetch(`${url}`)
if (!res.headers.get("content-type")?.startsWith("text/html")) {
return res
}
// reading the body can only be done once, so we need to clone the response
// to allow the caller to read it if it's was not a redirect
const text = await res.clone().text()
const [_, redirect] = text.match(canonicalRegex) ?? []
return redirect ? fetch(redirect) : res
}

View File

@ -105,6 +105,9 @@ export const TagPage: QuartzEmitterPlugin<Partial<TagPageOptions>> = (userOpts)
const tag = slug.slice("tags/".length) const tag = slug.slice("tags/".length)
if (tags.has(tag)) { if (tags.has(tag)) {
tagDescriptions[tag] = [tree, file] tagDescriptions[tag] = [tree, file]
if (file.data.frontmatter?.title === tag) {
file.data.frontmatter.title = `${i18n(cfg.locale).pages.tagContent.tag}: ${tag}`
}
} }
} }
} }

View File

@ -608,3 +608,14 @@ iframe.pdf {
width: 100%; width: 100%;
border-radius: 5px; border-radius: 5px;
} }
.navigation-progress {
position: fixed;
top: 0;
left: 0;
width: 0;
height: 3px;
background: var(--secondary);
transition: width 0.2s ease;
z-index: 9999;
}