From 9aa6a18be2eae0d84c7897470a46ede19d5ac191 Mon Sep 17 00:00:00 2001 From: Aaron Pham <29749331+aarnphm@users.noreply.github.com> Date: Thu, 1 Feb 2024 15:56:42 -0500 Subject: [PATCH 001/115] fix(search): improve more general usability (closes #781) (#782) * fix(search): improve more general usability Signed-off-by: Aaron <29749331+aarnphm@users.noreply.github.com> * fix: revert naming Signed-off-by: Aaron <29749331+aarnphm@users.noreply.github.com> * fix: correct check for enter event on no-match cases Signed-off-by: Aaron <29749331+aarnphm@users.noreply.github.com> * Update quartz/components/scripts/search.inline.ts Co-authored-by: Jacky Zhao * chore: remove unecessary class for tracking mouse Signed-off-by: Aaron <29749331+aarnphm@users.noreply.github.com> --------- Signed-off-by: Aaron <29749331+aarnphm@users.noreply.github.com> Co-authored-by: Jacky Zhao --- quartz/components/scripts/search.inline.ts | 66 ++++++++++++++-------- 1 file changed, 44 insertions(+), 22 deletions(-) diff --git a/quartz/components/scripts/search.inline.ts b/quartz/components/scripts/search.inline.ts index 170a8f014ba20..43332a6d8044a 100644 --- a/quartz/components/scripts/search.inline.ts +++ b/quartz/components/scripts/search.inline.ts @@ -122,6 +122,9 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => { if (preview) { removeAllChildren(preview) } + if (searchLayout) { + searchLayout.style.opacity = "0" + } searchType = "basic" // reset search type after closing } @@ -135,6 +138,8 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => { searchBar?.focus() } + let currentHover: HTMLInputElement | null = null + async function shortcutHandler(e: HTMLElementEventMap["keydown"]) { if (e.key === "k" && (e.ctrlKey || e.metaKey) && !e.shiftKey) { e.preventDefault() @@ -150,51 +155,61 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => { if (searchBar) searchBar.value = "#" } - const resultCards = document.getElementsByClassName("result-card") + if (currentHover) { + currentHover.classList.remove("focus") + } // If search is active, then we will render the first result and display accordingly if (!container?.classList.contains("active")) return - else if (results?.contains(document.activeElement)) { - const active = document.activeElement as HTMLInputElement - await displayPreview(active) - if (e.key === "Enter") { + else if (e.key === "Enter") { + // If result has focus, navigate to that one, otherwise pick first result + if (results?.contains(document.activeElement)) { + const active = document.activeElement as HTMLInputElement + if (active.classList.contains("no-match")) return + await displayPreview(active) active.click() + } else { + const anchor = document.getElementsByClassName("result-card")[0] as HTMLInputElement | null + if (!anchor || anchor?.classList.contains("no-match")) return + await displayPreview(anchor) + anchor.click() } - } else { - const anchor = resultCards[0] as HTMLInputElement | null - await displayPreview(anchor) - if (e.key === "Enter") { - anchor?.click() - } - } - - if (e.key === "ArrowUp" || (e.shiftKey && e.key === "Tab")) { + } else if (e.key === "ArrowUp" || (e.shiftKey && e.key === "Tab")) { e.preventDefault() if (results?.contains(document.activeElement)) { // If an element in results-container already has focus, focus previous one - const currentResult = document.activeElement as HTMLInputElement | null + const currentResult = currentHover + ? currentHover + : (document.activeElement as HTMLInputElement | null) const prevResult = currentResult?.previousElementSibling as HTMLInputElement | null currentResult?.classList.remove("focus") await displayPreview(prevResult) prevResult?.focus() + currentHover = prevResult } } else if (e.key === "ArrowDown" || e.key === "Tab") { e.preventDefault() // The results should already been focused, so we need to find the next one. // The activeElement is the search bar, so we need to find the first result and focus it. if (!results?.contains(document.activeElement)) { - const firstResult = resultCards[0] as HTMLInputElement | null + const firstResult = currentHover + ? currentHover + : (document.getElementsByClassName("result-card")[0] as HTMLInputElement | null) const secondResult = firstResult?.nextElementSibling as HTMLInputElement | null firstResult?.classList.remove("focus") await displayPreview(secondResult) secondResult?.focus() + currentHover = secondResult } else { // If an element in results-container already has focus, focus next one - const active = document.activeElement as HTMLInputElement | null + const active = currentHover + ? currentHover + : (document.activeElement as HTMLInputElement | null) active?.classList.remove("focus") const nextResult = active?.nextElementSibling as HTMLInputElement | null await displayPreview(nextResult) nextResult?.focus() + currentHover = nextResult } } } @@ -282,15 +297,17 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => { async function onMouseEnter(ev: MouseEvent) { // Actually when we hover, we need to clean all highlights within the result childs + if (!ev.target) return for (const el of document.getElementsByClassName( "result-card", ) as HTMLCollectionOf) { el.classList.remove("focus") el.blur() } - const target = ev.target as HTMLAnchorElement - target.classList.add("focus") + const target = ev.target as HTMLInputElement await displayPreview(target) + currentHover = target + currentHover.classList.remove("focus") } async function onMouseLeave(ev: MouseEvent) { @@ -320,7 +337,7 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => { removeAllChildren(results) if (finalResults.length === 0) { - results.innerHTML = ` + results.innerHTML = `

No results.

Try another search term?

` @@ -329,8 +346,13 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => { } // focus on first result, then also dispatch preview immediately if (results?.firstElementChild) { - results?.firstElementChild?.classList.add("focus") - await displayPreview(results?.firstElementChild as HTMLElement) + const firstChild = results.firstElementChild as HTMLElement + if (firstChild.classList.contains("no-match")) { + removeAllChildren(preview as HTMLElement) + } else { + firstChild.classList.add("focus") + await displayPreview(firstChild) + } } } From 756acc7f975f313f9d9139b42be9d57805014454 Mon Sep 17 00:00:00 2001 From: Aaron Pham <29749331+aarnphm@users.noreply.github.com> Date: Thu, 1 Feb 2024 16:48:27 -0500 Subject: [PATCH 002/115] feat(search): highlight on preview (#783) * feat: primitive full-text search on preview Signed-off-by: Aaron <29749331+aarnphm@users.noreply.github.com> * fix: remove invalid regex and unused code path Signed-off-by: Aaron <29749331+aarnphm@users.noreply.github.com> --------- Signed-off-by: Aaron <29749331+aarnphm@users.noreply.github.com> --- quartz/components/scripts/search.inline.ts | 67 +++++++++++++++++++--- quartz/components/styles/search.scss | 10 ++-- 2 files changed, 64 insertions(+), 13 deletions(-) diff --git a/quartz/components/scripts/search.inline.ts b/quartz/components/scripts/search.inline.ts index 43332a6d8044a..3bbfa7bf27e9b 100644 --- a/quartz/components/scripts/search.inline.ts +++ b/quartz/components/scripts/search.inline.ts @@ -11,23 +11,29 @@ interface Item { tags: string[] } -let index: FlexSearch.Document | undefined = undefined - // Can be expanded with things like "term" in the future type SearchType = "basic" | "tags" // Current searchType let searchType: SearchType = "basic" +// Current search term // TODO: exact match +let currentSearchTerm: string = "" +// index for search +let index: FlexSearch.Document | undefined = undefined const contextWindowWords = 30 const numSearchResults = 8 const numTagResults = 5 -function highlight(searchTerm: string, text: string, trim?: boolean) { - // try to highlight longest tokens first - const tokenizedTerms = searchTerm + +const tokenizeTerm = (term: string) => + term .split(/\s+/) .filter((t) => t !== "") .sort((a, b) => b.length - a.length) + +function highlight(searchTerm: string, text: string, trim?: boolean) { + // try to highlight longest tokens first + const tokenizedTerms = tokenizeTerm(searchTerm) let tokenizedText = text.split(/\s+/).filter((t) => t !== "") let startIndex = 0 @@ -64,6 +70,7 @@ function highlight(searchTerm: string, text: string, trim?: boolean) { } return tok }) + .slice(startIndex, endIndex + 1) .join(" ") return `${startIndex === 0 ? "" : "..."}${slice}${ @@ -71,6 +78,45 @@ function highlight(searchTerm: string, text: string, trim?: boolean) { }` } +function highlightHTML(searchTerm: string, el: HTMLElement) { + // try to highlight longest tokens first + const p = new DOMParser() + const tokenizedTerms = tokenizeTerm(searchTerm) + const html = p.parseFromString(el.innerHTML, "text/html") + + const createHighlightSpan = (text: string) => { + const span = document.createElement("span") + span.className = "highlight" + span.textContent = text + return span + } + + const highlightTextNodes = (node: Node) => { + if (node.nodeType === Node.TEXT_NODE) { + let nodeText = node.nodeValue || "" + tokenizedTerms.forEach((term) => { + const regex = new RegExp(term.toLowerCase(), "gi") + const matches = nodeText.match(regex) + const spanContainer = document.createElement("span") + let lastIndex = 0 + matches?.forEach((match) => { + const matchIndex = nodeText.indexOf(match, lastIndex) + spanContainer.appendChild(document.createTextNode(nodeText.slice(lastIndex, matchIndex))) + spanContainer.appendChild(createHighlightSpan(match)) + lastIndex = matchIndex + match.length + }) + spanContainer.appendChild(document.createTextNode(nodeText.slice(lastIndex))) + node.parentNode?.replaceChild(spanContainer, node) + }) + } else if (node.nodeType === Node.ELEMENT_NODE) { + Array.from(node.childNodes).forEach(highlightTextNodes) + } + } + + highlightTextNodes(html.body) + return html.body +} + const p = new DOMParser() const encoder = (str: string) => str.toLowerCase().split(/([^a-z]|[^\x00-\x7F])/) let prevShortcutHandler: ((e: HTMLElementEventMap["keydown"]) => void) | undefined = undefined @@ -96,6 +142,7 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => { const enablePreview = searchLayout?.dataset?.preview === "true" let preview: HTMLDivElement | undefined = undefined + let previewInner: HTMLDivElement | undefined = undefined const results = document.createElement("div") results.id = "results-container" results.style.flexBasis = enablePreview ? "30%" : "100%" @@ -384,17 +431,21 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => { el.classList.add("focus") removeAllChildren(preview as HTMLElement) - const contentDetails = await fetchContent(slug) - const previewInner = document.createElement("div") + previewInner = document.createElement("div") previewInner.classList.add("preview-inner") preview?.appendChild(previewInner) - contentDetails?.forEach((elt) => previewInner.appendChild(elt)) + + const innerDiv = await fetchContent(slug).then((contents) => + contents.map((el) => highlightHTML(currentSearchTerm, el as HTMLElement)), + ) + previewInner.append(...innerDiv) } async function onType(e: HTMLElementEventMap["input"]) { let term = (e.target as HTMLInputElement).value let searchResults: FlexSearch.SimpleDocumentSearchResultSetUnit[] + currentSearchTerm = (e.target as HTMLInputElement).value if (searchLayout) { searchLayout.style.opacity = "1" diff --git a/quartz/components/styles/search.scss b/quartz/components/styles/search.scss index fb7dd745804b4..e84172e3503d1 100644 --- a/quartz/components/styles/search.scss +++ b/quartz/components/styles/search.scss @@ -121,6 +121,11 @@ } } + & .highlight { + color: var(--secondary); + font-weight: 700; + } + & > #preview-container { display: block; box-sizing: border-box; @@ -166,11 +171,6 @@ outline: none; font-weight: inherit; - & .highlight { - color: var(--secondary); - font-weight: 700; - } - &:hover, &:focus, &.focus { From 295b8fc9149e6702629717e3b71b2eff7d726a19 Mon Sep 17 00:00:00 2001 From: Aaron Pham <29749331+aarnphm@users.noreply.github.com> Date: Thu, 1 Feb 2024 19:44:33 -0500 Subject: [PATCH 003/115] fix(search): increase size on fullPageWidth viewport (#784) * fix(search): increase size on fullPageWidth viewport Signed-off-by: Aaron <29749331+aarnphm@users.noreply.github.com> * chore: fix width size to be consistent on multiple views Signed-off-by: Aaron <29749331+aarnphm@users.noreply.github.com> * chore: set layout to 0 if there is no term remove flashing by setting max-height Signed-off-by: Aaron <29749331+aarnphm@users.noreply.github.com> --------- Signed-off-by: Aaron <29749331+aarnphm@users.noreply.github.com> --- quartz/components/scripts/search.inline.ts | 4 ++++ quartz/components/styles/search.scss | 13 +++++++------ 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/quartz/components/scripts/search.inline.ts b/quartz/components/scripts/search.inline.ts index 3bbfa7bf27e9b..7871b39d630fa 100644 --- a/quartz/components/scripts/search.inline.ts +++ b/quartz/components/scripts/search.inline.ts @@ -451,6 +451,10 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => { searchLayout.style.opacity = "1" } + if (term === "" && searchLayout) { + searchLayout.style.opacity = "0" + } + if (term.toLowerCase().startsWith("#")) { searchType = "tags" } else { diff --git a/quartz/components/styles/search.scss b/quartz/components/styles/search.scss index e84172e3503d1..11e7c4e14b2a6 100644 --- a/quartz/components/styles/search.scss +++ b/quartz/components/styles/search.scss @@ -54,15 +54,11 @@ } & > #search-space { - width: 50%; + width: 75%; margin-top: 12vh; margin-left: auto; margin-right: auto; - @media all and (max-width: $fullPageWidth) { - width: 90%; - } - & > * { width: 100%; border-radius: 5px; @@ -94,7 +90,8 @@ border: 1px solid var(--lightgray); & > div { - height: calc(75vh - 20em); + // vh - #search-space.margin-top + height: calc(75vh - 12vh); background: none; &:first-child { @@ -146,6 +143,10 @@ 0 14px 50px rgba(27, 33, 48, 0.12), 0 10px 30px rgba(27, 33, 48, 0.16); } + + a.internal { + background-color: none; + } } & > #results-container { From f78b512436ebc293d10e9ebdd0fc5fbd1705dde4 Mon Sep 17 00:00:00 2001 From: Aaron Pham <29749331+aarnphm@users.noreply.github.com> Date: Thu, 1 Feb 2024 22:25:45 -0500 Subject: [PATCH 004/115] chore(search): check for input type and assignment of focus (#785) Signed-off-by: Aaron <29749331+aarnphm@users.noreply.github.com> --- quartz/components/scripts/search.inline.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/quartz/components/scripts/search.inline.ts b/quartz/components/scripts/search.inline.ts index 7871b39d630fa..8ead5c9523b80 100644 --- a/quartz/components/scripts/search.inline.ts +++ b/quartz/components/scripts/search.inline.ts @@ -238,7 +238,7 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => { e.preventDefault() // The results should already been focused, so we need to find the next one. // The activeElement is the search bar, so we need to find the first result and focus it. - if (!results?.contains(document.activeElement)) { + if (document.activeElement === searchBar || currentHover !== null) { const firstResult = currentHover ? currentHover : (document.getElementsByClassName("result-card")[0] as HTMLInputElement | null) @@ -398,6 +398,7 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => { removeAllChildren(preview as HTMLElement) } else { firstChild.classList.add("focus") + currentHover = firstChild as HTMLInputElement await displayPreview(firstChild) } } From 8a6ebd193933c2879c2a36e1b2f164889575d3bc Mon Sep 17 00:00:00 2001 From: Justin Fowler Date: Thu, 1 Feb 2024 22:17:21 -0600 Subject: [PATCH 005/115] docs: clarity for `RecentNotes` (#786) - Removed a word for clarity - added reference to layout file --- docs/features/recent notes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/features/recent notes.md b/docs/features/recent notes.md index 439d6d0503130..9236b7ce207d4 100644 --- a/docs/features/recent notes.md +++ b/docs/features/recent notes.md @@ -3,7 +3,7 @@ title: Recent Notes tags: component --- -Quartz can generate a list of recent notes for based on some filtering and sorting criteria. Though this component isn't included in any [[layout]] by default, you can add it by using `Component.RecentNotes`. +Quartz can generate a list of recent notes based on some filtering and sorting criteria. Though this component isn't included in any [[layout]] by default, you can add it by using `Component.RecentNotes` in `quartz.layout.ts`. ## Customization From c00089bd5728188ce554303b5b18754467c97c85 Mon Sep 17 00:00:00 2001 From: Jacky Zhao Date: Thu, 1 Feb 2024 20:07:14 -0800 Subject: [PATCH 006/115] chore: add window.addCleanup() for cleaning up handlers --- docs/advanced/creating components.md | 5 ++-- globals.d.ts | 1 + quartz/components/scripts/callout.inline.ts | 24 ++++++++--------- quartz/components/scripts/darkmode.inline.ts | 2 +- quartz/components/scripts/explorer.inline.ts | 6 ++--- quartz/components/scripts/graph.inline.ts | 2 +- quartz/components/scripts/popover.inline.ts | 2 +- quartz/components/scripts/search.inline.ts | 27 +++++-------------- quartz/components/scripts/spa.inline.ts | 7 +++++ quartz/components/scripts/toc.inline.ts | 2 +- quartz/components/scripts/util.ts | 4 +-- quartz/plugins/emitters/componentResources.ts | 14 +++++----- 12 files changed, 47 insertions(+), 49 deletions(-) diff --git a/docs/advanced/creating components.md b/docs/advanced/creating components.md index 1496b15b2afc5..27369abf22a42 100644 --- a/docs/advanced/creating components.md +++ b/docs/advanced/creating components.md @@ -156,12 +156,13 @@ document.addEventListener("nav", () => { // do page specific logic here // e.g. attach event listeners const toggleSwitch = document.querySelector("#switch") as HTMLInputElement - toggleSwitch.removeEventListener("change", switchTheme) toggleSwitch.addEventListener("change", switchTheme) + window.addCleanup(() => toggleSwitch.removeEventListener("change", switchTheme)) }) ``` -It is best practice to also unmount any existing event handlers to prevent memory leaks. +It is best practice to track any event handlers via `window.addCleanup` to prevent memory leaks. +This will get called on page navigation. #### Importing Code diff --git a/globals.d.ts b/globals.d.ts index 0509f2665b260..ee13005c98b6c 100644 --- a/globals.d.ts +++ b/globals.d.ts @@ -8,5 +8,6 @@ export declare global { } interface Window { spaNavigate(url: URL, isBack: boolean = false) + addCleanup(fn: (...args: any[]) => void) } } diff --git a/quartz/components/scripts/callout.inline.ts b/quartz/components/scripts/callout.inline.ts index d8cf5180a69c5..8f63df36f6d3e 100644 --- a/quartz/components/scripts/callout.inline.ts +++ b/quartz/components/scripts/callout.inline.ts @@ -1,21 +1,21 @@ function toggleCallout(this: HTMLElement) { const outerBlock = this.parentElement! - outerBlock.classList.toggle(`is-collapsed`) - const collapsed = outerBlock.classList.contains(`is-collapsed`) + outerBlock.classList.toggle("is-collapsed") + const collapsed = outerBlock.classList.contains("is-collapsed") const height = collapsed ? this.scrollHeight : outerBlock.scrollHeight - outerBlock.style.maxHeight = height + `px` + outerBlock.style.maxHeight = height + "px" // walk and adjust height of all parents let current = outerBlock let parent = outerBlock.parentElement while (parent) { - if (!parent.classList.contains(`callout`)) { + if (!parent.classList.contains("callout")) { return } - const collapsed = parent.classList.contains(`is-collapsed`) + const collapsed = parent.classList.contains("is-collapsed") const height = collapsed ? parent.scrollHeight : parent.scrollHeight + current.scrollHeight - parent.style.maxHeight = height + `px` + parent.style.maxHeight = height + "px" current = parent parent = parent.parentElement @@ -30,15 +30,15 @@ function setupCallout() { const title = div.firstElementChild if (title) { - title.removeEventListener(`click`, toggleCallout) - title.addEventListener(`click`, toggleCallout) + title.addEventListener("click", toggleCallout) + window.addCleanup(() => title.removeEventListener("click", toggleCallout)) - const collapsed = div.classList.contains(`is-collapsed`) + const collapsed = div.classList.contains("is-collapsed") const height = collapsed ? title.scrollHeight : div.scrollHeight - div.style.maxHeight = height + `px` + div.style.maxHeight = height + "px" } } } -document.addEventListener(`nav`, setupCallout) -window.addEventListener(`resize`, setupCallout) +document.addEventListener("nav", setupCallout) +window.addEventListener("resize", setupCallout) diff --git a/quartz/components/scripts/darkmode.inline.ts b/quartz/components/scripts/darkmode.inline.ts index 86735e3967aba..4c671aa41ecb4 100644 --- a/quartz/components/scripts/darkmode.inline.ts +++ b/quartz/components/scripts/darkmode.inline.ts @@ -19,8 +19,8 @@ document.addEventListener("nav", () => { // Darkmode toggle const toggleSwitch = document.querySelector("#darkmode-toggle") as HTMLInputElement - toggleSwitch.removeEventListener("change", switchTheme) toggleSwitch.addEventListener("change", switchTheme) + window.addCleanup(() => toggleSwitch.removeEventListener("change", switchTheme)) if (currentTheme === "dark") { toggleSwitch.checked = true } diff --git a/quartz/components/scripts/explorer.inline.ts b/quartz/components/scripts/explorer.inline.ts index 12546bbb01fb9..3eb25ead42e40 100644 --- a/quartz/components/scripts/explorer.inline.ts +++ b/quartz/components/scripts/explorer.inline.ts @@ -57,20 +57,20 @@ function setupExplorer() { for (const item of document.getElementsByClassName( "folder-button", ) as HTMLCollectionOf) { - item.removeEventListener("click", toggleFolder) item.addEventListener("click", toggleFolder) + window.addCleanup(() => item.removeEventListener("click", toggleFolder)) } } - explorer.removeEventListener("click", toggleExplorer) explorer.addEventListener("click", toggleExplorer) + window.addCleanup(() => explorer.removeEventListener("click", toggleExplorer)) // Set up click handlers for each folder (click handler on folder "icon") for (const item of document.getElementsByClassName( "folder-icon", ) as HTMLCollectionOf) { - item.removeEventListener("click", toggleFolder) item.addEventListener("click", toggleFolder) + window.addCleanup(() => item.removeEventListener("click", toggleFolder)) } // Get folder state from local storage diff --git a/quartz/components/scripts/graph.inline.ts b/quartz/components/scripts/graph.inline.ts index a76409c139cd6..c991e163ef0c8 100644 --- a/quartz/components/scripts/graph.inline.ts +++ b/quartz/components/scripts/graph.inline.ts @@ -325,6 +325,6 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => { await renderGraph("graph-container", slug) const containerIcon = document.getElementById("global-graph-icon") - containerIcon?.removeEventListener("click", renderGlobalGraph) containerIcon?.addEventListener("click", renderGlobalGraph) + window.addCleanup(() => containerIcon?.removeEventListener("click", renderGlobalGraph)) }) diff --git a/quartz/components/scripts/popover.inline.ts b/quartz/components/scripts/popover.inline.ts index 4d51e2a6f6df1..0251834cb5df7 100644 --- a/quartz/components/scripts/popover.inline.ts +++ b/quartz/components/scripts/popover.inline.ts @@ -76,7 +76,7 @@ async function mouseEnterHandler( document.addEventListener("nav", () => { const links = [...document.getElementsByClassName("internal")] as HTMLLinkElement[] for (const link of links) { - link.removeEventListener("mouseenter", mouseEnterHandler) link.addEventListener("mouseenter", mouseEnterHandler) + window.addCleanup(() => link.removeEventListener("mouseenter", mouseEnterHandler)) } }) diff --git a/quartz/components/scripts/search.inline.ts b/quartz/components/scripts/search.inline.ts index 8ead5c9523b80..797685adf11b1 100644 --- a/quartz/components/scripts/search.inline.ts +++ b/quartz/components/scripts/search.inline.ts @@ -13,14 +13,13 @@ interface Item { // Can be expanded with things like "term" in the future type SearchType = "basic" | "tags" - -// Current searchType let searchType: SearchType = "basic" -// Current search term // TODO: exact match let currentSearchTerm: string = "" -// index for search let index: FlexSearch.Document | undefined = undefined +const p = new DOMParser() +const encoder = (str: string) => str.toLowerCase().split(/([^a-z]|[^\x00-\x7F])/) +const fetchContentCache: Map = new Map() const contextWindowWords = 30 const numSearchResults = 8 const numTagResults = 5 @@ -79,7 +78,6 @@ function highlight(searchTerm: string, text: string, trim?: boolean) { } function highlightHTML(searchTerm: string, el: HTMLElement) { - // try to highlight longest tokens first const p = new DOMParser() const tokenizedTerms = tokenizeTerm(searchTerm) const html = p.parseFromString(el.innerHTML, "text/html") @@ -117,12 +115,6 @@ function highlightHTML(searchTerm: string, el: HTMLElement) { return html.body } -const p = new DOMParser() -const encoder = (str: string) => str.toLowerCase().split(/([^a-z]|[^\x00-\x7F])/) -let prevShortcutHandler: ((e: HTMLElementEventMap["keydown"]) => void) | undefined = undefined - -const fetchContentCache: Map = new Map() - document.addEventListener("nav", async (e: CustomEventMap["nav"]) => { const currentSlug = e.detail.url @@ -496,16 +488,12 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => { await displayResults(finalResults) } - if (prevShortcutHandler) { - document.removeEventListener("keydown", prevShortcutHandler) - } - document.addEventListener("keydown", shortcutHandler) - prevShortcutHandler = shortcutHandler - searchIcon?.removeEventListener("click", () => showSearch("basic")) + window.addCleanup(() => document.removeEventListener("keydown", shortcutHandler)) searchIcon?.addEventListener("click", () => showSearch("basic")) - searchBar?.removeEventListener("input", onType) + window.addCleanup(() => searchIcon?.removeEventListener("click", () => showSearch("basic"))) searchBar?.addEventListener("input", onType) + window.addCleanup(() => searchBar?.removeEventListener("input", onType)) // setup index if it hasn't been already if (!index) { @@ -546,13 +534,12 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => { async function fillDocument(index: FlexSearch.Document, data: any) { let id = 0 for (const [slug, fileData] of Object.entries(data)) { - await index.addAsync(id, { + await index.addAsync(id++, { id, slug: slug as FullSlug, title: fileData.title, content: fileData.content, tags: fileData.tags, }) - id++ } } diff --git a/quartz/components/scripts/spa.inline.ts b/quartz/components/scripts/spa.inline.ts index c2a44c9a8bb62..1790bcabccebd 100644 --- a/quartz/components/scripts/spa.inline.ts +++ b/quartz/components/scripts/spa.inline.ts @@ -39,6 +39,9 @@ function notifyNav(url: FullSlug) { document.dispatchEvent(event) } +const cleanupFns: Set<(...args: any[]) => void> = new Set() +window.addCleanup = (fn) => cleanupFns.add(fn) + let p: DOMParser async function navigate(url: URL, isBack: boolean = false) { p = p || new DOMParser() @@ -57,6 +60,10 @@ async function navigate(url: URL, isBack: boolean = false) { if (!contents) return + // cleanup old + cleanupFns.forEach((fn) => fn()) + cleanupFns.clear() + const html = p.parseFromString(contents, "text/html") normalizeRelativeURLs(html, url) diff --git a/quartz/components/scripts/toc.inline.ts b/quartz/components/scripts/toc.inline.ts index 2e1e52b0e3fd7..546859ed3261b 100644 --- a/quartz/components/scripts/toc.inline.ts +++ b/quartz/components/scripts/toc.inline.ts @@ -29,8 +29,8 @@ function setupToc() { const content = toc.nextElementSibling as HTMLElement | undefined if (!content) return content.style.maxHeight = collapsed ? "0px" : content.scrollHeight + "px" - toc.removeEventListener("click", toggleToc) toc.addEventListener("click", toggleToc) + window.addCleanup(() => toc.removeEventListener("click", toggleToc)) } } diff --git a/quartz/components/scripts/util.ts b/quartz/components/scripts/util.ts index 5fcabadc15b5c..4ffff29e28e46 100644 --- a/quartz/components/scripts/util.ts +++ b/quartz/components/scripts/util.ts @@ -12,10 +12,10 @@ export function registerEscapeHandler(outsideContainer: HTMLElement | null, cb: cb() } - outsideContainer?.removeEventListener("click", click) outsideContainer?.addEventListener("click", click) - document.removeEventListener("keydown", esc) + window.addCleanup(() => outsideContainer?.removeEventListener("click", click)) document.addEventListener("keydown", esc) + window.addCleanup(() => document.removeEventListener("keydown", esc)) } export function removeAllChildren(node: HTMLElement) { diff --git a/quartz/plugins/emitters/componentResources.ts b/quartz/plugins/emitters/componentResources.ts index accc2611effc7..5eb9718a953b4 100644 --- a/quartz/plugins/emitters/componentResources.ts +++ b/quartz/plugins/emitters/componentResources.ts @@ -131,9 +131,11 @@ function addGlobalPageResources( componentResources.afterDOMLoaded.push(spaRouterScript) } else { componentResources.afterDOMLoaded.push(` - window.spaNavigate = (url, _) => window.location.assign(url) - const event = new CustomEvent("nav", { detail: { url: document.body.dataset.slug } }) - document.dispatchEvent(event)`) + window.spaNavigate = (url, _) => window.location.assign(url) + window.addCleanup = () => {} + const event = new CustomEvent("nav", { detail: { url: document.body.dataset.slug } }) + document.dispatchEvent(event) + `) } let wsUrl = `ws://localhost:${ctx.argv.wsPort}` @@ -147,9 +149,9 @@ function addGlobalPageResources( loadTime: "afterDOMReady", contentType: "inline", script: ` - const socket = new WebSocket('${wsUrl}') - socket.addEventListener('message', () => document.location.reload()) - `, + const socket = new WebSocket('${wsUrl}') + socket.addEventListener('message', () => document.location.reload()) + `, }) } } From c0c0b24138c6718a7bc91926c7e4dd074845e620 Mon Sep 17 00:00:00 2001 From: Jacky Zhao Date: Thu, 1 Feb 2024 21:15:28 -0800 Subject: [PATCH 007/115] feat: improve search preview styling and tokenization --- quartz/components/scripts/search.inline.ts | 62 ++++++++++++---------- quartz/components/styles/search.scss | 11 ++-- quartz/styles/base.scss | 2 +- quartz/styles/variables.scss | 2 +- 4 files changed, 43 insertions(+), 34 deletions(-) diff --git a/quartz/components/scripts/search.inline.ts b/quartz/components/scripts/search.inline.ts index 797685adf11b1..82fdf826b9e2b 100644 --- a/quartz/components/scripts/search.inline.ts +++ b/quartz/components/scripts/search.inline.ts @@ -24,14 +24,20 @@ const contextWindowWords = 30 const numSearchResults = 8 const numTagResults = 5 -const tokenizeTerm = (term: string) => - term - .split(/\s+/) - .filter((t) => t !== "") - .sort((a, b) => b.length - a.length) +const tokenizeTerm = (term: string) => { + const tokens = term.split(/\s+/).filter((t) => t.trim() !== "") + + const tokenLen = tokens.length + if (tokenLen > 1) { + for (let i = 1; i < tokenLen; i++) { + tokens.push(tokens.slice(0, i + 1).join(" ")) + } + } + + return tokens.sort((a, b) => b.length - a.length) // always highlight longest terms first +} function highlight(searchTerm: string, text: string, trim?: boolean) { - // try to highlight longest tokens first const tokenizedTerms = tokenizeTerm(searchTerm) let tokenizedText = text.split(/\s+/).filter((t) => t !== "") @@ -69,7 +75,6 @@ function highlight(searchTerm: string, text: string, trim?: boolean) { } return tok }) - .slice(startIndex, endIndex + 1) .join(" ") return `${startIndex === 0 ? "" : "..."}${slice}${ @@ -89,29 +94,32 @@ function highlightHTML(searchTerm: string, el: HTMLElement) { return span } - const highlightTextNodes = (node: Node) => { + const highlightTextNodes = (node: Node, term: string) => { if (node.nodeType === Node.TEXT_NODE) { - let nodeText = node.nodeValue || "" - tokenizedTerms.forEach((term) => { - const regex = new RegExp(term.toLowerCase(), "gi") - const matches = nodeText.match(regex) - const spanContainer = document.createElement("span") - let lastIndex = 0 - matches?.forEach((match) => { - const matchIndex = nodeText.indexOf(match, lastIndex) - spanContainer.appendChild(document.createTextNode(nodeText.slice(lastIndex, matchIndex))) - spanContainer.appendChild(createHighlightSpan(match)) - lastIndex = matchIndex + match.length - }) - spanContainer.appendChild(document.createTextNode(nodeText.slice(lastIndex))) - node.parentNode?.replaceChild(spanContainer, node) - }) + const nodeText = node.nodeValue ?? "" + const regex = new RegExp(term.toLowerCase(), "gi") + const matches = nodeText.match(regex) + if (!matches || matches.length === 0) return + const spanContainer = document.createElement("span") + let lastIndex = 0 + for (const match of matches) { + const matchIndex = nodeText.indexOf(match, lastIndex) + spanContainer.appendChild(document.createTextNode(nodeText.slice(lastIndex, matchIndex))) + spanContainer.appendChild(createHighlightSpan(match)) + lastIndex = matchIndex + match.length + } + spanContainer.appendChild(document.createTextNode(nodeText.slice(lastIndex))) + node.parentNode?.replaceChild(spanContainer, node) } else if (node.nodeType === Node.ELEMENT_NODE) { - Array.from(node.childNodes).forEach(highlightTextNodes) + if ((node as HTMLElement).classList.contains("highlight")) return + Array.from(node.childNodes).forEach((child) => highlightTextNodes(child, term)) } } - highlightTextNodes(html.body) + for (const term of tokenizedTerms) { + highlightTextNodes(html.body, term) + } + return html.body } @@ -137,13 +145,13 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => { let previewInner: HTMLDivElement | undefined = undefined const results = document.createElement("div") results.id = "results-container" - results.style.flexBasis = enablePreview ? "30%" : "100%" + results.style.flexBasis = enablePreview ? "min(30%, 450px)" : "100%" appendLayout(results) if (enablePreview) { preview = document.createElement("div") preview.id = "preview-container" - preview.style.flexBasis = "70%" + preview.style.flexBasis = "100%" appendLayout(preview) } diff --git a/quartz/components/styles/search.scss b/quartz/components/styles/search.scss index 11e7c4e14b2a6..0a763ec55b353 100644 --- a/quartz/components/styles/search.scss +++ b/quartz/components/styles/search.scss @@ -54,7 +54,7 @@ } & > #search-space { - width: 75%; + width: 65%; margin-top: 12vh; margin-left: auto; margin-right: auto; @@ -85,7 +85,6 @@ & > #search-layout { display: flex; flex-direction: row; - justify-content: space-between; opacity: 0; border: 1px solid var(--lightgray); @@ -106,7 +105,7 @@ } } - @media all and (max-width: $mobileBreakpoint) { + @media all and (max-width: $tabletBreakpoint) { display: block; & > *:not(#results-container) { display: none !important; @@ -119,8 +118,8 @@ } & .highlight { - color: var(--secondary); - font-weight: 700; + background: color-mix(in srgb, var(--tertiary) 60%, transparent); + border-radius: 5px; } & > #preview-container { @@ -129,8 +128,10 @@ overflow: hidden; & .preview-inner { + margin: 0 auto; padding: 1em; height: 100%; + width: 100%; box-sizing: border-box; overflow-y: auto; font-family: inherit; diff --git a/quartz/styles/base.scss b/quartz/styles/base.scss index fc1db9581e3c7..0fa7a55674de6 100644 --- a/quartz/styles/base.scss +++ b/quartz/styles/base.scss @@ -26,7 +26,7 @@ section { } ::selection { - background: color-mix(in srgb, var(--tertiary) 75%, transparent); + background: color-mix(in srgb, var(--tertiary) 60%, transparent); color: var(--darkgray); } diff --git a/quartz/styles/variables.scss b/quartz/styles/variables.scss index 30004aa7b2646..db43f8ed9edd2 100644 --- a/quartz/styles/variables.scss +++ b/quartz/styles/variables.scss @@ -1,6 +1,6 @@ $pageWidth: 750px; $mobileBreakpoint: 600px; -$tabletBreakpoint: 1200px; +$tabletBreakpoint: 1000px; $sidePanelWidth: 380px; $topSpacing: 6rem; $fullPageWidth: $pageWidth + 2 * $sidePanelWidth; From e9fb0ecb96a2de53cf5f060c4e151f539ca4b089 Mon Sep 17 00:00:00 2001 From: Jacky Zhao Date: Thu, 1 Feb 2024 21:19:48 -0800 Subject: [PATCH 008/115] fix: border radius on search preview --- quartz/components/styles/search.scss | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/quartz/components/styles/search.scss b/quartz/components/styles/search.scss index 0a763ec55b353..784c114c123bc 100644 --- a/quartz/components/styles/search.scss +++ b/quartz/components/styles/search.scss @@ -139,7 +139,8 @@ line-height: 1.5em; font-weight: 400; background: var(--light); - border-radius: 5px; + border-top-right-radius: 5px; + border-bottom-right-radius: 5px; box-shadow: 0 14px 50px rgba(27, 33, 48, 0.12), 0 10px 30px rgba(27, 33, 48, 0.16); From 45b93a80f4538b43bf71993d05902308db786051 Mon Sep 17 00:00:00 2001 From: Jacky Zhao Date: Thu, 1 Feb 2024 22:22:06 -0800 Subject: [PATCH 009/115] fix: index setup, styling fixes --- quartz/components/scripts/search.inline.ts | 63 +++++++++++----------- quartz/components/styles/search.scss | 2 +- quartz/plugins/emitters/contentIndex.ts | 1 - 3 files changed, 31 insertions(+), 35 deletions(-) diff --git a/quartz/components/scripts/search.inline.ts b/quartz/components/scripts/search.inline.ts index 82fdf826b9e2b..55919cdc670c1 100644 --- a/quartz/components/scripts/search.inline.ts +++ b/quartz/components/scripts/search.inline.ts @@ -128,6 +128,7 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => { const data = await fetchData const container = document.getElementById("search-container") + const searchSpace = document.getElementById("search-space") const sidebar = container?.closest(".sidebar") as HTMLElement const searchIcon = document.getElementById("search-icon") const searchBar = document.getElementById("search-bar") as HTMLInputElement | null @@ -170,7 +171,7 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => { removeAllChildren(preview) } if (searchLayout) { - searchLayout.style.opacity = "0" + searchLayout.style.visibility = "hidden" } searchType = "basic" // reset search type after closing @@ -449,11 +450,11 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => { currentSearchTerm = (e.target as HTMLInputElement).value if (searchLayout) { - searchLayout.style.opacity = "1" + searchLayout.style.visibility = "visible" } if (term === "" && searchLayout) { - searchLayout.style.opacity = "0" + searchLayout.style.visibility = "hidden" } if (term.toLowerCase().startsWith("#")) { @@ -503,35 +504,8 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => { searchBar?.addEventListener("input", onType) window.addCleanup(() => searchBar?.removeEventListener("input", onType)) - // setup index if it hasn't been already - if (!index) { - index = new FlexSearch.Document({ - charset: "latin:extra", - encode: encoder, - document: { - id: "id", - index: [ - { - field: "title", - tokenize: "forward", - }, - { - field: "content", - tokenize: "forward", - }, - { - field: "tags", - tokenize: "forward", - }, - ], - }, - }) - - fillDocument(index, data) - } - - // register handlers - registerEscapeHandler(container, hideSearch) + index ??= await fillDocument(data) + registerEscapeHandler(searchSpace, hideSearch) }) /** @@ -539,7 +513,28 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => { * @param index index to fill * @param data data to fill index with */ -async function fillDocument(index: FlexSearch.Document, data: any) { +async function fillDocument(data: { [key: FullSlug]: ContentDetails }) { + const index = new FlexSearch.Document({ + charset: "latin:extra", + encode: encoder, + document: { + id: "id", + index: [ + { + field: "title", + tokenize: "forward", + }, + { + field: "content", + tokenize: "forward", + }, + { + field: "tags", + tokenize: "forward", + }, + ], + }, + }) let id = 0 for (const [slug, fileData] of Object.entries(data)) { await index.addAsync(id++, { @@ -550,4 +545,6 @@ async function fillDocument(index: FlexSearch.Document, data: any) tags: fileData.tags, }) } + + return index } diff --git a/quartz/components/styles/search.scss b/quartz/components/styles/search.scss index 784c114c123bc..0e6ecb5806c9e 100644 --- a/quartz/components/styles/search.scss +++ b/quartz/components/styles/search.scss @@ -85,7 +85,7 @@ & > #search-layout { display: flex; flex-direction: row; - opacity: 0; + visibility: hidden; border: 1px solid var(--lightgray); & > div { diff --git a/quartz/plugins/emitters/contentIndex.ts b/quartz/plugins/emitters/contentIndex.ts index 31e1d3e2a6313..5a0bed914e742 100644 --- a/quartz/plugins/emitters/contentIndex.ts +++ b/quartz/plugins/emitters/contentIndex.ts @@ -5,7 +5,6 @@ import { escapeHTML } from "../../util/escape" import { FilePath, FullSlug, SimpleSlug, joinSegments, simplifySlug } from "../../util/path" import { QuartzEmitterPlugin } from "../types" import { toHtml } from "hast-util-to-html" -import path from "path" import { write } from "./helpers" export type ContentIndex = Map From 9b8e0c9d1aa5857db3d27bfae229c03b2c8a8b59 Mon Sep 17 00:00:00 2001 From: Jacky Zhao Date: Thu, 1 Feb 2024 23:55:11 -0800 Subject: [PATCH 010/115] chore(cleanup): misc refactoring for cleanup, fix some search bugs --- quartz/components/scripts/clipboard.inline.ts | 6 +- quartz/components/scripts/darkmode.inline.ts | 21 +- quartz/components/scripts/search.inline.ts | 201 ++++++------------ quartz/components/styles/search.scss | 47 ++-- 4 files changed, 102 insertions(+), 173 deletions(-) diff --git a/quartz/components/scripts/clipboard.inline.ts b/quartz/components/scripts/clipboard.inline.ts index c604c9bc5e3d7..87182a154b45f 100644 --- a/quartz/components/scripts/clipboard.inline.ts +++ b/quartz/components/scripts/clipboard.inline.ts @@ -14,7 +14,7 @@ document.addEventListener("nav", () => { button.type = "button" button.innerHTML = svgCopy button.ariaLabel = "Copy source" - button.addEventListener("click", () => { + function onClick() { navigator.clipboard.writeText(source).then( () => { button.blur() @@ -26,7 +26,9 @@ document.addEventListener("nav", () => { }, (error) => console.error(error), ) - }) + } + button.addEventListener("click", onClick) + window.addCleanup(() => button.removeEventListener("click", onClick)) els[i].prepend(button) } } diff --git a/quartz/components/scripts/darkmode.inline.ts b/quartz/components/scripts/darkmode.inline.ts index 4c671aa41ecb4..48e0aa1f5df36 100644 --- a/quartz/components/scripts/darkmode.inline.ts +++ b/quartz/components/scripts/darkmode.inline.ts @@ -10,13 +10,21 @@ const emitThemeChangeEvent = (theme: "light" | "dark") => { } document.addEventListener("nav", () => { - const switchTheme = (e: any) => { - const newTheme = e.target.checked ? "dark" : "light" + const switchTheme = (e: Event) => { + const newTheme = (e.target as HTMLInputElement)?.checked ? "dark" : "light" document.documentElement.setAttribute("saved-theme", newTheme) localStorage.setItem("theme", newTheme) emitThemeChangeEvent(newTheme) } + const themeChange = (e: MediaQueryListEvent) => { + const newTheme = e.matches ? "dark" : "light" + document.documentElement.setAttribute("saved-theme", newTheme) + localStorage.setItem("theme", newTheme) + toggleSwitch.checked = e.matches + emitThemeChangeEvent(newTheme) + } + // Darkmode toggle const toggleSwitch = document.querySelector("#darkmode-toggle") as HTMLInputElement toggleSwitch.addEventListener("change", switchTheme) @@ -27,11 +35,6 @@ document.addEventListener("nav", () => { // Listen for changes in prefers-color-scheme const colorSchemeMediaQuery = window.matchMedia("(prefers-color-scheme: dark)") - colorSchemeMediaQuery.addEventListener("change", (e) => { - const newTheme = e.matches ? "dark" : "light" - document.documentElement.setAttribute("saved-theme", newTheme) - localStorage.setItem("theme", newTheme) - toggleSwitch.checked = e.matches - emitThemeChangeEvent(newTheme) - }) + colorSchemeMediaQuery.addEventListener("change", themeChange) + window.addCleanup(() => colorSchemeMediaQuery.removeEventListener("change", themeChange)) }) diff --git a/quartz/components/scripts/search.inline.ts b/quartz/components/scripts/search.inline.ts index 55919cdc670c1..c960f5e479e33 100644 --- a/quartz/components/scripts/search.inline.ts +++ b/quartz/components/scripts/search.inline.ts @@ -26,7 +26,6 @@ const numTagResults = 5 const tokenizeTerm = (term: string) => { const tokens = term.split(/\s+/).filter((t) => t.trim() !== "") - const tokenLen = tokens.length if (tokenLen > 1) { for (let i = 1; i < tokenLen; i++) { @@ -77,15 +76,14 @@ function highlight(searchTerm: string, text: string, trim?: boolean) { }) .join(" ") - return `${startIndex === 0 ? "" : "..."}${slice}${ - endIndex === tokenizedText.length - 1 ? "" : "..." - }` + return `${startIndex === 0 ? "" : "..."}${slice}${endIndex === tokenizedText.length - 1 ? "" : "..." + }` } -function highlightHTML(searchTerm: string, el: HTMLElement) { +function highlightHTML(searchTerm: string, innerHTML: string) { const p = new DOMParser() const tokenizedTerms = tokenizeTerm(searchTerm) - const html = p.parseFromString(el.innerHTML, "text/html") + const html = p.parseFromString(innerHTML, "text/html") const createHighlightSpan = (text: string) => { const span = document.createElement("span") @@ -125,10 +123,8 @@ function highlightHTML(searchTerm: string, el: HTMLElement) { document.addEventListener("nav", async (e: CustomEventMap["nav"]) => { const currentSlug = e.detail.url - const data = await fetchData const container = document.getElementById("search-container") - const searchSpace = document.getElementById("search-space") const sidebar = container?.closest(".sidebar") as HTMLElement const searchIcon = document.getElementById("search-icon") const searchBar = document.getElementById("search-bar") as HTMLInputElement | null @@ -193,6 +189,7 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => { e.preventDefault() const searchBarOpen = container?.classList.contains("active") searchBarOpen ? hideSearch() : showSearch("basic") + return } else if (e.shiftKey && (e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "k") { // Hotkey to open tag search e.preventDefault() @@ -201,6 +198,7 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => { // add "#" prefix for tag search if (searchBar) searchBar.value = "#" + return } if (currentHover) { @@ -262,69 +260,29 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => { } } - function trimContent(content: string) { - // works without escaping html like in `description.ts` - const sentences = content.replace(/\s+/g, " ").split(".") - let finalDesc = "" - let sentenceIdx = 0 - - // Roughly estimate characters by (words * 5). Matches description length in `description.ts`. - const len = contextWindowWords * 5 - while (finalDesc.length < len) { - const sentence = sentences[sentenceIdx] - if (!sentence) break - finalDesc += sentence + "." - sentenceIdx++ - } - - // If more content would be available, indicate it by finishing with "..." - if (finalDesc.length < content.length) { - finalDesc += ".." - } - - return finalDesc - } - const formatForDisplay = (term: string, id: number) => { const slug = idDataMap[id] return { id, slug, title: searchType === "tags" ? data[slug].title : highlight(term, data[slug].title ?? ""), - // if searchType is tag, display context from start of file and trim, otherwise use regular highlight - content: - searchType === "tags" - ? trimContent(data[slug].content) - : highlight(term, data[slug].content ?? "", true), - tags: highlightTags(term, data[slug].tags), + content: highlight(term, data[slug].content ?? "", true), + tags: highlightTags(term.substring(1), data[slug].tags), } } function highlightTags(term: string, tags: string[]) { - if (tags && searchType === "tags") { - // Find matching tags - const termLower = term.toLowerCase() - let matching = tags.filter((str) => str.includes(termLower)) - - // Subtract matching from original tags, then push difference - if (matching.length > 0) { - let difference = tags.filter((x) => !matching.includes(x)) - - // Convert to html (cant be done later as matches/term dont get passed to `resultToHTML`) - matching = matching.map((tag) => `
  • #${tag}

  • `) - difference = difference.map((tag) => `
  • #${tag}

  • `) - matching.push(...difference) - } - - // Only allow max of `numTagResults` in preview - if (tags.length > numTagResults) { - matching.splice(numTagResults) - } - - return matching - } else { + if (!tags || searchType !== "tags") { return [] } + + return tags.map(tag => { + if (tag.toLowerCase().includes(term.toLowerCase())) { + return `
  • #${tag}

  • ` + } else { + return `
  • #${tag}

  • ` + } + }).slice(0, numTagResults) } function resolveUrl(slug: FullSlug): URL { @@ -332,34 +290,26 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => { } const resultToHTML = ({ slug, title, content, tags }: Item) => { - const htmlTags = tags.length > 0 ? `
      ${tags.join("")}
    ` : `` - const resultContent = enablePreview && window.innerWidth > 600 ? "" : `

    ${content}

    ` - + const htmlTags = tags.length > 0 ? `
      ${tags.join("")}
    ` : `` const itemTile = document.createElement("a") itemTile.classList.add("result-card") - Object.assign(itemTile, { - id: slug, - href: resolveUrl(slug).toString(), - innerHTML: `

    ${title}

    ${htmlTags}${resultContent}`, - }) + itemTile.id = slug + itemTile.href = resolveUrl(slug).toString() + itemTile.innerHTML = `

    ${title}

    ${htmlTags}

    ${content}

    ` async function onMouseEnter(ev: MouseEvent) { - // Actually when we hover, we need to clean all highlights within the result childs if (!ev.target) return - for (const el of document.getElementsByClassName( - "result-card", - ) as HTMLCollectionOf) { - el.classList.remove("focus") - el.blur() - } + currentHover?.classList.remove('focus') + currentHover?.blur() const target = ev.target as HTMLInputElement await displayPreview(target) currentHover = target - currentHover.classList.remove("focus") + currentHover.classList.add("focus") } async function onMouseLeave(ev: MouseEvent) { - const target = ev.target as HTMLAnchorElement + if (!ev.target) return + const target = ev.target as HTMLElement target.classList.remove("focus") } @@ -373,9 +323,12 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => { hideSearch() }, ], - ] as [keyof HTMLElementEventMap, (this: HTMLElement) => void][] + ] as const - events.forEach(([event, handler]) => itemTile.addEventListener(event, handler)) + events.forEach(([event, handler]) => { + itemTile.addEventListener(event, handler) + window.addCleanup(() => itemTile.removeEventListener(event, handler)) + }) return itemTile } @@ -386,22 +339,22 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => { removeAllChildren(results) if (finalResults.length === 0) { results.innerHTML = ` -

    No results.

    -

    Try another search term?

    -
    ` +

    No results.

    +

    Try another search term?

    + ` } else { results.append(...finalResults.map(resultToHTML)) } - // focus on first result, then also dispatch preview immediately - if (results?.firstElementChild) { + + if (finalResults.length === 0 && preview) { + // no results, clear previous preview + removeAllChildren(preview) + } else { + // focus on first result, then also dispatch preview immediately const firstChild = results.firstElementChild as HTMLElement - if (firstChild.classList.contains("no-match")) { - removeAllChildren(preview as HTMLElement) - } else { - firstChild.classList.add("focus") - currentHover = firstChild as HTMLInputElement - await displayPreview(firstChild) - } + firstChild.classList.add("focus") + currentHover = firstChild as HTMLInputElement + await displayPreview(firstChild) } } @@ -427,59 +380,41 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => { } async function displayPreview(el: HTMLElement | null) { - if (!searchLayout || !enablePreview || !el) return - + if (!searchLayout || !enablePreview || !el || !preview) return const slug = el.id as FullSlug el.classList.add("focus") - - removeAllChildren(preview as HTMLElement) - previewInner = document.createElement("div") previewInner.classList.add("preview-inner") - preview?.appendChild(previewInner) - const innerDiv = await fetchContent(slug).then((contents) => - contents.map((el) => highlightHTML(currentSearchTerm, el as HTMLElement)), + contents.map((el) => highlightHTML(currentSearchTerm, el.innerHTML)), ) previewInner.append(...innerDiv) + preview.replaceChildren(previewInner) + + // scroll to longest + const highlights = [...preview.querySelectorAll(".highlight")].sort((a, b) => b.innerHTML.length - a.innerHTML.length) + highlights[0]?.scrollIntoView() } async function onType(e: HTMLElementEventMap["input"]) { - let term = (e.target as HTMLInputElement).value - let searchResults: FlexSearch.SimpleDocumentSearchResultSetUnit[] + if (!searchLayout || !index) return currentSearchTerm = (e.target as HTMLInputElement).value + searchLayout.style.visibility = currentSearchTerm === "" ? "hidden" : "visible" + searchType = currentSearchTerm.startsWith("#") ? "tags" : "basic" - if (searchLayout) { - searchLayout.style.visibility = "visible" - } - - if (term === "" && searchLayout) { - searchLayout.style.visibility = "hidden" - } - - if (term.toLowerCase().startsWith("#")) { - searchType = "tags" - } else { - searchType = "basic" - } - - switch (searchType) { - case "tags": { - term = term.substring(1) - searchResults = - (await index?.searchAsync({ query: term, limit: numSearchResults, index: ["tags"] })) ?? - [] - break - } - case "basic": - default: { - searchResults = - (await index?.searchAsync({ - query: term, - limit: numSearchResults, - index: ["title", "content"], - })) ?? [] - } + let searchResults: FlexSearch.SimpleDocumentSearchResultSetUnit[] + if (searchType === "tags") { + searchResults = await index.searchAsync({ + query: currentSearchTerm.substring(1), + limit: numSearchResults, + index: ["tags"], + }) + } else if (searchType === "basic") { + searchResults = await index.searchAsync({ + query: currentSearchTerm, + limit: numSearchResults, + index: ["title", "content"], + }) } const getByField = (field: string): number[] => { @@ -493,7 +428,7 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => { ...getByField("content"), ...getByField("tags"), ]) - const finalResults = [...allIds].map((id) => formatForDisplay(term, id)) + const finalResults = [...allIds].map((id) => formatForDisplay(currentSearchTerm, id)) await displayResults(finalResults) } @@ -505,7 +440,7 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => { window.addCleanup(() => searchBar?.removeEventListener("input", onType)) index ??= await fillDocument(data) - registerEscapeHandler(searchSpace, hideSearch) + registerEscapeHandler(container, hideSearch) }) /** diff --git a/quartz/components/styles/search.scss b/quartz/components/styles/search.scss index 0e6ecb5806c9e..df4f5bab56ee4 100644 --- a/quartz/components/styles/search.scss +++ b/quartz/components/styles/search.scss @@ -88,6 +88,14 @@ visibility: hidden; border: 1px solid var(--lightgray); + @media all and (min-width: $tabletBreakpoint) { + &[data-preview] { + & .result-card > p.preview { + display: none; + } + } + } + & > div { // vh - #search-space.margin-top height: calc(75vh - 12vh); @@ -174,7 +182,6 @@ outline: none; font-weight: inherit; - &:hover, &:focus, &.focus { background: var(--lightgray); @@ -184,41 +191,23 @@ margin: 0; } - & > ul > li { - margin: 0; - display: inline-block; - white-space: nowrap; - margin: 0; - overflow-wrap: normal; - } - - & > ul { - list-style: none; - display: flex; - padding-left: 0; - gap: 0.4rem; - margin: 0; + & > ul.tags { margin-top: 0.45rem; - box-sizing: border-box; - overflow: hidden; - background-clip: border-box; + margin-bottom: 0; } & > ul > li > p { border-radius: 8px; background-color: var(--highlight); - overflow: hidden; - background-clip: border-box; - padding: 0.03rem 0.4rem; - margin: 0; - color: var(--secondary); - opacity: 0.85; - } - - & > ul > li > .match-tag { - color: var(--tertiary); + padding: 0.2rem 0.4rem; + margin: 0 0.1rem; + line-height: 1.4rem; font-weight: bold; - opacity: 1; + color: var(--secondary); + + &.match-tag { + color: var(--tertiary); + } } & > p { From dc62aeb213aa68051aaaf3ddc2f25be4e4d6d466 Mon Sep 17 00:00:00 2001 From: Jacky Zhao Date: Thu, 1 Feb 2024 23:55:40 -0800 Subject: [PATCH 011/115] pkg: bump to 4.2.0 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index ef9313491b1d8..2d0468c4cc43d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@jackyzha0/quartz", - "version": "4.1.6", + "version": "4.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@jackyzha0/quartz", - "version": "4.1.6", + "version": "4.2.0", "license": "MIT", "dependencies": { "@clack/prompts": "^0.7.0", diff --git a/package.json b/package.json index ee4810f5071b0..fb4175e63d60d 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "@jackyzha0/quartz", "description": "🌱 publish your digital garden and notes as a website", "private": true, - "version": "4.1.6", + "version": "4.2.0", "type": "module", "author": "jackyzha0 ", "license": "MIT", From 970a30a139953c8d58705474b7910a64153e9466 Mon Sep 17 00:00:00 2001 From: Jacky Zhao Date: Thu, 1 Feb 2024 23:57:17 -0800 Subject: [PATCH 012/115] chore: fmt --- quartz/components/scripts/search.inline.ts | 27 +++++++++++++--------- quartz/components/styles/search.scss | 2 +- 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/quartz/components/scripts/search.inline.ts b/quartz/components/scripts/search.inline.ts index c960f5e479e33..17f3e1ba84bed 100644 --- a/quartz/components/scripts/search.inline.ts +++ b/quartz/components/scripts/search.inline.ts @@ -76,8 +76,9 @@ function highlight(searchTerm: string, text: string, trim?: boolean) { }) .join(" ") - return `${startIndex === 0 ? "" : "..."}${slice}${endIndex === tokenizedText.length - 1 ? "" : "..." - }` + return `${startIndex === 0 ? "" : "..."}${slice}${ + endIndex === tokenizedText.length - 1 ? "" : "..." + }` } function highlightHTML(searchTerm: string, innerHTML: string) { @@ -276,13 +277,15 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => { return [] } - return tags.map(tag => { - if (tag.toLowerCase().includes(term.toLowerCase())) { - return `
  • #${tag}

  • ` - } else { - return `
  • #${tag}

  • ` - } - }).slice(0, numTagResults) + return tags + .map((tag) => { + if (tag.toLowerCase().includes(term.toLowerCase())) { + return `
  • #${tag}

  • ` + } else { + return `
  • #${tag}

  • ` + } + }) + .slice(0, numTagResults) } function resolveUrl(slug: FullSlug): URL { @@ -299,7 +302,7 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => { async function onMouseEnter(ev: MouseEvent) { if (!ev.target) return - currentHover?.classList.remove('focus') + currentHover?.classList.remove("focus") currentHover?.blur() const target = ev.target as HTMLInputElement await displayPreview(target) @@ -392,7 +395,9 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => { preview.replaceChildren(previewInner) // scroll to longest - const highlights = [...preview.querySelectorAll(".highlight")].sort((a, b) => b.innerHTML.length - a.innerHTML.length) + const highlights = [...preview.querySelectorAll(".highlight")].sort( + (a, b) => b.innerHTML.length - a.innerHTML.length, + ) highlights[0]?.scrollIntoView() } diff --git a/quartz/components/styles/search.scss b/quartz/components/styles/search.scss index df4f5bab56ee4..b4bb64a00102b 100644 --- a/quartz/components/styles/search.scss +++ b/quartz/components/styles/search.scss @@ -204,7 +204,7 @@ line-height: 1.4rem; font-weight: bold; color: var(--secondary); - + &.match-tag { color: var(--tertiary); } From 3b596c9311fd5fe552d6c53e9b27841932a26587 Mon Sep 17 00:00:00 2001 From: Jacky Zhao Date: Fri, 2 Feb 2024 00:19:19 -0800 Subject: [PATCH 013/115] fix: flatmap children when highlighting rich preview to avoid body --- quartz/components/scripts/search.inline.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/quartz/components/scripts/search.inline.ts b/quartz/components/scripts/search.inline.ts index 17f3e1ba84bed..57067d1e3ecc8 100644 --- a/quartz/components/scripts/search.inline.ts +++ b/quartz/components/scripts/search.inline.ts @@ -81,10 +81,10 @@ function highlight(searchTerm: string, text: string, trim?: boolean) { }` } -function highlightHTML(searchTerm: string, innerHTML: string) { +function highlightHTML(searchTerm: string, el: HTMLElement) { const p = new DOMParser() const tokenizedTerms = tokenizeTerm(searchTerm) - const html = p.parseFromString(innerHTML, "text/html") + const html = p.parseFromString(el.innerHTML, "text/html") const createHighlightSpan = (text: string) => { const span = document.createElement("span") @@ -389,7 +389,7 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => { previewInner = document.createElement("div") previewInner.classList.add("preview-inner") const innerDiv = await fetchContent(slug).then((contents) => - contents.map((el) => highlightHTML(currentSearchTerm, el.innerHTML)), + contents.flatMap((el) => [...highlightHTML(currentSearchTerm, el as HTMLElement).children]), ) previewInner.append(...innerDiv) preview.replaceChildren(previewInner) From 0416c03ae646acf72422fe615445d07a327cc580 Mon Sep 17 00:00:00 2001 From: Jacky Zhao Date: Fri, 2 Feb 2024 00:25:05 -0800 Subject: [PATCH 014/115] fix: be more eager about constructing search index --- quartz/components/scripts/search.inline.ts | 49 ++++++++++------------ 1 file changed, 23 insertions(+), 26 deletions(-) diff --git a/quartz/components/scripts/search.inline.ts b/quartz/components/scripts/search.inline.ts index 57067d1e3ecc8..2924f398e671a 100644 --- a/quartz/components/scripts/search.inline.ts +++ b/quartz/components/scripts/search.inline.ts @@ -15,10 +15,30 @@ interface Item { type SearchType = "basic" | "tags" let searchType: SearchType = "basic" let currentSearchTerm: string = "" -let index: FlexSearch.Document | undefined = undefined -const p = new DOMParser() const encoder = (str: string) => str.toLowerCase().split(/([^a-z]|[^\x00-\x7F])/) +let index = new FlexSearch.Document({ + charset: "latin:extra", + encode: encoder, + document: { + id: "id", + index: [ + { + field: "title", + tokenize: "forward", + }, + { + field: "content", + tokenize: "forward", + }, + { + field: "tags", + tokenize: "forward", + }, + ], + }, +}) +const p = new DOMParser() const fetchContentCache: Map = new Map() const contextWindowWords = 30 const numSearchResults = 8 @@ -444,7 +464,7 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => { searchBar?.addEventListener("input", onType) window.addCleanup(() => searchBar?.removeEventListener("input", onType)) - index ??= await fillDocument(data) + await fillDocument(data) registerEscapeHandler(container, hideSearch) }) @@ -454,27 +474,6 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => { * @param data data to fill index with */ async function fillDocument(data: { [key: FullSlug]: ContentDetails }) { - const index = new FlexSearch.Document({ - charset: "latin:extra", - encode: encoder, - document: { - id: "id", - index: [ - { - field: "title", - tokenize: "forward", - }, - { - field: "content", - tokenize: "forward", - }, - { - field: "tags", - tokenize: "forward", - }, - ], - }, - }) let id = 0 for (const [slug, fileData] of Object.entries(data)) { await index.addAsync(id++, { @@ -485,6 +484,4 @@ async function fillDocument(data: { [key: FullSlug]: ContentDetails }) { tags: fileData.tags, }) } - - return index } From 5a36e5b68d9d49ddcbfc87421216bfa6b9d913cf Mon Sep 17 00:00:00 2001 From: Jacky Zhao Date: Fri, 2 Feb 2024 00:29:45 -0800 Subject: [PATCH 015/115] fix(style): reasonable page width for rich search preview --- quartz/components/styles/search.scss | 26 +++++++++++--------------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/quartz/components/styles/search.scss b/quartz/components/styles/search.scss index b4bb64a00102b..87033998cb1c8 100644 --- a/quartz/components/styles/search.scss +++ b/quartz/components/styles/search.scss @@ -134,24 +134,20 @@ display: block; box-sizing: border-box; overflow: hidden; + box-sizing: border-box; + font-family: inherit; + color: var(--dark); + line-height: 1.5em; + font-weight: 400; + background: var(--light); + border-top-right-radius: 5px; + border-bottom-right-radius: 5px; + overflow-y: auto; + padding: 1rem; & .preview-inner { margin: 0 auto; - padding: 1em; - height: 100%; - width: 100%; - box-sizing: border-box; - overflow-y: auto; - font-family: inherit; - color: var(--dark); - line-height: 1.5em; - font-weight: 400; - background: var(--light); - border-top-right-radius: 5px; - border-bottom-right-radius: 5px; - box-shadow: - 0 14px 50px rgba(27, 33, 48, 0.12), - 0 10px 30px rgba(27, 33, 48, 0.16); + width: min($pageWidth, 100%); } a.internal { From ee868b2d792559171fec6ad7426a196289cd5824 Mon Sep 17 00:00:00 2001 From: Aaron Pham <29749331+aarnphm@users.noreply.github.com> Date: Fri, 2 Feb 2024 03:35:53 -0500 Subject: [PATCH 016/115] fix(search): set correct attribute on hover icon (#787) Signed-off-by: Aaron <29749331+aarnphm@users.noreply.github.com> --- quartz/components/styles/search.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/quartz/components/styles/search.scss b/quartz/components/styles/search.scss index 87033998cb1c8..aac01bed5e820 100644 --- a/quartz/components/styles/search.scss +++ b/quartz/components/styles/search.scss @@ -151,7 +151,7 @@ } a.internal { - background-color: none; + background-color: inherit; } } From 18cd58617dbe6a1b887ab08e4d29694bb1b3d0e0 Mon Sep 17 00:00:00 2001 From: Jacky Zhao Date: Fri, 2 Feb 2024 00:45:02 -0800 Subject: [PATCH 017/115] fix: parallelize search indexing --- quartz/components/scripts/search.inline.ts | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/quartz/components/scripts/search.inline.ts b/quartz/components/scripts/search.inline.ts index 2924f398e671a..769483d4e297b 100644 --- a/quartz/components/scripts/search.inline.ts +++ b/quartz/components/scripts/search.inline.ts @@ -464,8 +464,8 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => { searchBar?.addEventListener("input", onType) window.addCleanup(() => searchBar?.removeEventListener("input", onType)) - await fillDocument(data) registerEscapeHandler(container, hideSearch) + await fillDocument(data) }) /** @@ -475,13 +475,18 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => { */ async function fillDocument(data: { [key: FullSlug]: ContentDetails }) { let id = 0 + const promises: Array> = [] for (const [slug, fileData] of Object.entries(data)) { - await index.addAsync(id++, { - id, - slug: slug as FullSlug, - title: fileData.title, - content: fileData.content, - tags: fileData.tags, - }) + promises.push( + index.addAsync(id++, { + id, + slug: slug as FullSlug, + title: fileData.title, + content: fileData.content, + tags: fileData.tags, + }), + ) } + + return await Promise.all(promises) } From 2b57a68e1f9528de3c08633251f37d5755c75698 Mon Sep 17 00:00:00 2001 From: Jacky Zhao Date: Fri, 2 Feb 2024 00:53:05 -0800 Subject: [PATCH 018/115] fix: font weight consistency --- quartz/components/styles/explorer.scss | 6 ++++-- quartz/components/styles/search.scss | 4 ++-- quartz/styles/base.scss | 2 +- quartz/styles/callouts.scss | 3 ++- quartz/styles/variables.scss | 2 ++ 5 files changed, 11 insertions(+), 6 deletions(-) diff --git a/quartz/components/styles/explorer.scss b/quartz/components/styles/explorer.scss index 304fd450093e9..34f180cf2c03d 100644 --- a/quartz/components/styles/explorer.scss +++ b/quartz/components/styles/explorer.scss @@ -1,3 +1,5 @@ +@use "../../styles/variables.scss" as *; + button#explorer { all: unset; background-color: transparent; @@ -85,7 +87,7 @@ svg { color: var(--secondary); font-family: var(--headerFont); font-size: 0.95rem; - font-weight: 600; + font-weight: $boldWeight; line-height: 1.5rem; display: inline-block; } @@ -110,7 +112,7 @@ svg { font-size: 0.95rem; display: inline-block; color: var(--secondary); - font-weight: 600; + font-weight: $boldWeight; margin: 0; line-height: 1.5rem; pointer-events: none; diff --git a/quartz/components/styles/search.scss b/quartz/components/styles/search.scss index aac01bed5e820..1471a77cb8761 100644 --- a/quartz/components/styles/search.scss +++ b/quartz/components/styles/search.scss @@ -138,7 +138,7 @@ font-family: inherit; color: var(--dark); line-height: 1.5em; - font-weight: 400; + font-weight: $normalWeight; background: var(--light); border-top-right-radius: 5px; border-bottom-right-radius: 5px; @@ -198,7 +198,7 @@ padding: 0.2rem 0.4rem; margin: 0 0.1rem; line-height: 1.4rem; - font-weight: bold; + font-weight: $boldWeight; color: var(--secondary); &.match-tag { diff --git a/quartz/styles/base.scss b/quartz/styles/base.scss index 0fa7a55674de6..a4fde06392a3e 100644 --- a/quartz/styles/base.scss +++ b/quartz/styles/base.scss @@ -54,7 +54,7 @@ ul, } a { - font-weight: 600; + font-weight: $boldWeight; text-decoration: none; transition: color 0.2s ease; color: var(--secondary); diff --git a/quartz/styles/callouts.scss b/quartz/styles/callouts.scss index ee62e39c5d1a3..d4f7069a06047 100644 --- a/quartz/styles/callouts.scss +++ b/quartz/styles/callouts.scss @@ -1,3 +1,4 @@ +@use "./variables.scss" as *; @use "sass:color"; .callout { @@ -156,6 +157,6 @@ } .callout-title-inner { - font-weight: 700; + font-weight: $boldWeight; } } diff --git a/quartz/styles/variables.scss b/quartz/styles/variables.scss index db43f8ed9edd2..8384b9c4ef89a 100644 --- a/quartz/styles/variables.scss +++ b/quartz/styles/variables.scss @@ -4,3 +4,5 @@ $tabletBreakpoint: 1000px; $sidePanelWidth: 380px; $topSpacing: 6rem; $fullPageWidth: $pageWidth + 2 * $sidePanelWidth; +$boldWeight: 700; +$normalWeight: 400; From d11a0e71a86eae41735d825d88c3bf7d8dcf949e Mon Sep 17 00:00:00 2001 From: Jacky Zhao Date: Fri, 2 Feb 2024 01:01:04 -0800 Subject: [PATCH 019/115] fix: font smoothing defaults --- quartz/styles/base.scss | 2 ++ 1 file changed, 2 insertions(+) diff --git a/quartz/styles/base.scss b/quartz/styles/base.scss index a4fde06392a3e..7bc799ae34896 100644 --- a/quartz/styles/base.scss +++ b/quartz/styles/base.scss @@ -7,6 +7,8 @@ html { text-size-adjust: none; overflow-x: hidden; width: 100vw; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } body, From 5ab922f3163a53418cb7d1c72cf7546c848159bc Mon Sep 17 00:00:00 2001 From: Jacky Zhao Date: Fri, 2 Feb 2024 01:15:10 -0800 Subject: [PATCH 020/115] fix(revert): font aliasing --- quartz/styles/base.scss | 2 -- 1 file changed, 2 deletions(-) diff --git a/quartz/styles/base.scss b/quartz/styles/base.scss index 7bc799ae34896..a4fde06392a3e 100644 --- a/quartz/styles/base.scss +++ b/quartz/styles/base.scss @@ -7,8 +7,6 @@ html { text-size-adjust: none; overflow-x: hidden; width: 100vw; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; } body, From a0b927da4aa9bb540b50c875e77f97bd4a7c279a Mon Sep 17 00:00:00 2001 From: Jacky Zhao Date: Fri, 2 Feb 2024 01:24:40 -0800 Subject: [PATCH 021/115] fix: use display instead of visibility for click handling pasthrough --- quartz/components/scripts/search.inline.ts | 4 ++-- quartz/components/styles/search.scss | 7 +++++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/quartz/components/scripts/search.inline.ts b/quartz/components/scripts/search.inline.ts index 769483d4e297b..abdef06dad05a 100644 --- a/quartz/components/scripts/search.inline.ts +++ b/quartz/components/scripts/search.inline.ts @@ -188,7 +188,7 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => { removeAllChildren(preview) } if (searchLayout) { - searchLayout.style.visibility = "hidden" + searchLayout.classList.remove("display-results") } searchType = "basic" // reset search type after closing @@ -424,7 +424,7 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => { async function onType(e: HTMLElementEventMap["input"]) { if (!searchLayout || !index) return currentSearchTerm = (e.target as HTMLInputElement).value - searchLayout.style.visibility = currentSearchTerm === "" ? "hidden" : "visible" + searchLayout.classList.toggle("display-results", currentSearchTerm !== "") searchType = currentSearchTerm.startsWith("#") ? "tags" : "basic" let searchResults: FlexSearch.SimpleDocumentSearchResultSetUnit[] diff --git a/quartz/components/styles/search.scss b/quartz/components/styles/search.scss index 1471a77cb8761..7ede3595cf742 100644 --- a/quartz/components/styles/search.scss +++ b/quartz/components/styles/search.scss @@ -83,11 +83,14 @@ } & > #search-layout { - display: flex; + display: none; flex-direction: row; - visibility: hidden; border: 1px solid var(--lightgray); + &.display-results { + display: flex; + } + @media all and (min-width: $tabletBreakpoint) { &[data-preview] { & .result-card > p.preview { From 3231ce6e7970dd0bc79c33aa8bc09cfa9ab59d09 Mon Sep 17 00:00:00 2001 From: Jacky Zhao Date: Fri, 2 Feb 2024 01:36:17 -0800 Subject: [PATCH 022/115] fix: search async ordering, scroll offset --- quartz/components/scripts/search.inline.ts | 16 ++++++++-------- quartz/components/styles/search.scss | 1 + 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/quartz/components/scripts/search.inline.ts b/quartz/components/scripts/search.inline.ts index abdef06dad05a..ec55f96b58dc4 100644 --- a/quartz/components/scripts/search.inline.ts +++ b/quartz/components/scripts/search.inline.ts @@ -224,6 +224,7 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => { if (currentHover) { currentHover.classList.remove("focus") + currentHover.blur() } // If search is active, then we will render the first result and display accordingly @@ -250,9 +251,9 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => { : (document.activeElement as HTMLInputElement | null) const prevResult = currentResult?.previousElementSibling as HTMLInputElement | null currentResult?.classList.remove("focus") - await displayPreview(prevResult) prevResult?.focus() currentHover = prevResult + await displayPreview(prevResult) } } else if (e.key === "ArrowDown" || e.key === "Tab") { e.preventDefault() @@ -264,9 +265,9 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => { : (document.getElementsByClassName("result-card")[0] as HTMLInputElement | null) const secondResult = firstResult?.nextElementSibling as HTMLInputElement | null firstResult?.classList.remove("focus") - await displayPreview(secondResult) secondResult?.focus() currentHover = secondResult + await displayPreview(secondResult) } else { // If an element in results-container already has focus, focus next one const active = currentHover @@ -274,9 +275,9 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => { : (document.activeElement as HTMLInputElement | null) active?.classList.remove("focus") const nextResult = active?.nextElementSibling as HTMLInputElement | null - await displayPreview(nextResult) nextResult?.focus() currentHover = nextResult + await displayPreview(nextResult) } } } @@ -325,9 +326,9 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => { currentHover?.classList.remove("focus") currentHover?.blur() const target = ev.target as HTMLInputElement - await displayPreview(target) currentHover = target currentHover.classList.add("focus") + await displayPreview(target) } async function onMouseLeave(ev: MouseEvent) { @@ -405,12 +406,11 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => { async function displayPreview(el: HTMLElement | null) { if (!searchLayout || !enablePreview || !el || !preview) return const slug = el.id as FullSlug - el.classList.add("focus") - previewInner = document.createElement("div") - previewInner.classList.add("preview-inner") const innerDiv = await fetchContent(slug).then((contents) => contents.flatMap((el) => [...highlightHTML(currentSearchTerm, el as HTMLElement).children]), ) + previewInner = document.createElement("div") + previewInner.classList.add("preview-inner") previewInner.append(...innerDiv) preview.replaceChildren(previewInner) @@ -418,7 +418,7 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => { const highlights = [...preview.querySelectorAll(".highlight")].sort( (a, b) => b.innerHTML.length - a.innerHTML.length, ) - highlights[0]?.scrollIntoView() + highlights[0]?.scrollIntoView({ block: "start" }) } async function onType(e: HTMLElementEventMap["input"]) { diff --git a/quartz/components/styles/search.scss b/quartz/components/styles/search.scss index 7ede3595cf742..d6202b5a83136 100644 --- a/quartz/components/styles/search.scss +++ b/quartz/components/styles/search.scss @@ -131,6 +131,7 @@ & .highlight { background: color-mix(in srgb, var(--tertiary) 60%, transparent); border-radius: 5px; + scroll-margin-top: 2rem; } & > #preview-container { From 44da82467ee7077a22f0054b7bc4d0f2a008e2e0 Mon Sep 17 00:00:00 2001 From: Jacky Zhao Date: Fri, 2 Feb 2024 01:45:15 -0800 Subject: [PATCH 023/115] fix(style): remove redundant selector --- quartz/components/styles/search.scss | 4 ---- 1 file changed, 4 deletions(-) diff --git a/quartz/components/styles/search.scss b/quartz/components/styles/search.scss index d6202b5a83136..23289d2c1cf41 100644 --- a/quartz/components/styles/search.scss +++ b/quartz/components/styles/search.scss @@ -153,10 +153,6 @@ margin: 0 auto; width: min($pageWidth, 100%); } - - a.internal { - background-color: inherit; - } } & > #results-container { From 34a8dfcd55789090d1a9b019539b8770c6ce98e5 Mon Sep 17 00:00:00 2001 From: Jacky Zhao Date: Fri, 2 Feb 2024 01:45:28 -0800 Subject: [PATCH 024/115] pkg: bump to 4.2.1 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 2d0468c4cc43d..66a772cde5e47 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@jackyzha0/quartz", - "version": "4.2.0", + "version": "4.2.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@jackyzha0/quartz", - "version": "4.2.0", + "version": "4.2.1", "license": "MIT", "dependencies": { "@clack/prompts": "^0.7.0", diff --git a/package.json b/package.json index fb4175e63d60d..d9f0f7bf24a70 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "@jackyzha0/quartz", "description": "🌱 publish your digital garden and notes as a website", "private": true, - "version": "4.2.0", + "version": "4.2.1", "type": "module", "author": "jackyzha0 ", "license": "MIT", From 18745a9dc68d26827ea0df99dcc709e311c74a98 Mon Sep 17 00:00:00 2001 From: Jacky Zhao Date: Fri, 2 Feb 2024 09:36:36 -0800 Subject: [PATCH 025/115] fix(style): correctly collapse on mobile --- quartz/components/styles/search.scss | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/quartz/components/styles/search.scss b/quartz/components/styles/search.scss index 23289d2c1cf41..24a72848f8c8f 100644 --- a/quartz/components/styles/search.scss +++ b/quartz/components/styles/search.scss @@ -117,7 +117,8 @@ } @media all and (max-width: $tabletBreakpoint) { - display: block; + display: block !important; + & > *:not(#results-container) { display: none !important; } From bece8fcab6e12db79617dcbcccb174d299140b51 Mon Sep 17 00:00:00 2001 From: Luis Michaelis Date: Fri, 2 Feb 2024 18:51:34 +0100 Subject: [PATCH 026/115] fix: properly handle absolute paths in `CreatedModifiedDate` (#790) When providing an absolute path to the content directory (e.g. when using an Obsidian Vault in another directory), the build step would fail with Failed to process `/absolute/path/to/file.md`: ENOENT: no such file or directory, stat '/current/working/directory/absolute/path/' This problem originated in the `CreatedModifiedDate` transformer which tries to construct a native filesystem path to the file to call `fs.stat` on. It did not however, account for the original file path contained in the received `VFile` being an absolute path and so, just concatenated the current working directory with the absolute path producing a nonexistent one. This patch adds a simple fix for this issue by checking if the original file path is already absolute before concatenating with the current working directory. --- quartz/plugins/transformers/lastmod.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/quartz/plugins/transformers/lastmod.ts b/quartz/plugins/transformers/lastmod.ts index 31c8c208420e0..2c7b9ce47fa8a 100644 --- a/quartz/plugins/transformers/lastmod.ts +++ b/quartz/plugins/transformers/lastmod.ts @@ -43,7 +43,7 @@ export const CreatedModifiedDate: QuartzTransformerPlugin | und let published: MaybeDate = undefined const fp = file.data.filePath! - const fullFp = path.posix.join(file.cwd, fp) + const fullFp = path.isAbsolute(fp) ? fp : path.posix.join(file.cwd, fp) for (const source of opts.priority) { if (source === "filesystem") { const st = await fs.promises.stat(fullFp) From 0a3379a8530f365e2bd85e8ea20a1dfc8126c39c Mon Sep 17 00:00:00 2001 From: Jacky Zhao Date: Fri, 2 Feb 2024 10:10:25 -0800 Subject: [PATCH 027/115] fix(search): null checks and focus fixes --- quartz/components/scripts/search.inline.ts | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/quartz/components/scripts/search.inline.ts b/quartz/components/scripts/search.inline.ts index ec55f96b58dc4..1ecf62fa40b30 100644 --- a/quartz/components/scripts/search.inline.ts +++ b/quartz/components/scripts/search.inline.ts @@ -224,12 +224,11 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => { if (currentHover) { currentHover.classList.remove("focus") - currentHover.blur() } // If search is active, then we will render the first result and display accordingly if (!container?.classList.contains("active")) return - else if (e.key === "Enter") { + if (e.key === "Enter") { // If result has focus, navigate to that one, otherwise pick first result if (results?.contains(document.activeElement)) { const active = document.activeElement as HTMLInputElement @@ -252,7 +251,7 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => { const prevResult = currentResult?.previousElementSibling as HTMLInputElement | null currentResult?.classList.remove("focus") prevResult?.focus() - currentHover = prevResult + if (prevResult) currentHover = prevResult await displayPreview(prevResult) } } else if (e.key === "ArrowDown" || e.key === "Tab") { @@ -266,18 +265,8 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => { const secondResult = firstResult?.nextElementSibling as HTMLInputElement | null firstResult?.classList.remove("focus") secondResult?.focus() - currentHover = secondResult + if (secondResult) currentHover = secondResult await displayPreview(secondResult) - } else { - // If an element in results-container already has focus, focus next one - const active = currentHover - ? currentHover - : (document.activeElement as HTMLInputElement | null) - active?.classList.remove("focus") - const nextResult = active?.nextElementSibling as HTMLInputElement | null - nextResult?.focus() - currentHover = nextResult - await displayPreview(nextResult) } } } From 260498a96b90ed44c120f4234238b6813272ec47 Mon Sep 17 00:00:00 2001 From: Jacky Zhao Date: Fri, 2 Feb 2024 10:23:24 -0800 Subject: [PATCH 028/115] fix(style): prevent callout icon from shrinking on long titles (closes #792) --- quartz/styles/callouts.scss | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/quartz/styles/callouts.scss b/quartz/styles/callouts.scss index d4f7069a06047..7fa52c506c860 100644 --- a/quartz/styles/callouts.scss +++ b/quartz/styles/callouts.scss @@ -120,7 +120,7 @@ .callout-title { display: flex; - align-items: center; + align-items: flex-start; gap: 5px; padding: 1rem 0; color: var(--color); @@ -131,8 +131,6 @@ transition: transform 0.15s ease; opacity: 0.8; cursor: pointer; - width: var(--icon-size); - height: var(--icon-size); --callout-icon: var(--callout-icon-fold); } @@ -145,6 +143,7 @@ & .fold-callout-icon { width: var(--icon-size); height: var(--icon-size); + flex: 0 0 var(--icon-size); // icon support background-size: var(--icon-size) var(--icon-size); @@ -154,6 +153,7 @@ mask-size: var(--icon-size) var(--icon-size); mask-position: center; mask-repeat: no-repeat; + padding: 0.2rem 0; } .callout-title-inner { From a2c46f442d26fe33c9b4cc00ddb9b805363edd20 Mon Sep 17 00:00:00 2001 From: Jacky Zhao Date: Fri, 2 Feb 2024 10:44:19 -0800 Subject: [PATCH 029/115] fix(search): dont rely on mouse to manipulate focus --- quartz/components/scripts/search.inline.ts | 36 +++------------------- quartz/components/styles/search.scss | 1 + 2 files changed, 6 insertions(+), 31 deletions(-) diff --git a/quartz/components/scripts/search.inline.ts b/quartz/components/scripts/search.inline.ts index 1ecf62fa40b30..b9b55bec64254 100644 --- a/quartz/components/scripts/search.inline.ts +++ b/quartz/components/scripts/search.inline.ts @@ -310,38 +310,12 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => { itemTile.href = resolveUrl(slug).toString() itemTile.innerHTML = `

    ${title}

    ${htmlTags}

    ${content}

    ` - async function onMouseEnter(ev: MouseEvent) { - if (!ev.target) return - currentHover?.classList.remove("focus") - currentHover?.blur() - const target = ev.target as HTMLInputElement - currentHover = target - currentHover.classList.add("focus") - await displayPreview(target) + const handler = (event: MouseEvent) => { + if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) return + hideSearch() } - - async function onMouseLeave(ev: MouseEvent) { - if (!ev.target) return - const target = ev.target as HTMLElement - target.classList.remove("focus") - } - - const events = [ - ["mouseenter", onMouseEnter], - ["mouseleave", onMouseLeave], - [ - "click", - (event: MouseEvent) => { - if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) return - hideSearch() - }, - ], - ] as const - - events.forEach(([event, handler]) => { - itemTile.addEventListener(event, handler) - window.addCleanup(() => itemTile.removeEventListener(event, handler)) - }) + itemTile.addEventListener("click", handler) + window.addCleanup(() => itemTile.removeEventListener("click", handler)) return itemTile } diff --git a/quartz/components/styles/search.scss b/quartz/components/styles/search.scss index 24a72848f8c8f..9cc85dfccc702 100644 --- a/quartz/components/styles/search.scss +++ b/quartz/components/styles/search.scss @@ -179,6 +179,7 @@ outline: none; font-weight: inherit; + &:hover, &:focus, &.focus { background: var(--lightgray); From 9ff1fdd280f4b4c554f1bddfa51689fcb1576558 Mon Sep 17 00:00:00 2001 From: Jacky Zhao Date: Fri, 2 Feb 2024 10:52:51 -0800 Subject: [PATCH 030/115] fix(search): oops restore ability to preview on hover lol --- quartz/components/scripts/search.inline.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/quartz/components/scripts/search.inline.ts b/quartz/components/scripts/search.inline.ts index b9b55bec64254..d707cfd662d0d 100644 --- a/quartz/components/scripts/search.inline.ts +++ b/quartz/components/scripts/search.inline.ts @@ -314,6 +314,15 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => { if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) return hideSearch() } + + async function onMouseEnter(ev: MouseEvent) { + if (!ev.target) return + const target = ev.target as HTMLInputElement + await displayPreview(target) + } + + itemTile.addEventListener("mouseenter", onMouseEnter) + window.addCleanup(() => itemTile.removeEventListener("mouseenter", onMouseEnter)) itemTile.addEventListener("click", handler) window.addCleanup(() => itemTile.removeEventListener("click", handler)) From 742b8832569e338848476fa415b858ce57b99e1b Mon Sep 17 00:00:00 2001 From: Jacky Zhao Date: Fri, 2 Feb 2024 12:18:02 -0800 Subject: [PATCH 031/115] fix(search): flex basis and card highlighting --- quartz/components/scripts/search.inline.ts | 2 - quartz/components/styles/search.scss | 53 +++++++++++----------- quartz/styles/base.scss | 2 +- 3 files changed, 28 insertions(+), 29 deletions(-) diff --git a/quartz/components/scripts/search.inline.ts b/quartz/components/scripts/search.inline.ts index d707cfd662d0d..59942ebf584f5 100644 --- a/quartz/components/scripts/search.inline.ts +++ b/quartz/components/scripts/search.inline.ts @@ -163,13 +163,11 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => { let previewInner: HTMLDivElement | undefined = undefined const results = document.createElement("div") results.id = "results-container" - results.style.flexBasis = enablePreview ? "min(30%, 450px)" : "100%" appendLayout(results) if (enablePreview) { preview = document.createElement("div") preview.id = "preview-container" - preview.style.flexBasis = "100%" appendLayout(preview) } diff --git a/quartz/components/styles/search.scss b/quartz/components/styles/search.scss index 9cc85dfccc702..3ad774ed39259 100644 --- a/quartz/components/styles/search.scss +++ b/quartz/components/styles/search.scss @@ -61,7 +61,7 @@ & > * { width: 100%; - border-radius: 5px; + border-radius: 7px; background: var(--light); box-shadow: 0 14px 50px rgba(27, 33, 48, 0.12), @@ -86,69 +86,70 @@ display: none; flex-direction: row; border: 1px solid var(--lightgray); + flex: 0 0 100%; + box-sizing: border-box; &.display-results { display: flex; } + &[data-preview] > #results-container { + flex: 0 0 min(30%, 450px); + } + @media all and (min-width: $tabletBreakpoint) { &[data-preview] { & .result-card > p.preview { display: none; } + + & > div { + &:first-child { + border-right: 1px solid var(--lightgray); + border-top-right-radius: unset; + border-bottom-right-radius: unset; + } + + &:last-child { + border-top-left-radius: unset; + border-bottom-left-radius: unset; + } + } } } & > div { - // vh - #search-space.margin-top height: calc(75vh - 12vh); - background: none; - - &:first-child { - border-top-left-radius: 5px; - border-bottom-left-radius: 5px; - border-right: 1px solid var(--lightgray); - } - - &:last-child { - border-top-right-radius: 5px; - border-bottom-right-radius: 5px; - } + border-radius: 5px; } @media all and (max-width: $tabletBreakpoint) { - display: block !important; - - & > *:not(#results-container) { + & > #preview-container { display: none !important; } - & > #results-container { + &[data-preview] > #results-container { width: 100%; height: auto; + flex: 0 0 100%; } } & .highlight { - background: color-mix(in srgb, var(--tertiary) 60%, transparent); + background: color-mix(in srgb, var(--tertiary) 60%, rgba(255, 255, 255, 0)); border-radius: 5px; scroll-margin-top: 2rem; } & > #preview-container { display: block; - box-sizing: border-box; overflow: hidden; - box-sizing: border-box; font-family: inherit; color: var(--dark); line-height: 1.5em; font-weight: $normalWeight; - background: var(--light); - border-top-right-radius: 5px; - border-bottom-right-radius: 5px; overflow-y: auto; - padding: 1rem; + padding: 0 2rem; & .preview-inner { margin: 0 auto; @@ -160,6 +161,7 @@ overflow-y: auto; & .result-card { + overflow: hidden; padding: 1em; cursor: pointer; transition: background 0.2s ease; @@ -175,7 +177,6 @@ margin: 0; text-transform: none; text-align: left; - background: var(--light); outline: none; font-weight: inherit; diff --git a/quartz/styles/base.scss b/quartz/styles/base.scss index a4fde06392a3e..33d6267f80fa4 100644 --- a/quartz/styles/base.scss +++ b/quartz/styles/base.scss @@ -26,7 +26,7 @@ section { } ::selection { - background: color-mix(in srgb, var(--tertiary) 60%, transparent); + background: color-mix(in srgb, var(--tertiary) 60%, rgba(255, 255, 255, 0)); color: var(--darkgray); } From 3fb3930df8d3bc61bbf6ac69360a1b5949270cca Mon Sep 17 00:00:00 2001 From: Jacky Zhao Date: Sat, 3 Feb 2024 19:44:24 -0800 Subject: [PATCH 032/115] fix: calculate heading after latex (closes #719) --- quartz.config.ts | 2 +- quartz/plugins/transformers/toc.ts | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/quartz.config.ts b/quartz.config.ts index 1f7399a88f569..d4fc5d38ae93a 100644 --- a/quartz.config.ts +++ b/quartz.config.ts @@ -45,7 +45,6 @@ const config: QuartzConfig = { plugins: { transformers: [ Plugin.FrontMatter(), - Plugin.TableOfContents(), Plugin.CreatedModifiedDate({ // you can add 'git' here for last modified from Git // if you do rely on git for dates, ensure defaultDateType is 'modified' @@ -55,6 +54,7 @@ const config: QuartzConfig = { Plugin.SyntaxHighlighting(), Plugin.ObsidianFlavoredMarkdown({ enableInHtmlEmbed: false }), Plugin.GitHubFlavoredMarkdown(), + Plugin.TableOfContents(), Plugin.CrawlLinks({ markdownLinkResolution: "shortest" }), Plugin.Description(), ], diff --git a/quartz/plugins/transformers/toc.ts b/quartz/plugins/transformers/toc.ts index d0781ec202ad0..e831f9452afb0 100644 --- a/quartz/plugins/transformers/toc.ts +++ b/quartz/plugins/transformers/toc.ts @@ -26,6 +26,7 @@ interface TocEntry { } const regexMdLinks = new RegExp(/\[([^\[]+)\](\(.*\))/, "g") +const slugAnchor = new Slugger() export const TableOfContents: QuartzTransformerPlugin | undefined> = ( userOpts, ) => { @@ -38,7 +39,7 @@ export const TableOfContents: QuartzTransformerPlugin | undefin return async (tree: Root, file) => { const display = file.data.frontmatter?.enableToc ?? opts.showByDefault if (display) { - const slugAnchor = new Slugger() + slugAnchor.reset() const toc: TocEntry[] = [] let highestDepth: number = opts.maxDepth visit(tree, "heading", (node) => { From dbbc672c67aa5ac0a915d22af5cf44c4e7011aae Mon Sep 17 00:00:00 2001 From: Mara-Li Date: Sun, 4 Feb 2024 04:55:24 +0100 Subject: [PATCH 033/115] feat: Adding support for i18n (closes #462) (#738) * fix: alt error mix with height/width More granular detection of alt and resize in image * fix: format * feat: init i18n * feat: add translation * style: prettier for test * fix: build-up the locale to fusion with dateLocale * style: run prettier * remove cursed file * refactor: remove i18n library and use locale way instead * format with prettier * forgot to remove test * prevent merging error * format * format * fix: allow string for locale - Check during translation if valid / existing locale - Allow to use "en" and "en-US" for example - Add fallback directly in the function - Add default key in the function - Add docstring to cfg.ts * forgot item translation * remove unused locale variable * forgot to remove fr-FR testing * format --- quartz.config.ts | 1 + quartz/cfg.ts | 2 +- quartz/components/Backlinks.tsx | 7 ++-- quartz/components/Darkmode.tsx | 7 ++-- quartz/components/Footer.tsx | 6 ++-- quartz/components/Graph.tsx | 5 +-- quartz/components/Head.tsx | 6 ++-- quartz/components/RecentNotes.tsx | 9 ++++- quartz/components/Search.tsx | 5 +-- quartz/components/TableOfContents.tsx | 10 +++--- quartz/components/pages/404.tsx | 7 ++-- quartz/components/pages/FolderContent.tsx | 8 +++-- quartz/components/pages/TagContent.tsx | 19 ++++++++--- quartz/components/scripts/search.inline.ts | 8 ++++- quartz/i18n/i18next.ts | 37 +++++++++++++++++++++ quartz/i18n/locales/en.json | 37 +++++++++++++++++++++ quartz/i18n/locales/fr.json | 38 ++++++++++++++++++++++ 17 files changed, 180 insertions(+), 32 deletions(-) create mode 100644 quartz/i18n/i18next.ts create mode 100644 quartz/i18n/locales/en.json create mode 100644 quartz/i18n/locales/fr.json diff --git a/quartz.config.ts b/quartz.config.ts index d4fc5d38ae93a..4921a11855f3f 100644 --- a/quartz.config.ts +++ b/quartz.config.ts @@ -9,6 +9,7 @@ const config: QuartzConfig = { analytics: { provider: "plausible", }, + locale: "en-US", baseUrl: "quartz.jzhao.xyz", ignorePatterns: ["private", "templates", ".obsidian"], defaultDateType: "created", diff --git a/quartz/cfg.ts b/quartz/cfg.ts index a7f79e3b80c54..e7ae783f80295 100644 --- a/quartz/cfg.ts +++ b/quartz/cfg.ts @@ -37,8 +37,8 @@ export interface GlobalConfiguration { baseUrl?: string theme: Theme /** - * The locale to use for date formatting. Default to "en-US" * Allow to translate the date in the language of your choice. + * Also used for UI translation (default: en-US) * Need to be formated following the IETF language tag format (https://en.wikipedia.org/wiki/IETF_language_tag) */ locale?: string diff --git a/quartz/components/Backlinks.tsx b/quartz/components/Backlinks.tsx index d5bdc0b9514e5..1688db62d57fe 100644 --- a/quartz/components/Backlinks.tsx +++ b/quartz/components/Backlinks.tsx @@ -1,14 +1,15 @@ import { QuartzComponentConstructor, QuartzComponentProps } from "./types" import style from "./styles/backlinks.scss" import { resolveRelative, simplifySlug } from "../util/path" +import { i18n } from "../i18n/i18next" import { classNames } from "../util/lang" -function Backlinks({ fileData, allFiles, displayClass }: QuartzComponentProps) { +function Backlinks({ fileData, allFiles, displayClass, cfg }: QuartzComponentProps) { const slug = simplifySlug(fileData.slug!) const backlinkFiles = allFiles.filter((file) => file.links?.includes(slug)) return (
    -

    Backlinks

    +

    {i18n(cfg.locale, "backlinks.backlinks")}

      {backlinkFiles.length > 0 ? ( backlinkFiles.map((f) => ( @@ -19,7 +20,7 @@ function Backlinks({ fileData, allFiles, displayClass }: QuartzComponentProps) { )) ) : ( -
    • No backlinks found
    • +
    • {i18n(cfg.locale, "backlinks.noBlacklinksFound")}
    • )}
    diff --git a/quartz/components/Darkmode.tsx b/quartz/components/Darkmode.tsx index 6d10bb99e7b33..056e684d30853 100644 --- a/quartz/components/Darkmode.tsx +++ b/quartz/components/Darkmode.tsx @@ -4,9 +4,10 @@ import darkmodeScript from "./scripts/darkmode.inline" import styles from "./styles/darkmode.scss" import { QuartzComponentConstructor, QuartzComponentProps } from "./types" +import { i18n } from "../i18n/i18next" import { classNames } from "../util/lang" -function Darkmode({ displayClass }: QuartzComponentProps) { +function Darkmode({ displayClass, cfg }: QuartzComponentProps) { return (
    @@ -22,7 +23,7 @@ function Darkmode({ displayClass }: QuartzComponentProps) { style="enable-background:new 0 0 35 35" xmlSpace="preserve" > - Light mode + {i18n(cfg.locale, "darkmode.lightMode")} @@ -38,7 +39,7 @@ function Darkmode({ displayClass }: QuartzComponentProps) { style="enable-background:new 0 0 100 100" xmlSpace="preserve" > - Dark mode + {i18n(cfg.locale, "darkmode.lightMode")} diff --git a/quartz/components/Footer.tsx b/quartz/components/Footer.tsx index 54440cffd9859..40faef9c62a86 100644 --- a/quartz/components/Footer.tsx +++ b/quartz/components/Footer.tsx @@ -1,20 +1,22 @@ import { QuartzComponentConstructor, QuartzComponentProps } from "./types" import style from "./styles/footer.scss" import { version } from "../../package.json" +import { i18n } from "../i18n/i18next" interface Options { links: Record } export default ((opts?: Options) => { - function Footer({ displayClass }: QuartzComponentProps) { + function Footer({ displayClass, cfg }: QuartzComponentProps) { const year = new Date().getFullYear() const links = opts?.links ?? [] return (

    - Created with Quartz v{version}, © {year} + {i18n(cfg.locale, "footer.createdWith")}{" "} + Quartz v{version}, © {year}

      {Object.entries(links).map(([text, link]) => ( diff --git a/quartz/components/Graph.tsx b/quartz/components/Graph.tsx index 756a46bb7d13d..f728c5e5d59ac 100644 --- a/quartz/components/Graph.tsx +++ b/quartz/components/Graph.tsx @@ -2,6 +2,7 @@ import { QuartzComponentConstructor, QuartzComponentProps } from "./types" // @ts-ignore import script from "./scripts/graph.inline" import style from "./styles/graph.scss" +import { i18n } from "../i18n/i18next" import { classNames } from "../util/lang" export interface D3Config { @@ -53,12 +54,12 @@ const defaultOptions: GraphOptions = { } export default ((opts?: GraphOptions) => { - function Graph({ displayClass }: QuartzComponentProps) { + function Graph({ displayClass, cfg }: QuartzComponentProps) { const localGraph = { ...defaultOptions.localGraph, ...opts?.localGraph } const globalGraph = { ...defaultOptions.globalGraph, ...opts?.globalGraph } return (
      -

      Graph View

      +

      {i18n(cfg.locale, "graph.graphView")}

      { function Head({ cfg, fileData, externalResources }: QuartzComponentProps) { - const title = fileData.frontmatter?.title ?? "Untitled" - const description = fileData.description?.trim() ?? "No description provided" + const title = fileData.frontmatter?.title ?? i18n(cfg.locale, "head.untitled") + const description = + fileData.description?.trim() ?? i18n(cfg.locale, "head.noDescriptionProvided") const { css, js } = externalResources const url = new URL(`https://${cfg.baseUrl ?? "example.com"}`) diff --git a/quartz/components/RecentNotes.tsx b/quartz/components/RecentNotes.tsx index 81b354d778a44..240ef98f3f388 100644 --- a/quartz/components/RecentNotes.tsx +++ b/quartz/components/RecentNotes.tsx @@ -5,6 +5,7 @@ import { byDateAndAlphabetical } from "./PageList" import style from "./styles/recentNotes.scss" import { Date, getDate } from "./Date" import { GlobalConfiguration } from "../cfg" +import { i18n } from "../i18n/i18next" import { classNames } from "../util/lang" interface Options { @@ -70,7 +71,13 @@ export default ((userOpts?: Partial) => {
    {opts.linkToMore && remaining > 0 && (

    - See {remaining} more → + + {" "} + {i18n(cfg.locale, "recentNotes.seeRemainingMore", { + remaining: remaining.toString(), + })}{" "} + → +

    )}
    diff --git a/quartz/components/Search.tsx b/quartz/components/Search.tsx index 239bc033bba6a..b73ce0bfc5429 100644 --- a/quartz/components/Search.tsx +++ b/quartz/components/Search.tsx @@ -3,6 +3,7 @@ import style from "./styles/search.scss" // @ts-ignore import script from "./scripts/search.inline" import { classNames } from "../util/lang" +import { i18n } from "../i18n/i18next" export interface SearchOptions { enablePreview: boolean @@ -13,13 +14,13 @@ const defaultOptions: SearchOptions = { } export default ((userOpts?: Partial) => { - function Search({ displayClass }: QuartzComponentProps) { + function Search({ displayClass, cfg }: QuartzComponentProps) { const opts = { ...defaultOptions, ...userOpts } return (
    -

    Search

    +

    {i18n(cfg.locale, "search")}

    Table of Contents

    +

    {i18n(cfg.locale, "tableOfContent")}

    -

    Table of Contents

    +

    {i18n(cfg.locale, "tableOfContent")}

      {fileData.toc.map((tocEntry) => ( diff --git a/quartz/components/pages/404.tsx b/quartz/components/pages/404.tsx index c276f568d1026..56adbf981cf2b 100644 --- a/quartz/components/pages/404.tsx +++ b/quartz/components/pages/404.tsx @@ -1,10 +1,11 @@ -import { QuartzComponentConstructor } from "../types" +import { i18n } from "../../i18n/i18next" +import { QuartzComponentConstructor, QuartzComponentProps } from "../types" -function NotFound() { +function NotFound({ cfg }: QuartzComponentProps) { return (

      404

      -

      Either this page is private or doesn't exist.

      +

      {i18n(cfg.locale, "404")}

      ) } diff --git a/quartz/components/pages/FolderContent.tsx b/quartz/components/pages/FolderContent.tsx index 47fb02f1be742..02938e32c2686 100644 --- a/quartz/components/pages/FolderContent.tsx +++ b/quartz/components/pages/FolderContent.tsx @@ -7,6 +7,7 @@ import { _stripSlashes, simplifySlug } from "../../util/path" import { Root } from "hast" import { pluralize } from "../../util/lang" import { htmlToJsx } from "../../util/jsx" +import { i18n } from "../../i18n/i18next" interface FolderContentOptions { /** @@ -23,7 +24,7 @@ export default ((opts?: Partial) => { const options: FolderContentOptions = { ...defaultOptions, ...opts } function FolderContent(props: QuartzComponentProps) { - const { tree, fileData, allFiles } = props + const { tree, fileData, allFiles, cfg } = props const folderSlug = _stripSlashes(simplifySlug(fileData.slug!)) const allPagesInFolder = allFiles.filter((file) => { const fileSlug = _stripSlashes(simplifySlug(file.slug!)) @@ -52,7 +53,10 @@ export default ((opts?: Partial) => {
      {options.showFolderCount && ( -

      {pluralize(allPagesInFolder.length, "item")} under this folder.

      +

      + {pluralize(allPagesInFolder.length, i18n(cfg.locale, "common.item"))}{" "} + {i18n(cfg.locale, "folderContent.underThisFolder")}. +

      )}
      diff --git a/quartz/components/pages/TagContent.tsx b/quartz/components/pages/TagContent.tsx index ec30c5ff946ab..57a6c321678d4 100644 --- a/quartz/components/pages/TagContent.tsx +++ b/quartz/components/pages/TagContent.tsx @@ -6,10 +6,11 @@ import { QuartzPluginData } from "../../plugins/vfile" import { Root } from "hast" import { pluralize } from "../../util/lang" import { htmlToJsx } from "../../util/jsx" +import { i18n } from "../../i18n/i18next" const numPages = 10 function TagContent(props: QuartzComponentProps) { - const { tree, fileData, allFiles } = props + const { tree, fileData, allFiles, cfg } = props const slug = fileData.slug if (!(slug?.startsWith("tags/") || slug === "tags")) { @@ -43,7 +44,10 @@ function TagContent(props: QuartzComponentProps) {

      {content}

      -

      Found {tags.length} total tags.

      +

      + {i18n(cfg.locale, "tagContent.found")} {tags.length}{" "} + {i18n(cfg.locale, "tagContent.totalTags")}. +

      {tags.map((tag) => { const pages = tagItemMap.get(tag)! @@ -64,8 +68,10 @@ function TagContent(props: QuartzComponentProps) { {content &&

      {content}

      }

      - {pluralize(pages.length, "item")} with this tag.{" "} - {pages.length > numPages && `Showing first ${numPages}.`} + {pluralize(pages.length, i18n(cfg.locale, "common.item"))}{" "} + {i18n(cfg.locale, "tagContent.withThisTag")}.{" "} + {pages.length > numPages && + `${i18n(cfg.locale, "tagContent.showingFirst")} ${numPages}.`}

      @@ -86,7 +92,10 @@ function TagContent(props: QuartzComponentProps) {
      {content}
      -

      {pluralize(pages.length, "item")} with this tag.

      +

      + {pluralize(pages.length, i18n(cfg.locale, "common.item"))}{" "} + {i18n(cfg.locale, "tagContent.withThisTag")}. +

      diff --git a/quartz/components/scripts/search.inline.ts b/quartz/components/scripts/search.inline.ts index 59942ebf584f5..a75f4ff460eb9 100644 --- a/quartz/components/scripts/search.inline.ts +++ b/quartz/components/scripts/search.inline.ts @@ -306,7 +306,13 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => { itemTile.classList.add("result-card") itemTile.id = slug itemTile.href = resolveUrl(slug).toString() - itemTile.innerHTML = `

      ${title}

      ${htmlTags}

      ${content}

      ` + itemTile.innerHTML = `

      ${title}

      ${htmlTags}${ + enablePreview && window.innerWidth > 600 ? "" : `

      ${content}

      ` + }` + itemTile.addEventListener("click", (event) => { + if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) return + hideSearch() + }) const handler = (event: MouseEvent) => { if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) return diff --git a/quartz/i18n/i18next.ts b/quartz/i18n/i18next.ts new file mode 100644 index 0000000000000..39c446132d797 --- /dev/null +++ b/quartz/i18n/i18next.ts @@ -0,0 +1,37 @@ +import en from "./locales/en.json" +import fr from "./locales/fr.json" + +const TRANSLATION = { + "en-US": en, + "fr-FR": fr, +} as const + +type TranslationOptions = { + [key: string]: string +} + +export const i18n = (lang = "en-US", key: string, options?: TranslationOptions) => { + const locale = + Object.keys(TRANSLATION).find( + (key) => + key.toLowerCase() === lang.toLowerCase() || key.toLowerCase().includes(lang.toLowerCase()), + ) ?? "en-US" + const getTranslation = (key: string) => { + const keys = key.split(".") + let translationString: string | Record = + TRANSLATION[locale as keyof typeof TRANSLATION] + keys.forEach((key) => { + // @ts-ignore + translationString = translationString[key] + }) + return translationString + } + if (options) { + let translationString = getTranslation(key).toString() + Object.keys(options).forEach((key) => { + translationString = translationString.replace(`{{${key}}}`, options[key]) + }) + return translationString + } + return getTranslation(key).toString() +} diff --git a/quartz/i18n/locales/en.json b/quartz/i18n/locales/en.json new file mode 100644 index 0000000000000..28b6dff2d2aa1 --- /dev/null +++ b/quartz/i18n/locales/en.json @@ -0,0 +1,37 @@ +{ + "404": "Either this page is private or doesn't exist.", + "backlinks": { + "backlinks": "Backlinks", + "noBlacklinksFound": "No backlinks found" + }, + "common": { + "item": "item" + }, + "darkmode": { + "lightMode": "Light mode" + }, + "folderContent": { + "underThisFolder": "under this folder" + }, + "footer": { + "createdWith": "Created with" + }, + "graph": { + "graphView": "Graph View" + }, + "head": { + "noDescriptionProvided": "No description provided", + "untitled": "Untitled" + }, + "recentNotes": { + "seeRemainingMore": "See {{remaining}} more" + }, + "search": "Search", + "tableOfContent": "Table of Contents", + "tagContent": { + "showingFirst": "Showing first", + "totalTags": "total tags", + "withThisTag": "with this tag", + "found": "Found" + } +} diff --git a/quartz/i18n/locales/fr.json b/quartz/i18n/locales/fr.json new file mode 100644 index 0000000000000..97f8f31bc37a0 --- /dev/null +++ b/quartz/i18n/locales/fr.json @@ -0,0 +1,38 @@ +{ + "404": "Soit cette page est privée, soit elle n'existe pas.", + "backlinks": { + "backlinks": "Rétroliens", + "noBlacklinksFound": "Aucun rétrolien trouvé" + }, + "common": { + "item": "fichier" + }, + "darkmode": { + "darkmode": "Thème sombre", + "lightMode": "Thème clair" + }, + "folderContent": { + "underThisFolder": "dans ce dossier" + }, + "footer": { + "createdWith": "Créé avec" + }, + "graph": { + "graphView": "Vue Graphique" + }, + "head": { + "noDescriptionProvided": "Aucune description n'a été fournie", + "untitled": "Sans titre" + }, + "recentNotes": { + "seeRemainingMore": "Voir {{remaining}} plus" + }, + "search": "Rechercher", + "tableOfContent": "Table des Matières", + "tagContent": { + "showingFirst": "Afficher en premier", + "totalTags": "tags totaux", + "withThisTag": "avec ce tag", + "found": "Trouvé" + } +} From 5b90fbd0d0ac93a6ef5921f7693d36574224531c Mon Sep 17 00:00:00 2001 From: Aaron Pham <29749331+aarnphm@users.noreply.github.com> Date: Sun, 4 Feb 2024 00:51:55 -0500 Subject: [PATCH 034/115] feat(ofm): parsing all type of arrow (#797) * feat(ofm): parsing all type of arrow Signed-off-by: Aaron <29749331+aarnphm@users.noreply.github.com> * fix: use html value instead of decimal Signed-off-by: Aaron <29749331+aarnphm@users.noreply.github.com> * fix: skip parsing arrow if it is not a valid supported mapping Signed-off-by: Aaron <29749331+aarnphm@users.noreply.github.com> --------- Signed-off-by: Aaron <29749331+aarnphm@users.noreply.github.com> --- quartz/plugins/transformers/ofm.ts | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/quartz/plugins/transformers/ofm.ts b/quartz/plugins/transformers/ofm.ts index 44df3fa9ee269..18ff6b438aa02 100644 --- a/quartz/plugins/transformers/ofm.ts +++ b/quartz/plugins/transformers/ofm.ts @@ -74,6 +74,17 @@ const calloutMapping = { cite: "quote", } as const +const arrowMapping: Record = { + "->": "→", + "-->": "⇒", + "=>": "⇒", + "==>": "⇒", + "<-": "←", + "<--": "⇐", + "<=": "⇐", + "<==": "⇐", +} + function canonicalizeCallout(calloutName: string): keyof typeof calloutMapping { const normalizedCallout = calloutName.toLowerCase() as keyof typeof calloutMapping // if callout is not recognized, make it a custom one @@ -82,7 +93,7 @@ function canonicalizeCallout(calloutName: string): keyof typeof calloutMapping { export const externalLinkRegex = /^https?:\/\//i -export const arrowRegex = new RegExp(/-{1,2}>/, "g") +export const arrowRegex = new RegExp(/(-{1,2}>|={1,2}>|<-{1,2}|<={1,2})/, "g") // !? -> optional embedding // \[\[ -> open brace @@ -271,10 +282,12 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin if (opts.parseArrows) { replacements.push([ arrowRegex, - (_value: string, ..._capture: string[]) => { + (value: string, ..._capture: string[]) => { + const maybeArrow = arrowMapping[value] + if (maybeArrow === undefined) return SKIP return { type: "html", - value: ``, + value: `${maybeArrow}`, } }, ]) From dff4b063135297aaa2f0605fd3267a874baaa90d Mon Sep 17 00:00:00 2001 From: Mats Fangohr <83273529+MatsFangohr@users.noreply.github.com> Date: Sun, 4 Feb 2024 15:48:31 +0100 Subject: [PATCH 035/115] fix(i18n): backlinks naming in mapping (#800) --- quartz/components/Backlinks.tsx | 2 +- quartz/i18n/locales/en.json | 2 +- quartz/i18n/locales/fr.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/quartz/components/Backlinks.tsx b/quartz/components/Backlinks.tsx index 1688db62d57fe..458e48b2454b6 100644 --- a/quartz/components/Backlinks.tsx +++ b/quartz/components/Backlinks.tsx @@ -20,7 +20,7 @@ function Backlinks({ fileData, allFiles, displayClass, cfg }: QuartzComponentPro )) ) : ( -
    • {i18n(cfg.locale, "backlinks.noBlacklinksFound")}
    • +
    • {i18n(cfg.locale, "backlinks.noBacklinksFound")}
    • )}
    diff --git a/quartz/i18n/locales/en.json b/quartz/i18n/locales/en.json index 28b6dff2d2aa1..fc3cebbb6f735 100644 --- a/quartz/i18n/locales/en.json +++ b/quartz/i18n/locales/en.json @@ -2,7 +2,7 @@ "404": "Either this page is private or doesn't exist.", "backlinks": { "backlinks": "Backlinks", - "noBlacklinksFound": "No backlinks found" + "noBacklinksFound": "No backlinks found" }, "common": { "item": "item" diff --git a/quartz/i18n/locales/fr.json b/quartz/i18n/locales/fr.json index 97f8f31bc37a0..8b526b5429ce3 100644 --- a/quartz/i18n/locales/fr.json +++ b/quartz/i18n/locales/fr.json @@ -2,7 +2,7 @@ "404": "Soit cette page est privée, soit elle n'existe pas.", "backlinks": { "backlinks": "Rétroliens", - "noBlacklinksFound": "Aucun rétrolien trouvé" + "noBacklinksFound": "Aucun rétrolien trouvé" }, "common": { "item": "fichier" From 36e4cc41a9e74faddabfd22878ea13b6c504209c Mon Sep 17 00:00:00 2001 From: Jacky Zhao Date: Sun, 4 Feb 2024 20:57:10 -0800 Subject: [PATCH 036/115] chore(i18n): refactor and cleanup (#805) * checkpoint * finish * docs --- docs/advanced/making plugins.md | 2 +- docs/configuration.md | 1 + docs/features/i18n.md | 18 ++++++ docs/index.md | 2 +- quartz/cfg.ts | 8 ++- quartz/components/ArticleTitle.tsx | 1 + quartz/components/Backlinks.tsx | 6 +- quartz/components/Darkmode.tsx | 6 +- quartz/components/Date.tsx | 5 +- quartz/components/Explorer.tsx | 6 +- quartz/components/ExplorerNode.tsx | 2 +- quartz/components/Footer.tsx | 6 +- quartz/components/Graph.tsx | 4 +- quartz/components/Head.tsx | 6 +- quartz/components/PageTitle.tsx | 3 +- quartz/components/RecentNotes.tsx | 15 ++--- quartz/components/Search.tsx | 10 ++-- quartz/components/TableOfContents.tsx | 6 +- quartz/components/pages/404.tsx | 4 +- quartz/components/pages/FolderContent.tsx | 8 +-- quartz/components/pages/TagContent.tsx | 23 +++----- quartz/components/renderPage.tsx | 20 ++++++- quartz/i18n/i18next.ts | 37 ------------ quartz/i18n/index.ts | 11 ++++ quartz/i18n/locales/definition.ts | 63 +++++++++++++++++++++ quartz/i18n/locales/en-US.ts | 65 ++++++++++++++++++++++ quartz/i18n/locales/en.json | 37 ------------ quartz/i18n/locales/fr-FR.ts | 65 ++++++++++++++++++++++ quartz/i18n/locales/fr.json | 38 ------------- quartz/plugins/emitters/404.tsx | 10 ++-- quartz/plugins/emitters/contentIndex.ts | 5 +- quartz/plugins/emitters/contentPage.tsx | 2 +- quartz/plugins/emitters/folderPage.tsx | 8 ++- quartz/plugins/emitters/tagPage.tsx | 8 ++- quartz/plugins/transformers/frontmatter.ts | 5 +- quartz/plugins/transformers/toc.ts | 13 +---- quartz/util/lang.ts | 8 --- 37 files changed, 326 insertions(+), 211 deletions(-) create mode 100644 docs/features/i18n.md delete mode 100644 quartz/i18n/i18next.ts create mode 100644 quartz/i18n/index.ts create mode 100644 quartz/i18n/locales/definition.ts create mode 100644 quartz/i18n/locales/en-US.ts delete mode 100644 quartz/i18n/locales/en.json create mode 100644 quartz/i18n/locales/fr-FR.ts delete mode 100644 quartz/i18n/locales/fr.json diff --git a/docs/advanced/making plugins.md b/docs/advanced/making plugins.md index 65209a2caea2b..565f5bdba9752 100644 --- a/docs/advanced/making plugins.md +++ b/docs/advanced/making plugins.md @@ -278,7 +278,7 @@ export const ContentPage: QuartzEmitterPlugin = () => { allFiles, } - const content = renderPage(slug, componentData, opts, externalResources) + const content = renderPage(cfg, slug, componentData, opts, externalResources) const fp = await emit({ content, slug: file.data.slug!, diff --git a/docs/configuration.md b/docs/configuration.md index 047f6ca6badc5..33d5a5744269e 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -27,6 +27,7 @@ This part of the configuration concerns anything that can affect the whole site. - `null`: don't use analytics; - `{ provider: 'plausible' }`: use [Plausible](https://plausible.io/), a privacy-friendly alternative to Google Analytics; or - `{ provider: 'google', tagId: }`: use Google Analytics +- `locale`: used for [[i18n]] and date formatting - `baseUrl`: this is used for sitemaps and RSS feeds that require an absolute URL to know where the canonical 'home' of your site lives. This is normally the deployed URL of your site (e.g. `quartz.jzhao.xyz` for this site). Do not include the protocol (i.e. `https://`) or any leading or trailing slashes. - This should also include the subpath if you are [[hosting]] on GitHub pages without a custom domain. For example, if my repository is `jackyzha0/quartz`, GitHub pages would deploy to `https://jackyzha0.github.io/quartz` and the `baseUrl` would be `jackyzha0.github.io/quartz` - Note that Quartz 4 will avoid using this as much as possible and use relative URLs whenever it can to make sure your site works no matter _where_ you end up actually deploying it. diff --git a/docs/features/i18n.md b/docs/features/i18n.md new file mode 100644 index 0000000000000..57547ddadbe40 --- /dev/null +++ b/docs/features/i18n.md @@ -0,0 +1,18 @@ +--- +title: Internationalization +--- + +Internationalization allows users to translate text in the Quartz interface into various supported languages without needing to make extensive code changes. This can be changed via the `locale` [[configuration]] field in `quartz.config.ts`. + +The locale field generally follows a certain format: `{language}-{REGION}` + +- `{language}` is usually a [2-letter lowercase language code](https://en.wikipedia.org/wiki/List_of_ISO_639_language_codes). +- `{REGION}` is usually a [2-letter uppercase region code](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2) + +> [!tip] Interested in contributing? +> We [gladly welcome translation PRs](https://github.com/jackyzha0/quartz/tree/v4/quartz/i18n/locales)! To contribute a translation, do the following things: +> +> 1. In the `quartz/i18n/locales` folder, copy the `en-US.ts` file. +> 2. Rename it to `{language}-{REGION}.ts` so it matches a locale of the format shown above. +> 3. Fill in the translations! +> 4. Add the entry under `TRANSLATIONS` in `quartz/i18n/index.ts`. diff --git a/docs/index.md b/docs/index.md index cbf8719d1ca62..f25b6e24425d4 100644 --- a/docs/index.md +++ b/docs/index.md @@ -31,7 +31,7 @@ If you prefer instructions in a video format you can try following Nicole van de ## 🔧 Features -- [[Obsidian compatibility]], [[full-text search]], [[graph view]], note transclusion, [[wikilinks]], [[backlinks]], [[Latex]], [[syntax highlighting]], [[popover previews]], [[Docker Support]], and [many more](./features) right out of the box +- [[Obsidian compatibility]], [[full-text search]], [[graph view]], note transclusion, [[wikilinks]], [[backlinks]], [[Latex]], [[syntax highlighting]], [[popover previews]], [[Docker Support]], [[i18n|internationalization]] and [many more](./features) right out of the box - Hot-reload for both configuration and content - Simple JSX layouts and [[creating components|page components]] - [[SPA Routing|Ridiculously fast page loads]] and tiny bundle sizes diff --git a/quartz/cfg.ts b/quartz/cfg.ts index e7ae783f80295..a477db057f732 100644 --- a/quartz/cfg.ts +++ b/quartz/cfg.ts @@ -1,5 +1,6 @@ import { ValidDateType } from "./components/Date" import { QuartzComponent } from "./components/types" +import { ValidLocale } from "./i18n" import { PluginTypes } from "./plugins/types" import { Theme } from "./util/theme" @@ -39,9 +40,12 @@ export interface GlobalConfiguration { /** * Allow to translate the date in the language of your choice. * Also used for UI translation (default: en-US) - * Need to be formated following the IETF language tag format (https://en.wikipedia.org/wiki/IETF_language_tag) + * Need to be formated following BCP 47: https://en.wikipedia.org/wiki/IETF_language_tag + * The first part is the language (en) and the second part is the script/region (US) + * Language Codes: https://en.wikipedia.org/wiki/List_of_ISO_639_language_codes + * Region Codes: https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2 */ - locale?: string + locale: ValidLocale } export interface QuartzConfig { diff --git a/quartz/components/ArticleTitle.tsx b/quartz/components/ArticleTitle.tsx index 2484c946a4650..7768de6cb0ec7 100644 --- a/quartz/components/ArticleTitle.tsx +++ b/quartz/components/ArticleTitle.tsx @@ -9,6 +9,7 @@ function ArticleTitle({ fileData, displayClass }: QuartzComponentProps) { return null } } + ArticleTitle.css = ` .article-title { margin: 2rem 0 0 0; diff --git a/quartz/components/Backlinks.tsx b/quartz/components/Backlinks.tsx index 458e48b2454b6..573c1c39195d3 100644 --- a/quartz/components/Backlinks.tsx +++ b/quartz/components/Backlinks.tsx @@ -1,7 +1,7 @@ import { QuartzComponentConstructor, QuartzComponentProps } from "./types" import style from "./styles/backlinks.scss" import { resolveRelative, simplifySlug } from "../util/path" -import { i18n } from "../i18n/i18next" +import { i18n } from "../i18n" import { classNames } from "../util/lang" function Backlinks({ fileData, allFiles, displayClass, cfg }: QuartzComponentProps) { @@ -9,7 +9,7 @@ function Backlinks({ fileData, allFiles, displayClass, cfg }: QuartzComponentPro const backlinkFiles = allFiles.filter((file) => file.links?.includes(slug)) return (
    -

    {i18n(cfg.locale, "backlinks.backlinks")}

    +

    {i18n(cfg.locale).components.backlinks.title}

      {backlinkFiles.length > 0 ? ( backlinkFiles.map((f) => ( @@ -20,7 +20,7 @@ function Backlinks({ fileData, allFiles, displayClass, cfg }: QuartzComponentPro )) ) : ( -
    • {i18n(cfg.locale, "backlinks.noBacklinksFound")}
    • +
    • {i18n(cfg.locale).components.backlinks.noBacklinksFound}
    • )}
    diff --git a/quartz/components/Darkmode.tsx b/quartz/components/Darkmode.tsx index 056e684d30853..62d3c2382dec5 100644 --- a/quartz/components/Darkmode.tsx +++ b/quartz/components/Darkmode.tsx @@ -4,7 +4,7 @@ import darkmodeScript from "./scripts/darkmode.inline" import styles from "./styles/darkmode.scss" import { QuartzComponentConstructor, QuartzComponentProps } from "./types" -import { i18n } from "../i18n/i18next" +import { i18n } from "../i18n" import { classNames } from "../util/lang" function Darkmode({ displayClass, cfg }: QuartzComponentProps) { @@ -23,7 +23,7 @@ function Darkmode({ displayClass, cfg }: QuartzComponentProps) { style="enable-background:new 0 0 35 35" xmlSpace="preserve" > - {i18n(cfg.locale, "darkmode.lightMode")} + {i18n(cfg.locale).components.themeToggle.darkMode} @@ -39,7 +39,7 @@ function Darkmode({ displayClass, cfg }: QuartzComponentProps) { style="enable-background:new 0 0 100 100" xmlSpace="preserve" > - {i18n(cfg.locale, "darkmode.lightMode")} + {i18n(cfg.locale).components.themeToggle.lightMode} diff --git a/quartz/components/Date.tsx b/quartz/components/Date.tsx index 6feac178cacb6..26b59647c281d 100644 --- a/quartz/components/Date.tsx +++ b/quartz/components/Date.tsx @@ -1,9 +1,10 @@ import { GlobalConfiguration } from "../cfg" +import { ValidLocale } from "../i18n" import { QuartzPluginData } from "../plugins/vfile" interface Props { date: Date - locale?: string + locale?: ValidLocale } export type ValidDateType = keyof Required["dates"] @@ -17,7 +18,7 @@ export function getDate(cfg: GlobalConfiguration, data: QuartzPluginData): Date return data.dates?.[cfg.defaultDateType] } -export function formatDate(d: Date, locale = "en-US"): string { +export function formatDate(d: Date, locale: ValidLocale = "en-US"): string { return d.toLocaleDateString(locale, { year: "numeric", month: "short", diff --git a/quartz/components/Explorer.tsx b/quartz/components/Explorer.tsx index e964c5a410f18..f7017342e963f 100644 --- a/quartz/components/Explorer.tsx +++ b/quartz/components/Explorer.tsx @@ -6,10 +6,10 @@ import script from "./scripts/explorer.inline" import { ExplorerNode, FileNode, Options } from "./ExplorerNode" import { QuartzPluginData } from "../plugins/vfile" import { classNames } from "../util/lang" +import { i18n } from "../i18n" // Options interface defined in `ExplorerNode` to avoid circular dependency const defaultOptions = { - title: "Explorer", folderClickBehavior: "collapse", folderDefaultState: "collapsed", useSavedState: true, @@ -75,7 +75,7 @@ export default ((userOpts?: Partial) => { jsonTree = JSON.stringify(folders) } - function Explorer({ allFiles, displayClass, fileData }: QuartzComponentProps) { + function Explorer({ cfg, allFiles, displayClass, fileData }: QuartzComponentProps) { constructFileTree(allFiles) return (
    @@ -87,7 +87,7 @@ export default ((userOpts?: Partial) => { data-savestate={opts.useSavedState} data-tree={jsonTree} > -

    {opts.title}

    +

    {opts.title ?? i18n(cfg.locale).components.explorer.title}

    @@ -15,8 +15,8 @@ export default ((opts?: Options) => {

    - {i18n(cfg.locale, "footer.createdWith")}{" "} - Quartz v{version}, © {year} + {i18n(cfg.locale).components.footer.createdWith}{" "} + Quartz v{version} © {year}

      {Object.entries(links).map(([text, link]) => ( diff --git a/quartz/components/Graph.tsx b/quartz/components/Graph.tsx index f728c5e5d59ac..9fce9bd8f02ff 100644 --- a/quartz/components/Graph.tsx +++ b/quartz/components/Graph.tsx @@ -2,7 +2,7 @@ import { QuartzComponentConstructor, QuartzComponentProps } from "./types" // @ts-ignore import script from "./scripts/graph.inline" import style from "./styles/graph.scss" -import { i18n } from "../i18n/i18next" +import { i18n } from "../i18n" import { classNames } from "../util/lang" export interface D3Config { @@ -59,7 +59,7 @@ export default ((opts?: GraphOptions) => { const globalGraph = { ...defaultOptions.globalGraph, ...opts?.globalGraph } return (
      -

      {i18n(cfg.locale, "graph.graphView")}

      +

      {i18n(cfg.locale).components.graph.title}

      { function Head({ cfg, fileData, externalResources }: QuartzComponentProps) { - const title = fileData.frontmatter?.title ?? i18n(cfg.locale, "head.untitled") + const title = fileData.frontmatter?.title ?? i18n(cfg.locale).propertyDefaults.title const description = - fileData.description?.trim() ?? i18n(cfg.locale, "head.noDescriptionProvided") + fileData.description?.trim() ?? i18n(cfg.locale).propertyDefaults.description const { css, js } = externalResources const url = new URL(`https://${cfg.baseUrl ?? "example.com"}`) diff --git a/quartz/components/PageTitle.tsx b/quartz/components/PageTitle.tsx index fb1660a7c82bb..d129602647351 100644 --- a/quartz/components/PageTitle.tsx +++ b/quartz/components/PageTitle.tsx @@ -1,9 +1,10 @@ import { pathToRoot } from "../util/path" import { QuartzComponentConstructor, QuartzComponentProps } from "./types" import { classNames } from "../util/lang" +import { i18n } from "../i18n" function PageTitle({ fileData, cfg, displayClass }: QuartzComponentProps) { - const title = cfg?.pageTitle ?? "Untitled Quartz" + const title = cfg?.pageTitle ?? i18n(cfg.locale).propertyDefaults.title const baseDir = pathToRoot(fileData.slug!) return (

      diff --git a/quartz/components/RecentNotes.tsx b/quartz/components/RecentNotes.tsx index 240ef98f3f388..f8f6de41f76cd 100644 --- a/quartz/components/RecentNotes.tsx +++ b/quartz/components/RecentNotes.tsx @@ -5,11 +5,11 @@ import { byDateAndAlphabetical } from "./PageList" import style from "./styles/recentNotes.scss" import { Date, getDate } from "./Date" import { GlobalConfiguration } from "../cfg" -import { i18n } from "../i18n/i18next" +import { i18n } from "../i18n" import { classNames } from "../util/lang" interface Options { - title: string + title?: string limit: number linkToMore: SimpleSlug | false filter: (f: QuartzPluginData) => boolean @@ -17,7 +17,6 @@ interface Options { } const defaultOptions = (cfg: GlobalConfiguration): Options => ({ - title: "Recent Notes", limit: 3, linkToMore: false, filter: () => true, @@ -31,10 +30,10 @@ export default ((userOpts?: Partial) => { const remaining = Math.max(0, pages.length - opts.limit) return (
      -

      {opts.title}

      +

      {opts.title ?? i18n(cfg.locale).components.recentNotes.title}

        {pages.slice(0, opts.limit).map((page) => { - const title = page.frontmatter?.title + const title = page.frontmatter?.title ?? i18n(cfg.locale).propertyDefaults.title const tags = page.frontmatter?.tags ?? [] return ( @@ -72,11 +71,7 @@ export default ((userOpts?: Partial) => { {opts.linkToMore && remaining > 0 && (

        - {" "} - {i18n(cfg.locale, "recentNotes.seeRemainingMore", { - remaining: remaining.toString(), - })}{" "} - → + {i18n(cfg.locale).components.recentNotes.seeRemainingMore({ remaining })}

        )} diff --git a/quartz/components/Search.tsx b/quartz/components/Search.tsx index b73ce0bfc5429..a07dbc4fd1d6d 100644 --- a/quartz/components/Search.tsx +++ b/quartz/components/Search.tsx @@ -3,7 +3,7 @@ import style from "./styles/search.scss" // @ts-ignore import script from "./scripts/search.inline" import { classNames } from "../util/lang" -import { i18n } from "../i18n/i18next" +import { i18n } from "../i18n" export interface SearchOptions { enablePreview: boolean @@ -16,11 +16,11 @@ const defaultOptions: SearchOptions = { export default ((userOpts?: Partial) => { function Search({ displayClass, cfg }: QuartzComponentProps) { const opts = { ...defaultOptions, ...userOpts } - + const searchPlaceholder = i18n(cfg.locale).components.search.searchBarPlaceholder return (
        -

        {i18n(cfg.locale, "search")}

        +

        {i18n(cfg.locale).components.search.title}

        ) => { id="search-bar" name="search" type="text" - aria-label="Search for something" - placeholder="Search for something" + aria-label={searchPlaceholder} + placeholder={searchPlaceholder} />
        diff --git a/quartz/components/TableOfContents.tsx b/quartz/components/TableOfContents.tsx index 2e015076fccba..2abc74b5308f5 100644 --- a/quartz/components/TableOfContents.tsx +++ b/quartz/components/TableOfContents.tsx @@ -5,7 +5,7 @@ import { classNames } from "../util/lang" // @ts-ignore import script from "./scripts/toc.inline" -import { i18n } from "../i18n/i18next" +import { i18n } from "../i18n" interface Options { layout: "modern" | "legacy" @@ -23,7 +23,7 @@ function TableOfContents({ fileData, displayClass, cfg }: QuartzComponentProps) return (