mirror of
https://github.com/jackyzha0/quartz.git
synced 2025-05-18 22:44:14 +02:00
Compare commits
3 Commits
99027dad59
...
fc4a52b9a3
Author | SHA1 | Date | |
---|---|---|---|
![]() |
fc4a52b9a3 | ||
![]() |
fbc45548f7 | ||
![]() |
0bafa4c94a |
@ -36,6 +36,7 @@ Component.Graph({
|
|||||||
opacityScale: 1, // how quickly do we fade out the labels when zooming out?
|
opacityScale: 1, // how quickly do we fade out the labels when zooming out?
|
||||||
removeTags: [], // what tags to remove from the graph
|
removeTags: [], // what tags to remove from the graph
|
||||||
showTags: true, // whether to show tags in the graph
|
showTags: true, // whether to show tags in the graph
|
||||||
|
enableRadial: false, // whether to constrain the graph, similar to Obsidian
|
||||||
},
|
},
|
||||||
globalGraph: {
|
globalGraph: {
|
||||||
drag: true,
|
drag: true,
|
||||||
@ -49,6 +50,7 @@ Component.Graph({
|
|||||||
opacityScale: 1,
|
opacityScale: 1,
|
||||||
removeTags: [], // what tags to remove from the graph
|
removeTags: [], // what tags to remove from the graph
|
||||||
showTags: true, // whether to show tags in the graph
|
showTags: true, // whether to show tags in the graph
|
||||||
|
enableRadial: true, // whether to constrain the graph, similar to Obsidian
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
```
|
```
|
||||||
|
@ -40,11 +40,8 @@ const defaultOptions: BreadcrumbOptions = {
|
|||||||
showCurrentPage: true,
|
showCurrentPage: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatCrumb(displayName: string, baseSlug: FullSlug, currentSlug: SimpleSlug): CrumbData {
|
function newCrumb(displayName: string, baseSlug: FullSlug, currentSlug: SimpleSlug): CrumbData {
|
||||||
return {
|
return { displayName, path: resolveRelative(baseSlug, currentSlug) }
|
||||||
displayName: displayName.replaceAll("-", " "),
|
|
||||||
path: resolveRelative(baseSlug, currentSlug),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ((opts?: Partial<BreadcrumbOptions>) => {
|
export default ((opts?: Partial<BreadcrumbOptions>) => {
|
||||||
@ -65,7 +62,7 @@ export default ((opts?: Partial<BreadcrumbOptions>) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Format entry for root element
|
// Format entry for root element
|
||||||
const firstEntry = formatCrumb(options.rootName, fileData.slug!, "/" as SimpleSlug)
|
const firstEntry = newCrumb(options.rootName, fileData.slug!, "/" as SimpleSlug)
|
||||||
const crumbs: CrumbData[] = [firstEntry]
|
const crumbs: CrumbData[] = [firstEntry]
|
||||||
|
|
||||||
if (!folderIndex && options.resolveFrontmatterTitle) {
|
if (!folderIndex && options.resolveFrontmatterTitle) {
|
||||||
@ -81,6 +78,7 @@ export default ((opts?: Partial<BreadcrumbOptions>) => {
|
|||||||
|
|
||||||
// Split slug into hierarchy/parts
|
// Split slug into hierarchy/parts
|
||||||
const slugParts = fileData.slug?.split("/")
|
const slugParts = fileData.slug?.split("/")
|
||||||
|
const pathParts = fileData.relativePath?.split("/")
|
||||||
if (slugParts) {
|
if (slugParts) {
|
||||||
// is tag breadcrumb?
|
// is tag breadcrumb?
|
||||||
const isTagPath = slugParts[0] === "tags"
|
const isTagPath = slugParts[0] === "tags"
|
||||||
@ -89,7 +87,7 @@ export default ((opts?: Partial<BreadcrumbOptions>) => {
|
|||||||
let currentPath = ""
|
let currentPath = ""
|
||||||
|
|
||||||
for (let i = 0; i < slugParts.length - 1; i++) {
|
for (let i = 0; i < slugParts.length - 1; i++) {
|
||||||
let curPathSegment = slugParts[i]
|
let curPathSegment = pathParts?.[i] ?? slugParts[i]
|
||||||
|
|
||||||
// Try to resolve frontmatter folder title
|
// Try to resolve frontmatter folder title
|
||||||
const currentFile = folderIndex?.get(slugParts.slice(0, i + 1).join("/"))
|
const currentFile = folderIndex?.get(slugParts.slice(0, i + 1).join("/"))
|
||||||
@ -105,7 +103,7 @@ export default ((opts?: Partial<BreadcrumbOptions>) => {
|
|||||||
const includeTrailingSlash = !isTagPath || i < 1
|
const includeTrailingSlash = !isTagPath || i < 1
|
||||||
|
|
||||||
// Format and add current crumb
|
// Format and add current crumb
|
||||||
const crumb = formatCrumb(
|
const crumb = newCrumb(
|
||||||
curPathSegment,
|
curPathSegment,
|
||||||
fileData.slug!,
|
fileData.slug!,
|
||||||
(currentPath + (includeTrailingSlash ? "/" : "")) as SimpleSlug,
|
(currentPath + (includeTrailingSlash ? "/" : "")) as SimpleSlug,
|
||||||
|
@ -18,6 +18,7 @@ export interface D3Config {
|
|||||||
removeTags: string[]
|
removeTags: string[]
|
||||||
showTags: boolean
|
showTags: boolean
|
||||||
focusOnHover?: boolean
|
focusOnHover?: boolean
|
||||||
|
enableRadial?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
interface GraphOptions {
|
interface GraphOptions {
|
||||||
@ -39,6 +40,7 @@ const defaultOptions: GraphOptions = {
|
|||||||
showTags: true,
|
showTags: true,
|
||||||
removeTags: [],
|
removeTags: [],
|
||||||
focusOnHover: false,
|
focusOnHover: false,
|
||||||
|
enableRadial: false,
|
||||||
},
|
},
|
||||||
globalGraph: {
|
globalGraph: {
|
||||||
drag: true,
|
drag: true,
|
||||||
@ -53,10 +55,11 @@ const defaultOptions: GraphOptions = {
|
|||||||
showTags: true,
|
showTags: true,
|
||||||
removeTags: [],
|
removeTags: [],
|
||||||
focusOnHover: true,
|
focusOnHover: true,
|
||||||
|
enableRadial: true,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ((opts?: GraphOptions) => {
|
export default ((opts?: Partial<GraphOptions>) => {
|
||||||
const Graph: QuartzComponent = ({ displayClass, cfg }: QuartzComponentProps) => {
|
const Graph: QuartzComponent = ({ displayClass, cfg }: QuartzComponentProps) => {
|
||||||
const localGraph = { ...defaultOptions.localGraph, ...opts?.localGraph }
|
const localGraph = { ...defaultOptions.localGraph, ...opts?.localGraph }
|
||||||
const globalGraph = { ...defaultOptions.globalGraph, ...opts?.globalGraph }
|
const globalGraph = { ...defaultOptions.globalGraph, ...opts?.globalGraph }
|
||||||
|
@ -23,6 +23,11 @@ const defaultOptions: FolderContentOptions = {
|
|||||||
showSubfolders: true,
|
showSubfolders: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Subfolder = {
|
||||||
|
name: string
|
||||||
|
contents: QuartzPluginData[]
|
||||||
|
}
|
||||||
|
|
||||||
export default ((opts?: Partial<FolderContentOptions>) => {
|
export default ((opts?: Partial<FolderContentOptions>) => {
|
||||||
const options: FolderContentOptions = { ...defaultOptions, ...opts }
|
const options: FolderContentOptions = { ...defaultOptions, ...opts }
|
||||||
|
|
||||||
@ -31,51 +36,56 @@ export default ((opts?: Partial<FolderContentOptions>) => {
|
|||||||
const folderSlug = stripSlashes(simplifySlug(fileData.slug!))
|
const folderSlug = stripSlashes(simplifySlug(fileData.slug!))
|
||||||
const folderParts = folderSlug.split(path.posix.sep)
|
const folderParts = folderSlug.split(path.posix.sep)
|
||||||
|
|
||||||
const allPagesInFolder: QuartzPluginData[] = []
|
const shownPages: QuartzPluginData[] = []
|
||||||
const allPagesInSubfolders: Map<FullSlug, QuartzPluginData[]> = new Map()
|
const subfolders: Map<FullSlug, Subfolder> = new Map()
|
||||||
|
|
||||||
allFiles.forEach((file) => {
|
for (const file of allFiles) {
|
||||||
const fileSlug = stripSlashes(simplifySlug(file.slug!))
|
const fileSlug = stripSlashes(simplifySlug(file.slug!))
|
||||||
const prefixed = fileSlug.startsWith(folderSlug) && fileSlug !== folderSlug
|
// check only files in our folder or nested folders
|
||||||
const fileParts = fileSlug.split(path.posix.sep)
|
if (!fileSlug.startsWith(folderSlug) || fileSlug === folderSlug) {
|
||||||
const isDirectChild = fileParts.length === folderParts.length + 1
|
continue
|
||||||
|
|
||||||
if (!prefixed) {
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isDirectChild) {
|
const fileParts = fileSlug.split(path.posix.sep)
|
||||||
allPagesInFolder.push(file)
|
|
||||||
} else if (options.showSubfolders) {
|
// If the file is directly in the folder we just show it
|
||||||
|
if (fileParts.length === folderParts.length + 1) {
|
||||||
|
shownPages.push(file)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.showSubfolders) {
|
||||||
const subfolderSlug = joinSegments(
|
const subfolderSlug = joinSegments(
|
||||||
...fileParts.slice(0, folderParts.length + 1),
|
...fileParts.slice(0, folderParts.length + 1),
|
||||||
) as FullSlug
|
) as FullSlug
|
||||||
const pagesInFolder = allPagesInSubfolders.get(subfolderSlug) || []
|
|
||||||
allPagesInSubfolders.set(subfolderSlug, [...pagesInFolder, file])
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
allPagesInSubfolders.forEach((files, subfolderSlug) => {
|
let subfolder = subfolders.get(subfolderSlug)
|
||||||
const hasIndex = allPagesInFolder.some(
|
if (!subfolder) {
|
||||||
(file) => subfolderSlug === stripSlashes(simplifySlug(file.slug!)),
|
const subfolderName = file.relativePath!.split(path.posix.sep).at(folderParts.length)!
|
||||||
)
|
subfolders.set(subfolderSlug, (subfolder = { name: subfolderName, contents: [] }))
|
||||||
|
}
|
||||||
|
subfolder.contents.push(file)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [slug, subfolder] of subfolders.entries()) {
|
||||||
|
const hasIndex = shownPages.some((file) => slug === stripSlashes(simplifySlug(file.slug!)))
|
||||||
if (!hasIndex) {
|
if (!hasIndex) {
|
||||||
const subfolderDates = files.sort(byDateAndAlphabetical(cfg))[0].dates
|
const subfolderDates = subfolder.contents.sort(byDateAndAlphabetical(cfg))[0].dates
|
||||||
const subfolderTitle = subfolderSlug.split(path.posix.sep).at(-1)!
|
shownPages.push({
|
||||||
allPagesInFolder.push({
|
slug: slug,
|
||||||
slug: subfolderSlug,
|
|
||||||
dates: subfolderDates,
|
dates: subfolderDates,
|
||||||
frontmatter: { title: subfolderTitle, tags: ["folder"] },
|
frontmatter: { title: subfolder.name, tags: ["folder"] },
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
|
||||||
const cssClasses: string[] = fileData.frontmatter?.cssclasses ?? []
|
const cssClasses: string[] = fileData.frontmatter?.cssclasses ?? []
|
||||||
const classes = cssClasses.join(" ")
|
const classes = cssClasses.join(" ")
|
||||||
const listProps = {
|
const listProps = {
|
||||||
...props,
|
...props,
|
||||||
sort: options.sort,
|
sort: options.sort,
|
||||||
allFiles: allPagesInFolder,
|
allFiles: shownPages,
|
||||||
}
|
}
|
||||||
|
|
||||||
const content =
|
const content =
|
||||||
@ -90,7 +100,7 @@ export default ((opts?: Partial<FolderContentOptions>) => {
|
|||||||
{options.showFolderCount && (
|
{options.showFolderCount && (
|
||||||
<p>
|
<p>
|
||||||
{i18n(cfg.locale).pages.folderContent.itemsUnderFolder({
|
{i18n(cfg.locale).pages.folderContent.itemsUnderFolder({
|
||||||
count: allPagesInFolder.length,
|
count: shownPages.length,
|
||||||
})}
|
})}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
@ -8,6 +8,7 @@ import {
|
|||||||
forceCenter,
|
forceCenter,
|
||||||
forceLink,
|
forceLink,
|
||||||
forceCollide,
|
forceCollide,
|
||||||
|
forceRadial,
|
||||||
zoomIdentity,
|
zoomIdentity,
|
||||||
select,
|
select,
|
||||||
drag,
|
drag,
|
||||||
@ -87,6 +88,7 @@ async function renderGraph(container: string, fullSlug: FullSlug) {
|
|||||||
removeTags,
|
removeTags,
|
||||||
showTags,
|
showTags,
|
||||||
focusOnHover,
|
focusOnHover,
|
||||||
|
enableRadial,
|
||||||
} = JSON.parse(graph.dataset["cfg"]!) as D3Config
|
} = JSON.parse(graph.dataset["cfg"]!) as D3Config
|
||||||
|
|
||||||
const data: Map<SimpleSlug, ContentDetails> = new Map(
|
const data: Map<SimpleSlug, ContentDetails> = new Map(
|
||||||
@ -161,15 +163,20 @@ async function renderGraph(container: string, fullSlug: FullSlug) {
|
|||||||
})),
|
})),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const width = graph.offsetWidth
|
||||||
|
const height = Math.max(graph.offsetHeight, 250)
|
||||||
|
|
||||||
// we virtualize the simulation and use pixi to actually render it
|
// we virtualize the simulation and use pixi to actually render it
|
||||||
|
// Calculate the radius of the container circle
|
||||||
|
const radius = Math.min(width, height) / 2 - 40 // 40px padding
|
||||||
const simulation: Simulation<NodeData, LinkData> = forceSimulation<NodeData>(graphData.nodes)
|
const simulation: Simulation<NodeData, LinkData> = forceSimulation<NodeData>(graphData.nodes)
|
||||||
.force("charge", forceManyBody().strength(-100 * repelForce))
|
.force("charge", forceManyBody().strength(-100 * repelForce))
|
||||||
.force("center", forceCenter().strength(centerForce))
|
.force("center", forceCenter().strength(centerForce))
|
||||||
.force("link", forceLink(graphData.links).distance(linkDistance))
|
.force("link", forceLink(graphData.links).distance(linkDistance))
|
||||||
.force("collide", forceCollide<NodeData>((n) => nodeRadius(n)).iterations(3))
|
.force("collide", forceCollide<NodeData>((n) => nodeRadius(n)).iterations(3))
|
||||||
|
|
||||||
const width = graph.offsetWidth
|
if (enableRadial)
|
||||||
const height = Math.max(graph.offsetHeight, 250)
|
simulation.force("radial", forceRadial(radius * 0.8, width / 2, height / 2).strength(0.3))
|
||||||
|
|
||||||
// precompute style prop strings as pixi doesn't support css variables
|
// precompute style prop strings as pixi doesn't support css variables
|
||||||
const cssVars = [
|
const cssVars = [
|
||||||
|
@ -74,13 +74,27 @@ export const FolderPage: QuartzEmitterPlugin<Partial<FolderPageOptions>> = (user
|
|||||||
const allFiles = content.map((c) => c[1].data)
|
const allFiles = content.map((c) => c[1].data)
|
||||||
const cfg = ctx.cfg.configuration
|
const cfg = ctx.cfg.configuration
|
||||||
|
|
||||||
|
const folderNames: Record<SimpleSlug, string> = {}
|
||||||
|
|
||||||
const folders: Set<SimpleSlug> = new Set(
|
const folders: Set<SimpleSlug> = new Set(
|
||||||
allFiles.flatMap((data) => {
|
allFiles.flatMap((data) => {
|
||||||
return data.slug
|
if (!data.slug || !data.relativePath) {
|
||||||
? _getFolders(data.slug).filter(
|
return []
|
||||||
(folderName) => folderName !== "." && folderName !== "tags",
|
}
|
||||||
)
|
let folderSlug = path.dirname(data.slug) as SimpleSlug
|
||||||
: []
|
let folderFs = path.dirname(data.relativePath) as SimpleSlug
|
||||||
|
folderNames[folderSlug] = folderFs
|
||||||
|
|
||||||
|
const folders = [folderSlug]
|
||||||
|
while (folderSlug !== ".") {
|
||||||
|
folderSlug = path.dirname(folderSlug) as SimpleSlug
|
||||||
|
folders.push(folderSlug)
|
||||||
|
|
||||||
|
folderFs = path.dirname(folderFs) as SimpleSlug
|
||||||
|
folderNames[folderSlug] = folderFs
|
||||||
|
}
|
||||||
|
|
||||||
|
return folders.filter((f) => f !== "." && f !== "tags")
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -89,8 +103,9 @@ export const FolderPage: QuartzEmitterPlugin<Partial<FolderPageOptions>> = (user
|
|||||||
folder,
|
folder,
|
||||||
defaultProcessedContent({
|
defaultProcessedContent({
|
||||||
slug: joinSegments(folder, "index") as FullSlug,
|
slug: joinSegments(folder, "index") as FullSlug,
|
||||||
|
relativePath: joinSegments(folderNames[folder], "index.html") as FilePath, // this is used by breadcrumbs
|
||||||
frontmatter: {
|
frontmatter: {
|
||||||
title: `${i18n(cfg.locale).pages.folderContent.folder}: ${folder}`,
|
title: `${i18n(cfg.locale).pages.folderContent.folder}: ${folderNames[folder]}`,
|
||||||
tags: [],
|
tags: [],
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
@ -100,7 +115,14 @@ export const FolderPage: QuartzEmitterPlugin<Partial<FolderPageOptions>> = (user
|
|||||||
for (const [tree, file] of content) {
|
for (const [tree, file] of content) {
|
||||||
const slug = stripSlashes(simplifySlug(file.data.slug!)) as SimpleSlug
|
const slug = stripSlashes(simplifySlug(file.data.slug!)) as SimpleSlug
|
||||||
if (folders.has(slug)) {
|
if (folders.has(slug)) {
|
||||||
folderDescriptions[slug] = [tree, file]
|
if (file.data.frontmatter?.title === "index") {
|
||||||
|
// sadly we need to avoid changing the original file title for things like explorer to work
|
||||||
|
const clonedFile = structuredClone(file)
|
||||||
|
clonedFile.data.frontmatter!.title = `${i18n(cfg.locale).pages.folderContent.folder}: ${folderNames[slug]}`
|
||||||
|
folderDescriptions[slug] = [tree, clonedFile]
|
||||||
|
} else {
|
||||||
|
folderDescriptions[slug] = [tree, file]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -132,14 +154,3 @@ export const FolderPage: QuartzEmitterPlugin<Partial<FolderPageOptions>> = (user
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function _getFolders(slug: FullSlug): SimpleSlug[] {
|
|
||||||
var folderName = path.dirname(slug ?? "") as SimpleSlug
|
|
||||||
const parentFolderNames = [folderName]
|
|
||||||
|
|
||||||
while (folderName !== ".") {
|
|
||||||
folderName = path.dirname(folderName ?? "") as SimpleSlug
|
|
||||||
parentFolderNames.push(folderName)
|
|
||||||
}
|
|
||||||
return parentFolderNames
|
|
||||||
}
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user