mirror of
https://github.com/Patiun/com.gregkilmer.www.git
synced 2026-11-08 04:08:24 +00:00
a2f723ca2d
Phase 1 of the functional design plan. - Move the old site into legacy/, unchanged, for reference. It still builds from there until cutover. - Add packages/schema: the one definition of all 20 note types, with field titles and descriptions, note and vault checks, and JSON Schema export so later editing tools and the MCP server share it. - Add vault-kit: Obsidian templates generated from the schema, and the Site guide for 24.01_PersonalWebsite. - Add tools/migrate: turns the legacy JSON, collections, store data and page text into 545 Site folder notes and 84 images, and checks the result with the schema. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012aSbo2D1qP8cMFQCykf7kx
65 lines
2.1 KiB
TypeScript
65 lines
2.1 KiB
TypeScript
/**
|
||
* Names, slugs and wikilinks.
|
||
*
|
||
* Every note is addressed three ways: its file name in the vault (what
|
||
* Obsidian wikilinks resolve), its slug (what the URL uses), and its type.
|
||
* These helpers are the only place those rules live.
|
||
*/
|
||
|
||
/** Characters Obsidian or common file systems refuse in a file name. */
|
||
const UNSAFE_FILE_CHARS = /[\\/:*?"<>|#^[\]]/g;
|
||
|
||
/** A file name (without `.md`) safe for Obsidian on every platform. */
|
||
export function safeFileName(title: string): string {
|
||
return title
|
||
.replace(UNSAFE_FILE_CHARS, " ")
|
||
.replace(/\s+/g, " ")
|
||
.trim()
|
||
.replace(/[. ]+$/, "");
|
||
}
|
||
|
||
/** URL slug from a title: lowercase ASCII words joined by hyphens. */
|
||
export function slugify(text: string): string {
|
||
return text
|
||
.normalize("NFKD")
|
||
.replace(/[̀-ͯ]/g, "")
|
||
.toLowerCase()
|
||
.replace(/&/g, " and ")
|
||
.replace(/['’]/g, "")
|
||
.replace(/[^a-z0-9]+/g, "-")
|
||
.replace(/^-+|-+$/g, "");
|
||
}
|
||
|
||
const WIKILINK = /^\[\[([^[\]|#]+)(?:#[^[\]|]*)?(?:\|[^[\]]*)?\]\]$/;
|
||
const WIKILINK_GLOBAL = /\[\[([^[\]|#]+)(?:#[^[\]|]*)?(?:\|[^[\]]*)?\]\]/g;
|
||
|
||
/** `Name` → `[[Name]]`. */
|
||
export function toWikilink(target: string): string {
|
||
return `[[${target}]]`;
|
||
}
|
||
|
||
/** `[[Name|alias]]` → `Name`, or null when the value is not a wikilink. */
|
||
export function wikilinkTarget(value: string): string | null {
|
||
const m = WIKILINK.exec(value.trim());
|
||
return m ? m[1]!.trim() : null;
|
||
}
|
||
|
||
/** Every wikilink target in a block of Markdown, in order, without repeats. */
|
||
export function wikilinksIn(markdown: string): string[] {
|
||
const seen = new Set<string>();
|
||
for (const m of markdown.matchAll(WIKILINK_GLOBAL)) seen.add(m[1]!.trim());
|
||
return [...seen];
|
||
}
|
||
|
||
/**
|
||
* An unquoted `[[Name]]` in YAML parses as a nested list (`[["Name"]]`).
|
||
* Obsidian quotes them, but hand edits may not, so accept both shapes.
|
||
*/
|
||
export function normalizeWikilinkInput(value: unknown): unknown {
|
||
if (Array.isArray(value) && value.length === 1 && Array.isArray(value[0]) && value[0].length === 1) {
|
||
const inner = value[0][0];
|
||
if (typeof inner === "string") return toWikilink(inner);
|
||
}
|
||
return value;
|
||
}
|