feat: publish HoloLake model-native living system source
This commit is contained in:
parent
6ad10edde1
commit
c395dd3a99
2467 changed files with 615073 additions and 0 deletions
294
product-source/hololake-platform/src/utils/wikilink.ts
Normal file
294
product-source/hololake-platform/src/utils/wikilink.ts
Normal file
|
|
@ -0,0 +1,294 @@
|
|||
/** Utility functions for parsing wikilink syntax: [[target|display]] */
|
||||
|
||||
import type { VaultEntry } from '../types'
|
||||
import { slugifyNoteStem } from './noteSlug'
|
||||
import { workspaceForEntry, workspacePathForEntry } from './workspaces'
|
||||
|
||||
export type AbsoluteNotePath = string
|
||||
export type NoteTitleOrTarget = string
|
||||
export type VaultPath = string
|
||||
export type WikilinkReference = string
|
||||
export type WikilinkTarget = string
|
||||
|
||||
/** Extracts the target path from a wikilink reference (strips [[ ]] and display text). */
|
||||
export function wikilinkTarget(ref: WikilinkReference): WikilinkTarget {
|
||||
const inner = ref.replace(/^\[\[|\]\]$/g, '')
|
||||
const pipeIdx = inner.indexOf('|')
|
||||
return pipeIdx !== -1 ? inner.slice(0, pipeIdx) : inner
|
||||
}
|
||||
|
||||
/** Extracts the display label from a wikilink reference. Falls back to humanised path stem. */
|
||||
export function wikilinkDisplay(ref: WikilinkReference): string {
|
||||
const inner = ref.replace(/^\[\[|\]\]$/g, '')
|
||||
const pipeIdx = inner.indexOf('|')
|
||||
if (pipeIdx !== -1) return inner.slice(pipeIdx + 1)
|
||||
const last = inner.split('/').pop() ?? inner
|
||||
return last.replace(/-/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase())
|
||||
}
|
||||
|
||||
function stripWindowsExtendedPathPrefix(path: AbsoluteNotePath | VaultPath): string {
|
||||
return path
|
||||
.replace(/^\\\\\?\\UNC\\/i, '//')
|
||||
.replace(/^\\\\\?\\/, '')
|
||||
}
|
||||
|
||||
function normalizeFilesystemPath(path: AbsoluteNotePath | VaultPath): string {
|
||||
return stripWindowsExtendedPathPrefix(path)
|
||||
.replace(/\\/g, '/')
|
||||
.replace(/\/+$/g, '')
|
||||
}
|
||||
|
||||
function withoutMarkdownExtension(pathStem: WikilinkTarget): WikilinkTarget {
|
||||
return pathStem.replace(/\.md$/i, '')
|
||||
}
|
||||
|
||||
/** Extract the vault-relative path stem (no leading slash, no .md extension). */
|
||||
export function relativePathStem(absolutePath: AbsoluteNotePath, vaultPath: VaultPath): WikilinkTarget {
|
||||
const normalizedAbsolutePath = normalizeFilesystemPath(absolutePath)
|
||||
const normalizedVaultPath = normalizeFilesystemPath(vaultPath)
|
||||
const prefix = normalizedVaultPath.endsWith('/') ? normalizedVaultPath : normalizedVaultPath + '/'
|
||||
if (normalizedAbsolutePath.toLowerCase().startsWith(prefix.toLowerCase())) {
|
||||
return withoutMarkdownExtension(normalizedAbsolutePath.slice(prefix.length))
|
||||
}
|
||||
// Fallback: just the filename stem
|
||||
const filename = normalizedAbsolutePath.split('/').pop() ?? normalizedAbsolutePath
|
||||
return withoutMarkdownExtension(filename)
|
||||
}
|
||||
|
||||
/** Slugify a human-readable title into the canonical wikilink filename stem. */
|
||||
export const slugifyWikilinkTarget = slugifyNoteStem
|
||||
|
||||
function shouldPrefixWorkspaceAlias(entryAlias?: string, sourceAlias?: string): boolean {
|
||||
return !!entryAlias && !!sourceAlias && entryAlias !== sourceAlias
|
||||
}
|
||||
|
||||
/** Build the canonical wikilink target for a vault entry. */
|
||||
export function canonicalWikilinkTargetForEntry(entry: VaultEntry, vaultPath: VaultPath, sourceEntry?: VaultEntry): WikilinkTarget {
|
||||
const entryWorkspace = workspaceForEntry(entry)
|
||||
const sourceWorkspace = sourceEntry ? workspaceForEntry(sourceEntry) : null
|
||||
const entryVaultPath = workspacePathForEntry(entry) ?? vaultPath
|
||||
const localTarget = relativePathStem(entry.path, entryVaultPath)
|
||||
const entryAlias = entryWorkspace?.alias
|
||||
if (shouldPrefixWorkspaceAlias(entryAlias, sourceWorkspace?.alias)) {
|
||||
return `${entryAlias}/${localTarget}`
|
||||
}
|
||||
return localTarget
|
||||
}
|
||||
|
||||
/** Resolve a user-facing title/path input to the canonical wikilink target. */
|
||||
export function canonicalWikilinkTargetForTitle(
|
||||
titleOrTarget: NoteTitleOrTarget,
|
||||
entries: VaultEntry[],
|
||||
vaultPath: VaultPath,
|
||||
sourceEntry?: VaultEntry,
|
||||
): WikilinkTarget {
|
||||
const trimmed = titleOrTarget.trim()
|
||||
const resolved = resolveEntry(entries, trimmed, sourceEntry)
|
||||
return resolved
|
||||
? canonicalWikilinkTargetForEntry(resolved, vaultPath, sourceEntry)
|
||||
: trimmed.includes('/')
|
||||
? trimmed.replace(/^\/+/, '').replace(/\.md$/, '')
|
||||
: slugifyWikilinkTarget(trimmed)
|
||||
}
|
||||
|
||||
/** Wrap a target in wikilink syntax. */
|
||||
export function formatWikilinkRef(target: WikilinkTarget): WikilinkReference {
|
||||
return `[[${target}]]`
|
||||
}
|
||||
|
||||
interface ResolutionKey {
|
||||
exactTarget: string
|
||||
workspaceAlias: string | null
|
||||
targetWithoutWorkspace: string
|
||||
lastSegment: string
|
||||
pathSuffixes: string[]
|
||||
humanizedTarget: string | null
|
||||
}
|
||||
|
||||
interface IndexedResolutionEntry {
|
||||
aliases: string[]
|
||||
entry: VaultEntry
|
||||
filenameStem: string
|
||||
normalizedPath: string
|
||||
title: string
|
||||
workspaceAlias: string | null
|
||||
}
|
||||
|
||||
interface ResolutionIndex {
|
||||
entries: IndexedResolutionEntry[]
|
||||
entriesByWorkspaceAlias: Map<string, IndexedResolutionEntry[]>
|
||||
resolutionCache: Map<string, VaultEntry | null>
|
||||
workspaceAliases: Set<string>
|
||||
}
|
||||
|
||||
type EntryMatcher = (entry: IndexedResolutionEntry, resolutionKey: ResolutionKey) => boolean
|
||||
|
||||
const resolutionIndexesByEntries = new WeakMap<VaultEntry[], ResolutionIndex>()
|
||||
|
||||
function buildResolutionKey(rawTarget: WikilinkTarget, knownWorkspaceAliases: Set<string> = new Set()): ResolutionKey {
|
||||
const exactTarget = rawTarget.includes('|') ? rawTarget.split('|')[0] : rawTarget
|
||||
const normalizedTarget = exactTarget.toLowerCase()
|
||||
const segments = exactTarget.split('/').filter(Boolean)
|
||||
const candidateWorkspaceAlias = segments.length > 1 ? segments[0].toLowerCase() : null
|
||||
const workspaceAlias = candidateWorkspaceAlias && knownWorkspaceAliases.has(candidateWorkspaceAlias)
|
||||
? candidateWorkspaceAlias
|
||||
: null
|
||||
const targetWithoutWorkspace = workspaceAlias ? segments.slice(1).join('/') : exactTarget
|
||||
const normalizedLocalTarget = targetWithoutWorkspace.toLowerCase()
|
||||
const normalizedPathTarget = normalizedLocalTarget.replace(/^\/+/, '')
|
||||
const pathSuffixes = normalizedPathTarget.includes('/')
|
||||
? [`/${normalizedPathTarget}`, ...normalizedPathTarget.endsWith('.md') ? [] : [`/${normalizedPathTarget}.md`]]
|
||||
: []
|
||||
const lastSegment = targetWithoutWorkspace.includes('/') ? (targetWithoutWorkspace.split('/').pop() ?? targetWithoutWorkspace).toLowerCase() : normalizedLocalTarget
|
||||
const humanizedTarget = lastSegment.replace(/-/g, ' ')
|
||||
|
||||
return {
|
||||
exactTarget: normalizedTarget,
|
||||
workspaceAlias,
|
||||
targetWithoutWorkspace: normalizedLocalTarget,
|
||||
lastSegment,
|
||||
pathSuffixes,
|
||||
humanizedTarget: humanizedTarget === normalizedLocalTarget ? null : humanizedTarget,
|
||||
}
|
||||
}
|
||||
|
||||
function workspaceAliasForEntry(entry: VaultEntry | undefined): string | null {
|
||||
return entry ? (workspaceForEntry(entry)?.alias.toLowerCase() ?? null) : null
|
||||
}
|
||||
|
||||
function buildIndexedResolutionEntry(entry: VaultEntry): IndexedResolutionEntry {
|
||||
return {
|
||||
aliases: entry.aliases.map((alias) => alias.toLowerCase()),
|
||||
entry,
|
||||
filenameStem: entry.filename.replace(/\.md$/, '').toLowerCase(),
|
||||
normalizedPath: normalizeFilesystemPath(entry.path).toLowerCase(),
|
||||
title: entry.title.toLowerCase(),
|
||||
workspaceAlias: workspaceAliasForEntry(entry),
|
||||
}
|
||||
}
|
||||
|
||||
function buildResolutionIndex(entries: VaultEntry[]): ResolutionIndex {
|
||||
const indexedEntries = entries.map(buildIndexedResolutionEntry)
|
||||
const entriesByWorkspaceAlias = new Map<string, IndexedResolutionEntry[]>()
|
||||
const workspaceAliases = new Set<string>()
|
||||
|
||||
for (const entry of indexedEntries) {
|
||||
if (!entry.workspaceAlias) continue
|
||||
workspaceAliases.add(entry.workspaceAlias)
|
||||
const workspaceEntries = entriesByWorkspaceAlias.get(entry.workspaceAlias) ?? []
|
||||
workspaceEntries.push(entry)
|
||||
entriesByWorkspaceAlias.set(entry.workspaceAlias, workspaceEntries)
|
||||
}
|
||||
|
||||
return {
|
||||
entries: indexedEntries,
|
||||
entriesByWorkspaceAlias,
|
||||
resolutionCache: new Map(),
|
||||
workspaceAliases,
|
||||
}
|
||||
}
|
||||
|
||||
function resolutionIndexForEntries(entries: VaultEntry[]): ResolutionIndex {
|
||||
const cached = resolutionIndexesByEntries.get(entries)
|
||||
if (cached) return cached
|
||||
|
||||
const index = buildResolutionIndex(entries)
|
||||
resolutionIndexesByEntries.set(entries, index)
|
||||
return index
|
||||
}
|
||||
|
||||
function resolutionCacheKey(resolutionKey: ResolutionKey, sourceWorkspaceAlias: string | null): string {
|
||||
return `${sourceWorkspaceAlias ?? ''}\n${resolutionKey.exactTarget}`
|
||||
}
|
||||
|
||||
function findIndexedEntry(entries: IndexedResolutionEntry[], resolutionKey: ResolutionKey, matcher: EntryMatcher): VaultEntry | undefined {
|
||||
for (const entry of entries) {
|
||||
if (matcher(entry, resolutionKey)) return entry.entry
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
function findPrioritizedEntry(
|
||||
index: ResolutionIndex,
|
||||
resolutionKey: ResolutionKey,
|
||||
sourceWorkspaceAlias: string | null,
|
||||
matcher: EntryMatcher,
|
||||
): VaultEntry | undefined {
|
||||
if (resolutionKey.workspaceAlias) {
|
||||
return findIndexedEntry(index.entriesByWorkspaceAlias.get(resolutionKey.workspaceAlias) ?? [], resolutionKey, matcher)
|
||||
}
|
||||
if (!sourceWorkspaceAlias) return findIndexedEntry(index.entries, resolutionKey, matcher)
|
||||
|
||||
const workspaceMatch = findIndexedEntry(index.entriesByWorkspaceAlias.get(sourceWorkspaceAlias) ?? [], resolutionKey, matcher)
|
||||
if (workspaceMatch) return workspaceMatch
|
||||
|
||||
for (const entry of index.entries) {
|
||||
if (entry.workspaceAlias === sourceWorkspaceAlias) continue
|
||||
if (matcher(entry, resolutionKey)) return entry.entry
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
function matchesPathSuffix(entry: IndexedResolutionEntry, resolutionKey: ResolutionKey): boolean {
|
||||
return resolutionKey.pathSuffixes.some((pathSuffix) => entry.normalizedPath.endsWith(pathSuffix))
|
||||
}
|
||||
|
||||
function matchesFilename(entry: IndexedResolutionEntry, resolutionKey: ResolutionKey): boolean {
|
||||
return entry.filenameStem === resolutionKey.exactTarget
|
||||
|| entry.filenameStem === resolutionKey.targetWithoutWorkspace
|
||||
|| entry.filenameStem === resolutionKey.lastSegment
|
||||
}
|
||||
|
||||
function matchesAlias(entry: IndexedResolutionEntry, resolutionKey: ResolutionKey): boolean {
|
||||
return entry.aliases.some((alias) => (
|
||||
alias === resolutionKey.exactTarget || alias === resolutionKey.targetWithoutWorkspace
|
||||
))
|
||||
}
|
||||
|
||||
function matchesTitle(entry: IndexedResolutionEntry, resolutionKey: ResolutionKey): boolean {
|
||||
return entry.title === resolutionKey.exactTarget
|
||||
|| entry.title === resolutionKey.targetWithoutWorkspace
|
||||
|| entry.title === resolutionKey.lastSegment
|
||||
}
|
||||
|
||||
function matchesHumanizedTitle(entry: IndexedResolutionEntry, resolutionKey: ResolutionKey): boolean {
|
||||
return !!resolutionKey.humanizedTarget && entry.title === resolutionKey.humanizedTarget
|
||||
}
|
||||
|
||||
function resolveEntryFromIndex(
|
||||
index: ResolutionIndex,
|
||||
resolutionKey: ResolutionKey,
|
||||
sourceWorkspaceAlias: string | null,
|
||||
): VaultEntry | undefined {
|
||||
return (
|
||||
(resolutionKey.pathSuffixes.length > 0
|
||||
? findPrioritizedEntry(index, resolutionKey, sourceWorkspaceAlias, matchesPathSuffix)
|
||||
: undefined)
|
||||
?? findPrioritizedEntry(index, resolutionKey, sourceWorkspaceAlias, matchesFilename)
|
||||
?? findPrioritizedEntry(index, resolutionKey, sourceWorkspaceAlias, matchesAlias)
|
||||
?? findPrioritizedEntry(index, resolutionKey, sourceWorkspaceAlias, matchesTitle)
|
||||
?? findPrioritizedEntry(index, resolutionKey, sourceWorkspaceAlias, matchesHumanizedTitle)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Unified wikilink resolution: find the VaultEntry matching a wikilink target.
|
||||
* Handles pipe syntax, case-insensitive matching.
|
||||
* Resolution order (multi-pass, global priority):
|
||||
* 1. Path-suffix match (for path-style targets like "docs/adr/0031-foo")
|
||||
* 2. Filename stem match (strongest for flat vaults)
|
||||
* 3. Alias match
|
||||
* 4. Exact title match
|
||||
* 5. Humanized title match (kebab-case → words)
|
||||
*/
|
||||
export function resolveEntry(entries: VaultEntry[], rawTarget: WikilinkTarget, sourceEntry?: VaultEntry): VaultEntry | undefined {
|
||||
const index = resolutionIndexForEntries(entries)
|
||||
const resolutionKey = buildResolutionKey(rawTarget, index.workspaceAliases)
|
||||
const sourceWorkspaceAlias = workspaceAliasForEntry(sourceEntry)
|
||||
const cacheKey = resolutionCacheKey(resolutionKey, sourceWorkspaceAlias)
|
||||
if (index.resolutionCache.has(cacheKey)) return index.resolutionCache.get(cacheKey) ?? undefined
|
||||
|
||||
const resolved = resolveEntryFromIndex(index, resolutionKey, sourceWorkspaceAlias)
|
||||
index.resolutionCache.set(cacheKey, resolved ?? null)
|
||||
return resolved
|
||||
}
|
||||
Loading…
Reference in a new issue