Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions quartz/components/renderPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -410,6 +410,9 @@ export function renderPage(
data-read-later-saved={readLater.saved}
data-read-later-removed={readLater.removed}
data-read-later-failed={readLater.failed}
data-read-later-export={readLater.exportList}
data-read-later-exported={readLater.exported}
data-read-later-export-failed={readLater.exportFailed}
data-resume-reading-continue={resumeReading.continueFrom}
data-resume-reading-dismiss={resumeReading.dismiss}
data-resume-reading-region={resumeReading.region}
Expand Down
31 changes: 31 additions & 0 deletions quartz/components/scripts/readLater.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { readLaterScript } from "./readLater"
import {
READ_LATER_LIMIT,
parseReadLaterEntries,
readLaterEntriesToMarkdown,
toggleReadLaterEntry,
type ReadLaterEntry,
} from "./readLaterStorage"
Expand All @@ -25,13 +26,18 @@ test("read-later browser script handles navigation and in-place renders", () =>
assert.match(readLaterScript, /window\.addCleanup\(cleanupReadLater\)/)
assert.match(readLaterScript, /event\.key !== null && event\.key !== READ_LATER_KEY/)
assert.match(readLaterScript, /const current = parseReadLaterEntry\(/)
assert.match(readLaterScript, /navigator\.clipboard\.writeText\(markdown\)/)
assert.match(readLaterScript, /exportButton\.disabled = entries\.length === 0/)
})

test("read-later labels use the central locale catalog", () => {
assert.equal(enUs.components.readLater.trigger({ count: 2 }), "Read later, 2 saved")
assert.equal(zhCn.components.readLater.trigger({ count: 2 }), "稍后读,已保存 2 篇")
assert.equal(zhTw.components.readLater.trigger({ count: 2 }), "稍後讀,已儲存 2 篇")
assert.equal(enUs.components.readLater.removeItem({ title: "Note" }), "Remove Note")
assert.equal(enUs.components.readLater.exportList, "Copy Markdown checklist")
assert.equal(zhCn.components.readLater.exported, "Markdown 清单已复制")
assert.equal(zhTw.components.readLater.exportFailed, "瀏覽器未能複製 Markdown 清單")
})

describe("read-later storage", () => {
Expand Down Expand Up @@ -127,4 +133,29 @@ describe("read-later storage", () => {
false,
)
})

test("exports an Obsidian-ready Markdown checklist in saved order", () => {
const entries: readonly ReadLaterEntry[] = [
{ path: "/notes/newest?q=one", title: " Newest\n note ", savedAt: 20 },
{ path: "/notes/road-map", title: String.raw`Road \ [Map]`, savedAt: 10 },
]

const markdown = readLaterEntriesToMarkdown(entries, "https://garden.example/current")

assert.equal(
markdown,
[
"- [ ] [Newest note](<https://garden.example/notes/newest?q=one>)",
String.raw`- [ ] [Road \\ \[Map\]](<https://garden.example/notes/road-map>)`,
].join("\n"),
)
})

test("returns no checklist for empty entries or a non-web base URL", () => {
const entries: readonly ReadLaterEntry[] = [{ path: "/note", title: "Note", savedAt: 10 }]

assert.equal(readLaterEntriesToMarkdown([], "https://garden.example/"), "")
assert.equal(readLaterEntriesToMarkdown(entries, "not a URL"), "")
assert.equal(readLaterEntriesToMarkdown(entries, "file:///garden/index.html"), "")
})
})
37 changes: 35 additions & 2 deletions quartz/components/scripts/readLater.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@ import {
READ_LATER_LIMIT,
parseReadLaterEntries,
parseReadLaterEntry,
readLaterEntriesToMarkdown,
toggleReadLaterEntry,
} from "./readLaterStorage"

export const readLaterScript = `
const READ_LATER_KEY = "dg3.readLater.v1"
const parseReadLaterEntry = ${parseReadLaterEntry.toString()}
const parseReadLaterEntries = ${parseReadLaterEntries.toString()}
const readLaterEntriesToMarkdown = ${readLaterEntriesToMarkdown.toString()}
const toggleReadLaterEntry = ${toggleReadLaterEntry.toString()}
const READ_LATER_LIMIT = ${READ_LATER_LIMIT}

Expand Down Expand Up @@ -55,6 +57,9 @@ function getReadLaterLabels() {
readLaterSaved: saved,
readLaterRemoved: removed,
readLaterFailed: failed,
readLaterExport: exportList,
readLaterExported: exported,
readLaterExportFailed: exportFailed,
} = document.body.dataset
if (
!title ||
Expand All @@ -66,7 +71,10 @@ function getReadLaterLabels() {
!empty ||
!saved ||
!removed ||
!failed
!failed ||
!exportList ||
!exported ||
!exportFailed
) {
return undefined
}
Expand Down Expand Up @@ -96,6 +104,9 @@ function getReadLaterLabels() {
saved,
removed,
failed,
exportList,
exported,
exportFailed,
}
}

Expand Down Expand Up @@ -155,10 +166,14 @@ function initializeReadLater() {
currentButton.append(createBookmarkIcon(), document.createElement("span"))
const list = document.createElement("ul")
list.className = "read-later-list"
const exportButton = document.createElement("button")
exportButton.type = "button"
exportButton.className = "read-later-export"
exportButton.textContent = labels.exportList
const status = document.createElement("p")
status.className = "read-later-status"
status.setAttribute("aria-live", "polite")
panel.append(header, currentButton, list, status)
panel.append(header, currentButton, list, exportButton, status)
root.append(trigger, panel)
anchor.insertAdjacentElement("afterend", root)

Expand All @@ -178,6 +193,7 @@ function initializeReadLater() {
count.textContent = String(entries.length)
trigger.title = labels.trigger(entries.length)
trigger.setAttribute("aria-label", labels.trigger(entries.length))
exportButton.disabled = entries.length === 0
list.replaceChildren()
if (entries.length === 0) {
const empty = document.createElement("li")
Expand Down Expand Up @@ -231,6 +247,23 @@ function initializeReadLater() {
status.textContent = wasSaved ? labels.removed : labels.saved
render()
})
exportButton.addEventListener("click", async () => {
const markdown = readLaterEntriesToMarkdown(entries, location.href)
if (markdown.length === 0 || navigator.clipboard === undefined) {
status.textContent = labels.exportFailed
return
}

exportButton.disabled = true
try {
await navigator.clipboard.writeText(markdown)
status.textContent = labels.exported
} catch {
status.textContent = labels.exportFailed
} finally {
exportButton.disabled = entries.length === 0
}
})
const dismissOutside = (event) => {
if (event.target instanceof Node && !root.contains(event.target)) closePanel(false)
}
Expand Down
29 changes: 29 additions & 0 deletions quartz/components/scripts/readLaterStorage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,35 @@ export function parseReadLaterEntries(raw: string | null): readonly ReadLaterEnt
.slice(0, READ_LATER_LIMIT)
}

export function readLaterEntriesToMarkdown(
entries: readonly ReadLaterEntry[],
baseUrl: string,
): string {
let base: URL
try {
base = new URL(baseUrl)
} catch {
return ""
}
if (base.protocol !== "http:" && base.protocol !== "https:") return ""

const lines: string[] = []
for (const candidate of entries) {
const entry = parseReadLaterEntry(candidate)
if (entry === undefined) continue

const destination = new URL(entry.path, base)
if (destination.origin !== base.origin) continue
const label = entry.title
.replace(/\s+/gu, " ")
.trim()
.replace(/([\\[\]])/gu, "\\$1")
lines.push(`- [ ] [${label}](<${destination.href}>)`)
}

return lines.join("\n")
}

export function toggleReadLaterEntry(
entries: readonly ReadLaterEntry[],
current: ReadLaterEntry,
Expand Down
26 changes: 24 additions & 2 deletions quartz/components/styles/readLater.scss
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@
.read-later-trigger,
.read-later-close,
.read-later-current,
.read-later-remove {
.read-later-remove,
.read-later-export {
appearance: none;
box-sizing: border-box;
border: 1px solid color-mix(in srgb, var(--gray) 54%, transparent);
Expand Down Expand Up @@ -40,6 +41,15 @@
outline: 2px solid var(--secondary);
outline-offset: 2px;
}

&:disabled {
border-color: color-mix(in srgb, var(--gray) 36%, transparent);
background-color: var(--light);
color: var(--gray);
cursor: not-allowed;
opacity: 0.7;
transform: none;
}
}

.read-later-trigger {
Expand Down Expand Up @@ -148,6 +158,17 @@
text-align: start;
}

.read-later-export {
display: flex;
width: 100%;
min-height: 2.5rem;
margin-top: 0.75rem;
padding: 0.5rem 0.75rem;
align-items: center;
justify-content: center;
text-align: center;
}

.read-later-list {
max-height: 14rem;
margin: 0.75rem 0 0;
Expand Down Expand Up @@ -192,7 +213,8 @@
.read-later-trigger,
.read-later-close,
.read-later-current,
.read-later-remove {
.read-later-remove,
.read-later-export {
transition: none;

&:active {
Expand Down
3 changes: 3 additions & 0 deletions quartz/i18n/locales/definition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,9 @@ export interface Translation {
saved: string
removed: string
failed: string
exportList: string
exported: string
exportFailed: string
}
resumeReading?: {
continueFrom: string
Expand Down
3 changes: 3 additions & 0 deletions quartz/i18n/locales/en-US.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,9 @@ export default {
saved: "Saved for later",
removed: "Removed from read later",
failed: "The browser could not save this change",
exportList: "Copy Markdown checklist",
exported: "Markdown checklist copied",
exportFailed: "The browser could not copy the Markdown checklist",
},
resumeReading: {
continueFrom: "Continue from {percent}%",
Expand Down
3 changes: 3 additions & 0 deletions quartz/i18n/locales/zh-CN.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,9 @@ export default {
saved: "已加入稍后读",
removed: "已从稍后读移除",
failed: "浏览器未能保存更改",
exportList: "复制 Markdown 清单",
exported: "Markdown 清单已复制",
exportFailed: "浏览器未能复制 Markdown 清单",
},
resumeReading: {
continueFrom: "从 {percent}% 继续阅读",
Expand Down
3 changes: 3 additions & 0 deletions quartz/i18n/locales/zh-TW.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@ export default {
saved: "已加入稍後讀",
removed: "已從稍後讀移除",
failed: "瀏覽器未能儲存變更",
exportList: "複製 Markdown 清單",
exported: "Markdown 清單已複製",
exportFailed: "瀏覽器未能複製 Markdown 清單",
},
resumeReading: {
continueFrom: "從 {percent}% 繼續閱讀",
Expand Down