import { QuartzPluginData } from "../../../plugins/vfile" import { FilePath, FullSlug, simplifySlug, slugifyFilePath, splitAnchor } from "../../path" import { parseWikilink, resolveWikilinkTarget } from "../../wikilinks" import { BinaryExpr, Literal, Span } from "./ast" import { BaseExpressionDiagnostic } from "./diagnostics" import { ProgramIR, Instruction } from "./ir" export type NullValue = { kind: "null" } export type BooleanValue = { kind: "boolean"; value: boolean } export type NumberValue = { kind: "number"; value: number } export type StringValue = { kind: "string"; value: string } export type DateValue = { kind: "date"; value: Date } export type DurationValue = { kind: "duration"; value: number; months: number } export type ListValue = { kind: "list"; value: Value[] } export type ObjectValue = { kind: "object"; value: Record } export type FileValue = { kind: "file"; value: QuartzPluginData } export type LinkValue = { kind: "link"; value: string; display?: string } export type RegexValue = { kind: "regex"; value: RegExp } export type HtmlValue = { kind: "html"; value: string } export type IconValue = { kind: "icon"; value: string } export type ImageValue = { kind: "image"; value: string } export type Value = | NullValue | BooleanValue | NumberValue | StringValue | DateValue | DurationValue | ListValue | ObjectValue | FileValue | LinkValue | RegexValue | HtmlValue | IconValue | ImageValue export type ValueKind = Value["kind"] export type ValueOf = Extract export function isValueKind(value: Value, kind: "null"): value is NullValue export function isValueKind(value: Value, kind: "boolean"): value is BooleanValue export function isValueKind(value: Value, kind: "number"): value is NumberValue export function isValueKind(value: Value, kind: "string"): value is StringValue export function isValueKind(value: Value, kind: "date"): value is DateValue export function isValueKind(value: Value, kind: "duration"): value is DurationValue export function isValueKind(value: Value, kind: "list"): value is ListValue export function isValueKind(value: Value, kind: "object"): value is ObjectValue export function isValueKind(value: Value, kind: "file"): value is FileValue export function isValueKind(value: Value, kind: "link"): value is LinkValue export function isValueKind(value: Value, kind: "regex"): value is RegexValue export function isValueKind(value: Value, kind: "html"): value is HtmlValue export function isValueKind(value: Value, kind: "icon"): value is IconValue export function isValueKind(value: Value, kind: "image"): value is ImageValue export function isValueKind(value: Value, kind: ValueKind): value is Value { return value.kind === kind } export type EvalContext = { file: QuartzPluginData thisFile?: QuartzPluginData allFiles: QuartzPluginData[] rows?: QuartzPluginData[] fileIndex?: Map backlinksIndex?: Map formulas?: Record formulaSources?: Record formulaCache?: Map formulaStack?: Set locals?: Record values?: Value[] diagnostics?: BaseExpressionDiagnostic[] diagnosticContext?: string diagnosticSource?: string diagnosticSet?: Set propertyCache?: Map } const nullValue: NullValue = { kind: "null" } const makeNull = (): NullValue => nullValue const makeBoolean = (value: boolean): BooleanValue => ({ kind: "boolean", value }) const makeNumber = (value: number): NumberValue => ({ kind: "number", value }) const makeString = (value: string): StringValue => ({ kind: "string", value }) const makeDate = (value: Date): DateValue => ({ kind: "date", value }) const makeDuration = (value: number, months = 0): DurationValue => ({ kind: "duration", value, months, }) const makeList = (value: Value[]): ListValue => ({ kind: "list", value }) const makeObject = (value: Record): ObjectValue => ({ kind: "object", value }) const makeFile = (value: QuartzPluginData): FileValue => ({ kind: "file", value }) const makeLink = (value: string, display?: string): LinkValue => ({ kind: "link", value, display }) const makeRegex = (value: RegExp): RegexValue => ({ kind: "regex", value }) const makeHtml = (value: string): HtmlValue => ({ kind: "html", value }) const makeIcon = (value: string): IconValue => ({ kind: "icon", value }) const makeImage = (value: string): ImageValue => ({ kind: "image", value }) const isRecord = (value: unknown): value is Record => typeof value === "object" && value !== null && !Array.isArray(value) const isValue = (value: unknown): value is Value => typeof value === "object" && value !== null && "kind" in value const isNumberValue = (value: Value): value is NumberValue => isValueKind(value, "number") const isStringValue = (value: Value): value is StringValue => isValueKind(value, "string") const isBooleanValue = (value: Value): value is BooleanValue => isValueKind(value, "boolean") const isListValue = (value: Value): value is ListValue => isValueKind(value, "list") const isObjectValue = (value: Value): value is ObjectValue => isValueKind(value, "object") const isDateValue = (value: Value): value is DateValue => isValueKind(value, "date") const isDurationValue = (value: Value): value is DurationValue => isValueKind(value, "duration") const isFileValue = (value: Value): value is FileValue => isValueKind(value, "file") const isLinkValue = (value: Value): value is LinkValue => isValueKind(value, "link") const isRegexValue = (value: Value): value is RegexValue => isValueKind(value, "regex") const stringMethods = new Set([ "contains", "containsAny", "containsAll", "startsWith", "endsWith", "isEmpty", "lower", "title", "trim", "replace", "repeat", "reverse", "slice", "split", "length", ]) const numberMethods = new Set(["abs", "ceil", "floor", "round", "toFixed", "isEmpty"]) const listMethods = new Set([ "contains", "containsAny", "containsAll", "flat", "join", "reverse", "slice", "sort", "unique", "isEmpty", "length", "sum", "mean", "average", "median", "stddev", "min", "max", ]) const dateMethods = new Set([ "date", "format", "time", "relative", "isEmpty", "year", "month", "day", "hour", "minute", "second", "millisecond", ]) const fileMethods = new Set(["asLink", "hasTag", "inFolder", "hasProperty", "hasLink"]) const linkMethods = new Set(["asFile", "linksTo"]) const objectMethods = new Set(["isEmpty", "keys", "values"]) const formatLinkValue = (value: LinkValue): string => { const target = value.value const display = value.display?.trim() if (display && display.length > 0) { return `[[${target}|${display}]]` } return `[[${target}]]` } const valueToString = (value: Value): string => { switch (value.kind) { case "null": return "" case "boolean": return value.value ? "true" : "false" case "number": return Number.isFinite(value.value) ? String(value.value) : "" case "string": return value.value case "date": return formatDate(value.value) case "duration": return String(durationToMs(value)) case "list": return value.value.map(valueToString).join(", ") case "object": return Object.keys(value.value).length > 0 ? "[object]" : "" case "file": return value.value.slug ? String(value.value.slug) : "" case "link": return value.value case "regex": return value.value.source case "html": return value.value case "icon": return value.value case "image": return value.value } } const valueToNumber = (value: Value): number => { switch (value.kind) { case "number": return value.value case "duration": return durationToMs(value) case "boolean": return value.value ? 1 : 0 case "string": { const num = Number(value.value) return Number.isFinite(num) ? num : Number.NaN } case "date": return value.value.getTime() default: return Number.NaN } } const coerceToNumber = (value: Value): number | null => { const num = valueToNumber(value) return Number.isFinite(num) ? num : null } const coerceToDate = (value: Value): Date | null => { if (isDateValue(value)) return value.value if (isNumberValue(value)) { const date = new Date(value.value) return Number.isNaN(date.getTime()) ? null : date } if (isStringValue(value)) { const parsed = new Date(value.value) return Number.isNaN(parsed.getTime()) ? null : parsed } return null } const isScalarValue = (value: Value): boolean => !( value.kind === "list" || value.kind === "object" || value.kind === "file" || value.kind === "link" || value.kind === "regex" || value.kind === "html" || value.kind === "icon" || value.kind === "image" ) const valueToBoolean = (value: Value): boolean => { switch (value.kind) { case "null": return false case "boolean": return value.value case "number": return Number.isFinite(value.value) && value.value !== 0 case "string": return value.value.length > 0 case "date": return true case "duration": return durationToMs(value) !== 0 case "list": return value.value.length > 0 case "object": return Object.keys(value.value).length > 0 case "file": return true case "link": return value.value.length > 0 case "regex": return true case "html": return value.value.length > 0 case "icon": return value.value.length > 0 case "image": return value.value.length > 0 } } const valueEquals = (left: Value, right: Value, ctx: EvalContext): boolean => { const leftLinkish = isLinkValue(left) || isFileValue(left) const rightLinkish = isLinkValue(right) || isFileValue(right) if ( (leftLinkish && (rightLinkish || isStringValue(right))) || (rightLinkish && isStringValue(left)) ) { const leftKey = resolveLinkComparisonKey(left, ctx) const rightKey = resolveLinkComparisonKey(right, ctx) if (leftKey.slug && rightKey.slug) return leftKey.slug === rightKey.slug if (!leftKey.slug && !rightKey.slug) return leftKey.text === rightKey.text return false } if (left.kind !== right.kind) { if (isScalarValue(left) && isScalarValue(right)) { const leftDate = coerceToDate(left) const rightDate = coerceToDate(right) if (leftDate && rightDate) return leftDate.getTime() === rightDate.getTime() const leftNum = coerceToNumber(left) const rightNum = coerceToNumber(right) if (leftNum !== null && rightNum !== null) return leftNum === rightNum return valueToString(left) === valueToString(right) } return false } if (left.kind === "null") return true if (isBooleanValue(left) && isBooleanValue(right)) return left.value === right.value if (isNumberValue(left) && isNumberValue(right)) return left.value === right.value if (isStringValue(left) && isStringValue(right)) return left.value === right.value if (isDateValue(left) && isDateValue(right)) return left.value.getTime() === right.value.getTime() if (isDurationValue(left) && isDurationValue(right)) { return left.value === right.value && left.months === right.months } if (isRegexValue(left) && isRegexValue(right)) return left.value.source === right.value.source if (isListValue(left) && isListValue(right)) { if (left.value.length !== right.value.length) return false for (let i = 0; i < left.value.length; i += 1) { if (!valueEquals(left.value[i], right.value[i], ctx)) return false } return true } if (isObjectValue(left) && isObjectValue(right)) { const leftKeys = Object.keys(left.value) const rightKeys = Object.keys(right.value) if (leftKeys.length !== rightKeys.length) return false for (const key of leftKeys) { const l = left.value[key] const r = right.value[key] if (!r || !valueEquals(l, r, ctx)) return false } return true } return false } const formatDate = (date: Date): string => { const year = String(date.getUTCFullYear()).padStart(4, "0") const month = String(date.getUTCMonth() + 1).padStart(2, "0") const day = String(date.getUTCDate()).padStart(2, "0") return `${year}-${month}-${day}` } const formatTime = (date: Date): string => { const hour = String(date.getUTCHours()).padStart(2, "0") const minute = String(date.getUTCMinutes()).padStart(2, "0") const second = String(date.getUTCSeconds()).padStart(2, "0") return `${hour}:${minute}:${second}` } const formatDatePattern = (date: Date, pattern: string): string => { const replacements: Record = { YYYY: String(date.getUTCFullYear()).padStart(4, "0"), YY: String(date.getUTCFullYear() % 100).padStart(2, "0"), MM: String(date.getUTCMonth() + 1).padStart(2, "0"), DD: String(date.getUTCDate()).padStart(2, "0"), HH: String(date.getUTCHours()).padStart(2, "0"), mm: String(date.getUTCMinutes()).padStart(2, "0"), ss: String(date.getUTCSeconds()).padStart(2, "0"), SSS: String(date.getUTCMilliseconds()).padStart(3, "0"), } let result = pattern for (const [token, replacement] of Object.entries(replacements)) { result = result.split(token).join(replacement) } return result } const formatRelative = (date: Date): string => { const now = Date.now() const diff = date.getTime() - now const abs = Math.abs(diff) const seconds = Math.round(abs / 1000) const minutes = Math.round(abs / 60000) const hours = Math.round(abs / 3600000) const days = Math.round(abs / 86400000) const weeks = Math.round(abs / 604800000) const direction = diff < 0 ? "ago" : "from now" if (seconds < 60) return `${seconds}s ${direction}` if (minutes < 60) return `${minutes}m ${direction}` if (hours < 24) return `${hours}h ${direction}` if (days < 7) return `${days}d ${direction}` return `${weeks}w ${direction}` } const durationToMs = (duration: DurationValue): number => duration.value + duration.months * 30 * 24 * 60 * 60 * 1000 const parseDurationParts = (input: string): { months: number; ms: number } => { const trimmed = input.trim() const asNumber = Number(trimmed) if (!isNaN(asNumber)) { return { months: 0, ms: asNumber } } let months = 0 let ms = 0 const regex = /(\d+(?:\.\d+)?)\s*([a-zA-Z]+)/g let match while ((match = regex.exec(trimmed)) !== null) { const value = parseFloat(match[1]) const unitRaw = match[2] const unit = unitRaw.toLowerCase() if (unitRaw === "M" || unit === "mo" || unit === "month" || unit === "months") { months += value continue } if (unit === "y" || unit === "yr" || unit === "yrs" || unit === "year" || unit === "years") { months += value * 12 continue } if (unit === "ms" || unit === "millisecond" || unit === "milliseconds") { ms += value continue } if ( unit === "s" || unit === "sec" || unit === "secs" || unit === "second" || unit === "seconds" ) { ms += value * 1000 continue } if ( unit === "m" || unit === "min" || unit === "mins" || unit === "minute" || unit === "minutes" ) { ms += value * 60 * 1000 continue } if (unit === "h" || unit === "hr" || unit === "hrs" || unit === "hour" || unit === "hours") { ms += value * 60 * 60 * 1000 continue } if (unit === "d" || unit === "day" || unit === "days") { ms += value * 24 * 60 * 60 * 1000 continue } if (unit === "w" || unit === "week" || unit === "weeks") { ms += value * 7 * 24 * 60 * 60 * 1000 continue } } return { months, ms } } const addDurationToDate = (date: Date, duration: DurationValue, direction: 1 | -1): Date => { const result = new Date(date.getTime()) if (duration.months !== 0) { const totalMonths = duration.months * direction const wholeMonths = Math.trunc(totalMonths) const fractional = totalMonths - wholeMonths if (wholeMonths !== 0) { result.setUTCMonth(result.getUTCMonth() + wholeMonths) } if (fractional !== 0) { result.setTime(result.getTime() + fractional * 30 * 24 * 60 * 60 * 1000) } } if (duration.value !== 0) { result.setTime(result.getTime() + direction * duration.value) } return result } const pushRuntimeDiagnostic = (ctx: EvalContext, message: string, span: Span) => { if (!ctx.diagnostics || !ctx.diagnosticContext) return const source = ctx.diagnosticSource ?? "" const key = `${ctx.diagnosticContext}|${message}|${span.start.offset}|${span.end.offset}|${source}` if (ctx.diagnosticSet) { if (ctx.diagnosticSet.has(key)) return ctx.diagnosticSet.add(key) } ctx.diagnostics.push({ kind: "runtime", message, span, context: ctx.diagnosticContext, source }) } const parseDurationValue = (raw: Value): DurationValue | null => { if (isDurationValue(raw)) return raw if (isNumberValue(raw)) return makeDuration(raw.value) if (isStringValue(raw)) { const parsed = parseDurationParts(raw.value) return makeDuration(parsed.ms, parsed.months) } return null } const toValue = (input: unknown): Value => { if (input === null || input === undefined) return makeNull() if (typeof input === "boolean") return makeBoolean(input) if (typeof input === "number") return makeNumber(input) if (typeof input === "string") { const trimmed = input.trim() if (/^\d{4}-\d{2}-\d{2}/.test(trimmed)) { const parsed = new Date(trimmed) if (!Number.isNaN(parsed.getTime())) { return makeDate(parsed) } } return makeString(input) } if (input instanceof Date) return makeDate(input) if (input instanceof RegExp) return makeRegex(input) if (Array.isArray(input)) return makeList(input.map(toValue)) if (isRecord(input)) { const obj: Record = {} for (const [key, value] of Object.entries(input)) { obj[key] = toValue(value) } return makeObject(obj) } return makeNull() } export const valueToUnknown = (value: Value): unknown => { switch (value.kind) { case "null": return undefined case "boolean": return value.value case "number": return value.value case "string": return value.value case "date": return value.value case "duration": return durationToMs(value) case "list": return value.value.map(valueToUnknown) case "object": { const obj: Record = {} for (const [key, entry] of Object.entries(value.value)) { obj[key] = valueToUnknown(entry) } return obj } case "file": return value.value case "link": return formatLinkValue(value) case "regex": return value.value case "html": return value.value case "icon": return value.value case "image": return value.value } } const literalToValue = (expr: Literal): Value => { if (expr.kind === "number") return makeNumber(expr.value) if (expr.kind === "string") return makeString(expr.value) if (expr.kind === "boolean") return makeBoolean(expr.value) if (expr.kind === "null") return makeNull() if (expr.kind === "date") return makeDate(new Date(expr.value)) if (expr.kind === "duration") { const parsed = parseDurationParts(expr.value) return makeDuration(parsed.ms, parsed.months) } if (expr.kind === "regex") { const regex = new RegExp(expr.value, expr.flags) return makeRegex(regex) } return makeNull() } const evaluateProgram = (program: ProgramIR, ctx: EvalContext): Value => { const stack: Value[] = [] const instructions = program.instructions let ip = 0 const popValue = (): Value => stack.pop() ?? makeNull() const popArgs = (count: number): Value[] => { if (count <= 0) return [] const start = Math.max(0, stack.length - count) return stack.splice(start, stack.length - start) } while (ip < instructions.length) { const instr = instructions[ip] as Instruction switch (instr.op) { case "const": stack.push(literalToValue(instr.literal)) break case "ident": stack.push(resolveIdentifier(instr.name, ctx)) break case "load_formula": stack.push(resolveFormulaProperty(instr.name, ctx)) break case "load_formula_index": { const indexValue = popValue() if (isStringValue(indexValue)) { stack.push(resolveFormulaProperty(indexValue.value, ctx)) } else { stack.push(makeNull()) } break } case "member": { const objectValue = popValue() stack.push(accessProperty(objectValue, instr.property, ctx)) break } case "index": { const indexValue = popValue() const objectValue = popValue() stack.push(accessIndex(objectValue, indexValue)) break } case "list": { const count = Math.max(0, instr.count) const items = count > 0 ? stack.splice(stack.length - count, count) : [] stack.push(makeList(items)) break } case "unary": { const value = popValue() stack.push(applyUnary(instr.operator, value)) break } case "binary": { const right = popValue() const left = popValue() stack.push(applyBinary(instr.operator, left, right, ctx)) break } case "to_bool": { const value = popValue() stack.push(makeBoolean(valueToBoolean(value))) break } case "call_global": { const args = popArgs(instr.argc) stack.push(evalGlobalCallValues(instr.name, args, ctx, instr.span)) break } case "call_method": { const args = popArgs(instr.argc) const receiver = popValue() stack.push(evalMethodCallValues(receiver, instr.name, args, ctx, instr.span)) break } case "call_dynamic": { const calleeValue = popValue() if (calleeValue.kind === "html") { stack.push(makeHtml(calleeValue.value)) } else { stack.push(makeNull()) } break } case "filter": { const receiver = popValue() stack.push(applyListFilter(receiver, instr.program, ctx)) break } case "map": { const receiver = popValue() stack.push(applyListMap(receiver, instr.program, ctx)) break } case "reduce": { const receiver = popValue() stack.push(applyListReduce(receiver, instr.program, instr.initial, ctx)) break } case "jump": ip = instr.target continue case "jump_if_false": { const value = popValue() if (!valueToBoolean(value)) { ip = instr.target continue } break } case "jump_if_true": { const value = popValue() if (valueToBoolean(value)) { ip = instr.target continue } break } } ip += 1 } return popValue() } export const evaluateExpression = (program: ProgramIR, ctx: EvalContext): Value => evaluateProgram(program, ctx) export const evaluateFilterExpression = (program: ProgramIR, ctx: EvalContext): boolean => valueToBoolean(evaluateExpression(program, ctx)) export const evaluateSummaryExpression = ( program: ProgramIR, values: unknown[], ctx: EvalContext, ): Value => { const valueList = values.map(toValue) const summaryCtx: EvalContext = { ...ctx, values: valueList } return evaluateExpression(program, summaryCtx) } const resolveIdentifier = (name: string, ctx: EvalContext): Value => { if (name === "this") return ctx.thisFile ? makeFile(ctx.thisFile) : makeNull() if (ctx.locals && name in ctx.locals) { const local = ctx.locals[name] if (isValue(local)) return local } if (name === "file") return makeFile(ctx.file) if (name === "note") { const fm = ctx.file.frontmatter return toValue(fm) } if (name === "values" && ctx.values) { return makeList(ctx.values) } if (name === "rows" && ctx.rows) { return makeList(ctx.rows.map((row) => makeFile(row))) } if (name === "formula") { return makeObject({}) } const raw: unknown = ctx.file.frontmatter ? ctx.file.frontmatter[name] : undefined return toValue(raw) } const applyUnary = (operator: "!" | "-", value: Value): Value => { if (operator === "!") { return makeBoolean(!valueToBoolean(value)) } const num = valueToNumber(value) return Number.isFinite(num) ? makeNumber(-num) : makeNull() } const applyBinary = ( operator: BinaryExpr["operator"], left: Value, right: Value, ctx: EvalContext, ): Value => { if (operator === "==") return makeBoolean(valueEquals(left, right, ctx)) if (operator === "!=") return makeBoolean(!valueEquals(left, right, ctx)) if (operator === "+" || operator === "-") { return evalAdditive(operator, left, right) } if (operator === "*" || operator === "/" || operator === "%") { if (isDurationValue(left)) { const rightNum = valueToNumber(right) if (!Number.isFinite(rightNum)) return makeNull() if (operator === "*") return makeDuration(left.value * rightNum, left.months * rightNum) if (operator === "/") { if (rightNum === 0) return makeNull() return makeDuration(left.value / rightNum, left.months / rightNum) } return makeNull() } if (isDurationValue(right)) return makeNull() const leftNum = valueToNumber(left) const rightNum = valueToNumber(right) if (!Number.isFinite(leftNum) || !Number.isFinite(rightNum)) return makeNull() if (operator === "*") return makeNumber(leftNum * rightNum) if (operator === "/") return makeNumber(rightNum === 0 ? Number.NaN : leftNum / rightNum) return makeNumber(rightNum === 0 ? Number.NaN : leftNum % rightNum) } const compare = compareValues(left, right) if (compare === null) return makeNull() if (operator === ">") return makeBoolean(compare > 0) if (operator === ">=") return makeBoolean(compare >= 0) if (operator === "<") return makeBoolean(compare < 0) if (operator === "<=") return makeBoolean(compare <= 0) return makeNull() } const evalAdditive = (operator: "+" | "-", left: Value, right: Value): Value => { if (isDateValue(left) && isDateValue(right) && operator === "-") { return makeDuration(left.value.getTime() - right.value.getTime()) } if (isDateValue(left)) { const duration = parseDurationValue(right) if (duration === null) return makeNull() return makeDate(addDurationToDate(left.value, duration, operator === "+" ? 1 : -1)) } if (isDateValue(right) && operator === "+") { const duration = parseDurationValue(left) if (duration === null) return makeNull() return makeDate(addDurationToDate(right.value, duration, 1)) } if (operator === "+" && (isStringValue(left) || isStringValue(right))) { return makeString(`${valueToString(left)}${valueToString(right)}`) } if (isDurationValue(left) && isDurationValue(right)) { return makeDuration( operator === "+" ? left.value + right.value : left.value - right.value, operator === "+" ? left.months + right.months : left.months - right.months, ) } const leftNum = valueToNumber(left) const rightNum = valueToNumber(right) if (!Number.isFinite(leftNum) || !Number.isFinite(rightNum)) return makeNull() return makeNumber(operator === "+" ? leftNum + rightNum : leftNum - rightNum) } const compareValues = (left: Value, right: Value): number | null => { const leftDate = coerceToDate(left) const rightDate = coerceToDate(right) if (leftDate && rightDate) return leftDate.getTime() - rightDate.getTime() const leftNum = coerceToNumber(left) const rightNum = coerceToNumber(right) if (leftNum !== null && rightNum !== null) return leftNum - rightNum if (isScalarValue(left) && isScalarValue(right)) { const leftStr = valueToString(left) const rightStr = valueToString(right) if (leftStr === rightStr) return 0 return leftStr > rightStr ? 1 : -1 } return null } const accessIndex = (objectValue: Value, indexValue: Value): Value => { if (isListValue(objectValue)) { const index = Math.trunc(valueToNumber(indexValue)) if (!Number.isFinite(index)) return makeNull() const item = objectValue.value[index] return item ?? makeNull() } if (isObjectValue(objectValue) && isStringValue(indexValue)) { const item = objectValue.value[indexValue.value] return item ?? makeNull() } return makeNull() } const evalGlobalCallValues = (name: string, args: Value[], ctx: EvalContext, span: Span): Value => { if (name === "if") { if (args.length < 2) { pushRuntimeDiagnostic(ctx, "if() expects at least 2 arguments", span) } const condition = args[0] ?? makeNull() if (valueToBoolean(condition)) { return args[1] ?? makeNull() } return args[2] ?? makeNull() } if (name === "now") return makeDate(new Date()) if (name === "today") { const d = new Date() d.setUTCHours(0, 0, 0, 0) return makeDate(d) } if (name === "date") { if (args.length < 1) { pushRuntimeDiagnostic(ctx, "date() expects 1 argument", span) } const arg = args[0] ?? makeNull() const str = valueToString(arg) const parsed = new Date(str) if (Number.isNaN(parsed.getTime())) return makeNull() return makeDate(parsed) } if (name === "duration") { if (args.length < 1) { pushRuntimeDiagnostic(ctx, "duration() expects 1 argument", span) } const arg = args[0] ?? makeNull() const parsed = parseDurationParts(valueToString(arg)) return makeDuration(parsed.ms, parsed.months) } if (name === "min" || name === "max") { if (args.length < 1) { pushRuntimeDiagnostic(ctx, `${name}() expects at least 1 argument`, span) } const values = args.map((arg) => valueToNumber(arg)) const nums = values.filter((value) => Number.isFinite(value)) if (nums.length === 0) return makeNull() return makeNumber(name === "min" ? Math.min(...nums) : Math.max(...nums)) } if (name === "number") { if (args.length < 1) { pushRuntimeDiagnostic(ctx, "number() expects 1 argument", span) } const value = args[0] ?? makeNull() const num = valueToNumber(value) return Number.isFinite(num) ? makeNumber(num) : makeNull() } if (name === "link") { if (args.length < 1) { pushRuntimeDiagnostic(ctx, "link() expects at least 1 argument", span) } const target = args[0] ?? makeNull() const display = args[1] ?? makeNull() const targetStr = valueToString(target) const displayStr = valueToString(display) return makeLink(targetStr, displayStr.length > 0 ? displayStr : undefined) } if (name === "list") { if (args.length < 1) { pushRuntimeDiagnostic(ctx, "list() expects 1 argument", span) } const value = args[0] ?? makeNull() if (isListValue(value)) return value return makeList([value]) } if (name === "file") { if (args.length < 1) { pushRuntimeDiagnostic(ctx, "file() expects 1 argument", span) } const arg = args[0] ?? makeNull() const target = valueToString(arg) const file = findFileByTarget(target, ctx) return file ? makeFile(file) : makeNull() } if (name === "image") { if (args.length < 1) { pushRuntimeDiagnostic(ctx, "image() expects 1 argument", span) } const arg = args[0] ?? makeNull() const target = valueToString(arg) return makeImage(target) } if (name === "icon") { if (args.length < 1) { pushRuntimeDiagnostic(ctx, "icon() expects 1 argument", span) } const arg = args[0] ?? makeNull() const target = valueToString(arg) return makeIcon(target) } if (name === "html") { if (args.length < 1) { pushRuntimeDiagnostic(ctx, "html() expects 1 argument", span) } const arg = args[0] ?? makeNull() const target = valueToString(arg) return makeHtml(target) } if (name === "escapeHTML") { if (args.length < 1) { pushRuntimeDiagnostic(ctx, "escapeHTML() expects 1 argument", span) } const arg = args[0] ?? makeNull() const target = valueToString(arg) const escaped = target .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """) .replace(/'/g, "'") return makeString(escaped) } pushRuntimeDiagnostic(ctx, `unknown function: ${name}`, span) return makeNull() } const evalMethodCallValues = ( receiver: Value, method: string, args: Value[], ctx: EvalContext, span: Span, ): Value => { if (method === "isTruthy") { return makeBoolean(valueToBoolean(receiver)) } if (method === "isType") { const arg = args[0] ?? makeNull() const typeName = valueToString(arg).toLowerCase() return makeBoolean(isValueType(receiver, typeName)) } if (method === "toString") { return makeString(valueToString(receiver)) } if (receiver.kind === "null") { if (method === "isEmpty") return makeBoolean(true) if (method === "length") return makeNumber(0) if ( method === "contains" || method === "containsAny" || method === "containsAll" || method === "startsWith" || method === "endsWith" || method === "matches" ) { return makeBoolean(false) } if (method === "asFile" || method === "asLink") { return makeNull() } return makeNull() } if (isStringValue(receiver)) { if (method === "asFile") { const file = findFileByTarget(receiver.value, ctx) return file ? makeFile(file) : makeNull() } if (!stringMethods.has(method)) { pushRuntimeDiagnostic(ctx, `unknown string method: ${method}`, span) return makeNull() } return evalStringMethod(receiver, method, args) } if (isNumberValue(receiver)) { if (!numberMethods.has(method)) { pushRuntimeDiagnostic(ctx, `unknown number method: ${method}`, span) return makeNull() } return evalNumberMethod(receiver, method, args) } if (isListValue(receiver)) { if (!listMethods.has(method)) { pushRuntimeDiagnostic(ctx, `unknown list method: ${method}`, span) return makeNull() } return evalListMethod(receiver, method, args, ctx) } if (isDateValue(receiver)) { if (!dateMethods.has(method)) { pushRuntimeDiagnostic(ctx, `unknown date method: ${method}`, span) return makeNull() } return evalDateMethod(receiver, method, args) } if (isFileValue(receiver)) { if (!fileMethods.has(method)) { pushRuntimeDiagnostic(ctx, `unknown file method: ${method}`, span) return makeNull() } return evalFileMethod(receiver, method, args, ctx) } if (isLinkValue(receiver)) { if (!linkMethods.has(method)) { pushRuntimeDiagnostic(ctx, `unknown link method: ${method}`, span) return makeNull() } return evalLinkMethod(receiver, method, args, ctx) } if (isObjectValue(receiver)) { if (!objectMethods.has(method)) { pushRuntimeDiagnostic(ctx, `unknown object method: ${method}`, span) return makeNull() } return evalObjectMethod(receiver, method) } if (isRegexValue(receiver)) { if (method === "matches") { const value = args[0] ?? makeNull() return makeBoolean(receiver.value.test(valueToString(value))) } pushRuntimeDiagnostic(ctx, `unknown regex method: ${method}`, span) return makeNull() } pushRuntimeDiagnostic(ctx, `unknown ${receiver.kind} method: ${method}`, span) return makeNull() } const evalStringMethod = (receiver: StringValue, method: string, args: Value[]): Value => { const value = receiver.value if (method === "contains") { const arg = args[0] ?? makeNull() return makeBoolean(value.includes(valueToString(arg))) } if (method === "containsAny") { const values = args.map((arg) => valueToString(arg)) return makeBoolean(values.some((entry) => value.includes(entry))) } if (method === "containsAll") { const values = args.map((arg) => valueToString(arg)) return makeBoolean(values.every((entry) => value.includes(entry))) } if (method === "startsWith") { const arg = args[0] ?? makeNull() return makeBoolean(value.startsWith(valueToString(arg))) } if (method === "endsWith") { const arg = args[0] ?? makeNull() return makeBoolean(value.endsWith(valueToString(arg))) } if (method === "isEmpty") { return makeBoolean(value.length === 0) } if (method === "lower") { return makeString(value.toLowerCase()) } if (method === "title") { const parts = value.split(/\s+/).map((part) => { const lower = part.toLowerCase() return lower.length > 0 ? `${lower[0].toUpperCase()}${lower.slice(1)}` : lower }) return makeString(parts.join(" ")) } if (method === "trim") { return makeString(value.trim()) } if (method === "replace") { const patternVal = args[0] ?? makeNull() const replacementVal = args[1] ?? makeNull() const replacement = valueToString(replacementVal) if (isRegexValue(patternVal)) { return makeString(value.replace(patternVal.value, replacement)) } return makeString(value.replace(valueToString(patternVal), replacement)) } if (method === "repeat") { const count = args[0] ? valueToNumber(args[0]) : 0 return makeString(value.repeat(Number.isFinite(count) ? Math.max(0, count) : 0)) } if (method === "reverse") { return makeString(value.split("").reverse().join("")) } if (method === "slice") { const start = args[0] ? valueToNumber(args[0]) : 0 const end = args[1] ? valueToNumber(args[1]) : undefined const startIndex = Number.isFinite(start) ? Math.trunc(start) : 0 const endIndex = end !== undefined && Number.isFinite(end) ? Math.trunc(end) : undefined return makeString(value.slice(startIndex, endIndex)) } if (method === "split") { const separatorValue = args[0] ?? makeString("") const separator = isRegexValue(separatorValue) ? separatorValue.value : valueToString(separatorValue) const limitValue = args[1] ? valueToNumber(args[1]) : undefined const limit = limitValue !== undefined && Number.isFinite(limitValue) ? Math.trunc(limitValue) : undefined const parts = limit !== undefined ? value.split(separator, limit) : value.split(separator) return makeList(parts.map((entry) => makeString(entry))) } if (method === "length") { return makeNumber(value.length) } return makeNull() } const evalNumberMethod = (receiver: NumberValue, method: string, args: Value[]): Value => { const value = receiver.value if (method === "abs") return makeNumber(Math.abs(value)) if (method === "ceil") return makeNumber(Math.ceil(value)) if (method === "floor") return makeNumber(Math.floor(value)) if (method === "round") { const digits = args[0] ? valueToNumber(args[0]) : 0 if (!Number.isFinite(digits)) return makeNumber(Math.round(value)) const factor = 10 ** Math.trunc(digits) return makeNumber(Math.round(value * factor) / factor) } if (method === "toFixed") { const digits = args[0] ? valueToNumber(args[0]) : 0 const precision = Number.isFinite(digits) ? Math.trunc(digits) : 0 return makeString(value.toFixed(Math.max(0, precision))) } if (method === "isEmpty") { return makeBoolean(!Number.isFinite(value)) } return makeNull() } const evalListMethod = ( receiver: ListValue, method: string, args: Value[], ctx: EvalContext, ): Value => { const list = receiver.value if (method === "contains") { const arg = args[0] ?? makeNull() return makeBoolean(list.some((entry) => valueEquals(entry, arg, ctx))) } if (method === "containsAny") { const values = args return makeBoolean(values.some((entry) => list.some((item) => valueEquals(item, entry, ctx)))) } if (method === "containsAll") { const values = args return makeBoolean(values.every((entry) => list.some((item) => valueEquals(item, entry, ctx)))) } if (method === "flat") { const flattened: Value[] = [] for (const item of list) { if (isListValue(item)) { flattened.push(...item.value) } else { flattened.push(item) } } return makeList(flattened) } if (method === "join") { const separator = args[0] ? valueToString(args[0]) : "," return makeString(list.map(valueToString).join(separator)) } if (method === "reverse") { return makeList([...list].reverse()) } if (method === "slice") { const start = args[0] ? valueToNumber(args[0]) : 0 const end = args[1] ? valueToNumber(args[1]) : undefined const startIndex = Number.isFinite(start) ? Math.trunc(start) : 0 const endIndex = end !== undefined && Number.isFinite(end) ? Math.trunc(end) : undefined return makeList(list.slice(startIndex, endIndex)) } if (method === "sort") { const sorted = [...list].sort((a, b) => { const aNum = valueToNumber(a) const bNum = valueToNumber(b) if (Number.isFinite(aNum) && Number.isFinite(bNum)) return aNum - bNum const aStr = valueToString(a) const bStr = valueToString(b) if (aStr === bStr) return 0 return aStr > bStr ? 1 : -1 }) return makeList(sorted) } if (method === "unique") { const unique: Value[] = [] for (const item of list) { if (!unique.some((entry) => valueEquals(entry, item, ctx))) { unique.push(item) } } return makeList(unique) } if (method === "sum") { const nums = list.map(valueToNumber).filter((value) => Number.isFinite(value)) if (nums.length === 0) return makeNull() return makeNumber(nums.reduce((acc, value) => acc + value, 0)) } if (method === "mean" || method === "average") { const nums = list.map(valueToNumber).filter((value) => Number.isFinite(value)) if (nums.length === 0) return makeNull() const sum = nums.reduce((acc, value) => acc + value, 0) return makeNumber(sum / nums.length) } if (method === "median") { const nums = list.map(valueToNumber).filter((value) => Number.isFinite(value)) if (nums.length === 0) return makeNull() const sorted = [...nums].sort((a, b) => a - b) const mid = Math.floor(sorted.length / 2) if (sorted.length % 2 === 0) { return makeNumber((sorted[mid - 1] + sorted[mid]) / 2) } return makeNumber(sorted[mid]) } if (method === "stddev") { const nums = list.map(valueToNumber).filter((value) => Number.isFinite(value)) if (nums.length === 0) return makeNull() const mean = nums.reduce((acc, value) => acc + value, 0) / nums.length const variance = nums.reduce((acc, value) => acc + (value - mean) ** 2, 0) / nums.length return makeNumber(Math.sqrt(variance)) } if (method === "min" || method === "max") { const nums = list.map(valueToNumber).filter((value) => Number.isFinite(value)) if (nums.length === 0) return makeNull() const value = method === "min" ? Math.min(...nums) : Math.max(...nums) return makeNumber(value) } if (method === "isEmpty") { return makeBoolean(list.length === 0) } if (method === "length") { return makeNumber(list.length) } return makeNull() } const applyListFilter = (receiver: Value, program: ProgramIR | null, ctx: EvalContext): Value => { if (!isListValue(receiver)) return makeNull() if (!program) return makeList(receiver.value) const list = receiver.value const filtered = list.filter((value, index) => { const locals: Record = { value, index: makeNumber(index) } const baseLocals = ctx.locals ? ctx.locals : {} const fileCtx = isFileValue(value) ? { ...ctx, file: value.value, propertyCache: undefined, formulaCache: undefined, formulaStack: new Set(), } : ctx const nextCtx: EvalContext = { ...fileCtx, locals: { ...baseLocals, ...locals } } return valueToBoolean(evaluateProgram(program, nextCtx)) }) return makeList(filtered) } const applyListMap = (receiver: Value, program: ProgramIR | null, ctx: EvalContext): Value => { if (!isListValue(receiver)) return makeNull() if (!program) return makeList(receiver.value) const list = receiver.value const mapped = list.map((value, index) => { const locals: Record = { value, index: makeNumber(index) } const baseLocals = ctx.locals ? ctx.locals : {} const fileCtx = isFileValue(value) ? { ...ctx, file: value.value, propertyCache: undefined, formulaCache: undefined, formulaStack: new Set(), } : ctx const nextCtx: EvalContext = { ...fileCtx, locals: { ...baseLocals, ...locals } } return evaluateProgram(program, nextCtx) }) return makeList(mapped) } const applyListReduce = ( receiver: Value, program: ProgramIR | null, initial: ProgramIR | null, ctx: EvalContext, ): Value => { if (!isListValue(receiver)) return makeNull() const initialValue = initial ? evaluateProgram(initial, ctx) : makeNull() if (!program) return initialValue let acc = initialValue const list = receiver.value for (let index = 0; index < list.length; index += 1) { const value = list[index] const locals: Record = { value, index: makeNumber(index), acc } const baseLocals = ctx.locals ? ctx.locals : {} const fileCtx = isFileValue(value) ? { ...ctx, file: value.value, propertyCache: undefined, formulaCache: undefined, formulaStack: new Set(), } : ctx const nextCtx: EvalContext = { ...fileCtx, locals: { ...baseLocals, ...locals } } acc = evaluateProgram(program, nextCtx) } return acc } const evalDateMethod = (receiver: DateValue, method: string, args: Value[]): Value => { const value = receiver.value if (method === "date") { const date = new Date(value.getTime()) date.setUTCHours(0, 0, 0, 0) return makeDate(date) } if (method === "format") { const pattern = args[0] ? valueToString(args[0]) : "YYYY-MM-DD" return makeString(formatDatePattern(value, pattern)) } if (method === "time") { return makeString(formatTime(value)) } if (method === "relative") { return makeString(formatRelative(value)) } if (method === "isEmpty") { return makeBoolean(false) } if (method === "year") return makeNumber(value.getUTCFullYear()) if (method === "month") return makeNumber(value.getUTCMonth() + 1) if (method === "day") return makeNumber(value.getUTCDate()) if (method === "hour") return makeNumber(value.getUTCHours()) if (method === "minute") return makeNumber(value.getUTCMinutes()) if (method === "second") return makeNumber(value.getUTCSeconds()) if (method === "millisecond") return makeNumber(value.getUTCMilliseconds()) return makeNull() } const evalObjectMethod = (receiver: ObjectValue, method: string): Value => { const entries = Object.entries(receiver.value) if (method === "isEmpty") return makeBoolean(entries.length === 0) if (method === "keys") return makeList(entries.map(([key]) => makeString(key))) if (method === "values") return makeList(entries.map(([, value]) => value)) return makeNull() } const evalFileMethod = ( receiver: FileValue, method: string, args: Value[], ctx: EvalContext, ): Value => { const file = receiver.value if (method === "asLink") { const display = args[0] ? valueToString(args[0]) : undefined const slug = file.slug ? String(file.slug) : "" return makeLink(slug, display) } if (method === "hasTag") { const tags = args.map((arg) => valueToString(arg)) const rawTags = file.frontmatter?.tags const fileTags = Array.isArray(rawTags) ? rawTags : typeof rawTags === "string" ? [rawTags] : [] return makeBoolean(tags.some((tag) => fileTags.includes(tag))) } if (method === "inFolder") { const folder = args[0] ? valueToString(args[0]) : "" const slug = file.slug ? String(file.slug) : "" const normalized = folder.endsWith("/") ? folder : `${folder}/` return makeBoolean(slug.startsWith(normalized)) } if (method === "hasProperty") { const prop = args[0] ? valueToString(args[0]) : "" const fm = file.frontmatter return makeBoolean(Boolean(fm && prop in fm)) } if (method === "hasLink") { const arg = args[0] ?? makeNull() const targetSlug = resolveLinkSlugFromValue(arg, ctx) if (!targetSlug) return makeBoolean(false) const links = Array.isArray(file.links) ? file.links.map((link) => String(link)) : [] return makeBoolean(links.includes(targetSlug)) } return makeNull() } const evalLinkMethod = ( receiver: LinkValue, method: string, args: Value[], ctx: EvalContext, ): Value => { if (method === "asFile") { const file = findFileByTarget(receiver.value, ctx) return file ? makeFile(file) : makeNull() } if (method === "linksTo") { const arg = args[0] ?? makeNull() const targetSlug = resolveLinkSlugFromValue(arg, ctx) const receiverSlug = resolveLinkSlugFromText(receiver.value, ctx) if (!targetSlug || !receiverSlug) return makeBoolean(false) return makeBoolean(receiverSlug === targetSlug) } return makeNull() } const resolveFormulaProperty = (name: string, ctx: EvalContext): Value => { if (!ctx.formulas || !ctx.formulas[name]) return makeNull() if (!ctx.formulaCache) ctx.formulaCache = new Map() if (!ctx.formulaStack) ctx.formulaStack = new Set() const cached = ctx.formulaCache.get(name) if (cached) return cached if (ctx.formulaStack.has(name)) return makeNull() ctx.formulaStack.add(name) const expr = ctx.formulas[name] const nextCtx: EvalContext = { ...ctx, diagnosticContext: `formula.${name}`, diagnosticSource: ctx.formulaSources?.[name] ?? ctx.diagnosticSource, } const value = evaluateExpression(expr, nextCtx) ctx.formulaCache.set(name, value) ctx.formulaStack.delete(name) return value } const resolveFileProperty = (file: QuartzPluginData, property: string, ctx: EvalContext): Value => { if (property === "file") return makeFile(file) if (property === "name" || property === "basename") { const filePath = typeof file.filePath === "string" ? file.filePath : "" const source = filePath.length > 0 ? filePath : file.slug ? String(file.slug) : "" const segment = source.split("/").pop() || "" if (property === "name") return makeString(segment) const basename = segment.replace(/\.[^/.]+$/, "") return makeString(basename) } if (property === "title") { const title = typeof file.frontmatter?.title === "string" ? file.frontmatter.title : "" if (title.length > 0) return makeString(title) return resolveFileProperty(file, "basename", ctx) } if (property === "path") { const path = file.filePath || file.slug || "" return makeString(String(path)) } if (property === "folder") { const slug = file.slug ? String(file.slug) : "" const parts = slug.split("/") const folder = parts.length > 1 ? parts.slice(0, -1).join("/") : "" return makeString(folder) } if (property === "ext") { const filePath = typeof file.filePath === "string" ? file.filePath : "" const slug = file.slug ? String(file.slug) : "" const source = filePath.length > 0 ? filePath : slug const match = source.match(/\.([^.]+)$/) return makeString(match ? match[1] : "md") } if (property === "size") { if (isRecord(file) && typeof file.size === "number") { return makeNumber(file.size) } return makeNull() } if (property === "ctime") { const created = file.dates?.created if (!created) return makeNull() return makeDate(new Date(created)) } if (property === "mtime") { const modified = file.dates?.modified if (!modified) return makeNull() return makeDate(new Date(modified)) } if (property === "tags") { const rawTags = file.frontmatter?.tags const tags = Array.isArray(rawTags) ? rawTags : typeof rawTags === "string" ? [rawTags] : [] return makeList(tags.map((tag) => makeString(String(tag)))) } if (property === "aliases") { const aliases = file.frontmatter?.aliases if (!aliases) return makeList([]) const list = Array.isArray(aliases) ? aliases : [aliases] return makeList(list.map((alias) => makeString(String(alias)))) } if (property === "links" || property === "outlinks") { const links = Array.isArray(file.links) ? file.links : [] return makeList(links.map((link) => makeLink(String(link)))) } if (property === "backlinks" || property === "inlinks") { const slug = file.slug ? String(file.slug) : "" const key = simplifySlug(slug as FullSlug) const backlinks = ctx.backlinksIndex?.get(key) ?? ctx.allFiles .filter((entry) => { const links = Array.isArray(entry.links) ? entry.links.map((link) => String(link)) : [] return key.length > 0 && links.includes(key) }) .map((entry) => (entry.slug ? simplifySlug(entry.slug as FullSlug) : "")) .filter((entry) => entry.length > 0) return makeList(backlinks.map((link) => makeLink(String(link)))) } if (property === "embeds") { const embeds = isRecord(file) && Array.isArray(file.embeds) ? file.embeds : [] return makeList(embeds.map((entry) => makeString(String(entry)))) } if (property === "properties") { return toValue(file.frontmatter) } if (property === "link") { const slug = file.slug ? String(file.slug) : "" return makeLink(slug) } const raw: unknown = file.frontmatter ? file.frontmatter[property] : undefined return toValue(raw) } const accessProperty = (value: Value, property: string, ctx: EvalContext): Value => { if (isStringValue(value) && property === "length") return makeNumber(value.value.length) if (isListValue(value) && property === "length") return makeNumber(value.value.length) if (isDateValue(value)) { if (property === "year") return makeNumber(value.value.getUTCFullYear()) if (property === "month") return makeNumber(value.value.getUTCMonth() + 1) if (property === "day") return makeNumber(value.value.getUTCDate()) if (property === "hour") return makeNumber(value.value.getUTCHours()) if (property === "minute") return makeNumber(value.value.getUTCMinutes()) if (property === "second") return makeNumber(value.value.getUTCSeconds()) if (property === "millisecond") return makeNumber(value.value.getUTCMilliseconds()) } if (isObjectValue(value)) { return value.value[property] ?? makeNull() } if (isFileValue(value)) { return resolveFileProperty(value.value, property, ctx) } if (isLinkValue(value)) { if (property === "value") return makeString(value.value) } return makeNull() } const isValueType = (value: Value, typeName: string): boolean => { if (typeName === "null" || typeName === "undefined") return value.kind === "null" if (typeName === "string") return value.kind === "string" if (typeName === "number") return value.kind === "number" if (typeName === "boolean") return value.kind === "boolean" if (typeName === "array" || typeName === "list") return value.kind === "list" if (typeName === "object") return value.kind === "object" if (typeName === "date") return value.kind === "date" if (typeName === "duration") return value.kind === "duration" if (typeName === "file") return value.kind === "file" if (typeName === "link") return value.kind === "link" return false } const resolveFileSlug = (file: QuartzPluginData): string | undefined => { if (!file.slug) return undefined return simplifySlug(file.slug as FullSlug) } const normalizeLinkText = (value: string): string => value.trim() const resolveLinkSlugFromText = (raw: string, ctx: EvalContext): string | undefined => { const trimmed = raw.trim() if (!trimmed) return undefined if (/^[a-z][a-z0-9+.-]*:/.test(trimmed)) return undefined const currentSlug = ctx.file.slug ? String(ctx.file.slug) : ctx.thisFile?.slug ? String(ctx.thisFile.slug) : undefined const parsed = parseWikilink(trimmed) if (parsed) { if (/^[a-z][a-z0-9+.-]*:/.test(parsed.target)) return undefined if (currentSlug) { const resolved = resolveWikilinkTarget(parsed, currentSlug as FullSlug) return resolved ? simplifySlug(resolved.slug) : undefined } const parsedTarget = parsed.target.trim() if (!parsedTarget) return undefined const slug = slugifyFilePath(parsedTarget as FilePath) return simplifySlug(slug) } const [target, anchor] = splitAnchor(trimmed) if (currentSlug) { const resolved = resolveWikilinkTarget( { raw: trimmed, target, anchor: anchor.length > 0 ? anchor : undefined, alias: undefined, embed: trimmed.startsWith("!"), }, currentSlug as FullSlug, ) if (resolved) return simplifySlug(resolved.slug) } if (!target) return undefined const normalized = target.replace(/\\/g, "/").replace(/^\/+/, "") if (!normalized) return undefined const slug = slugifyFilePath(normalized as FilePath) return simplifySlug(slug) } const resolveLinkSlugFromValue = (value: Value, ctx: EvalContext): string | undefined => { if (isFileValue(value)) return resolveFileSlug(value.value) if (isLinkValue(value)) return resolveLinkSlugFromText(value.value, ctx) if (isStringValue(value)) return resolveLinkSlugFromText(value.value, ctx) return undefined } const resolveLinkComparisonKey = ( value: Value, ctx: EvalContext, ): { slug?: string; text: string } => { if (isFileValue(value)) { const slug = resolveFileSlug(value.value) return { slug, text: slug ?? "" } } if (isLinkValue(value)) { const slug = resolveLinkSlugFromText(value.value, ctx) const text = value.display && value.display.length > 0 ? value.display : value.value return { slug, text: normalizeLinkText(text) } } if (isStringValue(value)) { const slug = resolveLinkSlugFromText(value.value, ctx) return { slug, text: normalizeLinkText(value.value) } } return { text: normalizeLinkText(valueToString(value)) } } const findFileByTarget = (target: string, ctx: EvalContext): QuartzPluginData | undefined => { const slug = resolveLinkSlugFromText(target, ctx) if (!slug) return undefined const indexed = ctx.fileIndex?.get(slug) if (indexed) return indexed return ctx.allFiles.find((entry) => resolveFileSlug(entry) === slug) }