@hoardodile/ui 0.1.6 → 0.1.8
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.
- package/dist/chunk-2BVZEYNW.js +97 -0
- package/dist/chunk-2BVZEYNW.js.map +1 -0
- package/dist/chunk-2DTR6CD5.js +47 -0
- package/dist/chunk-2DTR6CD5.js.map +1 -0
- package/dist/chunk-2HWSCY4T.js +20 -0
- package/dist/chunk-2HWSCY4T.js.map +1 -0
- package/dist/chunk-363CCKQ7.js +75 -0
- package/dist/chunk-363CCKQ7.js.map +1 -0
- package/dist/chunk-3ZQJWBVB.js +30 -0
- package/dist/chunk-3ZQJWBVB.js.map +1 -0
- package/dist/chunk-4D36OO5W.js +277 -0
- package/dist/chunk-4D36OO5W.js.map +1 -0
- package/dist/chunk-4UITFLIN.js +31 -0
- package/dist/chunk-4UITFLIN.js.map +1 -0
- package/dist/chunk-6HAIUXNZ.js +11 -0
- package/dist/chunk-6HAIUXNZ.js.map +1 -0
- package/dist/chunk-7JMMFY67.js +62 -0
- package/dist/chunk-7JMMFY67.js.map +1 -0
- package/dist/chunk-7MWWHI6T.js +1149 -0
- package/dist/chunk-7MWWHI6T.js.map +1 -0
- package/dist/chunk-AEHTAUAQ.js +65 -0
- package/dist/chunk-AEHTAUAQ.js.map +1 -0
- package/dist/chunk-ASRGQ5BD.js +130 -0
- package/dist/chunk-ASRGQ5BD.js.map +1 -0
- package/dist/chunk-BFGOE5AI.js +187 -0
- package/dist/chunk-BFGOE5AI.js.map +1 -0
- package/dist/chunk-BRT25IDQ.js +46 -0
- package/dist/chunk-BRT25IDQ.js.map +1 -0
- package/dist/chunk-BU5DQOPM.js +29 -0
- package/dist/chunk-BU5DQOPM.js.map +1 -0
- package/dist/chunk-BWASOT5X.js +266 -0
- package/dist/chunk-BWASOT5X.js.map +1 -0
- package/dist/chunk-F32V4Z7Q.js +26 -0
- package/dist/chunk-F32V4Z7Q.js.map +1 -0
- package/dist/chunk-F7RHDRRJ.js +24 -0
- package/dist/chunk-F7RHDRRJ.js.map +1 -0
- package/dist/chunk-FH5RNCFR.js +21 -0
- package/dist/chunk-FH5RNCFR.js.map +1 -0
- package/dist/chunk-GKSX5ULZ.js +513 -0
- package/dist/chunk-GKSX5ULZ.js.map +1 -0
- package/dist/chunk-JPFDNRCA.js +26 -0
- package/dist/chunk-JPFDNRCA.js.map +1 -0
- package/dist/chunk-KNPRUZ4H.js +13 -0
- package/dist/chunk-KNPRUZ4H.js.map +1 -0
- package/dist/chunk-KOFWEPPB.js +254 -0
- package/dist/chunk-KOFWEPPB.js.map +1 -0
- package/dist/chunk-M5OJN54O.js +10 -0
- package/dist/chunk-M5OJN54O.js.map +1 -0
- package/dist/chunk-OEVALKQ5.js +51 -0
- package/dist/chunk-OEVALKQ5.js.map +1 -0
- package/dist/chunk-OW5FWLPH.js +33 -0
- package/dist/chunk-OW5FWLPH.js.map +1 -0
- package/dist/chunk-POX7TXBH.js +41 -0
- package/dist/chunk-POX7TXBH.js.map +1 -0
- package/dist/chunk-PWJBHUM2.js +58 -0
- package/dist/chunk-PWJBHUM2.js.map +1 -0
- package/dist/chunk-QGMDWGJT.js +23 -0
- package/dist/chunk-QGMDWGJT.js.map +1 -0
- package/dist/chunk-QGTQQNUL.js +36 -0
- package/dist/chunk-QGTQQNUL.js.map +1 -0
- package/dist/chunk-SAY52ZKI.js +152 -0
- package/dist/chunk-SAY52ZKI.js.map +1 -0
- package/dist/chunk-SKDPGSRL.js +166 -0
- package/dist/chunk-SKDPGSRL.js.map +1 -0
- package/dist/chunk-T56ARCUK.js +38 -0
- package/dist/chunk-T56ARCUK.js.map +1 -0
- package/dist/chunk-TDDHQZ62.js +227 -0
- package/dist/chunk-TDDHQZ62.js.map +1 -0
- package/dist/chunk-TMQ5OMBK.js +29 -0
- package/dist/chunk-TMQ5OMBK.js.map +1 -0
- package/dist/chunk-UEVND7AS.js +23 -0
- package/dist/chunk-UEVND7AS.js.map +1 -0
- package/dist/chunk-UPWHCG4S.js +40 -0
- package/dist/chunk-UPWHCG4S.js.map +1 -0
- package/dist/chunk-VWUNF7VX.js +3 -0
- package/dist/chunk-VWUNF7VX.js.map +1 -0
- package/dist/chunk-WI6O7GZK.js +45 -0
- package/dist/chunk-WI6O7GZK.js.map +1 -0
- package/dist/chunk-WVEK3WCX.js +21 -0
- package/dist/chunk-WVEK3WCX.js.map +1 -0
- package/dist/chunk-XBPQEZES.js +48 -0
- package/dist/chunk-XBPQEZES.js.map +1 -0
- package/dist/chunk-ZYAGKKR7.js +142 -0
- package/dist/chunk-ZYAGKKR7.js.map +1 -0
- package/dist/components/alert.js +2 -21
- package/dist/components/alert.js.map +1 -1
- package/dist/components/app-dialog.js +10 -654
- package/dist/components/app-dialog.js.map +1 -1
- package/dist/components/badge.js +2 -23
- package/dist/components/badge.js.map +1 -1
- package/dist/components/breadcrumb.js +5 -501
- package/dist/components/breadcrumb.js.map +1 -1
- package/dist/components/button.js +3 -76
- package/dist/components/button.js.map +1 -1
- package/dist/components/caption-bar.js +7 -518
- package/dist/components/caption-bar.js.map +1 -1
- package/dist/components/checkbox.js +4 -74
- package/dist/components/checkbox.js.map +1 -1
- package/dist/components/chip-button.js +4 -85
- package/dist/components/chip-button.js.map +1 -1
- package/dist/components/close-confirm-dialog.js +12 -662
- package/dist/components/close-confirm-dialog.js.map +1 -1
- package/dist/components/color-picker.js +7 -530
- package/dist/components/color-picker.js.map +1 -1
- package/dist/components/combobox.js +15 -934
- package/dist/components/combobox.js.map +1 -1
- package/dist/components/confirm-by-typing-dialog.js +12 -669
- package/dist/components/confirm-by-typing-dialog.js.map +1 -1
- package/dist/components/confirm-dialog.js +11 -637
- package/dist/components/confirm-dialog.js.map +1 -1
- package/dist/components/count-badge.js +2 -21
- package/dist/components/count-badge.js.map +1 -1
- package/dist/components/dialog.js +8 -569
- package/dist/components/dialog.js.map +1 -1
- package/dist/components/dropdown-menu.js +9 -1004
- package/dist/components/dropdown-menu.js.map +1 -1
- package/dist/components/dropdown-select.js +9 -880
- package/dist/components/dropdown-select.js.map +1 -1
- package/dist/components/empty.js +2 -21
- package/dist/components/empty.js.map +1 -1
- package/dist/components/filter-rail.js +7 -145
- package/dist/components/filter-rail.js.map +1 -1
- package/dist/components/fixed-action-bar.js +2 -21
- package/dist/components/fixed-action-bar.js.map +1 -1
- package/dist/components/font-picker.js +11 -586
- package/dist/components/font-picker.js.map +1 -1
- package/dist/components/form.js +9 -66
- package/dist/components/form.js.map +1 -1
- package/dist/components/icon-tile.js +3 -43
- package/dist/components/icon-tile.js.map +1 -1
- package/dist/components/icon-toggle.js +3 -43
- package/dist/components/icon-toggle.js.map +1 -1
- package/dist/components/icon.js +3 -47
- package/dist/components/icon.js.map +1 -1
- package/dist/components/image-crop-panel.js +19 -772
- package/dist/components/image-crop-panel.js.map +1 -1
- package/dist/components/image-cropper.js +1 -164
- package/dist/components/image-cropper.js.map +1 -1
- package/dist/components/input-group.js +6 -268
- package/dist/components/input-group.js.map +1 -1
- package/dist/components/input.js +3 -58
- package/dist/components/input.js.map +1 -1
- package/dist/components/label.js +3 -38
- package/dist/components/label.js.map +1 -1
- package/dist/components/list-empty-row.js +2 -21
- package/dist/components/list-empty-row.js.map +1 -1
- package/dist/components/management-empty.js +2 -21
- package/dist/components/management-empty.js.map +1 -1
- package/dist/components/management-skeleton.js +4 -59
- package/dist/components/management-skeleton.js.map +1 -1
- package/dist/components/meta-chip.js +2 -21
- package/dist/components/meta-chip.js.map +1 -1
- package/dist/components/mobile-drawer.js +7 -268
- package/dist/components/mobile-drawer.js.map +1 -1
- package/dist/components/page-header.js +3 -42
- package/dist/components/page-header.js.map +1 -1
- package/dist/components/page-scaffold.js +5 -76
- package/dist/components/page-scaffold.js.map +1 -1
- package/dist/components/pagination-bar.js +13 -614
- package/dist/components/pagination-bar.js.map +1 -1
- package/dist/components/pagination.js +6 -681
- package/dist/components/pagination.js.map +1 -1
- package/dist/components/panel-toolbar.js +11 -713
- package/dist/components/panel-toolbar.js.map +1 -1
- package/dist/components/pill-tabs.js +2 -21
- package/dist/components/pill-tabs.js.map +1 -1
- package/dist/components/plugin-download-consent.js +13 -663
- package/dist/components/plugin-download-consent.js.map +1 -1
- package/dist/components/popover.js +5 -242
- package/dist/components/popover.js.map +1 -1
- package/dist/components/preview-card.js +2 -21
- package/dist/components/preview-card.js.map +1 -1
- package/dist/components/progress.js +2 -20
- package/dist/components/progress.js.map +1 -1
- package/dist/components/radio-group.js +2 -21
- package/dist/components/radio-group.js.map +1 -1
- package/dist/components/scroll-area.js +2 -21
- package/dist/components/scroll-area.js.map +1 -1
- package/dist/components/search-field.js +6 -587
- package/dist/components/search-field.js.map +1 -1
- package/dist/components/secondary-stat.js +3 -40
- package/dist/components/secondary-stat.js.map +1 -1
- package/dist/components/section-header.js +3 -43
- package/dist/components/section-header.js.map +1 -1
- package/dist/components/section-label.js +3 -44
- package/dist/components/section-label.js.map +1 -1
- package/dist/components/section-tabs.js +2 -21
- package/dist/components/section-tabs.js.map +1 -1
- package/dist/components/separator.js +3 -47
- package/dist/components/separator.js.map +1 -1
- package/dist/components/sheet.js +8 -467
- package/dist/components/sheet.js.map +1 -1
- package/dist/components/sidebar.js +24 -1044
- package/dist/components/sidebar.js.map +1 -1
- package/dist/components/skeleton.js +3 -39
- package/dist/components/skeleton.js.map +1 -1
- package/dist/components/slider.js +2 -21
- package/dist/components/slider.js.map +1 -1
- package/dist/components/sortable-chip-list.js +2 -21
- package/dist/components/sortable-chip-list.js.map +1 -1
- package/dist/components/special-tag-surface.js +3 -531
- package/dist/components/special-tag-surface.js.map +1 -1
- package/dist/components/spinner.js +3 -63
- package/dist/components/spinner.js.map +1 -1
- package/dist/components/surface.js +3 -49
- package/dist/components/surface.js.map +1 -1
- package/dist/components/switch.js +3 -51
- package/dist/components/switch.js.map +1 -1
- package/dist/components/table.js +2 -20
- package/dist/components/table.js.map +1 -1
- package/dist/components/tag-chip.js +6 -617
- package/dist/components/tag-chip.js.map +1 -1
- package/dist/components/textarea.js +3 -56
- package/dist/components/textarea.js.map +1 -1
- package/dist/components/toast.js +8 -806
- package/dist/components/toast.js.map +1 -1
- package/dist/components/toggle.js +2 -20
- package/dist/components/toggle.js.map +1 -1
- package/dist/components/tooltip.js +3 -80
- package/dist/components/tooltip.js.map +1 -1
- package/dist/hooks/use-dialog-footer-actions.js +1 -9
- package/dist/hooks/use-dialog-footer-actions.js.map +1 -1
- package/dist/hooks/use-form-field.js +1 -34
- package/dist/hooks/use-form-field.js.map +1 -1
- package/dist/hooks/use-mobile.js +2 -27
- package/dist/hooks/use-mobile.js.map +1 -1
- package/dist/hooks/useMobileBackToClose.js +3 -232
- package/dist/hooks/useMobileBackToClose.js.map +1 -1
- package/dist/icons/actions.js +4 -521
- package/dist/icons/actions.js.map +1 -1
- package/dist/icons/icon-style.js +1 -49
- package/dist/icons/icon-style.js.map +1 -1
- package/dist/icons/marks.js +1 -46
- package/dist/icons/marks.js.map +1 -1
- package/dist/icons/registry.d.ts +2 -1
- package/dist/icons/registry.js +2 -1186
- package/dist/icons/registry.js.map +1 -1
- package/dist/index.js +10 -651
- package/dist/index.js.map +1 -1
- package/dist/lib/colors.js +1 -63
- package/dist/lib/colors.js.map +1 -1
- package/dist/lib/merge-config.js +1 -18
- package/dist/lib/merge-config.js.map +1 -1
- package/dist/lib/pagination.js +1 -24
- package/dist/lib/pagination.js.map +1 -1
- package/dist/lib/tag-surface.js +5 -613
- package/dist/lib/tag-surface.js.map +1 -1
- package/dist/lib/utils.js +2 -24
- package/dist/lib/utils.js.map +1 -1
- package/dist/res-card-template/format.d.ts +21 -0
- package/dist/res-card-template/format.js +3 -0
- package/dist/res-card-template/format.js.map +1 -0
- package/dist/res-card-template/icon.d.ts +38 -0
- package/dist/res-card-template/icon.js +3 -0
- package/dist/res-card-template/icon.js.map +1 -0
- package/dist/res-card-template/index.d.ts +4 -0
- package/dist/res-card-template/index.js +5 -0
- package/dist/res-card-template/index.js.map +1 -0
- package/dist/res-card-template/template.d.ts +48 -0
- package/dist/res-card-template/template.js +5 -0
- package/dist/res-card-template/template.js.map +1 -0
- package/dist/viewport.js +1 -11
- package/dist/viewport.js.map +1 -1
- package/package.json +9 -2
- package/src/components/image-crop-panel.dist.test.tsx +54 -0
- package/src/icons/registry.ts +8 -0
- package/src/res-card-template/format.test.ts +45 -0
- package/src/res-card-template/format.ts +36 -0
- package/src/res-card-template/icon.test.ts +100 -0
- package/src/res-card-template/icon.ts +86 -0
- package/src/res-card-template/index.ts +22 -0
- package/src/res-card-template/template.test.ts +457 -0
- package/src/res-card-template/template.ts +429 -0
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type TemplateArg as Arg,
|
|
3
|
+
type TemplateExpr as Expr,
|
|
4
|
+
parseTemplateExpression,
|
|
5
|
+
parseTemplateFragments,
|
|
6
|
+
} from "@hoardodile/sdk-types/template"
|
|
7
|
+
import type { ReactNode } from "react"
|
|
8
|
+
import { formatBytes, formatClockDuration } from "./format.ts"
|
|
9
|
+
import { parseIconRef, type RenderIcon } from "./icon.ts"
|
|
10
|
+
|
|
11
|
+
// The template grammar (fragment splitting, tokenising, parsing) lives
|
|
12
|
+
// in @hoardodile/sdk-types/template — shared verbatim with the CLI's
|
|
13
|
+
// build-time template lint so the two can never drift. This module only
|
|
14
|
+
// evaluates the AST. The parser is lenient: unparseable expressions
|
|
15
|
+
// render as the empty string.
|
|
16
|
+
|
|
17
|
+
// ── Evaluation context ───────────────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
/** How a referenced icon is drawn; the caller owns the SVG/image resolution. */
|
|
20
|
+
export type TemplateContext = {
|
|
21
|
+
readonly locale: string
|
|
22
|
+
readonly pluginId: string
|
|
23
|
+
readonly manifest: {
|
|
24
|
+
readonly i18n?: Record<string, string | Record<string, string>>
|
|
25
|
+
readonly ui?: {
|
|
26
|
+
readonly search?: {
|
|
27
|
+
readonly kinds?: readonly {
|
|
28
|
+
readonly key: string
|
|
29
|
+
readonly icon?: string
|
|
30
|
+
}[]
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
readonly iconClassName?: string
|
|
35
|
+
/** Renders a parsed {@link IconRef} (glyph or asset) inside the template. */
|
|
36
|
+
readonly renderIcon: RenderIcon
|
|
37
|
+
/** Resolves a manifest-relative `asset('path')` reference to a URL. */
|
|
38
|
+
readonly buildAssetUrl: (pluginId: string, path: string) => string
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
type EvalValue = string | ReactNode
|
|
42
|
+
|
|
43
|
+
type TemplateScope = {
|
|
44
|
+
readonly file: unknown
|
|
45
|
+
readonly source: unknown
|
|
46
|
+
readonly searchMeta?: unknown
|
|
47
|
+
readonly data?: unknown
|
|
48
|
+
readonly coverMeta?: unknown
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ── Scope resolution (paths) ─────────────────────────────────────────────────
|
|
52
|
+
|
|
53
|
+
function resolvePath(
|
|
54
|
+
segments: readonly string[],
|
|
55
|
+
scope: TemplateScope,
|
|
56
|
+
): unknown {
|
|
57
|
+
const [namespace, ...rest] = segments
|
|
58
|
+
const root =
|
|
59
|
+
namespace === "file"
|
|
60
|
+
? scope.file
|
|
61
|
+
: namespace === "searchMeta"
|
|
62
|
+
? scope.searchMeta
|
|
63
|
+
: namespace === "data"
|
|
64
|
+
? scope.data
|
|
65
|
+
: namespace === "coverMeta"
|
|
66
|
+
? scope.coverMeta
|
|
67
|
+
: scope.source
|
|
68
|
+
if (root === undefined || root === null) return undefined
|
|
69
|
+
let current: unknown = root
|
|
70
|
+
for (const key of rest) {
|
|
71
|
+
if (typeof current !== "object" || current === null) return undefined
|
|
72
|
+
current = (current as Record<string, unknown>)[key]
|
|
73
|
+
}
|
|
74
|
+
return current
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ── Locale resolution ────────────────────────────────────────────────────────
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Resolve a locale-aware template value: if the value is a plain string,
|
|
81
|
+
* return it directly; if it is a `Record<locale, string>`, pick the best
|
|
82
|
+
* match for the current language. Falls back to the first available key
|
|
83
|
+
* when nothing matches.
|
|
84
|
+
*/
|
|
85
|
+
export function resolveLocaleString(
|
|
86
|
+
value: string | Record<string, string>,
|
|
87
|
+
locale: string,
|
|
88
|
+
): string {
|
|
89
|
+
if (typeof value === "string") return value
|
|
90
|
+
const exact = value[locale]
|
|
91
|
+
if (exact !== undefined) return exact
|
|
92
|
+
const base = locale.split("-")[0] ?? locale
|
|
93
|
+
const partial = value[base]
|
|
94
|
+
if (partial !== undefined) return partial
|
|
95
|
+
const first = Object.values(value)[0]
|
|
96
|
+
return first ?? ""
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ── Expression evaluation ────────────────────────────────────────────────────
|
|
100
|
+
|
|
101
|
+
function evaluateExpression(
|
|
102
|
+
expr: Expr,
|
|
103
|
+
scope: TemplateScope,
|
|
104
|
+
ctx: TemplateContext,
|
|
105
|
+
): EvalValue {
|
|
106
|
+
if (expr.kind === "path") {
|
|
107
|
+
const value = resolvePath(expr.segments, scope)
|
|
108
|
+
if (value === undefined || value === null) return ""
|
|
109
|
+
return String(value)
|
|
110
|
+
}
|
|
111
|
+
return evaluateCall(expr.name, expr.args, scope, ctx)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function evaluateCall(
|
|
115
|
+
name: string,
|
|
116
|
+
args: readonly Arg[],
|
|
117
|
+
scope: TemplateScope,
|
|
118
|
+
ctx: TemplateContext,
|
|
119
|
+
): EvalValue {
|
|
120
|
+
switch (name) {
|
|
121
|
+
case "bytes":
|
|
122
|
+
return callPipe(args, scope, ctx, (value) =>
|
|
123
|
+
typeof value === "number" ? formatBytes(value) : "",
|
|
124
|
+
)
|
|
125
|
+
case "duration":
|
|
126
|
+
return callPipe(args, scope, ctx, (value) =>
|
|
127
|
+
typeof value === "number" ? formatClockDuration(value) : "",
|
|
128
|
+
)
|
|
129
|
+
case "number":
|
|
130
|
+
return callPipe(args, scope, ctx, (value) =>
|
|
131
|
+
typeof value === "number" && Number.isFinite(value)
|
|
132
|
+
? value.toLocaleString()
|
|
133
|
+
: "",
|
|
134
|
+
)
|
|
135
|
+
case "inc":
|
|
136
|
+
return callPipe(args, scope, ctx, (value) =>
|
|
137
|
+
typeof value === "number" && Number.isFinite(value)
|
|
138
|
+
? String(value + 1)
|
|
139
|
+
: "",
|
|
140
|
+
)
|
|
141
|
+
case "eq":
|
|
142
|
+
return compareCall(args, scope, ctx, (a, b) => a === b)
|
|
143
|
+
case "ne":
|
|
144
|
+
return compareCall(args, scope, ctx, (a, b) => a !== b)
|
|
145
|
+
case "gt":
|
|
146
|
+
return compareCall(args, scope, ctx, (a, b) => {
|
|
147
|
+
if (typeof a !== "number" || typeof b !== "number") return false
|
|
148
|
+
return a > b
|
|
149
|
+
})
|
|
150
|
+
case "lt":
|
|
151
|
+
return compareCall(args, scope, ctx, (a, b) => {
|
|
152
|
+
if (typeof a !== "number" || typeof b !== "number") return false
|
|
153
|
+
return a < b
|
|
154
|
+
})
|
|
155
|
+
case "gte":
|
|
156
|
+
return compareCall(args, scope, ctx, (a, b) => {
|
|
157
|
+
if (typeof a !== "number" || typeof b !== "number") return false
|
|
158
|
+
return a >= b
|
|
159
|
+
})
|
|
160
|
+
case "lte":
|
|
161
|
+
return compareCall(args, scope, ctx, (a, b) => {
|
|
162
|
+
if (typeof a !== "number" || typeof b !== "number") return false
|
|
163
|
+
return a <= b
|
|
164
|
+
})
|
|
165
|
+
case "if":
|
|
166
|
+
return callIf(args, scope, ctx)
|
|
167
|
+
case "t":
|
|
168
|
+
return callT(args, ctx)
|
|
169
|
+
case "icon":
|
|
170
|
+
return callIcon(args, ctx)
|
|
171
|
+
case "asset":
|
|
172
|
+
return callAsset(args, ctx)
|
|
173
|
+
case "kind":
|
|
174
|
+
return callKind(args, scope, ctx)
|
|
175
|
+
case "searchKindIcons":
|
|
176
|
+
return callSearchKindIcons(scope, ctx)
|
|
177
|
+
case "join":
|
|
178
|
+
return callJoin(args, scope, ctx)
|
|
179
|
+
default:
|
|
180
|
+
return ""
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function callPipe(
|
|
185
|
+
args: readonly Arg[],
|
|
186
|
+
scope: TemplateScope,
|
|
187
|
+
ctx: TemplateContext,
|
|
188
|
+
pipeFn: (value: unknown) => string,
|
|
189
|
+
): string {
|
|
190
|
+
const value = evaluateArgAsPrimitive(args[0], scope, ctx)
|
|
191
|
+
return pipeFn(value)
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function callT(args: readonly Arg[], ctx: TemplateContext): string {
|
|
195
|
+
const key = evaluateArgAsString(args[0])
|
|
196
|
+
if (key.length === 0) return ""
|
|
197
|
+
const map = ctx.manifest.i18n
|
|
198
|
+
if (map === undefined) return ""
|
|
199
|
+
const value = map[key]
|
|
200
|
+
if (value === undefined) return ""
|
|
201
|
+
return resolveLocaleString(value, ctx.locale)
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function callIcon(args: readonly Arg[], ctx: TemplateContext): ReactNode {
|
|
205
|
+
const name = evaluateArgAsString(args[0])
|
|
206
|
+
if (name.length === 0) return null
|
|
207
|
+
return ctx.renderIcon({ kind: "icon", name }, ctx.iconClassName)
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function callAsset(args: readonly Arg[], ctx: TemplateContext): ReactNode {
|
|
211
|
+
const path = evaluateArgAsString(args[0])
|
|
212
|
+
if (path.length === 0) return null
|
|
213
|
+
const ref = parseIconRef(path, ctx.pluginId, ctx.buildAssetUrl)
|
|
214
|
+
if (ref === undefined) return null
|
|
215
|
+
return ctx.renderIcon(ref, ctx.iconClassName)
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function callKind(
|
|
219
|
+
args: readonly Arg[],
|
|
220
|
+
scope: TemplateScope,
|
|
221
|
+
ctx: TemplateContext,
|
|
222
|
+
): ReactNode {
|
|
223
|
+
const key = evaluateArgAsString(args[0])
|
|
224
|
+
if (key.length === 0) return null
|
|
225
|
+
const kinds = ctx.manifest.ui?.search?.kinds
|
|
226
|
+
if (kinds === undefined) return null
|
|
227
|
+
const match = kinds.find((k) => k.key === key)
|
|
228
|
+
if (match === undefined || match.icon === undefined) return null
|
|
229
|
+
const rendered = renderCardTemplate(match.icon, scope, ctx)
|
|
230
|
+
if (rendered === null || rendered === undefined || rendered === "")
|
|
231
|
+
return null
|
|
232
|
+
return rendered
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function callSearchKindIcons(
|
|
236
|
+
scope: TemplateScope,
|
|
237
|
+
ctx: TemplateContext,
|
|
238
|
+
): readonly ReactNode[] {
|
|
239
|
+
if (
|
|
240
|
+
scope.searchMeta === undefined ||
|
|
241
|
+
scope.searchMeta === null ||
|
|
242
|
+
typeof scope.searchMeta !== "object"
|
|
243
|
+
)
|
|
244
|
+
return []
|
|
245
|
+
const facets = (scope.searchMeta as Record<string, unknown>).facets
|
|
246
|
+
if (facets === undefined || typeof facets !== "object" || facets === null)
|
|
247
|
+
return []
|
|
248
|
+
const kinds = ctx.manifest.ui?.search?.kinds
|
|
249
|
+
if (kinds === undefined) return []
|
|
250
|
+
const results: ReactNode[] = []
|
|
251
|
+
for (const kind of kinds) {
|
|
252
|
+
if (kind.icon === undefined) continue
|
|
253
|
+
const active = (facets as Record<string, boolean>)[kind.key]
|
|
254
|
+
if (active !== true) continue
|
|
255
|
+
const rendered = renderCardTemplate(kind.icon, scope, ctx)
|
|
256
|
+
if (rendered === null || rendered === undefined || rendered === "") continue
|
|
257
|
+
results.push(rendered)
|
|
258
|
+
}
|
|
259
|
+
return results
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function callJoin(
|
|
263
|
+
args: readonly Arg[],
|
|
264
|
+
scope: TemplateScope,
|
|
265
|
+
ctx: TemplateContext,
|
|
266
|
+
): string | readonly ReactNode[] {
|
|
267
|
+
if (args.length === 0) return ""
|
|
268
|
+
|
|
269
|
+
const separator = evaluateArgAsString(args[0])
|
|
270
|
+
const items: ReactNode[] = []
|
|
271
|
+
for (let i = 1; i < args.length; i++) {
|
|
272
|
+
const val = evaluateArg(args[i], scope, ctx)
|
|
273
|
+
if (Array.isArray(val)) {
|
|
274
|
+
for (const item of val) {
|
|
275
|
+
if (item !== null && item !== undefined && item !== "") {
|
|
276
|
+
items.push(item)
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
continue
|
|
280
|
+
}
|
|
281
|
+
if (val !== null && val !== undefined && val !== "") {
|
|
282
|
+
items.push(val)
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
if (items.length === 0) return ""
|
|
286
|
+
if (items.every((item) => typeof item === "string")) {
|
|
287
|
+
return (items as readonly string[]).join(separator)
|
|
288
|
+
}
|
|
289
|
+
if (separator.length === 0) return items
|
|
290
|
+
const interleaved: ReactNode[] = []
|
|
291
|
+
for (let i = 0; i < items.length; i++) {
|
|
292
|
+
if (i > 0) interleaved.push(separator)
|
|
293
|
+
interleaved.push(items[i])
|
|
294
|
+
}
|
|
295
|
+
return interleaved
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// ── Argument helpers ─────────────────────────────────────────────────────────
|
|
299
|
+
|
|
300
|
+
function evaluateArgAsPrimitive(
|
|
301
|
+
arg: Arg | undefined,
|
|
302
|
+
scope: TemplateScope,
|
|
303
|
+
ctx: TemplateContext,
|
|
304
|
+
): unknown {
|
|
305
|
+
if (arg === undefined) return undefined
|
|
306
|
+
if (arg.kind === "string") return arg.value
|
|
307
|
+
if (arg.expr.kind === "path") {
|
|
308
|
+
const segments = arg.expr.segments
|
|
309
|
+
if (segments.length === 1) {
|
|
310
|
+
const seg = segments[0]!
|
|
311
|
+
const num = Number(seg)
|
|
312
|
+
if (!Number.isNaN(num)) return num
|
|
313
|
+
}
|
|
314
|
+
return resolvePath(segments, scope)
|
|
315
|
+
}
|
|
316
|
+
if (arg.expr.kind === "call") {
|
|
317
|
+
const result = evaluateCall(arg.expr.name, arg.expr.args, scope, ctx)
|
|
318
|
+
if (typeof result === "string" || typeof result === "number") return result
|
|
319
|
+
if (Array.isArray(result)) return result
|
|
320
|
+
return undefined
|
|
321
|
+
}
|
|
322
|
+
return undefined
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function evaluateArg(
|
|
326
|
+
arg: Arg | undefined,
|
|
327
|
+
scope: TemplateScope,
|
|
328
|
+
ctx: TemplateContext,
|
|
329
|
+
): EvalValue {
|
|
330
|
+
if (arg === undefined) return undefined
|
|
331
|
+
if (arg.kind === "string") return arg.value
|
|
332
|
+
return evaluateExpression(arg.expr, scope, ctx)
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function compareCall(
|
|
336
|
+
args: readonly Arg[],
|
|
337
|
+
scope: TemplateScope,
|
|
338
|
+
ctx: TemplateContext,
|
|
339
|
+
cmp: (a: unknown, b: unknown) => boolean,
|
|
340
|
+
): string {
|
|
341
|
+
const a = evaluateArgAsPrimitive(args[0], scope, ctx)
|
|
342
|
+
const b = evaluateArgAsPrimitive(args[1], scope, ctx)
|
|
343
|
+
return cmp(a, b) ? "true" : ""
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function callIf(
|
|
347
|
+
args: readonly Arg[],
|
|
348
|
+
scope: TemplateScope,
|
|
349
|
+
ctx: TemplateContext,
|
|
350
|
+
): EvalValue {
|
|
351
|
+
const cond = evaluateArgAsPrimitive(args[0], scope, ctx)
|
|
352
|
+
const truthy =
|
|
353
|
+
cond !== undefined &&
|
|
354
|
+
cond !== null &&
|
|
355
|
+
cond !== "" &&
|
|
356
|
+
cond !== false &&
|
|
357
|
+
cond !== 0
|
|
358
|
+
if (truthy) {
|
|
359
|
+
const raw = evaluateArgAsPrimitive(args[1], scope, ctx)
|
|
360
|
+
if (raw !== undefined) return raw as EvalValue
|
|
361
|
+
return evaluateArg(args[1], scope, ctx)
|
|
362
|
+
}
|
|
363
|
+
if (args.length > 2) {
|
|
364
|
+
const raw = evaluateArgAsPrimitive(args[2], scope, ctx)
|
|
365
|
+
if (raw !== undefined) return raw as EvalValue
|
|
366
|
+
return evaluateArg(args[2], scope, ctx)
|
|
367
|
+
}
|
|
368
|
+
return ""
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function evaluateArgAsString(arg: Arg | undefined): string {
|
|
372
|
+
if (arg === undefined) return ""
|
|
373
|
+
if (arg.kind === "string") return arg.value
|
|
374
|
+
return ""
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// ── Public API ───────────────────────────────────────────────────────────────
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Render one res-card template string. Returns a ReactNode so that
|
|
381
|
+
* icon-producing functions (`icon`, `asset`, `kind`) can inject
|
|
382
|
+
* components inline with text.
|
|
383
|
+
*/
|
|
384
|
+
export function renderCardTemplate(
|
|
385
|
+
template: string,
|
|
386
|
+
scope: TemplateScope,
|
|
387
|
+
ctx: TemplateContext,
|
|
388
|
+
): ReactNode {
|
|
389
|
+
if (template.length === 0) return ""
|
|
390
|
+
const fragments = parseTemplateFragments(template)
|
|
391
|
+
if (fragments.length === 0) return ""
|
|
392
|
+
|
|
393
|
+
const results: ReactNode[] = []
|
|
394
|
+
for (const frag of fragments) {
|
|
395
|
+
if (frag.kind === "text") {
|
|
396
|
+
results.push(frag.value)
|
|
397
|
+
continue
|
|
398
|
+
}
|
|
399
|
+
const expr = parseTemplateExpression(frag.source)
|
|
400
|
+
if (expr === undefined) {
|
|
401
|
+
continue
|
|
402
|
+
}
|
|
403
|
+
const value = evaluateExpression(expr, scope, ctx)
|
|
404
|
+
if (value !== null && value !== undefined && value !== "") {
|
|
405
|
+
results.push(value)
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
if (results.length === 0) return null
|
|
410
|
+
if (results.length === 1) return results[0]
|
|
411
|
+
const allStrings = results.every((r) => typeof r === "string")
|
|
412
|
+
if (allStrings) return results.join("")
|
|
413
|
+
return results
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/** Render every badge in a {@link CoverKindUi} slot array. */
|
|
417
|
+
export function renderSlotBadges(
|
|
418
|
+
slotValues: readonly string[],
|
|
419
|
+
scope: TemplateScope,
|
|
420
|
+
ctx: TemplateContext,
|
|
421
|
+
): readonly ReactNode[] {
|
|
422
|
+
return slotValues
|
|
423
|
+
.map((template) => renderCardTemplate(template, scope, ctx))
|
|
424
|
+
.filter((n): n is Exclude<typeof n, null | undefined | ""> => {
|
|
425
|
+
if (n === null || n === undefined || n === "") return false
|
|
426
|
+
if (Array.isArray(n) && n.length === 0) return false
|
|
427
|
+
return true
|
|
428
|
+
})
|
|
429
|
+
}
|