secondbrain-py 0.2.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- brain/__init__.py +0 -0
- brain/__main__.py +18 -0
- brain/_capture_command.py +445 -0
- brain/_compose.py +52 -0
- brain/activity.py +206 -0
- brain/ask.py +631 -0
- brain/audio.py +591 -0
- brain/backfill/__init__.py +12 -0
- brain/backfill/search_extras.py +141 -0
- brain/backfill/source_rows.py +101 -0
- brain/bin/__init__.py +1 -0
- brain/bin/_launcher.py +107 -0
- brain/bin/down.py +8 -0
- brain/bin/launchd.py +268 -0
- brain/bin/monitor.py +570 -0
- brain/bin/rebuild.py +8 -0
- brain/bin/status.py +8 -0
- brain/bin/up.py +8 -0
- brain/brief.py +272 -0
- brain/capture.py +49 -0
- brain/chat.py +293 -0
- brain/cli.py +9760 -0
- brain/cli_claude.py +81 -0
- brain/cli_connect.py +285 -0
- brain/cli_demo.py +266 -0
- brain/config.py +1949 -0
- brain/connect.py +925 -0
- brain/db.py +540 -0
- brain/demo/__init__.py +452 -0
- brain/demo/corpus/manifest.json +403 -0
- brain/demo/embedder.py +74 -0
- brain/durations.py +84 -0
- brain/edit_session.py +156 -0
- brain/editor.py +67 -0
- brain/elicit/__init__.py +16 -0
- brain/elicit/detectors.py +250 -0
- brain/elicit/drafter.py +70 -0
- brain/elicit/queue.py +220 -0
- brain/elicit/schema.py +48 -0
- brain/elicit/session.py +445 -0
- brain/embedding_targets.py +54 -0
- brain/embeddings.py +424 -0
- brain/enrichment.py +808 -0
- brain/errors.py +357 -0
- brain/eval/__init__.py +129 -0
- brain/eval/answer_eval.py +281 -0
- brain/eval/baseline.py +265 -0
- brain/eval/concept_extraction.py +378 -0
- brain/eval/corpus.py +152 -0
- brain/eval/errors.py +19 -0
- brain/eval/graph_baseline.py +226 -0
- brain/eval/graph_retrieval.py +202 -0
- brain/eval/graph_runner.py +319 -0
- brain/eval/metrics.py +101 -0
- brain/eval/runner.py +223 -0
- brain/format.py +783 -0
- brain/gaps.py +390 -0
- brain/graph_rag/__init__.py +94 -0
- brain/graph_rag/_retrieval_common.py +113 -0
- brain/graph_rag/aggregates.py +303 -0
- brain/graph_rag/aliases/__init__.py +583 -0
- brain/graph_rag/backends/__init__.py +10 -0
- brain/graph_rag/backends/_age_helpers.py +473 -0
- brain/graph_rag/backends/age.py +782 -0
- brain/graph_rag/backends/base.py +272 -0
- brain/graph_rag/build.py +344 -0
- brain/graph_rag/communities.py +644 -0
- brain/graph_rag/communities_summary.py +437 -0
- brain/graph_rag/concepts.py +202 -0
- brain/graph_rag/cooccur.py +193 -0
- brain/graph_rag/cross_type.py +312 -0
- brain/graph_rag/extract.py +885 -0
- brain/graph_rag/fuse.py +371 -0
- brain/graph_rag/global_.py +412 -0
- brain/graph_rag/grouping.py +372 -0
- brain/graph_rag/person_resolver.py +167 -0
- brain/graph_rag/reconcile.py +792 -0
- brain/graph_rag/relational.py +353 -0
- brain/graph_rag/retrieve.py +526 -0
- brain/graph_rag/router.py +288 -0
- brain/graph_rag/schema.py +320 -0
- brain/graph_rag/sync.py +237 -0
- brain/graph_rag/tenancy.py +43 -0
- brain/graph_rag/themes.py +501 -0
- brain/graph_rag/weighting.py +202 -0
- brain/ingest/__init__.py +1926 -0
- brain/ingest/chunker.py +249 -0
- brain/ingest/docx.py +40 -0
- brain/ingest/gmail.py +621 -0
- brain/ingest/markdown.py +37 -0
- brain/ingest/pdf.py +61 -0
- brain/ingest/stdin.py +22 -0
- brain/ingest/sub_tokens.py +91 -0
- brain/ingest/text.py +16 -0
- brain/interactions.py +205 -0
- brain/maintenance.py +355 -0
- brain/mcp_server.py +3405 -0
- brain/migrations/001_init.sql +43 -0
- brain/migrations/002_qwen3_embedding.sql +17 -0
- brain/migrations/003_vault_model.sql +41 -0
- brain/migrations/004_relax_content_hash_uniqueness.sql +18 -0
- brain/migrations/005_derived_links.sql +67 -0
- brain/migrations/006_dedup_file_by_source_path.sql +25 -0
- brain/migrations/007_email_thread_and_draft.sql +15 -0
- brain/migrations/008_gmail_thread_unique.sql +11 -0
- brain/migrations/009_chunks_weighted_tsv.sql +28 -0
- brain/migrations/010_interactions.sql +30 -0
- brain/migrations/011_documents_summary.sql +23 -0
- brain/migrations/012_graphrag.sql +171 -0
- brain/migrations/013_graphrag_communities.sql +125 -0
- brain/migrations/014_graphrag_community_summary_hash.sql +33 -0
- brain/migrations/015_interactions_graph_targets.sql +89 -0
- brain/migrations/016_index_hygiene.sql +61 -0
- brain/migrations/017_elicit.sql +30 -0
- brain/migrations/018_review_gap_signal_kinds.sql +40 -0
- brain/migrations/019_search_queries.sql +35 -0
- brain/migrations/020_link_suggestions.sql +40 -0
- brain/migrations/021_timeline_doc_date.sql +34 -0
- brain/migrations/022_link_suggestions_undirected.sql +84 -0
- brain/migrations/023_search_queries_fts_count.sql +28 -0
- brain/quartz_overrides/__init__.py +8 -0
- brain/quartz_overrides/quartz/bootstrap-cli.mjs +65 -0
- brain/quartz_overrides/quartz/build.ts +568 -0
- brain/quartz_overrides/quartz/cli/args.js +152 -0
- brain/quartz_overrides/quartz/cli/build_partial_handler.js +544 -0
- brain/quartz_overrides/quartz/cli/handlers.js +636 -0
- brain/quartz_overrides/quartz/components/CommandPalette.tsx +172 -0
- brain/quartz_overrides/quartz/components/Explorer.tsx +198 -0
- brain/quartz_overrides/quartz/components/Footer.tsx +27 -0
- brain/quartz_overrides/quartz/components/Graph.tsx +468 -0
- brain/quartz_overrides/quartz/components/PageTitle.tsx +72 -0
- brain/quartz_overrides/quartz/components/RelatedDocs.tsx +38 -0
- brain/quartz_overrides/quartz/components/Search.tsx +161 -0
- brain/quartz_overrides/quartz/components/SummaryLede.tsx +72 -0
- brain/quartz_overrides/quartz/components/index.ts +92 -0
- brain/quartz_overrides/quartz/components/pages/TagContent.tsx +272 -0
- brain/quartz_overrides/quartz/components/scripts/commandPalette.inline.ts +665 -0
- brain/quartz_overrides/quartz/components/scripts/explorer.inline.ts +768 -0
- brain/quartz_overrides/quartz/components/scripts/graph.inline.ts +2302 -0
- brain/quartz_overrides/quartz/components/scripts/relatedDocs.inline.ts +163 -0
- brain/quartz_overrides/quartz/components/scripts/search.inline.ts +1011 -0
- brain/quartz_overrides/quartz/plugins/emitters/contentIndex.ts +546 -0
- brain/quartz_overrides/quartz/plugins/transformers/codeCopy.ts +94 -0
- brain/quartz_overrides/quartz/plugins/transformers/derivedFenceMark.ts +302 -0
- brain/quartz_overrides/quartz/plugins/transformers/emailThread.ts +148 -0
- brain/quartz_overrides/quartz/plugins/transformers/emptyDoorFilter.ts +213 -0
- brain/quartz_overrides/quartz/plugins/transformers/index.ts +114 -0
- brain/quartz_overrides/quartz/plugins/transformers/linkKindMark.ts +205 -0
- brain/quartz_overrides/quartz/plugins/transformers/linkSourceTag.ts +104 -0
- brain/quartz_overrides/quartz/plugins/transformers/relativeDate.ts +100 -0
- brain/quartz_overrides/quartz/plugins/transformers/reloadSignal.ts +131 -0
- brain/quartz_overrides/quartz/processors/parse.ts +371 -0
- brain/quartz_overrides/quartz/processors/parser_cache.ts +78 -0
- brain/quartz_overrides/quartz/static/brain-logo-dark.png +0 -0
- brain/quartz_overrides/quartz/static/brain-logo-light.png +0 -0
- brain/quartz_overrides/quartz/static/codeCopy.js +196 -0
- brain/quartz_overrides/quartz/static/emailThread.js +334 -0
- brain/quartz_overrides/quartz/static/favicon.ico +0 -0
- brain/quartz_overrides/quartz/static/icon.png +0 -0
- brain/quartz_overrides/quartz/static/linkSourceTag.js +104 -0
- brain/quartz_overrides/quartz/static/relativeDate.js +142 -0
- brain/quartz_overrides/quartz/static/reload.js +168 -0
- brain/quartz_overrides/quartz/styles/brain/_article.scss +252 -0
- brain/quartz_overrides/quartz/styles/brain/_atmosphere.scss +113 -0
- brain/quartz_overrides/quartz/styles/brain/_callouts.scss +180 -0
- brain/quartz_overrides/quartz/styles/brain/_cmdk.scss +7 -0
- brain/quartz_overrides/quartz/styles/brain/_code.scss +208 -0
- brain/quartz_overrides/quartz/styles/brain/_command_palette.scss +369 -0
- brain/quartz_overrides/quartz/styles/brain/_email_thread.scss +228 -0
- brain/quartz_overrides/quartz/styles/brain/_explorer.scss +142 -0
- brain/quartz_overrides/quartz/styles/brain/_home.scss +182 -0
- brain/quartz_overrides/quartz/styles/brain/_links.scss +322 -0
- brain/quartz_overrides/quartz/styles/brain/_marginalia.scss +117 -0
- brain/quartz_overrides/quartz/styles/brain/_motion.scss +175 -0
- brain/quartz_overrides/quartz/styles/brain/_people_hub.scss +100 -0
- brain/quartz_overrides/quartz/styles/brain/_related_docs.scss +137 -0
- brain/quartz_overrides/quartz/styles/brain/_search.scss +252 -0
- brain/quartz_overrides/quartz/styles/brain/_sidebar.scss +468 -0
- brain/quartz_overrides/quartz/styles/brain/_summary_lede.scss +56 -0
- brain/quartz_overrides/quartz/styles/brain/_surface.scss +43 -0
- brain/quartz_overrides/quartz/styles/brain/_tag_content.scss +118 -0
- brain/quartz_overrides/quartz/styles/brain/_tokens.scss +197 -0
- brain/quartz_overrides/quartz/styles/brain/_typography.scss +92 -0
- brain/quartz_overrides/quartz/styles/custom.scss +89 -0
- brain/quartz_overrides/quartz/styles/graph.scss +505 -0
- brain/quartz_overrides/quartz/util/ctx.ts +92 -0
- brain/quartz_overrides/quartz/util/fastpath_manifest.ts +608 -0
- brain/quartz_overrides/quartz/util/path.ts +358 -0
- brain/quartz_overrides/quartz/util/sourceIcons.ts +55 -0
- brain/quartz_overrides/quartz.config.ts +270 -0
- brain/quartz_overrides/quartz.layout.ts +314 -0
- brain/queries.py +1188 -0
- brain/rank_fusion.py +8 -0
- brain/resurface.py +210 -0
- brain/review/__init__.py +26 -0
- brain/review/emit.py +27 -0
- brain/review/queries.py +436 -0
- brain/review/render.py +196 -0
- brain/review/scans.py +355 -0
- brain/review/weekly.py +413 -0
- brain/search.py +704 -0
- brain/set_similarity.py +15 -0
- brain/setup.py +1205 -0
- brain/tags.py +56 -0
- brain/templates/Caddyfile.j2 +9 -0
- brain/templates/__init__.py +1 -0
- brain/templates/bin/__init__.py +1 -0
- brain/templates/bin/_brain-brief-fg.sh +25 -0
- brain/templates/bin/_brain-build-fg.sh +53 -0
- brain/templates/bin/_brain-watcher-fg.sh +65 -0
- brain/templates/bin/brain-down.sh +89 -0
- brain/templates/bin/brain-status.sh +83 -0
- brain/templates/bin/brain-up.sh +221 -0
- brain/templates/docker/age/Dockerfile +79 -0
- brain/templates/docker-compose.stock.yml.j2 +26 -0
- brain/templates/docker-compose.yml.j2 +34 -0
- brain/templates/env.example +190 -0
- brain/templates/launchd/__init__.py +1 -0
- brain/templates/launchd/com.brain.brief.plist.j2 +45 -0
- brain/templates/launchd/com.brain.build.plist.j2 +46 -0
- brain/templates/launchd/com.brain.watcher.plist.j2 +46 -0
- brain/templates/skill/SKILL.md +63 -0
- brain/templates/skill/__init__.py +1 -0
- brain/timeline.py +834 -0
- brain/todo.py +124 -0
- brain/uninstall.py +185 -0
- brain/vault/__init__.py +115 -0
- brain/vault/_atomic.py +25 -0
- brain/vault/daily_index.py +228 -0
- brain/vault/derived_links/__init__.py +50 -0
- brain/vault/derived_links/directory.py +683 -0
- brain/vault/derived_links/fence.py +408 -0
- brain/vault/derived_links/gws.py +64 -0
- brain/vault/derived_links/participants.py +143 -0
- brain/vault/derived_links/pass_runner.py +362 -0
- brain/vault/derived_links/rules.py +137 -0
- brain/vault/export.py +683 -0
- brain/vault/frontmatter.py +165 -0
- brain/vault/graph.py +620 -0
- brain/vault/graph_format.py +388 -0
- brain/vault/link_rewrite.py +235 -0
- brain/vault/links.py +260 -0
- brain/vault/note_builder.py +211 -0
- brain/vault/paths.py +55 -0
- brain/vault/quartz_overlay.py +236 -0
- brain/vault/rename.py +591 -0
- brain/vault/resolver.py +304 -0
- brain/vault/slug.py +127 -0
- brain/vault/sync.py +1513 -0
- brain/vault/sync_summaries.py +264 -0
- brain/vault/templates.py +145 -0
- brain/vault/watch.py +1052 -0
- brain/wiki/__init__.py +6 -0
- brain/wiki/_github_slugger.py +76 -0
- brain/wiki/_person_name.py +314 -0
- brain/wiki/build_homepage.py +541 -0
- brain/wiki/build_partial.py +273 -0
- brain/wiki/build_people.py +934 -0
- brain/wiki/build_related.py +758 -0
- brain/wiki/build_swap.py +585 -0
- brain/wiki/build_watcher.py +975 -0
- brain/wiki/edit_classifier.py +215 -0
- brain/wiki/errors.py +10 -0
- brain/wiki/fastpath_manifest.py +475 -0
- brain/wiki/fastpath_state.py +174 -0
- brain/wiki/install.py +296 -0
- brain/wiki/slug.py +111 -0
- secondbrain_py-0.2.1.dist-info/METADATA +195 -0
- secondbrain_py-0.2.1.dist-info/RECORD +273 -0
- secondbrain_py-0.2.1.dist-info/WHEEL +5 -0
- secondbrain_py-0.2.1.dist-info/entry_points.txt +11 -0
- secondbrain_py-0.2.1.dist-info/licenses/LICENSE +21 -0
- secondbrain_py-0.2.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
// Sample Quartz v4 path utilities for brain vaults.
|
|
2
|
+
//
|
|
3
|
+
// This file is a TEMPLATE. It is installed at
|
|
4
|
+
// `<vault>/.quartz/quartz/util/path.ts` by `brain vault render --overlay`,
|
|
5
|
+
// replacing the stock upstream module verbatim.
|
|
6
|
+
//
|
|
7
|
+
// Tested against Quartz v4.5.x (April 2026). If a future Quartz version
|
|
8
|
+
// changes the public API of this module — `transformInternalLink`,
|
|
9
|
+
// `transformLink`, `slugifyFilePath`, etc. — pull the latest from
|
|
10
|
+
// https://github.com/jackyzha0/quartz/blob/v4/quartz/util/path.ts and
|
|
11
|
+
// re-apply the brain tweaks flagged below with `// brain:` comments.
|
|
12
|
+
//
|
|
13
|
+
// What this brain tweak does, and why: a Quartz build will crash with
|
|
14
|
+
// `URIError: URI malformed` if any inline-HTML `href`/`src` (or any
|
|
15
|
+
// markdown link target) contains a raw `%` not followed by valid
|
|
16
|
+
// hex — typical for ERB / Liquid / Jinja template fragments embedded
|
|
17
|
+
// in pasted email/HTML content (`<%= loginurl %>`, `{{ url }}`, etc.).
|
|
18
|
+
// Stock Quartz calls `decodeURI(link)` directly, which throws on
|
|
19
|
+
// malformed input. The brain patch wraps that single call in a
|
|
20
|
+
// `safeDecodeURI` helper that catches `URIError` and returns the raw
|
|
21
|
+
// string unchanged, so the build keeps going and the doc still
|
|
22
|
+
// renders (the malformed `href` becomes an opaque link, but no crash).
|
|
23
|
+
// Everything else in this file is upstream verbatim — diff against
|
|
24
|
+
// upstream to see the brain delta isolated to two lines.
|
|
25
|
+
import { slug as slugAnchor } from "github-slugger"
|
|
26
|
+
import type { Element as HastElement } from "hast"
|
|
27
|
+
import { clone } from "./clone"
|
|
28
|
+
|
|
29
|
+
// this file must be isomorphic so it can't use node libs (e.g. path)
|
|
30
|
+
|
|
31
|
+
export const QUARTZ = "quartz"
|
|
32
|
+
|
|
33
|
+
/// Utility type to simulate nominal types in TypeScript
|
|
34
|
+
type SlugLike<T> = string & { __brand: T }
|
|
35
|
+
|
|
36
|
+
/** Cannot be relative and must have a file extension. */
|
|
37
|
+
export type FilePath = SlugLike<"filepath">
|
|
38
|
+
export function isFilePath(s: string): s is FilePath {
|
|
39
|
+
const validStart = !s.startsWith(".")
|
|
40
|
+
return validStart && _hasFileExtension(s)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Cannot be relative and may not have leading or trailing slashes. It can have `index` as it's last segment. Use this wherever possible is it's the most 'general' interpretation of a slug. */
|
|
44
|
+
export type FullSlug = SlugLike<"full">
|
|
45
|
+
export function isFullSlug(s: string): s is FullSlug {
|
|
46
|
+
const validStart = !(s.startsWith(".") || s.startsWith("/"))
|
|
47
|
+
const validEnding = !s.endsWith("/")
|
|
48
|
+
return validStart && validEnding && !containsForbiddenCharacters(s)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Shouldn't be a relative path and shouldn't have `/index` as an ending or a file extension. It _can_ however have a trailing slash to indicate a folder path. */
|
|
52
|
+
export type SimpleSlug = SlugLike<"simple">
|
|
53
|
+
export function isSimpleSlug(s: string): s is SimpleSlug {
|
|
54
|
+
const validStart = !(s.startsWith(".") || (s.length > 1 && s.startsWith("/")))
|
|
55
|
+
const validEnding = !endsWith(s, "index")
|
|
56
|
+
return validStart && !containsForbiddenCharacters(s) && validEnding && !_hasFileExtension(s)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Can be found on `href`s but can also be constructed for client-side navigation (e.g. search and graph) */
|
|
60
|
+
export type RelativeURL = SlugLike<"relative">
|
|
61
|
+
export function isRelativeURL(s: string): s is RelativeURL {
|
|
62
|
+
const validStart = /^\.{1,2}/.test(s)
|
|
63
|
+
const validEnding = !endsWith(s, "index")
|
|
64
|
+
return validStart && validEnding && ![".md", ".html"].includes(getFileExtension(s) ?? "")
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function isAbsoluteURL(s: string): boolean {
|
|
68
|
+
try {
|
|
69
|
+
new URL(s)
|
|
70
|
+
} catch {
|
|
71
|
+
return false
|
|
72
|
+
}
|
|
73
|
+
return true
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function getFullSlug(window: Window): FullSlug {
|
|
77
|
+
const res = window.document.body.dataset.slug! as FullSlug
|
|
78
|
+
return res
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function sluggify(s: string): string {
|
|
82
|
+
return s
|
|
83
|
+
.split("/")
|
|
84
|
+
.map((segment) =>
|
|
85
|
+
segment
|
|
86
|
+
.replace(/\s/g, "-")
|
|
87
|
+
.replace(/&/g, "-and-")
|
|
88
|
+
.replace(/%/g, "-percent")
|
|
89
|
+
.replace(/\?/g, "")
|
|
90
|
+
.replace(/#/g, ""),
|
|
91
|
+
)
|
|
92
|
+
.join("/") // always use / as sep
|
|
93
|
+
.replace(/\/$/, "")
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function slugifyFilePath(fp: FilePath, excludeExt?: boolean): FullSlug {
|
|
97
|
+
fp = stripSlashes(fp) as FilePath
|
|
98
|
+
let ext = getFileExtension(fp)
|
|
99
|
+
const withoutFileExt = fp.replace(new RegExp(ext + "$"), "")
|
|
100
|
+
if (excludeExt || [".md", ".html", undefined].includes(ext)) {
|
|
101
|
+
ext = ""
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
let slug = sluggify(withoutFileExt)
|
|
105
|
+
|
|
106
|
+
// treat _index as index
|
|
107
|
+
if (endsWith(slug, "_index")) {
|
|
108
|
+
slug = slug.replace(/_index$/, "index")
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return (slug + ext) as FullSlug
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function simplifySlug(fp: FullSlug): SimpleSlug {
|
|
115
|
+
const res = stripSlashes(trimSuffix(fp, "index"), true)
|
|
116
|
+
return (res.length === 0 ? "/" : res) as SimpleSlug
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// brain: tolerant wrapper around `decodeURI`. ERB/Liquid/Jinja template
|
|
120
|
+
// fragments pasted into ingested docs (`<%= loginurl %>`, `{{ url }}`,
|
|
121
|
+
// etc.) hit Quartz's link transformer as inline-HTML `href` values and
|
|
122
|
+
// crash the entire build with `URIError: URI malformed` on the lone `%`.
|
|
123
|
+
// Falling back to the raw string lets the build complete; the malformed
|
|
124
|
+
// link is an opaque slug that won't resolve to a vault page, but the
|
|
125
|
+
// surrounding doc still renders and the rest of the corpus is unaffected.
|
|
126
|
+
function safeDecodeURI(link: string): string {
|
|
127
|
+
try {
|
|
128
|
+
return decodeURI(link)
|
|
129
|
+
} catch {
|
|
130
|
+
return link
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function transformInternalLink(link: string): RelativeURL {
|
|
135
|
+
// brain: was `decodeURI(link)` upstream — see safeDecodeURI above.
|
|
136
|
+
let [fplike, anchor] = splitAnchor(safeDecodeURI(link))
|
|
137
|
+
|
|
138
|
+
const folderPath = isFolderPath(fplike)
|
|
139
|
+
let segments = fplike.split("/").filter((x) => x.length > 0)
|
|
140
|
+
let prefix = segments.filter(isRelativeSegment).join("/")
|
|
141
|
+
let fp = segments.filter((seg) => !isRelativeSegment(seg) && seg !== "").join("/")
|
|
142
|
+
|
|
143
|
+
// manually add ext here as we want to not strip 'index' if it has an extension
|
|
144
|
+
const simpleSlug = simplifySlug(slugifyFilePath(fp as FilePath))
|
|
145
|
+
const joined = joinSegments(stripSlashes(prefix), stripSlashes(simpleSlug))
|
|
146
|
+
const trail = folderPath ? "/" : ""
|
|
147
|
+
const res = (_addRelativeToStart(joined) + trail + anchor) as RelativeURL
|
|
148
|
+
return res
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// from micromorph/src/utils.ts
|
|
152
|
+
// https://github.com/natemoo-re/micromorph/blob/main/src/utils.ts#L5
|
|
153
|
+
const _rebaseHtmlElement = (el: Element, attr: string, newBase: string | URL) => {
|
|
154
|
+
const rebased = new URL(el.getAttribute(attr)!, newBase)
|
|
155
|
+
el.setAttribute(attr, rebased.pathname + rebased.hash)
|
|
156
|
+
}
|
|
157
|
+
export function normalizeRelativeURLs(el: Element | Document, destination: string | URL) {
|
|
158
|
+
el.querySelectorAll('[href=""], [href^="./"], [href^="../"]').forEach((item) =>
|
|
159
|
+
_rebaseHtmlElement(item, "href", destination),
|
|
160
|
+
)
|
|
161
|
+
el.querySelectorAll('[src=""], [src^="./"], [src^="../"]').forEach((item) =>
|
|
162
|
+
_rebaseHtmlElement(item, "src", destination),
|
|
163
|
+
)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const _rebaseHastElement = (
|
|
167
|
+
el: HastElement,
|
|
168
|
+
attr: string,
|
|
169
|
+
curBase: FullSlug,
|
|
170
|
+
newBase: FullSlug,
|
|
171
|
+
) => {
|
|
172
|
+
if (el.properties?.[attr]) {
|
|
173
|
+
if (!isRelativeURL(String(el.properties[attr]))) {
|
|
174
|
+
return
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const rel = joinSegments(resolveRelative(curBase, newBase), "..", el.properties[attr] as string)
|
|
178
|
+
el.properties[attr] = rel
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export function normalizeHastElement(rawEl: HastElement, curBase: FullSlug, newBase: FullSlug) {
|
|
183
|
+
const el = clone(rawEl) // clone so we dont modify the original page
|
|
184
|
+
_rebaseHastElement(el, "src", curBase, newBase)
|
|
185
|
+
_rebaseHastElement(el, "href", curBase, newBase)
|
|
186
|
+
if (el.children) {
|
|
187
|
+
el.children = el.children.map((child) =>
|
|
188
|
+
normalizeHastElement(child as HastElement, curBase, newBase),
|
|
189
|
+
)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return el
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// resolve /a/b/c to ../..
|
|
196
|
+
export function pathToRoot(slug: FullSlug): RelativeURL {
|
|
197
|
+
let rootPath = slug
|
|
198
|
+
.split("/")
|
|
199
|
+
.filter((x) => x !== "")
|
|
200
|
+
.slice(0, -1)
|
|
201
|
+
.map((_) => "..")
|
|
202
|
+
.join("/")
|
|
203
|
+
|
|
204
|
+
if (rootPath.length === 0) {
|
|
205
|
+
rootPath = "."
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return rootPath as RelativeURL
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export function resolveRelative(current: FullSlug, target: FullSlug | SimpleSlug): RelativeURL {
|
|
212
|
+
const res = joinSegments(pathToRoot(current), simplifySlug(target as FullSlug)) as RelativeURL
|
|
213
|
+
return res
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export function splitAnchor(link: string): [string, string] {
|
|
217
|
+
let [fp, anchor] = link.split("#", 2)
|
|
218
|
+
if (fp.endsWith(".pdf")) {
|
|
219
|
+
return [fp, anchor === undefined ? "" : `#${anchor}`]
|
|
220
|
+
}
|
|
221
|
+
anchor = anchor === undefined ? "" : "#" + slugAnchor(anchor)
|
|
222
|
+
return [fp, anchor]
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export function slugTag(tag: string) {
|
|
226
|
+
return tag
|
|
227
|
+
.split("/")
|
|
228
|
+
.map((tagSegment) => sluggify(tagSegment))
|
|
229
|
+
.join("/")
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export function joinSegments(...args: string[]): string {
|
|
233
|
+
if (args.length === 0) {
|
|
234
|
+
return ""
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
let joined = args
|
|
238
|
+
.filter((segment) => segment !== "" && segment !== "/")
|
|
239
|
+
.map((segment) => stripSlashes(segment))
|
|
240
|
+
.join("/")
|
|
241
|
+
|
|
242
|
+
// if the first segment starts with a slash, add it back
|
|
243
|
+
if (args[0].startsWith("/")) {
|
|
244
|
+
joined = "/" + joined
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// if the last segment is a folder, add a trailing slash
|
|
248
|
+
if (args[args.length - 1].endsWith("/")) {
|
|
249
|
+
joined = joined + "/"
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return joined
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export function getAllSegmentPrefixes(tags: string): string[] {
|
|
256
|
+
const segments = tags.split("/")
|
|
257
|
+
const results: string[] = []
|
|
258
|
+
for (let i = 0; i < segments.length; i++) {
|
|
259
|
+
results.push(segments.slice(0, i + 1).join("/"))
|
|
260
|
+
}
|
|
261
|
+
return results
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
export interface TransformOptions {
|
|
265
|
+
strategy: "absolute" | "relative" | "shortest"
|
|
266
|
+
allSlugs: FullSlug[]
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
export function transformLink(src: FullSlug, target: string, opts: TransformOptions): RelativeURL {
|
|
270
|
+
let targetSlug = transformInternalLink(target)
|
|
271
|
+
|
|
272
|
+
if (opts.strategy === "relative") {
|
|
273
|
+
return targetSlug as RelativeURL
|
|
274
|
+
} else {
|
|
275
|
+
const folderTail = isFolderPath(targetSlug) ? "/" : ""
|
|
276
|
+
const canonicalSlug = stripSlashes(targetSlug.slice(".".length))
|
|
277
|
+
let [targetCanonical, targetAnchor] = splitAnchor(canonicalSlug)
|
|
278
|
+
|
|
279
|
+
if (opts.strategy === "shortest") {
|
|
280
|
+
// if the file name is unique, then it's just the filename
|
|
281
|
+
const matchingFileNames = opts.allSlugs.filter((slug) => {
|
|
282
|
+
const parts = slug.split("/")
|
|
283
|
+
const fileName = parts.at(-1)
|
|
284
|
+
return targetCanonical === fileName
|
|
285
|
+
})
|
|
286
|
+
|
|
287
|
+
// only match, just use it
|
|
288
|
+
if (matchingFileNames.length === 1) {
|
|
289
|
+
const targetSlug = matchingFileNames[0]
|
|
290
|
+
return (resolveRelative(src, targetSlug) + targetAnchor) as RelativeURL
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// if it's not unique, then it's the absolute path from the vault root
|
|
295
|
+
return (joinSegments(pathToRoot(src), canonicalSlug) + folderTail) as RelativeURL
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// path helpers
|
|
300
|
+
export function isFolderPath(fplike: string): boolean {
|
|
301
|
+
return (
|
|
302
|
+
fplike.endsWith("/") ||
|
|
303
|
+
endsWith(fplike, "index") ||
|
|
304
|
+
endsWith(fplike, "index.md") ||
|
|
305
|
+
endsWith(fplike, "index.html")
|
|
306
|
+
)
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
export function endsWith(s: string, suffix: string): boolean {
|
|
310
|
+
return s === suffix || s.endsWith("/" + suffix)
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
export function trimSuffix(s: string, suffix: string): string {
|
|
314
|
+
if (endsWith(s, suffix)) {
|
|
315
|
+
s = s.slice(0, -suffix.length)
|
|
316
|
+
}
|
|
317
|
+
return s
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function containsForbiddenCharacters(s: string): boolean {
|
|
321
|
+
return s.includes(" ") || s.includes("#") || s.includes("?") || s.includes("&")
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function _hasFileExtension(s: string): boolean {
|
|
325
|
+
return getFileExtension(s) !== undefined
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
export function getFileExtension(s: string): string | undefined {
|
|
329
|
+
return s.match(/\.[A-Za-z0-9]+$/)?.[0]
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function isRelativeSegment(s: string): boolean {
|
|
333
|
+
return /^\.{0,2}$/.test(s)
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
export function stripSlashes(s: string, onlyStripPrefix?: boolean): string {
|
|
337
|
+
if (s.startsWith("/")) {
|
|
338
|
+
s = s.substring(1)
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
if (!onlyStripPrefix && s.endsWith("/")) {
|
|
342
|
+
s = s.slice(0, -1)
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
return s
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function _addRelativeToStart(s: string): string {
|
|
349
|
+
if (s === "") {
|
|
350
|
+
s = "."
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
if (!s.startsWith(".")) {
|
|
354
|
+
s = joinSegments(".", s)
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
return s
|
|
358
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// Brain shared source-icon vocabulary — Phase 3.3 of the Wiki UX Overhaul.
|
|
2
|
+
//
|
|
3
|
+
// This file is a TEMPLATE. It is installed at
|
|
4
|
+
// `<vault>/.quartz/quartz/util/sourceIcons.ts` by `brain vault render
|
|
5
|
+
// --overlay`. It does NOT compile or run from the brain repo itself;
|
|
6
|
+
// imports are resolved by the cloned Quartz workspace.
|
|
7
|
+
//
|
|
8
|
+
// Why this exists: P3.2 hard-coded the source-icon table at module
|
|
9
|
+
// scope inside `Search.tsx` (and a fallback copy inside
|
|
10
|
+
// `search.inline.ts`). P3.3 needs the same vocabulary on the
|
|
11
|
+
// server-rendered tag-content rows. Rather than copy a third time, we
|
|
12
|
+
// factor the table into this small util module so a future ingest
|
|
13
|
+
// source (e.g. notion) is a single-line change. Search, TagContent,
|
|
14
|
+
// RelatedDocs, and CommandPalette all import this canonical table.
|
|
15
|
+
|
|
16
|
+
// brain: source-icon mapping. Adding a key here is necessary but not
|
|
17
|
+
// sufficient — source chip styles and any source-specific palettes must
|
|
18
|
+
// also gain the new entry. Static tests keep the consumers in lockstep.
|
|
19
|
+
export const SOURCE_ICONS: Record<string, string> = {
|
|
20
|
+
gmail: "📧",
|
|
21
|
+
krisp: "🎙️",
|
|
22
|
+
slack: "💬",
|
|
23
|
+
manual: "✍️",
|
|
24
|
+
vault: "🌱",
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// brain: ordered chip vocabulary — render order for any UI surface
|
|
28
|
+
// that lists every source. Pinned (vs derived from the index) so the
|
|
29
|
+
// rail renders identically when the live corpus is missing a source.
|
|
30
|
+
export const SOURCE_CHIP_ORDER: ReadonlyArray<keyof typeof SOURCE_ICONS> = [
|
|
31
|
+
"krisp",
|
|
32
|
+
"slack",
|
|
33
|
+
"gmail",
|
|
34
|
+
"manual",
|
|
35
|
+
"vault",
|
|
36
|
+
] as const
|
|
37
|
+
|
|
38
|
+
// brain: fall back to a slug-based source classification when the
|
|
39
|
+
// emitter's frontmatter lift didn't graft a `source` field (e.g.
|
|
40
|
+
// legacy entries written before P1.5). Mirrors the heuristic the
|
|
41
|
+
// contentIndex post-processor uses (`slug.startsWith("_ingested/")`
|
|
42
|
+
// ⇒ ingested-tier; the next path segment is the source).
|
|
43
|
+
export function inferSource(slug: string, source: string | undefined): string {
|
|
44
|
+
if (typeof source === "string" && source.length > 0) return source
|
|
45
|
+
const match = slug.match(/^_ingested\/([^/]+)\//)
|
|
46
|
+
if (match) return match[1]
|
|
47
|
+
return "vault"
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// brain: glyph lookup with a "vault" default fallback. Centralised so
|
|
51
|
+
// every consumer (Search row, tag-content row, future notebook chip,
|
|
52
|
+
// …) renders the same character even when the source is unrecognised.
|
|
53
|
+
export function sourceIconFor(source: string): string {
|
|
54
|
+
return SOURCE_ICONS[source] ?? SOURCE_ICONS["vault"]
|
|
55
|
+
}
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
// Sample Quartz v4 configuration for brain vaults.
|
|
2
|
+
//
|
|
3
|
+
// This file is a TEMPLATE. Copy it into your Quartz workspace as
|
|
4
|
+
// `<vault>/.quartz/quartz.config.ts` after cloning Quartz with
|
|
5
|
+
// `git clone https://github.com/jackyzha0/quartz.git <vault>/.quartz`.
|
|
6
|
+
// It does NOT compile or run from the brain repo itself; the imports
|
|
7
|
+
// below resolve against the dependencies Quartz pulls into the cloned
|
|
8
|
+
// workspace via `npm install`, not against any package brain ships.
|
|
9
|
+
//
|
|
10
|
+
// Tested against Quartz v4.5.x (April 2026). If a future Quartz version
|
|
11
|
+
// renames a plugin or its option shape, pull the latest config from
|
|
12
|
+
// https://quartz.jzhao.xyz/ and re-apply the brain-specific tweaks
|
|
13
|
+
// flagged below with `// brain:` comments.
|
|
14
|
+
//
|
|
15
|
+
// brain: this template wires up two brain-specific transformers:
|
|
16
|
+
//
|
|
17
|
+
// * `Plugin.DerivedFenceMark` (defined in
|
|
18
|
+
// `<vault>/.quartz/quartz/plugins/transformers/derivedFenceMark.ts`,
|
|
19
|
+
// installed by `brain vault render --overlay`). It must run after
|
|
20
|
+
// `Plugin.ObsidianFlavoredMarkdown()` so `[[wiki-link]]` syntax
|
|
21
|
+
// has been converted into mdast `link` nodes by the time the
|
|
22
|
+
// fence walker stamps `data-brain-derived` attributes on them.
|
|
23
|
+
// See the transformer's top-of-file comment for the full contract.
|
|
24
|
+
//
|
|
25
|
+
// * `Plugin.ReloadSignal` (defined in
|
|
26
|
+
// `<vault>/.quartz/quartz/plugins/transformers/reloadSignal.ts`,
|
|
27
|
+
// also installed by the overlay). Injects an ETag-aware polling reload
|
|
28
|
+
// `<script>` into every page when `BRAIN_WIKI_RELOAD=1` is set in
|
|
29
|
+
// the build env — replaces Quartz's `--serve` WebSocket reload
|
|
30
|
+
// path, which is dead in the brain blue-green serve flow because
|
|
31
|
+
// Caddy (not Quartz) serves the static output. See the
|
|
32
|
+
// transformer's top-of-file comment for the full contract.
|
|
33
|
+
|
|
34
|
+
import { QuartzConfig } from "./quartz/cfg"
|
|
35
|
+
import * as Plugin from "./quartz/plugins"
|
|
36
|
+
|
|
37
|
+
const config: QuartzConfig = {
|
|
38
|
+
configuration: {
|
|
39
|
+
// brain: friendlier title than the default "Quartz 4".
|
|
40
|
+
pageTitle: "Second Brain",
|
|
41
|
+
pageTitleSuffix: "",
|
|
42
|
+
enableSPA: true,
|
|
43
|
+
enablePopovers: true,
|
|
44
|
+
// brain: analytics off — the wiki is private and rendered locally.
|
|
45
|
+
// If you deploy the static output somewhere public, set this to your
|
|
46
|
+
// provider of choice (plausible, google, umami, etc.).
|
|
47
|
+
analytics: null,
|
|
48
|
+
locale: "en-US",
|
|
49
|
+
// brain: localhost for dev; flip to your domain before deploying.
|
|
50
|
+
baseUrl: "localhost:8080",
|
|
51
|
+
// brain: skip tooling-managed folders. `_templates` / `_attachments`
|
|
52
|
+
// are brain conventions; `.quartz` is the workspace itself; `dist` /
|
|
53
|
+
// `public` are build outputs that must NOT be re-walked as content.
|
|
54
|
+
ignorePatterns: [
|
|
55
|
+
"private",
|
|
56
|
+
"_templates",
|
|
57
|
+
"_attachments",
|
|
58
|
+
// brain: P4.7 — server-rendered partials (e.g. `_partials/recent.md`,
|
|
59
|
+
// the source-of-truth for the home-page "Recently captured" rail) live
|
|
60
|
+
// outside the public surface. The home note renders them inline via
|
|
61
|
+
// a fence; the partial itself must never appear as its own page.
|
|
62
|
+
"_partials",
|
|
63
|
+
"dist",
|
|
64
|
+
"public",
|
|
65
|
+
".obsidian",
|
|
66
|
+
".git",
|
|
67
|
+
".quartz",
|
|
68
|
+
],
|
|
69
|
+
defaultDateType: "modified",
|
|
70
|
+
theme: {
|
|
71
|
+
fontOrigin: "googleFonts",
|
|
72
|
+
cdnCaching: true,
|
|
73
|
+
// brain: Linear-style typography for the tech-forward variant of
|
|
74
|
+
// the 2026 redesign. Geist (Vercel's grotesk) for both display and
|
|
75
|
+
// body — clean, neutral, modern; Geist Mono for code and metadata.
|
|
76
|
+
// Both are on Google Fonts; Quartz's Google Fonts loader uses the
|
|
77
|
+
// family name verbatim.
|
|
78
|
+
typography: {
|
|
79
|
+
header: "Geist",
|
|
80
|
+
body: "Geist",
|
|
81
|
+
code: "Geist Mono",
|
|
82
|
+
},
|
|
83
|
+
// brain: Linear-style charcoal + indigo-violet palette. Dark mode
|
|
84
|
+
// is the headline aesthetic — Linear lives in dark — but light mode
|
|
85
|
+
// is a clean pure-neutral zinc scale for daytime reading.
|
|
86
|
+
colors: {
|
|
87
|
+
lightMode: {
|
|
88
|
+
light: "#ffffff",
|
|
89
|
+
lightgray: "#e4e4e7",
|
|
90
|
+
// brain: zinc-500 (#71717a) clears WCAG AA against pure white
|
|
91
|
+
// bg for muted text, metadata, dingbats.
|
|
92
|
+
gray: "#71717a",
|
|
93
|
+
darkgray: "#3f3f46",
|
|
94
|
+
dark: "#09090b",
|
|
95
|
+
secondary: "#5e6ad2",
|
|
96
|
+
tertiary: "#8b5cf6",
|
|
97
|
+
highlight: "rgba(94, 106, 210, 0.12)",
|
|
98
|
+
textHighlight: "#fde047b3",
|
|
99
|
+
},
|
|
100
|
+
darkMode: {
|
|
101
|
+
light: "#0a0a0a",
|
|
102
|
+
lightgray: "#27272a",
|
|
103
|
+
// brain: zinc-400 (#a1a1aa) clears WCAG AA against #0a0a0a.
|
|
104
|
+
gray: "#a1a1aa",
|
|
105
|
+
darkgray: "#d4d4d8",
|
|
106
|
+
dark: "#fafafa",
|
|
107
|
+
secondary: "#7170ff",
|
|
108
|
+
tertiary: "#a78bfa",
|
|
109
|
+
highlight: "rgba(113, 112, 255, 0.18)",
|
|
110
|
+
textHighlight: "#fde04766",
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
plugins: {
|
|
116
|
+
transformers: [
|
|
117
|
+
Plugin.FrontMatter(),
|
|
118
|
+
Plugin.CreatedModifiedDate({
|
|
119
|
+
priority: ["frontmatter", "git", "filesystem"],
|
|
120
|
+
}),
|
|
121
|
+
Plugin.SyntaxHighlighting({
|
|
122
|
+
theme: { light: "github-light", dark: "github-dark" },
|
|
123
|
+
keepBackground: false,
|
|
124
|
+
}),
|
|
125
|
+
Plugin.ObsidianFlavoredMarkdown({ enableInHtmlEmbed: false }),
|
|
126
|
+
// brain: tag derived-edge `<a>` tags inside Phase D fences with
|
|
127
|
+
// `data-brain-derived` / `data-brain-rule` / `data-brain-weight`
|
|
128
|
+
// so the custom contentIndex emitter can classify them as
|
|
129
|
+
// `kind: "derived"` in `static/contentIndex.json`. Must run after
|
|
130
|
+
// ObsidianFlavoredMarkdown so wiki-link syntax has already been
|
|
131
|
+
// converted to mdast link nodes.
|
|
132
|
+
Plugin.DerivedFenceMark(),
|
|
133
|
+
// brain: Lane B redesign — classify every mdast link node into
|
|
134
|
+
// one of five kinds (wiki / external / tag / ingested / derived)
|
|
135
|
+
// and stamp `data-brain-link-kind` on `node.data.hProperties`.
|
|
136
|
+
// The Lane B `_links.scss` consumes that attribute to give each
|
|
137
|
+
// kind a distinct visual treatment (terracotta underline / dotted
|
|
138
|
+
// + arrow / pill chip / source-tinted left rail / italic dashed).
|
|
139
|
+
// Must run AFTER `ObsidianFlavoredMarkdown` (so wiki-link mdast
|
|
140
|
+
// nodes exist) AND AFTER `DerivedFenceMark` (so its
|
|
141
|
+
// `data-brain-derived` stamp is already in place — the
|
|
142
|
+
// classifier reads it to short-circuit the kind to `derived`).
|
|
143
|
+
// See `quartz/plugins/transformers/linkKindMark.ts` for the
|
|
144
|
+
// full classification contract.
|
|
145
|
+
Plugin.LinkKindMark(),
|
|
146
|
+
// brain: Lane B redesign — inject the runtime source-tagger
|
|
147
|
+
// (`/static/linkSourceTag.js`) into every page. The tagger
|
|
148
|
+
// extracts the source segment from `_ingested/<source>/...`
|
|
149
|
+
// hrefs at `DOMContentLoaded` (and on Quartz SPA `nav` events)
|
|
150
|
+
// and stamps `data-brain-source="krisp"` etc. on each ingested
|
|
151
|
+
// link, so `_links.scss`'s source-tinted left-rail rules
|
|
152
|
+
// (`&[data-brain-source="krisp"]`) can pick the right
|
|
153
|
+
// `--brain-source-*` color from `_tokens.scss`. Always emits
|
|
154
|
+
// (no env-var gate, unlike `ReloadSignal`) — the script is part
|
|
155
|
+
// of the production redesign. No markdown ordering requirement;
|
|
156
|
+
// grouped here next to `LinkKindMark` so the Lane B plugins
|
|
157
|
+
// stay co-located. See `quartz/plugins/transformers/linkSourceTag.ts`
|
|
158
|
+
// for the inject contract and the static script for the
|
|
159
|
+
// tagging logic.
|
|
160
|
+
Plugin.LinkSourceTag(),
|
|
161
|
+
// brain: Recent rail — inject the live relative-date recomputer
|
|
162
|
+
// (`/static/relativeDate.js`) into every page. The home-page Recent
|
|
163
|
+
// rail emits `<span class="brain-rel-date" data-date="<ISO>">{abs}</span>`
|
|
164
|
+
// (see `brain.wiki.build_homepage`); this script reads each
|
|
165
|
+
// `data-date` and recomputes the relative text ("3d ago") live on
|
|
166
|
+
// `DOMContentLoaded` + Quartz SPA `nav`, so the rail never decays
|
|
167
|
+
// between daily builds. Bucket logic mirrors
|
|
168
|
+
// `build_homepage._format_relative_date`. Always emits (no env-var
|
|
169
|
+
// gate, like `LinkSourceTag`); no markdown ordering requirement;
|
|
170
|
+
// grouped here next to `LinkSourceTag` so the brain script-only
|
|
171
|
+
// transformers stay co-located. See
|
|
172
|
+
// `quartz/plugins/transformers/relativeDate.ts` for the inject
|
|
173
|
+
// contract and `quartz/static/relativeDate.js` for the recomputer.
|
|
174
|
+
Plugin.RelativeDate(),
|
|
175
|
+
// brain: Lane C redesign — inject the runtime code-copy injector
|
|
176
|
+
// (`/static/codeCopy.js`) into every page. The injector walks
|
|
177
|
+
// every `<pre>` in the article body, lifts the `data-language`
|
|
178
|
+
// attribute from the inner `<code>` to the outer `<pre>` (so
|
|
179
|
+
// the CSS-only language label in `_code.scss` can read it via
|
|
180
|
+
// `attr()`), and appends a brain-themed `.brain-code-copy`
|
|
181
|
+
// button. Stock Quartz's `.clipboard-button` is hidden via
|
|
182
|
+
// `_code.scss` so the two don't render side-by-side. Always
|
|
183
|
+
// emits (no env-var gate, like `LinkSourceTag`) — the script is
|
|
184
|
+
// part of the production redesign. No markdown ordering
|
|
185
|
+
// requirement; grouped here next to `LinkSourceTag` so the
|
|
186
|
+
// brain script-only transformers stay co-located. See
|
|
187
|
+
// `quartz/plugins/transformers/codeCopy.ts` for the inject
|
|
188
|
+
// contract and `quartz/static/codeCopy.js` for the injector.
|
|
189
|
+
Plugin.CodeCopy(),
|
|
190
|
+
// brain: P4.4 — inject the email-thread reading-mode runtime
|
|
191
|
+
// (`/static/emailThread.js`) plus an inline `window
|
|
192
|
+
// .BRAIN_USER_EMAIL` global (read from
|
|
193
|
+
// `process.env.BRAIN_USER_EMAIL` at build time). The runtime
|
|
194
|
+
// (a) annotates each per-message section in an `email_thread`
|
|
195
|
+
// body with `data-brain-thread-from` + `data-brain-is-mine`,
|
|
196
|
+
// and (b) renders a "Show only my replies" filter button at
|
|
197
|
+
// the top of `<article>` that toggles the
|
|
198
|
+
// `body.brain-replies-only` class — the SCSS partial
|
|
199
|
+
// `_email_thread.scss` then hides every
|
|
200
|
+
// `[data-brain-is-mine="false"]` section. Always emits (no
|
|
201
|
+
// env-var gate, like `LinkSourceTag` and `CodeCopy`) — the
|
|
202
|
+
// runtime is part of the production redesign and self-gates on
|
|
203
|
+
// an `_ingested/gmail/` URL pathname. No markdown ordering
|
|
204
|
+
// requirement; grouped here next to `CodeCopy` so the brain
|
|
205
|
+
// script-only transformers stay co-located. See
|
|
206
|
+
// `quartz/plugins/transformers/emailThread.ts` for the inject
|
|
207
|
+
// contract and `quartz/static/emailThread.js` for the runtime
|
|
208
|
+
// logic.
|
|
209
|
+
Plugin.EmailThreadReader(),
|
|
210
|
+
// brain: inject the ETag-aware reload watcher (`/static/reload.js`)
|
|
211
|
+
// into every page when `BRAIN_WIKI_RELOAD=1` at build time.
|
|
212
|
+
// Replaces Quartz's `--serve` WebSocket reload, which is dead in
|
|
213
|
+
// our blue-green serve flow because Caddy (not Quartz) is the
|
|
214
|
+
// static file server. `bin/brain-up` sets the env var; `brain
|
|
215
|
+
// vault render` (prod path) leaves it unset so prod pages ship
|
|
216
|
+
// without a polling client. The transformer has no markdown
|
|
217
|
+
// ordering requirement — it only contributes a `<script>` tag
|
|
218
|
+
// via `externalResources()` — but is grouped here next to
|
|
219
|
+
// `DerivedFenceMark` so the two brain-extension transformers
|
|
220
|
+
// stay co-located.
|
|
221
|
+
Plugin.ReloadSignal(),
|
|
222
|
+
Plugin.GitHubFlavoredMarkdown(),
|
|
223
|
+
Plugin.TableOfContents(),
|
|
224
|
+
// brain: Lane B redesign — `externalLinkIcon: false` disables
|
|
225
|
+
// Quartz's stock inline-SVG `↗` arrow on external links. The Lane B
|
|
226
|
+
// `_links.scss` paints its own `↗` via `::after` on
|
|
227
|
+
// `a[data-brain-link-kind="external"]` so the color/opacity track
|
|
228
|
+
// the redesign tokens; leaving stock's icon enabled would render
|
|
229
|
+
// every external link with two arrows side-by-side.
|
|
230
|
+
Plugin.CrawlLinks({ markdownLinkResolution: "shortest", externalLinkIcon: false }),
|
|
231
|
+
// brain: P4.1 — strip home-page `<li>`s whose link resolves to an
|
|
232
|
+
// empty folder (today: `daily/` when the user hasn't created any
|
|
233
|
+
// daily notes yet). Generic — extend the `folders` option to
|
|
234
|
+
// protect future top-level subdirs (e.g. a hypothetical
|
|
235
|
+
// `workbench/`) from the same 404. Must run AFTER `CrawlLinks` so
|
|
236
|
+
// the rehype-stage `<a href>`s have already been resolved to the
|
|
237
|
+
// shape the matcher expects (`daily/`, `daily/index`, …); without
|
|
238
|
+
// that ordering the matcher's normalizer sees raw markdown URLs
|
|
239
|
+
// and mis-classifies. The transformer reads `ctx.argv.directory`
|
|
240
|
+
// once per build to compute which folders are empty.
|
|
241
|
+
Plugin.EmptyDoorFilter(),
|
|
242
|
+
Plugin.Description(),
|
|
243
|
+
// strict: "ignore" suppresses KaTeX strict-mode build warnings for
|
|
244
|
+
// typographic chars (en/em-dashes, », ¢) in math contexts — cosmetic
|
|
245
|
+
// only, rendering unchanged.
|
|
246
|
+
Plugin.Latex({ renderEngine: "katex", katexOptions: { strict: "ignore" } }),
|
|
247
|
+
],
|
|
248
|
+
filters: [Plugin.RemoveDrafts()],
|
|
249
|
+
emitters: [
|
|
250
|
+
Plugin.AliasRedirects(),
|
|
251
|
+
Plugin.ComponentResources(),
|
|
252
|
+
Plugin.ContentPage(),
|
|
253
|
+
Plugin.FolderPage(),
|
|
254
|
+
Plugin.TagPage(),
|
|
255
|
+
Plugin.ContentIndex({
|
|
256
|
+
enableSiteMap: true,
|
|
257
|
+
enableRSS: true,
|
|
258
|
+
}),
|
|
259
|
+
Plugin.Assets(),
|
|
260
|
+
Plugin.Static(),
|
|
261
|
+
Plugin.NotFoundPage(),
|
|
262
|
+
// brain: the graph view is the killer feature for an Obsidian-style
|
|
263
|
+
// vault. The actual graph rendering happens via `Component.Graph(...)`
|
|
264
|
+
// in `quartz.layout.ts`; there is no `Plugin.Graph` emitter in stock
|
|
265
|
+
// Quartz v4.5.x — only the component is needed here.
|
|
266
|
+
],
|
|
267
|
+
},
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
export default config
|