@riebeckite/plugin-icons 0.0.0-stage → 0.0.33

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/index.js ADDED
@@ -0,0 +1,1735 @@
1
+ // index.ts
2
+ import {
3
+ createStyleAsset,
4
+ definePlugin
5
+ } from "@riebeckite/core";
6
+
7
+ // src/remark.ts
8
+ var SKIP = /* @__PURE__ */ new Set([
9
+ "code",
10
+ "inlineCode",
11
+ "html",
12
+ "link",
13
+ "linkReference",
14
+ "image",
15
+ "imageReference",
16
+ "definition",
17
+ "yaml",
18
+ "math",
19
+ "inlineMath",
20
+ "mathBlock",
21
+ "wikiLink",
22
+ "wikilink"
23
+ ]);
24
+ function remarkIcons(options) {
25
+ const identifier = options.identifier || ":";
26
+ const escaped = escapeRegExp(identifier);
27
+ const pattern = new RegExp(
28
+ `${escaped}((?:[\\p{L}\\p{N}_-]{1,64}):\\d{17,18}|[\\p{L}\\p{N}_-]{1,64})${escaped}`,
29
+ "gu"
30
+ );
31
+ return (tree, file) => {
32
+ walk(tree, false, file, pattern, options.resolve);
33
+ };
34
+ }
35
+ function walk(parent, skip, file, pattern, resolve) {
36
+ const children = parent.children;
37
+ for (let index = 0; index < children.length; index += 1) {
38
+ const child = children[index];
39
+ if (!child) continue;
40
+ if (child.type === "text" && !skip) {
41
+ const replacement = transformText(child, file, pattern, resolve);
42
+ if (replacement) {
43
+ children.splice(index, 1, ...replacement);
44
+ index += replacement.length - 1;
45
+ }
46
+ continue;
47
+ }
48
+ if ("children" in child) {
49
+ walk(
50
+ child,
51
+ skip || SKIP.has(child.type),
52
+ file,
53
+ pattern,
54
+ resolve
55
+ );
56
+ }
57
+ }
58
+ }
59
+ function transformText(node, file, pattern, resolve) {
60
+ const value = node.value;
61
+ if (!value) return void 0;
62
+ const matches = [...value.matchAll(pattern)];
63
+ if (matches.length === 0) return void 0;
64
+ const start = node.position?.start.offset;
65
+ const end = node.position?.end.offset;
66
+ const raw = start !== void 0 && end !== void 0 ? String(file.value).slice(start, end) : void 0;
67
+ const sourceMap = raw === void 0 ? void 0 : buildSourceMap(raw, value);
68
+ const parts = [];
69
+ let cursor = 0;
70
+ let changed = false;
71
+ for (const match of matches) {
72
+ const name = match[1];
73
+ const offset = match.index;
74
+ if (!name || offset === void 0 || name.includes(":")) continue;
75
+ if (raw !== void 0 && sourceMap !== void 0 && isEscaped(raw, sourceMap[offset])) {
76
+ continue;
77
+ }
78
+ const icon = resolve(name);
79
+ if (!icon) continue;
80
+ if (offset > cursor) {
81
+ parts.push({ type: "text", value: value.slice(cursor, offset) });
82
+ }
83
+ parts.push({ type: "html", value: icon.html });
84
+ cursor = offset + match[0].length;
85
+ changed = true;
86
+ }
87
+ if (!changed) return void 0;
88
+ if (cursor < value.length) {
89
+ parts.push({ type: "text", value: value.slice(cursor) });
90
+ }
91
+ return parts;
92
+ }
93
+ function buildSourceMap(raw, value) {
94
+ const map = [];
95
+ let rawIndex = 0;
96
+ let valueIndex = 0;
97
+ while (valueIndex < value.length) {
98
+ if (rawIndex >= raw.length) return void 0;
99
+ if (raw[rawIndex] === "\\" && raw[rawIndex + 1] === value[valueIndex]) {
100
+ map.push(rawIndex + 1);
101
+ rawIndex += 2;
102
+ valueIndex += 1;
103
+ continue;
104
+ }
105
+ if (raw[rawIndex] === value[valueIndex]) {
106
+ map.push(rawIndex);
107
+ rawIndex += 1;
108
+ valueIndex += 1;
109
+ continue;
110
+ }
111
+ return void 0;
112
+ }
113
+ return map;
114
+ }
115
+ function isEscaped(raw, index) {
116
+ if (index === void 0 || index === 0) return false;
117
+ let count = 0;
118
+ for (let cursor = index - 1; cursor >= 0 && raw[cursor] === "\\"; cursor -= 1) {
119
+ count += 1;
120
+ }
121
+ return count % 2 === 1;
122
+ }
123
+ function escapeRegExp(value) {
124
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
125
+ }
126
+
127
+ // src/color.ts
128
+ var NAMED_COLORS = /* @__PURE__ */ new Set([
129
+ "aqua",
130
+ "black",
131
+ "blue",
132
+ "brown",
133
+ "crimson",
134
+ "currentcolor",
135
+ "cyan",
136
+ "darkblue",
137
+ "darkgreen",
138
+ "darkorange",
139
+ "darkred",
140
+ "fuchsia",
141
+ "gold",
142
+ "gray",
143
+ "green",
144
+ "grey",
145
+ "indigo",
146
+ "lime",
147
+ "magenta",
148
+ "maroon",
149
+ "navy",
150
+ "olive",
151
+ "orange",
152
+ "pink",
153
+ "purple",
154
+ "red",
155
+ "silver",
156
+ "teal",
157
+ "transparent",
158
+ "violet",
159
+ "white",
160
+ "yellow"
161
+ ]);
162
+ var ICONIC_COLORS = {
163
+ red: "var(--rr-icon-red)",
164
+ orange: "var(--rr-icon-orange)",
165
+ yellow: "var(--rr-icon-yellow)",
166
+ green: "var(--rr-icon-green)",
167
+ cyan: "var(--rr-icon-cyan)",
168
+ blue: "var(--rr-icon-blue)",
169
+ purple: "var(--rr-icon-purple)",
170
+ pink: "var(--rr-icon-pink)",
171
+ gray: "var(--rr-icon-gray)"
172
+ };
173
+ function normalizeColor(value, options = {}) {
174
+ if (typeof value !== "string") return void 0;
175
+ const color = value.trim();
176
+ if (!color || color.length > 64) return void 0;
177
+ if (options.iconicNames) {
178
+ const named = ICONIC_COLORS[color.toLowerCase()];
179
+ if (named) return named;
180
+ }
181
+ if (/^#(?:[0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$/i.test(color)) {
182
+ return color.toLowerCase();
183
+ }
184
+ if (/^[0-9a-f]{1,6}$/i.test(color)) {
185
+ return `#${color.toLowerCase().padStart(6, "0")}`;
186
+ }
187
+ if (/^(?:rgb|rgba|hsl|hsla)\(\s*[-+.%\d\s,/a-z]+\)$/i.test(color) && !/url/i.test(color)) {
188
+ return color;
189
+ }
190
+ if (NAMED_COLORS.has(color.toLowerCase())) return color.toLowerCase();
191
+ return void 0;
192
+ }
193
+
194
+ // src/emoji.ts
195
+ var EMOJIS = {
196
+ "100": "💯",
197
+ airplane: "✈️",
198
+ apple: "🍎",
199
+ arrow_down: "⬇️",
200
+ arrow_left: "⬅️",
201
+ arrow_right: "➡️",
202
+ arrow_up: "⬆️",
203
+ baby: "👶",
204
+ ball: "⚽",
205
+ balloon: "🎈",
206
+ beer: "🍺",
207
+ bell: "🔔",
208
+ bike: "🚲",
209
+ bird: "🐦",
210
+ blossom: "🌼",
211
+ blue_book: "📘",
212
+ blue_heart: "💙",
213
+ bomb: "💣",
214
+ book: "📖",
215
+ bookmark: "🔖",
216
+ books: "📚",
217
+ brain: "🧠",
218
+ bug: "🐛",
219
+ bulb: "💡",
220
+ cake: "🍰",
221
+ calendar: "📅",
222
+ camera: "📷",
223
+ car: "🚗",
224
+ cat: "🐱",
225
+ chart_with_upwards_trend: "📈",
226
+ checkered_flag: "🏁",
227
+ clap: "👏",
228
+ clock: "🕒",
229
+ cloud: "☁️",
230
+ code: "💻",
231
+ coffee: "☕",
232
+ computer: "💻",
233
+ construction: "🚧",
234
+ crystal_ball: "🔮",
235
+ dog: "🐶",
236
+ dollar: "💵",
237
+ droplet: "💧",
238
+ earth_asia: "🌏",
239
+ email: "✉️",
240
+ exclamation: "❗",
241
+ eyes: "👀",
242
+ fire: "🔥",
243
+ fish: "🐟",
244
+ folder: "📁",
245
+ gear: "⚙️",
246
+ gift: "🎁",
247
+ globe_with_meridians: "🌐",
248
+ hammer: "🔨",
249
+ heart: "❤️",
250
+ hourglass: "⌛",
251
+ house: "🏠",
252
+ information_source: "ℹ️",
253
+ key: "🔑",
254
+ label: "🏷️",
255
+ laptop: "💻",
256
+ large_blue_circle: "🔵",
257
+ leaf: "🍃",
258
+ link: "🔗",
259
+ lock: "🔒",
260
+ mag: "🔍",
261
+ memo: "📝",
262
+ moneybag: "💰",
263
+ moon: "🌙",
264
+ muscle: "💪",
265
+ music: "🎵",
266
+ new: "🆕",
267
+ notebook: "📓",
268
+ open_book: "📖",
269
+ package: "📦",
270
+ page_facing_up: "📄",
271
+ pencil2: "✏️",
272
+ pizza: "🍕",
273
+ point_right: "👉",
274
+ pray: "🙏",
275
+ question: "❓",
276
+ recycle: "♻️",
277
+ rocket: "🚀",
278
+ seedling: "🌱",
279
+ shield: "🛡️",
280
+ smile: "😄",
281
+ sparkles: "✨",
282
+ star: "⭐",
283
+ sunny: "☀️",
284
+ tada: "🎉",
285
+ tea: "🍵",
286
+ thumbsdown: "👎",
287
+ thumbsup: "👍",
288
+ tree: "🌳",
289
+ trophy: "🏆",
290
+ umbrella: "☔",
291
+ unicorn: "🦄",
292
+ warning: "⚠️",
293
+ wave: "👋",
294
+ white_check_mark: "✅",
295
+ wine: "🍷",
296
+ wrench: "🔧",
297
+ x: "❌",
298
+ zap: "⚡"
299
+ };
300
+ function lookupEmoji(name) {
301
+ return EMOJIS[name.trim().toLowerCase().replace(/-/g, "_")];
302
+ }
303
+ function isEmoji(value) {
304
+ return /^\p{Extended_Pictographic}(?:\uFE0F|\u200D\p{Extended_Pictographic}|[\u{1F3FB}-\u{1F3FF}])*$/u.test(
305
+ value.trim()
306
+ );
307
+ }
308
+
309
+ // src/iconic.ts
310
+ var BOOLEAN_OPERATORS = /* @__PURE__ */ new Set(["isTrue", "isFalse"]);
311
+ var ARRAY_OPERATORS = /* @__PURE__ */ new Set([
312
+ "includes",
313
+ "allAre",
314
+ "allContain",
315
+ "allStartWith",
316
+ "allEndWith",
317
+ "allMatch",
318
+ "anyContain",
319
+ "anyStartWith",
320
+ "anyEndWith",
321
+ "anyMatch",
322
+ "noneContain",
323
+ "noneStartWith",
324
+ "noneEndWith",
325
+ "noneMatch",
326
+ "countIs",
327
+ "countIsLess",
328
+ "countIsMore"
329
+ ]);
330
+ var NUMBER_OPERATORS = /* @__PURE__ */ new Set(["equals", "isLess", "isMore", "isDivisible"]);
331
+ var STRING_OPERATORS = /* @__PURE__ */ new Set([
332
+ "is",
333
+ "contains",
334
+ "startsWith",
335
+ "endsWith",
336
+ "matches",
337
+ "hasValue",
338
+ "hasProperty"
339
+ ]);
340
+ function resolveIconic(data, target, options) {
341
+ const manual = data.fileIcons[target.path];
342
+ const rules = target.kind === "folder" ? data.folderRules : data.fileRules;
343
+ const rule = rules.find(
344
+ (candidate) => candidate.enabled !== false && (candidate.conditions?.length ?? 0) > 0 && matchesRule(candidate, target, manual, options.report)
345
+ );
346
+ const icon = rule?.icon ?? manual?.icon;
347
+ const rawColor = rule?.color ?? manual?.color;
348
+ if (!icon && !rawColor) return void 0;
349
+ const color = options.colors ? normalizeColor(rawColor, { iconicNames: true }) : void 0;
350
+ return {
351
+ ...icon ? { icon } : {},
352
+ ...color ? { color } : {},
353
+ ...rule ? { rule: rule.name ?? rule.id } : {}
354
+ };
355
+ }
356
+ function resolveIconicReference(icon) {
357
+ if (isEmoji(icon)) return `emoji:${icon}`;
358
+ if (/^lucide-/.test(icon)) return `lucide:${icon.slice(7)}`;
359
+ if (icon.includes(":")) {
360
+ const [namespace, ...rest] = icon.split(":");
361
+ if (namespace === "lucide" || namespace === "emoji") {
362
+ return `${namespace}:${rest.join(":")}`;
363
+ }
364
+ }
365
+ return `lucide:${icon}`;
366
+ }
367
+ function matchesRule(rule, target, manual, report) {
368
+ let unsupported = false;
369
+ const results = [];
370
+ for (const condition of rule.conditions ?? []) {
371
+ const result = evaluateCondition(condition, target, manual);
372
+ if (result === void 0) {
373
+ unsupported = true;
374
+ results.push(false);
375
+ } else {
376
+ results.push(result);
377
+ }
378
+ }
379
+ if (unsupported) {
380
+ report(rule, "uses a condition that cannot be evaluated in a static build");
381
+ }
382
+ const match = rule.match ?? "all";
383
+ if (match === "any") return results.some(Boolean);
384
+ if (match === "none") return !results.some(Boolean);
385
+ return results.length > 0 && results.every(Boolean);
386
+ }
387
+ function evaluateCondition(condition, target, manual) {
388
+ const source = condition.source?.trim();
389
+ const operator = condition.operator?.trim();
390
+ if (!source || !operator) return false;
391
+ const negate = operator.startsWith("!");
392
+ const op = negate ? operator.slice(1) : operator;
393
+ const rawValue = condition.value ?? "";
394
+ const resolved = readSource(source, target, manual);
395
+ if (resolved === void 0) return void 0;
396
+ const result = applyOperator(resolved, op, rawValue);
397
+ if (result === void 0) return void 0;
398
+ return negate ? !result : result;
399
+ }
400
+ function readSource(source, target, manual) {
401
+ if (source === "clock" || source === "headings" || source === "bookmarks") {
402
+ return void 0;
403
+ }
404
+ if (source === "created" || source === "modified") {
405
+ const value = source === "created" ? target.created : target.modified;
406
+ return value ? { kind: "date", value } : void 0;
407
+ }
408
+ if (source.startsWith("property:")) {
409
+ const key = source.slice("property:".length).trim().toLowerCase();
410
+ const frontmatter = target.frontmatter ?? {};
411
+ const found = Object.entries(frontmatter).find(
412
+ ([candidate]) => candidate.toLowerCase() === key
413
+ );
414
+ return {
415
+ kind: "property",
416
+ value: found?.[1],
417
+ exists: found !== void 0
418
+ };
419
+ }
420
+ const path3 = target.path;
421
+ switch (source) {
422
+ case "icon":
423
+ return { kind: "string", value: manual?.icon ?? "" };
424
+ case "color":
425
+ return { kind: "string", value: manual?.color ?? "" };
426
+ case "name": {
427
+ const name = path3.split("/").pop() ?? path3;
428
+ return {
429
+ kind: "string",
430
+ value: name.replace(/\.[^.]+$/, "")
431
+ };
432
+ }
433
+ case "filename":
434
+ return { kind: "string", value: path3.split("/").pop() ?? path3 };
435
+ case "extension": {
436
+ const name = path3.split("/").pop() ?? path3;
437
+ const dot = name.lastIndexOf(".");
438
+ return { kind: "string", value: dot > 0 ? name.slice(dot + 1) : "" };
439
+ }
440
+ case "tree":
441
+ return { kind: "string", value: path3.split("/").slice(0, -1).join("/") };
442
+ case "path":
443
+ return { kind: "string", value: path3 };
444
+ case "tags":
445
+ return { kind: "array", value: target.tags ?? [] };
446
+ case "links":
447
+ return { kind: "array", value: target.links ?? [] };
448
+ case "embeds":
449
+ return { kind: "array", value: target.embeds ?? [] };
450
+ default:
451
+ return void 0;
452
+ }
453
+ }
454
+ function applyOperator(source, operator, expected) {
455
+ if (BOOLEAN_OPERATORS.has(operator)) {
456
+ if (source.kind !== "boolean" && source.kind !== "property")
457
+ return void 0;
458
+ const value = source.kind === "boolean" ? source.value : toBoolean(source.value);
459
+ return operator === "isTrue" ? value : !value;
460
+ }
461
+ if (operator === "hasProperty") {
462
+ return source.kind === "property" ? source.exists : void 0;
463
+ }
464
+ if (source.kind === "property") {
465
+ if (Array.isArray(source.value)) {
466
+ return applyToArray(
467
+ source.value.map((item) => String(item)),
468
+ operator,
469
+ expected
470
+ );
471
+ }
472
+ if (typeof source.value === "number") {
473
+ return applyToNumber(source.value, operator, expected);
474
+ }
475
+ if (typeof source.value === "boolean") {
476
+ return applyToBoolean(source.value, operator, expected);
477
+ }
478
+ return applyToString(String(source.value ?? ""), operator, expected);
479
+ }
480
+ if (source.kind === "array") {
481
+ return applyToArray(source.value, operator, expected);
482
+ }
483
+ if (source.kind === "date") {
484
+ return applyToDate(source.value, operator, expected);
485
+ }
486
+ if (source.kind === "boolean") {
487
+ return applyToBoolean(source.value, operator, expected);
488
+ }
489
+ return applyToString(source.value, operator, expected);
490
+ }
491
+ function applyToString(value, operator, expected) {
492
+ switch (operator) {
493
+ case "is":
494
+ return value === expected;
495
+ case "contains":
496
+ return value.includes(expected);
497
+ case "startsWith":
498
+ return value.startsWith(expected);
499
+ case "endsWith":
500
+ return value.endsWith(expected);
501
+ case "matches":
502
+ return safeRegex(expected).test(value);
503
+ case "hasValue":
504
+ return value.length > 0;
505
+ default:
506
+ return ARRAY_OPERATORS.has(operator) || NUMBER_OPERATORS.has(operator) ? false : void 0;
507
+ }
508
+ }
509
+ function applyToArray(value, operator, expected) {
510
+ if (!ARRAY_OPERATORS.has(operator)) {
511
+ if (STRING_OPERATORS.has(operator))
512
+ return applyToString(value.join(","), operator, expected);
513
+ return void 0;
514
+ }
515
+ switch (operator) {
516
+ case "includes":
517
+ return value.includes(expected);
518
+ case "allAre":
519
+ return value.length > 0 && value.every((item) => item === expected);
520
+ case "allContain":
521
+ return value.length > 0 && value.every((item) => item.includes(expected));
522
+ case "allStartWith":
523
+ return value.length > 0 && value.every((item) => item.startsWith(expected));
524
+ case "allEndWith":
525
+ return value.length > 0 && value.every((item) => item.endsWith(expected));
526
+ case "allMatch": {
527
+ const regex = safeRegex(expected);
528
+ return value.length > 0 && value.every((item) => regex.test(item));
529
+ }
530
+ case "anyContain":
531
+ return value.some((item) => item.includes(expected));
532
+ case "anyStartWith":
533
+ return value.some((item) => item.startsWith(expected));
534
+ case "anyEndWith":
535
+ return value.some((item) => item.endsWith(expected));
536
+ case "anyMatch": {
537
+ const regex = safeRegex(expected);
538
+ return value.some((item) => regex.test(item));
539
+ }
540
+ case "noneContain":
541
+ return !value.some((item) => item.includes(expected));
542
+ case "noneStartWith":
543
+ return !value.some((item) => item.startsWith(expected));
544
+ case "noneEndWith":
545
+ return !value.some((item) => item.endsWith(expected));
546
+ case "noneMatch": {
547
+ const regex = safeRegex(expected);
548
+ return !value.some((item) => regex.test(item));
549
+ }
550
+ case "countIs":
551
+ return value.length === Number(expected);
552
+ case "countIsLess":
553
+ return value.length < Number(expected);
554
+ case "countIsMore":
555
+ return value.length > Number(expected);
556
+ default:
557
+ return void 0;
558
+ }
559
+ }
560
+ function applyToNumber(value, operator, expected) {
561
+ const number = Number(expected);
562
+ if (!Number.isFinite(number)) return false;
563
+ switch (operator) {
564
+ case "is":
565
+ case "equals":
566
+ return value === number;
567
+ case "isLess":
568
+ return value < number;
569
+ case "isMore":
570
+ return value > number;
571
+ case "isDivisible":
572
+ return number !== 0 && value % number === 0;
573
+ default:
574
+ return void 0;
575
+ }
576
+ }
577
+ function applyToBoolean(value, operator, expected) {
578
+ if (operator !== "is") return void 0;
579
+ return value === (expected === "true");
580
+ }
581
+ function applyToDate(value, operator, expected) {
582
+ if (!["is", "isLess", "isMore", "equals"].includes(operator)) {
583
+ return void 0;
584
+ }
585
+ const date = new Date(expected);
586
+ if (Number.isNaN(date.getTime())) return false;
587
+ const time = value.getTime();
588
+ if (operator === "isLess") return time < date.getTime();
589
+ if (operator === "isMore") return time > date.getTime();
590
+ return time === date.getTime();
591
+ }
592
+ function safeRegex(pattern) {
593
+ try {
594
+ return new RegExp(pattern.length > 512 ? "" : pattern);
595
+ } catch {
596
+ return /$a/;
597
+ }
598
+ }
599
+ function toBoolean(value) {
600
+ return value === true || value === "true";
601
+ }
602
+
603
+ // src/settings.ts
604
+ import { promises as fs } from "node:fs";
605
+ import path from "node:path";
606
+ import {
607
+ fingerprintExternalValue
608
+ } from "@riebeckite/core";
609
+ var ICONIZE_ID = "obsidian-icon-folder";
610
+ var ICONIC_ID = "iconic";
611
+ var MAX_SETTINGS_BYTES = 2 * 1024 * 1024;
612
+ var MAX_SVG_BYTES = 256 * 1024;
613
+ var DEFAULT_ICONIZE_SETTINGS = {
614
+ iconPacksPath: ".obsidian/icons",
615
+ iconColor: null,
616
+ iconInFrontmatterEnabled: false,
617
+ iconInFrontmatterFieldName: "icon",
618
+ iconColorInFrontmatterFieldName: "iconColor",
619
+ iconsInNotesEnabled: true,
620
+ iconInLinksEnabled: true,
621
+ iconIdentifier: ":",
622
+ lucideIconPackType: "native",
623
+ emojiStyle: "native",
624
+ rules: []
625
+ };
626
+ async function loadIconsSettings(options, config) {
627
+ const diagnostics = [];
628
+ const contentDirectory = config?.content.directory;
629
+ const vaultRoot = await resolveVaultRoot(options, contentDirectory);
630
+ const vaultPrefix = resolveVaultPrefix(options, vaultRoot, contentDirectory);
631
+ const dataRoot = vaultRoot ?? process.cwd();
632
+ let iconize;
633
+ if (options.iconize.enabled) {
634
+ const dataFile = await resolveDataFile({
635
+ explicit: options.iconize.dataFile,
636
+ pluginId: ICONIZE_ID,
637
+ root: dataRoot,
638
+ vaultRoot,
639
+ diagnostics,
640
+ source: "Iconize"
641
+ });
642
+ if (dataFile) {
643
+ const raw = await readJsonFile(dataFile, "Iconize", diagnostics);
644
+ if (raw !== void 0) iconize = parseIconizeData(raw, diagnostics);
645
+ }
646
+ }
647
+ let iconic;
648
+ if (options.iconic.enabled) {
649
+ const dataFile = await resolveDataFile({
650
+ explicit: options.iconic.dataFile,
651
+ pluginId: ICONIC_ID,
652
+ root: dataRoot,
653
+ vaultRoot,
654
+ diagnostics,
655
+ source: "Iconic"
656
+ });
657
+ if (dataFile) {
658
+ const raw = await readJsonFile(dataFile, "Iconic", diagnostics);
659
+ if (raw !== void 0) iconic = parseIconicData(raw, diagnostics);
660
+ }
661
+ }
662
+ const iconPacksRoot = vaultRoot ? resolvePackRoot(
663
+ vaultRoot,
664
+ options.iconize.iconPacks ?? iconize?.settings.iconPacksPath ?? DEFAULT_ICONIZE_SETTINGS.iconPacksPath
665
+ ) : void 0;
666
+ const iconPacks = iconPacksRoot ? await loadIconPacks(iconPacksRoot, diagnostics) : /* @__PURE__ */ new Map();
667
+ return {
668
+ ...iconize === void 0 ? {} : { iconize },
669
+ ...iconic === void 0 ? {} : { iconic },
670
+ iconPacks,
671
+ ...iconPacksRoot === void 0 ? {} : { iconPacksRoot },
672
+ vaultPrefix,
673
+ fingerprint: fingerprintExternalValue({
674
+ iconize: iconize ? { settings: iconize.settings, icons: [...iconize.icons] } : null,
675
+ iconic: iconic ?? null,
676
+ packs: [...iconPacks].map(([pack, files]) => [
677
+ pack,
678
+ [...files].map(([name, file]) => [name, file.size, file.modified])
679
+ ])
680
+ }),
681
+ diagnostics
682
+ };
683
+ }
684
+ function findIconPackFile(packs, packName, iconName) {
685
+ const pack = packs.get(packName.toLowerCase());
686
+ if (!pack) return void 0;
687
+ return pack.get(normalizeIconName(iconName).toLowerCase());
688
+ }
689
+ function normalizeIconName(name) {
690
+ return name.split(/[\s_-]+/).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
691
+ }
692
+ function normalizeVaultPath(value) {
693
+ const normalized = value.normalize("NFC").replace(/\\/g, "/").replace(/^\.\//, "").replace(/^\/+/, "").replace(/\/{2,}/g, "/");
694
+ if (!normalized || normalized.includes("\0")) return void 0;
695
+ const segments = normalized.split("/");
696
+ if (segments.some((segment) => segment === ".." || segment === ".")) {
697
+ return void 0;
698
+ }
699
+ return segments.filter(Boolean).join("/");
700
+ }
701
+ async function resolveVaultRoot(options, contentDirectory) {
702
+ if (options.vaultRoot) {
703
+ const explicit = path.resolve(options.vaultRoot);
704
+ const root = path.basename(explicit) === ".obsidian" ? path.dirname(explicit) : explicit;
705
+ return await isDirectory(root) ? root : void 0;
706
+ }
707
+ const starts = [contentDirectory, process.cwd()].filter(
708
+ (value) => typeof value === "string"
709
+ );
710
+ for (const start of starts) {
711
+ let current = path.resolve(start);
712
+ for (; ; ) {
713
+ if (await isDirectory(path.join(current, ".obsidian"))) return current;
714
+ const parent = path.dirname(current);
715
+ if (parent === current) break;
716
+ current = parent;
717
+ }
718
+ }
719
+ return void 0;
720
+ }
721
+ function resolveVaultPrefix(options, vaultRoot, contentDirectory) {
722
+ if (options.vaultPrefix !== void 0) {
723
+ return normalizeVaultPath(options.vaultPrefix) ?? "";
724
+ }
725
+ if (!vaultRoot || !contentDirectory) return "";
726
+ const relative = path.relative(vaultRoot, path.resolve(contentDirectory));
727
+ if (!relative || relative.startsWith("..") || path.isAbsolute(relative)) {
728
+ return "";
729
+ }
730
+ return `${normalizeVaultPath(relative) ?? ""}/`.replace(/^\/+/, "");
731
+ }
732
+ async function resolveDataFile(input) {
733
+ const root = input.vaultRoot ?? input.root;
734
+ if (input.explicit) {
735
+ const resolved = path.resolve(input.root, input.explicit);
736
+ if (!isWithin(root, resolved)) {
737
+ input.diagnostics.push({
738
+ code: "icons-untrusted-settings-path",
739
+ severity: "warning",
740
+ message: `${input.source} settings path is outside the allowed root; ignoring it.`,
741
+ meta: { source: input.source }
742
+ });
743
+ return void 0;
744
+ }
745
+ return await isFile(resolved) ? resolved : void 0;
746
+ }
747
+ const candidate = path.join(
748
+ root,
749
+ ".obsidian",
750
+ "plugins",
751
+ input.pluginId,
752
+ "data.json"
753
+ );
754
+ return await isFile(candidate) ? candidate : void 0;
755
+ }
756
+ async function readJsonFile(filePath, source, diagnostics) {
757
+ const text = await readTextFile(filePath, MAX_SETTINGS_BYTES);
758
+ if (text === void 0) {
759
+ diagnostics.push({
760
+ code: "icons-unreadable-settings",
761
+ severity: "warning",
762
+ message: `${source} settings could not be read; continuing without them.`,
763
+ meta: { source }
764
+ });
765
+ return void 0;
766
+ }
767
+ try {
768
+ return JSON.parse(text);
769
+ } catch {
770
+ diagnostics.push({
771
+ code: "icons-invalid-settings",
772
+ severity: "warning",
773
+ message: `${source} settings are not valid JSON; continuing without them.`,
774
+ meta: { source }
775
+ });
776
+ return void 0;
777
+ }
778
+ }
779
+ async function readTextFile(filePath, maxBytes) {
780
+ try {
781
+ const stat = await fs.lstat(filePath);
782
+ if (!stat.isFile() || stat.isSymbolicLink()) return void 0;
783
+ if (stat.size > maxBytes) return void 0;
784
+ return await fs.readFile(filePath, "utf8");
785
+ } catch {
786
+ return void 0;
787
+ }
788
+ }
789
+ function parseIconizeData(raw, diagnostics) {
790
+ if (!isRecord(raw)) {
791
+ diagnostics.push({
792
+ code: "icons-invalid-settings",
793
+ severity: "warning",
794
+ message: "Iconize settings are not an object; continuing without them.",
795
+ meta: { source: "Iconize" }
796
+ });
797
+ return { settings: DEFAULT_ICONIZE_SETTINGS, icons: /* @__PURE__ */ new Map() };
798
+ }
799
+ const rawSettings = isRecord(raw.settings) ? raw.settings : {};
800
+ const iconIdentifier = typeof rawSettings.iconIdentifier === "string" && rawSettings.iconIdentifier.length > 0 ? rawSettings.iconIdentifier : DEFAULT_ICONIZE_SETTINGS.iconIdentifier;
801
+ const settings = {
802
+ iconPacksPath: typeof rawSettings.iconPacksPath === "string" && rawSettings.iconPacksPath.trim() ? rawSettings.iconPacksPath : DEFAULT_ICONIZE_SETTINGS.iconPacksPath,
803
+ iconColor: typeof rawSettings.iconColor === "string" ? rawSettings.iconColor : null,
804
+ iconInFrontmatterEnabled: rawSettings.iconInFrontmatterEnabled === true,
805
+ iconInFrontmatterFieldName: nonEmptyString(rawSettings.iconInFrontmatterFieldName) ?? DEFAULT_ICONIZE_SETTINGS.iconInFrontmatterFieldName,
806
+ iconColorInFrontmatterFieldName: nonEmptyString(rawSettings.iconColorInFrontmatterFieldName) ?? DEFAULT_ICONIZE_SETTINGS.iconColorInFrontmatterFieldName,
807
+ iconsInNotesEnabled: rawSettings.iconsInNotesEnabled !== false,
808
+ iconInLinksEnabled: rawSettings.iconInLinksEnabled !== false,
809
+ iconIdentifier,
810
+ lucideIconPackType: isLucidePackType(rawSettings.lucideIconPackType) ? rawSettings.lucideIconPackType : DEFAULT_ICONIZE_SETTINGS.lucideIconPackType,
811
+ emojiStyle: rawSettings.emojiStyle === "twemoji" ? "twemoji" : "native",
812
+ rules: parseIconizeRules(rawSettings.rules)
813
+ };
814
+ const icons2 = /* @__PURE__ */ new Map();
815
+ for (const [key, value] of Object.entries(raw)) {
816
+ if (key === "settings" || key === "migrated") continue;
817
+ const normalized = normalizeVaultPath(key);
818
+ if (!normalized) continue;
819
+ if (typeof value === "string") {
820
+ if (value.trim()) icons2.set(normalized, { iconName: value.trim() });
821
+ continue;
822
+ }
823
+ if (!isRecord(value)) continue;
824
+ const iconName = value.iconName === null ? null : typeof value.iconName === "string" ? value.iconName.trim() || null : null;
825
+ const iconColor = typeof value.iconColor === "string" ? value.iconColor : void 0;
826
+ if (iconName === null && iconColor === void 0) continue;
827
+ icons2.set(normalized, {
828
+ iconName,
829
+ ...iconColor === void 0 ? {} : { iconColor }
830
+ });
831
+ }
832
+ if (settings.emojiStyle === "twemoji") {
833
+ diagnostics.push({
834
+ code: "icons-unsupported-emoji-style",
835
+ severity: "info",
836
+ message: "Iconize twemoji styling is not bundled; native emoji are used instead.",
837
+ meta: { source: "Iconize" }
838
+ });
839
+ }
840
+ return { settings, icons: icons2 };
841
+ }
842
+ function parseIconizeRules(value) {
843
+ if (!Array.isArray(value)) return [];
844
+ const rules = [];
845
+ for (const raw of value) {
846
+ if (!isRecord(raw)) continue;
847
+ const rule = nonEmptyString(raw.rule);
848
+ const icon = nonEmptyString(raw.icon);
849
+ if (!rule || !icon) continue;
850
+ rules.push({
851
+ rule,
852
+ icon,
853
+ order: typeof raw.order === "number" && Number.isFinite(raw.order) ? raw.order : rules.length,
854
+ ...typeof raw.color === "string" ? { color: raw.color } : {},
855
+ ...raw.useFilePath === true ? { useFilePath: true } : {},
856
+ ...raw.for === "everything" || raw.for === "files" || raw.for === "folders" ? { for: raw.for } : {}
857
+ });
858
+ }
859
+ return rules;
860
+ }
861
+ function parseIconicData(raw, diagnostics) {
862
+ if (!isRecord(raw)) {
863
+ diagnostics.push({
864
+ code: "icons-invalid-settings",
865
+ severity: "warning",
866
+ message: "Iconic settings are not an object; continuing without them.",
867
+ meta: { source: "Iconic" }
868
+ });
869
+ return {
870
+ fileIcons: {},
871
+ tagIcons: {},
872
+ propertyIcons: {},
873
+ fileRules: [],
874
+ folderRules: []
875
+ };
876
+ }
877
+ return {
878
+ fileIcons: parseIconicValues(raw.fileIcons),
879
+ tagIcons: parseIconicValues(raw.tagIcons),
880
+ propertyIcons: parseIconicValues(raw.propertyIcons),
881
+ fileRules: parseIconicRules(raw.fileRules),
882
+ folderRules: parseIconicRules(raw.folderRules)
883
+ };
884
+ }
885
+ function parseIconicValues(value) {
886
+ if (!isRecord(value)) return {};
887
+ const result = {};
888
+ for (const [key, raw] of Object.entries(value)) {
889
+ if (!isRecord(raw)) continue;
890
+ const normalized = key.includes("/") ? normalizeVaultPath(key) : key;
891
+ if (!normalized) continue;
892
+ const icon = nonEmptyString(raw.icon);
893
+ const color = nonEmptyString(raw.color);
894
+ if (!icon && !color) continue;
895
+ result[normalized] = {
896
+ ...icon ? { icon } : {},
897
+ ...color ? { color } : {}
898
+ };
899
+ }
900
+ return result;
901
+ }
902
+ function parseIconicRules(value) {
903
+ if (!Array.isArray(value)) return [];
904
+ const rules = [];
905
+ for (const raw of value) {
906
+ if (!isRecord(raw)) continue;
907
+ const conditions = Array.isArray(raw.conditions) ? raw.conditions.flatMap((condition) => {
908
+ if (!isRecord(condition)) return [];
909
+ return [
910
+ {
911
+ ...typeof condition.source === "string" ? { source: condition.source } : {},
912
+ ...typeof condition.operator === "string" ? { operator: condition.operator } : {},
913
+ ...typeof condition.value === "string" ? { value: condition.value } : {}
914
+ }
915
+ ];
916
+ }) : [];
917
+ const icon = nonEmptyString(raw.icon);
918
+ const color = nonEmptyString(raw.color);
919
+ rules.push({
920
+ ...typeof raw.id === "string" ? { id: raw.id } : {},
921
+ ...typeof raw.name === "string" ? { name: raw.name } : {},
922
+ ...icon ? { icon } : {},
923
+ ...color ? { color } : {},
924
+ ...raw.match === "any" || raw.match === "none" ? { match: raw.match } : { match: "all" },
925
+ conditions,
926
+ enabled: raw.enabled === true
927
+ });
928
+ }
929
+ return rules;
930
+ }
931
+ async function loadIconPacks(root, diagnostics) {
932
+ const packs = /* @__PURE__ */ new Map();
933
+ let entries;
934
+ try {
935
+ entries = await fs.readdir(root, { withFileTypes: true });
936
+ } catch {
937
+ return packs;
938
+ }
939
+ let zipCount = 0;
940
+ for (const entry of entries) {
941
+ if (entry.isSymbolicLink()) continue;
942
+ if (entry.isFile() && entry.name.toLowerCase().endsWith(".zip")) {
943
+ zipCount += 1;
944
+ continue;
945
+ }
946
+ if (!entry.isDirectory()) continue;
947
+ const packPath = path.join(root, entry.name);
948
+ const realPack = await safeRealPath(packPath);
949
+ if (!realPack || !isWithin(root, realPack)) continue;
950
+ let files;
951
+ try {
952
+ files = await fs.readdir(realPack, { withFileTypes: true });
953
+ } catch {
954
+ continue;
955
+ }
956
+ const pack = /* @__PURE__ */ new Map();
957
+ for (const file of files) {
958
+ if (!file.isFile() || !file.name.toLowerCase().endsWith(".svg")) {
959
+ continue;
960
+ }
961
+ const filePath = path.join(realPack, file.name);
962
+ const realFile = await safeRealPath(filePath);
963
+ if (!realFile || !isWithin(root, realFile)) continue;
964
+ try {
965
+ const stat = await fs.stat(realFile);
966
+ if (stat.size > MAX_SVG_BYTES) continue;
967
+ const iconName = normalizeIconName(
968
+ file.name.slice(0, -".svg".length)
969
+ ).toLowerCase();
970
+ if (!iconName) continue;
971
+ pack.set(iconName, {
972
+ name: file.name,
973
+ filePath: realFile,
974
+ size: stat.size,
975
+ modified: stat.mtimeMs
976
+ });
977
+ } catch {
978
+ }
979
+ }
980
+ if (pack.size > 0) {
981
+ const packName = normalizeIconName(entry.name).toLowerCase();
982
+ if (packName) packs.set(packName, pack);
983
+ }
984
+ }
985
+ if (zipCount > 0) {
986
+ diagnostics.push({
987
+ code: "icons-unsupported-zip-pack",
988
+ severity: "info",
989
+ message: "Zip-based Iconize icon packs are not read; extract them into folders under the icon packs directory.",
990
+ meta: { packCount: zipCount }
991
+ });
992
+ }
993
+ return packs;
994
+ }
995
+ function resolvePackRoot(vaultRoot, value) {
996
+ const resolved = path.resolve(vaultRoot, value);
997
+ return isWithin(vaultRoot, resolved) ? resolved : void 0;
998
+ }
999
+ function isLucidePackType(value) {
1000
+ return value === "none" || value === "custom" || value === "native";
1001
+ }
1002
+ function nonEmptyString(value) {
1003
+ return typeof value === "string" && value.trim() ? value.trim() : void 0;
1004
+ }
1005
+ function isRecord(value) {
1006
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1007
+ }
1008
+ function isWithin(root, target) {
1009
+ const relative = path.relative(path.resolve(root), path.resolve(target));
1010
+ return relative === "" || !relative.startsWith("..") && !path.isAbsolute(relative);
1011
+ }
1012
+ async function isDirectory(value) {
1013
+ try {
1014
+ const stat = await fs.lstat(value);
1015
+ return stat.isDirectory() && !stat.isSymbolicLink();
1016
+ } catch {
1017
+ return false;
1018
+ }
1019
+ }
1020
+ async function isFile(value) {
1021
+ try {
1022
+ const stat = await fs.lstat(value);
1023
+ return stat.isFile() && !stat.isSymbolicLink();
1024
+ } catch {
1025
+ return false;
1026
+ }
1027
+ }
1028
+ async function safeRealPath(value) {
1029
+ try {
1030
+ return await fs.realpath(value);
1031
+ } catch {
1032
+ return void 0;
1033
+ }
1034
+ }
1035
+
1036
+ // src/iconize.ts
1037
+ function resolveIconize(data, target, options) {
1038
+ const direct = data.icons.get(target.path);
1039
+ if (direct?.iconName) {
1040
+ return toResolution(direct, data, options);
1041
+ }
1042
+ const rule = firstMatchingRule(data.settings.rules, target);
1043
+ if (!rule) return void 0;
1044
+ return toResolution(
1045
+ { iconName: rule.icon, ...rule.color ? { iconColor: rule.color } : {} },
1046
+ data,
1047
+ options
1048
+ );
1049
+ }
1050
+ function parseIconReference(value, packNames) {
1051
+ const trimmed = value.trim();
1052
+ if (!trimmed) return void 0;
1053
+ if (isEmoji(trimmed)) return `emoji:${trimmed}`;
1054
+ const lower = trimmed.toLowerCase();
1055
+ const packs = [...packNames].map((name) => normalizeIconName(name)).filter(Boolean).toSorted((a, b) => b.length - a.length);
1056
+ for (const pack of packs) {
1057
+ if (lower.startsWith(pack.toLowerCase()) && trimmed.length > pack.length) {
1058
+ return `pack:${pack}:${trimmed.slice(pack.length)}`;
1059
+ }
1060
+ }
1061
+ if (/^Li[A-Z0-9]/.test(trimmed)) {
1062
+ return `lucide:${toKebab(trimmed.slice(2))}`;
1063
+ }
1064
+ if (lower.startsWith("lucide-")) {
1065
+ return `lucide:${lower.slice("lucide-".length)}`;
1066
+ }
1067
+ if (lower.startsWith("lucide:")) {
1068
+ return `lucide:${lower.slice("lucide:".length)}`;
1069
+ }
1070
+ return `lucide:${toKebab(trimmed)}`;
1071
+ }
1072
+ function toKebab(value) {
1073
+ return value.replace(/([a-z0-9])([A-Z])/g, "$1-$2").replace(/([A-Z]+)([A-Z][a-z])/g, "$1-$2").replace(/[\s_]+/g, "-").toLowerCase();
1074
+ }
1075
+ function toResolution(value, data, options) {
1076
+ if (!value.iconName) return void 0;
1077
+ const icon = parseIconReference(value.iconName, options.packs.keys());
1078
+ if (!icon) return void 0;
1079
+ const color = options.colors ? normalizeColor(value.iconColor) ?? normalizeColor(data.settings.iconColor) : void 0;
1080
+ return { icon, ...color ? { color } : {} };
1081
+ }
1082
+ function firstMatchingRule(rules, target) {
1083
+ const sorted = rules.toSorted((a, b) => a.order - b.order);
1084
+ for (const rule of sorted) {
1085
+ if (rule.for === "files" && target.kind !== "file") continue;
1086
+ if (rule.for === "folders" && target.kind !== "folder") continue;
1087
+ const subject = rule.useFilePath ? target.path : target.path.split("/").pop() ?? target.path;
1088
+ if (matchesRule2(rule.rule, subject)) return rule;
1089
+ }
1090
+ return void 0;
1091
+ }
1092
+ function matchesRule2(pattern, value) {
1093
+ if (pattern.length > 512) return false;
1094
+ try {
1095
+ return new RegExp(pattern).test(value);
1096
+ } catch {
1097
+ return value.includes(pattern);
1098
+ }
1099
+ }
1100
+
1101
+ // src/render.ts
1102
+ import { readFileSync } from "node:fs";
1103
+ import { createRequire } from "node:module";
1104
+ import path2 from "node:path";
1105
+ import {
1106
+ escapeHtml,
1107
+ escapeHtmlAttribute
1108
+ } from "@riebeckite/core";
1109
+ import sanitizeHtml from "sanitize-html";
1110
+ var MAX_SVG_BYTES2 = 256 * 1024;
1111
+ var ALLOWED_TAGS = [
1112
+ "svg",
1113
+ "g",
1114
+ "path",
1115
+ "circle",
1116
+ "ellipse",
1117
+ "line",
1118
+ "polyline",
1119
+ "polygon",
1120
+ "rect",
1121
+ "title",
1122
+ "desc",
1123
+ "defs",
1124
+ "lineargradient",
1125
+ "radialgradient",
1126
+ "stop"
1127
+ ];
1128
+ var PRESENTATION_ATTRIBUTES = [
1129
+ "fill",
1130
+ "stroke",
1131
+ "stroke-width",
1132
+ "stroke-linecap",
1133
+ "stroke-linejoin",
1134
+ "stroke-miterlimit",
1135
+ "fill-rule",
1136
+ "clip-rule",
1137
+ "opacity",
1138
+ "fill-opacity",
1139
+ "stroke-opacity",
1140
+ "transform",
1141
+ "vector-effect"
1142
+ ];
1143
+ var SANITIZE_OPTIONS = {
1144
+ allowedTags: ALLOWED_TAGS,
1145
+ allowedAttributes: {
1146
+ svg: ["xmlns", "viewBox", "width", "height", "role", "focusable"],
1147
+ circle: ["cx", "cy", "r"],
1148
+ ellipse: ["cx", "cy", "rx", "ry"],
1149
+ line: ["x1", "y1", "x2", "y2"],
1150
+ rect: ["x", "y", "width", "height", "rx", "ry"],
1151
+ polyline: ["points"],
1152
+ polygon: ["points"],
1153
+ path: ["d", "pathLength"],
1154
+ g: ["clip-path", "mask"],
1155
+ lineargradient: [
1156
+ "id",
1157
+ "gradientUnits",
1158
+ "gradientTransform",
1159
+ "x1",
1160
+ "y1",
1161
+ "x2",
1162
+ "y2"
1163
+ ],
1164
+ radialgradient: [
1165
+ "id",
1166
+ "gradientUnits",
1167
+ "gradientTransform",
1168
+ "cx",
1169
+ "cy",
1170
+ "r",
1171
+ "fx",
1172
+ "fy"
1173
+ ],
1174
+ stop: ["offset", "stop-color", "stop-opacity"],
1175
+ "*": PRESENTATION_ATTRIBUTES
1176
+ },
1177
+ allowedSchemes: [],
1178
+ allowedSchemesByTag: {},
1179
+ allowProtocolRelative: false,
1180
+ disallowedTagsMode: "discard",
1181
+ parser: {
1182
+ lowerCaseAttributeNames: false,
1183
+ lowerCaseTags: true
1184
+ }
1185
+ };
1186
+ var IconRenderer = class {
1187
+ #packs;
1188
+ #cache = /* @__PURE__ */ new Map();
1189
+ #lucideDirectory = resolveLucideDirectory();
1190
+ constructor(settings) {
1191
+ this.#packs = settings.iconPacks;
1192
+ }
1193
+ render(reference, options) {
1194
+ const inner = this.#inner(reference);
1195
+ if (inner === void 0) return void 0;
1196
+ const kind = reference.startsWith("emoji:") ? "emoji" : reference.startsWith("text:") ? "text" : "svg";
1197
+ const color = options.color;
1198
+ return {
1199
+ id: reference,
1200
+ kind,
1201
+ source: options.source,
1202
+ html: `<span class="rr-icon rr-icon--${kind}" aria-hidden="true"${color === void 0 ? "" : ` style="color:${escapeHtmlAttribute(color)}"`}>${inner}</span>`,
1203
+ ...color === void 0 ? {} : { color },
1204
+ ...options.label === void 0 ? {} : { label: options.label }
1205
+ };
1206
+ }
1207
+ #inner(reference) {
1208
+ const cached = this.#cache.get(reference);
1209
+ if (cached !== void 0) return cached ?? void 0;
1210
+ let html;
1211
+ if (reference.startsWith("emoji:")) {
1212
+ const raw = reference.slice("emoji:".length);
1213
+ const emoji = lookupEmoji(raw) ?? (raw.trim() ? raw : void 0);
1214
+ if (emoji) html = escapeHtml(emoji);
1215
+ } else if (reference.startsWith("pack:")) {
1216
+ const [, packName, ...rest] = reference.split(":");
1217
+ const file = packName ? findIconPackFile(this.#packs, packName, rest.join(":")) : void 0;
1218
+ if (file) html = renderSvg(readSvg(file.filePath));
1219
+ } else if (reference.startsWith("lucide:")) {
1220
+ const name = reference.slice("lucide:".length);
1221
+ if (/^[a-z0-9-]+$/.test(name) && this.#lucideDirectory) {
1222
+ html = renderSvg(
1223
+ readSvg(path2.join(this.#lucideDirectory, `${name}.svg`))
1224
+ );
1225
+ }
1226
+ } else if (reference.startsWith("text:")) {
1227
+ html = escapeHtml(reference.slice("text:".length));
1228
+ }
1229
+ this.#cache.set(reference, html ?? null);
1230
+ return html;
1231
+ }
1232
+ };
1233
+ function renderSvg(source) {
1234
+ if (!source || !/<svg[\s>]/i.test(source)) return void 0;
1235
+ const sanitized = sanitizeHtml(source, SANITIZE_OPTIONS);
1236
+ if (!/<svg[\s>]/i.test(sanitized) || /<script|<foreignObject|<style|<use|<image|<animate|<set|\son[a-z]+\s*=|javascript:|(?:xlink:)?href\s*=|url\s*\(/i.test(
1237
+ sanitized
1238
+ )) {
1239
+ return void 0;
1240
+ }
1241
+ return sanitized;
1242
+ }
1243
+ function readSvg(filePath) {
1244
+ if (!filePath) return void 0;
1245
+ try {
1246
+ const bytes = readFileSync(filePath);
1247
+ if (bytes.byteLength > MAX_SVG_BYTES2) return void 0;
1248
+ return bytes.toString("utf8");
1249
+ } catch {
1250
+ return void 0;
1251
+ }
1252
+ }
1253
+ function resolveLucideDirectory() {
1254
+ try {
1255
+ const require2 = createRequire(import.meta.url);
1256
+ try {
1257
+ return path2.join(
1258
+ path2.dirname(require2.resolve("lucide-static/package.json")),
1259
+ "icons"
1260
+ );
1261
+ } catch {
1262
+ let current = path2.dirname(require2.resolve("lucide-static"));
1263
+ for (; ; ) {
1264
+ if (path2.basename(current) === "lucide-static") {
1265
+ return path2.join(current, "icons");
1266
+ }
1267
+ const parent = path2.dirname(current);
1268
+ if (parent === current) return void 0;
1269
+ current = parent;
1270
+ }
1271
+ }
1272
+ } catch {
1273
+ return void 0;
1274
+ }
1275
+ }
1276
+
1277
+ // src/resolve.ts
1278
+ var DEFAULT_PRECEDENCE = [
1279
+ "config",
1280
+ "frontmatter",
1281
+ "iconic",
1282
+ "iconize"
1283
+ ];
1284
+ var SOURCES = /* @__PURE__ */ new Set(["iconize", "iconic"]);
1285
+ var PRECEDENCE = /* @__PURE__ */ new Set([
1286
+ "config",
1287
+ "frontmatter",
1288
+ "iconic",
1289
+ "iconize"
1290
+ ]);
1291
+ function resolveIconsOptions(options) {
1292
+ const sources = options?.sources?.length ? [...new Set(options.sources)] : ["iconize", "iconic"];
1293
+ const precedence = options?.precedence?.length ? [...new Set(options.precedence)] : [...DEFAULT_PRECEDENCE];
1294
+ return {
1295
+ sources,
1296
+ inline: options?.inline !== false,
1297
+ frontmatter: options?.frontmatter !== false,
1298
+ colors: options?.colors !== false,
1299
+ precedence,
1300
+ overrides: options?.overrides ?? {},
1301
+ ...options?.vaultRoot === void 0 ? {} : { vaultRoot: options.vaultRoot },
1302
+ ...options?.vaultPrefix === void 0 ? {} : { vaultPrefix: options.vaultPrefix },
1303
+ iconize: {
1304
+ enabled: sources.includes("iconize") && options?.iconize?.enabled !== false,
1305
+ ...options?.iconize?.dataFile === void 0 ? {} : { dataFile: options.iconize.dataFile },
1306
+ ...options?.iconize?.iconPacks === void 0 ? {} : { iconPacks: options.iconize.iconPacks }
1307
+ },
1308
+ iconic: {
1309
+ enabled: sources.includes("iconic") && options?.iconic?.enabled !== false,
1310
+ ...options?.iconic?.dataFile === void 0 ? {} : { dataFile: options.iconic.dataFile }
1311
+ }
1312
+ };
1313
+ }
1314
+ function validateIconsOptions(options) {
1315
+ if (options === void 0) return void 0;
1316
+ const issues = [];
1317
+ if (options.sources !== void 0) {
1318
+ if (!Array.isArray(options.sources) || options.sources.length === 0) {
1319
+ issues.push({ path: "sources", message: "must be a non-empty array." });
1320
+ } else {
1321
+ const seen = /* @__PURE__ */ new Set();
1322
+ options.sources.forEach((source, index) => {
1323
+ if (!SOURCES.has(source)) {
1324
+ issues.push({
1325
+ path: `sources[${index}]`,
1326
+ message: `unknown source "${String(source)}".`
1327
+ });
1328
+ } else if (seen.has(source)) {
1329
+ issues.push({
1330
+ path: `sources[${index}]`,
1331
+ message: `duplicate source "${source}".`
1332
+ });
1333
+ }
1334
+ seen.add(source);
1335
+ });
1336
+ }
1337
+ }
1338
+ if (options.precedence !== void 0) {
1339
+ if (!Array.isArray(options.precedence) || options.precedence.length === 0) {
1340
+ issues.push({
1341
+ path: "precedence",
1342
+ message: "must be a non-empty array."
1343
+ });
1344
+ } else {
1345
+ const seen = /* @__PURE__ */ new Set();
1346
+ options.precedence.forEach((source, index) => {
1347
+ if (!PRECEDENCE.has(source)) {
1348
+ issues.push({
1349
+ path: `precedence[${index}]`,
1350
+ message: `unknown precedence "${String(source)}".`
1351
+ });
1352
+ } else if (seen.has(source)) {
1353
+ issues.push({
1354
+ path: `precedence[${index}]`,
1355
+ message: `duplicate precedence "${source}".`
1356
+ });
1357
+ }
1358
+ seen.add(source);
1359
+ });
1360
+ }
1361
+ }
1362
+ for (const key of ["inline", "frontmatter", "colors"]) {
1363
+ if (options[key] !== void 0 && typeof options[key] !== "boolean") {
1364
+ issues.push({ path: key, message: "must be a boolean." });
1365
+ }
1366
+ }
1367
+ if (options.overrides !== void 0) {
1368
+ if (!isRecord2(options.overrides)) {
1369
+ issues.push({ path: "overrides", message: "must be an object." });
1370
+ } else {
1371
+ for (const [key, value] of Object.entries(options.overrides)) {
1372
+ if (typeof value === "string") continue;
1373
+ if (!isRecord2(value) || typeof value.icon !== "string" || value.color !== void 0 && typeof value.color !== "string") {
1374
+ issues.push({
1375
+ path: `overrides.${key}`,
1376
+ message: "must be a string or an object with an icon string."
1377
+ });
1378
+ }
1379
+ }
1380
+ }
1381
+ }
1382
+ for (const key of ["vaultRoot", "vaultPrefix"]) {
1383
+ if (options[key] !== void 0 && typeof options[key] !== "string") {
1384
+ issues.push({ path: key, message: "must be a string." });
1385
+ }
1386
+ }
1387
+ for (const key of ["iconize", "iconic"]) {
1388
+ const value = options[key];
1389
+ if (value !== void 0 && !isRecord2(value)) {
1390
+ issues.push({ path: key, message: "must be an object." });
1391
+ }
1392
+ }
1393
+ return issues.length > 0 ? issues : void 0;
1394
+ }
1395
+ function createIconResolver(input) {
1396
+ const { settings, options } = input;
1397
+ const renderer = new IconRenderer(settings);
1398
+ const warnings = [];
1399
+ const iconizeSettings = settings.iconize?.settings;
1400
+ const identifier = iconizeSettings?.iconIdentifier ?? ":";
1401
+ const packNames = [...settings.iconPacks.keys()];
1402
+ const reportUnknown = (reference) => {
1403
+ warnings.push(`Icon "${reference}" could not be rendered.`);
1404
+ };
1405
+ const render = (reference, source, target, color) => {
1406
+ const icon = renderer.render(reference, {
1407
+ source,
1408
+ target,
1409
+ ...color === void 0 ? {} : { color }
1410
+ });
1411
+ if (!icon) reportUnknown(reference);
1412
+ return icon;
1413
+ };
1414
+ const fromOverride = (target) => {
1415
+ const override = findOverride(options.overrides, target);
1416
+ if (override === void 0) return void 0;
1417
+ const value = typeof override === "string" ? { icon: override } : override;
1418
+ const reference = parseIconReference(value.icon, packNames);
1419
+ if (!reference) return void 0;
1420
+ const color = options.colors ? normalizeColor(value.color, { iconicNames: true }) : void 0;
1421
+ return render(reference, "config", target.path, color);
1422
+ };
1423
+ const fromFrontmatter = (target) => {
1424
+ if (!options.frontmatter || target.kind !== "file" || !target.frontmatter) {
1425
+ return void 0;
1426
+ }
1427
+ const iconField = iconizeSettings?.iconInFrontmatterFieldName ?? "icon";
1428
+ const colorField = iconizeSettings?.iconColorInFrontmatterFieldName ?? "iconColor";
1429
+ const raw = target.frontmatter[iconField];
1430
+ const iconValue = typeof raw === "string" ? raw.trim() : "";
1431
+ if (!iconValue) return void 0;
1432
+ const reference = parseIconReference(iconValue, packNames);
1433
+ if (!reference) return void 0;
1434
+ const rawColor = target.frontmatter[colorField];
1435
+ const color = options.colors ? normalizeColor(typeof rawColor === "string" ? rawColor : void 0, {
1436
+ iconicNames: true
1437
+ }) ?? normalizeColor(iconizeSettings?.iconColor) : void 0;
1438
+ return render(reference, "frontmatter", target.path, color);
1439
+ };
1440
+ const fromIconic = (target) => {
1441
+ if (!options.iconic.enabled || !settings.iconic) return void 0;
1442
+ const resolution = resolveIconic(settings.iconic, target, {
1443
+ colors: options.colors,
1444
+ report: input.reportRule
1445
+ });
1446
+ if (!resolution?.icon) return void 0;
1447
+ return render(
1448
+ resolveIconicReference(resolution.icon),
1449
+ "iconic",
1450
+ target.path,
1451
+ resolution.color
1452
+ );
1453
+ };
1454
+ const fromIconize = (target) => {
1455
+ if (!options.iconize.enabled || !settings.iconize) return void 0;
1456
+ const resolution = resolveIconize(settings.iconize, target, {
1457
+ colors: options.colors,
1458
+ packs: settings.iconPacks
1459
+ });
1460
+ if (!resolution) return void 0;
1461
+ return render(resolution.icon, "iconize", target.path, resolution.color);
1462
+ };
1463
+ const forTarget = (target) => {
1464
+ for (const source of options.precedence) {
1465
+ if (source === "config") {
1466
+ const icon = fromOverride(target);
1467
+ if (icon) return icon;
1468
+ } else if (source === "frontmatter") {
1469
+ const icon = fromFrontmatter(target);
1470
+ if (icon) return icon;
1471
+ } else if (source === "iconic") {
1472
+ const icon = fromIconic(target);
1473
+ if (icon) return icon;
1474
+ } else if (source === "iconize") {
1475
+ const icon = fromIconize(target);
1476
+ if (icon) return icon;
1477
+ }
1478
+ }
1479
+ return void 0;
1480
+ };
1481
+ return {
1482
+ identifier,
1483
+ forEntry(entry) {
1484
+ return forTarget(targetForEntry(entry, settings.vaultPrefix));
1485
+ },
1486
+ forFolder(folder) {
1487
+ return forTarget(iconTarget("folder", folder, settings.vaultPrefix));
1488
+ },
1489
+ forTag(tag) {
1490
+ if (!options.iconic.enabled || !settings.iconic) return void 0;
1491
+ const value = findKey(settings.iconic.tagIcons, tag.replace(/^#/, ""));
1492
+ if (!value) return void 0;
1493
+ return fromNamedValue(value, `tag:${tag}`, "iconic");
1494
+ },
1495
+ forProperty(property) {
1496
+ if (!options.iconic.enabled || !settings.iconic) return void 0;
1497
+ const value = findKey(settings.iconic.propertyIcons, property);
1498
+ if (!value) return void 0;
1499
+ return fromNamedValue(value, `property:${property}`, "iconic");
1500
+ },
1501
+ inline(name) {
1502
+ if (!options.inline || iconizeSettings?.iconsInNotesEnabled === false) {
1503
+ return void 0;
1504
+ }
1505
+ if (name.includes(":")) return void 0;
1506
+ const emoji = lookupEmoji(name) ?? (isEmoji(name) ? name : void 0);
1507
+ const color = options.colors ? normalizeColor(iconizeSettings?.iconColor) : void 0;
1508
+ if (emoji) {
1509
+ return render(`emoji:${emoji}`, "iconize", `inline:${name}`, color);
1510
+ }
1511
+ const lucideType = iconizeSettings?.lucideIconPackType ?? "native";
1512
+ const reference = parseIconReference(name, packNames);
1513
+ if (!reference) return void 0;
1514
+ if (lucideType === "none" && reference.startsWith("lucide:")) {
1515
+ return void 0;
1516
+ }
1517
+ return render(reference, "iconize", `inline:${name}`, color);
1518
+ },
1519
+ warnings() {
1520
+ return warnings;
1521
+ }
1522
+ };
1523
+ function fromNamedValue(value, target, source) {
1524
+ if (!value.icon) return void 0;
1525
+ const color = options.colors ? normalizeColor(value.color, { iconicNames: true }) : void 0;
1526
+ return render(resolveIconicReference(value.icon), source, target, color);
1527
+ }
1528
+ }
1529
+ function targetForEntry(entry, vaultPrefix) {
1530
+ const frontmatter = entry.frontmatter;
1531
+ const links = entry.links.map((link) => link.raw);
1532
+ return {
1533
+ kind: "file",
1534
+ path: vaultPath(vaultPrefix, `${entry.slug}.md`),
1535
+ slug: entry.slug,
1536
+ frontmatter,
1537
+ tags: entry.tags,
1538
+ links,
1539
+ embeds: entry.links.filter((link) => link.embed).map((link) => link.raw),
1540
+ ...entry.file?.ctime ? { created: entry.file.ctime } : {},
1541
+ ...entry.file?.mtime ? { modified: entry.file.mtime } : {}
1542
+ };
1543
+ }
1544
+ function iconTarget(kind, value, vaultPrefix) {
1545
+ return {
1546
+ kind,
1547
+ path: vaultPath(vaultPrefix, value),
1548
+ slug: value
1549
+ };
1550
+ }
1551
+ function vaultPath(prefix, value) {
1552
+ return normalizeVaultPath(`${prefix}${value}`) ?? value;
1553
+ }
1554
+ function findOverride(overrides, target) {
1555
+ const candidates = /* @__PURE__ */ new Set([
1556
+ target.path,
1557
+ target.slug,
1558
+ target.path.replace(/\.md$/, "")
1559
+ ]);
1560
+ for (const [key, value] of Object.entries(overrides)) {
1561
+ const normalized = normalizeVaultPath(key);
1562
+ if (!normalized) continue;
1563
+ if (candidates.has(normalized)) return value;
1564
+ if (candidates.has(normalized.replace(/\.md$/, ""))) return value;
1565
+ if (normalized.startsWith("./") && candidates.has(normalized.slice(2))) {
1566
+ return value;
1567
+ }
1568
+ }
1569
+ return void 0;
1570
+ }
1571
+ function findKey(values, key) {
1572
+ if (values[key] !== void 0) return values[key];
1573
+ const lower = key.toLowerCase();
1574
+ for (const [candidate, value] of Object.entries(values)) {
1575
+ if (candidate.toLowerCase() === lower) return value;
1576
+ }
1577
+ return void 0;
1578
+ }
1579
+ function isRecord2(value) {
1580
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1581
+ }
1582
+
1583
+ // index.ts
1584
+ var PLUGIN_NAME = "icons";
1585
+ var EXTERNAL_ID = "icons:settings";
1586
+ function icons(options = {}) {
1587
+ const resolvedOptions = resolveIconsOptions(options);
1588
+ let loaded;
1589
+ let resolver;
1590
+ const ruleDiagnostics = /* @__PURE__ */ new Map();
1591
+ const reportRule = (rule, message) => {
1592
+ const key = `${rule.id ?? rule.name ?? "rule"}:${message}`;
1593
+ ruleDiagnostics.set(key, {
1594
+ code: "icons-unsupported-rule",
1595
+ severity: "info",
1596
+ pluginName: PLUGIN_NAME,
1597
+ message: `Iconic rule "${rule.name ?? rule.id ?? "unnamed"}" ${message}; it did not match.`,
1598
+ meta: { ...rule.id ? { ruleId: rule.id } : {} }
1599
+ });
1600
+ };
1601
+ return definePlugin({
1602
+ name: PLUGIN_NAME,
1603
+ order: 20,
1604
+ options,
1605
+ contentFileMetadata: true,
1606
+ validateOptions: validateIconsOptions,
1607
+ processedContentCache: {
1608
+ version: "icons-v1",
1609
+ dependencyMode: "none",
1610
+ environmentFingerprint: () => loaded?.fingerprint
1611
+ },
1612
+ contentOutputDependencies: [{ type: "external", id: EXTERNAL_ID }],
1613
+ assets: [
1614
+ createStyleAsset(PLUGIN_NAME, "@riebeckite/plugin-icons/style.css")
1615
+ ],
1616
+ resolveExternalDependencies: async (context) => {
1617
+ ruleDiagnostics.clear();
1618
+ resolver = void 0;
1619
+ try {
1620
+ loaded = await loadIconsSettings(resolvedOptions, context.config);
1621
+ resolver = createIconResolver({
1622
+ settings: loaded,
1623
+ options: resolvedOptions,
1624
+ reportRule
1625
+ });
1626
+ } catch {
1627
+ loaded = void 0;
1628
+ resolver = void 0;
1629
+ ruleDiagnostics.set("icons-load-failed", {
1630
+ code: "icons-load-failed",
1631
+ severity: "warning",
1632
+ pluginName: PLUGIN_NAME,
1633
+ message: "Obsidian icon settings could not be loaded; continuing without icons."
1634
+ });
1635
+ }
1636
+ return [
1637
+ {
1638
+ kind: "resolved",
1639
+ snapshot: {
1640
+ id: EXTERNAL_ID,
1641
+ fingerprint: loaded?.fingerprint ?? "icons-unavailable",
1642
+ scope: { type: "all" },
1643
+ value: { version: 1 }
1644
+ }
1645
+ }
1646
+ ];
1647
+ },
1648
+ extendMarkdownPipeline: (pipeline) => {
1649
+ pipeline.use(remarkIcons, {
1650
+ identifier: resolver?.identifier ?? ":",
1651
+ resolve: (name) => resolver?.inline(name)
1652
+ });
1653
+ },
1654
+ onManifestCreated: ({ manifest }) => {
1655
+ if (!loaded || !resolver) return;
1656
+ for (const entry of manifest.entries) {
1657
+ const icon = resolver.forEntry(entry);
1658
+ if (icon) entry.icon = icon;
1659
+ }
1660
+ const folders = collectFolders(
1661
+ manifest.entries,
1662
+ manifest.folderLocations
1663
+ );
1664
+ const tags = /* @__PURE__ */ new Map();
1665
+ const properties = /* @__PURE__ */ new Map();
1666
+ const folderIcons = /* @__PURE__ */ new Map();
1667
+ for (const folder of folders) {
1668
+ const icon = resolver.forFolder(folder);
1669
+ if (icon) folderIcons.set(folder, icon);
1670
+ }
1671
+ for (const entry of manifest.entries) {
1672
+ for (const tag of entry.tags) {
1673
+ if (tags.has(tag)) continue;
1674
+ const icon = resolver.forTag(tag);
1675
+ if (icon) tags.set(tag, icon);
1676
+ }
1677
+ for (const property of Object.keys(entry.frontmatter)) {
1678
+ if (properties.has(property)) continue;
1679
+ const icon = resolver.forProperty(property);
1680
+ if (icon) properties.set(property, icon);
1681
+ }
1682
+ }
1683
+ for (const [pathname, location] of manifest.folderLocations) {
1684
+ const folder = location.folder ?? pathname;
1685
+ const icon = folderIcons.get(folder) ?? resolver.forFolder(folder);
1686
+ if (icon) location.icon = icon;
1687
+ }
1688
+ const registry = {
1689
+ ...tags.size ? { tags: Object.fromEntries(tags) } : {},
1690
+ ...properties.size ? { properties: Object.fromEntries(properties) } : {},
1691
+ ...folderIcons.size ? { folders: Object.fromEntries(folderIcons) } : {}
1692
+ };
1693
+ if (Object.keys(registry).length > 0) manifest.icons = registry;
1694
+ },
1695
+ addDiagnostics: () => {
1696
+ const diagnostics = [];
1697
+ for (const diagnostic of loaded?.diagnostics ?? []) {
1698
+ diagnostics.push({
1699
+ code: diagnostic.code,
1700
+ severity: diagnostic.severity,
1701
+ pluginName: PLUGIN_NAME,
1702
+ message: diagnostic.message,
1703
+ ...diagnostic.meta ? { meta: { ...diagnostic.meta } } : {}
1704
+ });
1705
+ }
1706
+ for (const warning of resolver?.warnings() ?? []) {
1707
+ diagnostics.push({
1708
+ code: "icons-unresolved-icon",
1709
+ severity: "warning",
1710
+ pluginName: PLUGIN_NAME,
1711
+ message: warning
1712
+ });
1713
+ }
1714
+ diagnostics.push(...ruleDiagnostics.values());
1715
+ return diagnostics;
1716
+ }
1717
+ });
1718
+ }
1719
+ function collectFolders(entries, folderLocations) {
1720
+ const folders = /* @__PURE__ */ new Set();
1721
+ for (const entry of entries) {
1722
+ const segments = entry.slug.split("/");
1723
+ segments.pop();
1724
+ for (let index = 1; index <= segments.length; index += 1) {
1725
+ folders.add(segments.slice(0, index).join("/"));
1726
+ }
1727
+ }
1728
+ for (const location of folderLocations.values()) {
1729
+ if (location.folder) folders.add(location.folder);
1730
+ }
1731
+ return [...folders].filter(Boolean).toSorted();
1732
+ }
1733
+ export {
1734
+ icons
1735
+ };