@cad0p/napkin 0.8.1
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/LICENSE +21 -0
- package/README.md +342 -0
- package/dist/commands/aliases.d.ts +7 -0
- package/dist/commands/aliases.js +25 -0
- package/dist/commands/bases.d.ts +23 -0
- package/dist/commands/bases.js +139 -0
- package/dist/commands/bookmarks.d.ts +15 -0
- package/dist/commands/bookmarks.js +51 -0
- package/dist/commands/canvas.d.ts +49 -0
- package/dist/commands/canvas.js +186 -0
- package/dist/commands/config.d.ts +13 -0
- package/dist/commands/config.js +48 -0
- package/dist/commands/crud.d.ts +40 -0
- package/dist/commands/crud.js +195 -0
- package/dist/commands/daily.d.ts +20 -0
- package/dist/commands/daily.js +58 -0
- package/dist/commands/files.d.ts +23 -0
- package/dist/commands/files.js +132 -0
- package/dist/commands/graph.d.ts +4 -0
- package/dist/commands/graph.js +461 -0
- package/dist/commands/init.d.ts +7 -0
- package/dist/commands/init.js +52 -0
- package/dist/commands/links.d.ts +26 -0
- package/dist/commands/links.js +119 -0
- package/dist/commands/outline.d.ts +7 -0
- package/dist/commands/outline.js +48 -0
- package/dist/commands/overview.d.ts +6 -0
- package/dist/commands/overview.js +40 -0
- package/dist/commands/properties.d.ts +24 -0
- package/dist/commands/properties.js +115 -0
- package/dist/commands/search.d.ts +13 -0
- package/dist/commands/search.js +48 -0
- package/dist/commands/tags.d.ts +13 -0
- package/dist/commands/tags.js +51 -0
- package/dist/commands/tasks.d.ts +22 -0
- package/dist/commands/tasks.js +106 -0
- package/dist/commands/templates.d.ts +16 -0
- package/dist/commands/templates.js +70 -0
- package/dist/commands/vault.d.ts +4 -0
- package/dist/commands/vault.js +17 -0
- package/dist/commands/wordcount.d.ts +7 -0
- package/dist/commands/wordcount.js +43 -0
- package/dist/core/aliases.d.ts +5 -0
- package/dist/core/aliases.js +26 -0
- package/dist/core/bases.d.ts +29 -0
- package/dist/core/bases.js +67 -0
- package/dist/core/bookmarks.d.ts +14 -0
- package/dist/core/bookmarks.js +34 -0
- package/dist/core/canvas.d.ts +74 -0
- package/dist/core/canvas.js +125 -0
- package/dist/core/config.d.ts +7 -0
- package/dist/core/config.js +35 -0
- package/dist/core/crud.d.ts +32 -0
- package/dist/core/crud.js +119 -0
- package/dist/core/daily.d.ts +12 -0
- package/dist/core/daily.js +102 -0
- package/dist/core/files.d.ts +15 -0
- package/dist/core/files.js +30 -0
- package/dist/core/init.d.ts +31 -0
- package/dist/core/init.js +119 -0
- package/dist/core/links.d.ts +11 -0
- package/dist/core/links.js +66 -0
- package/dist/core/outline.d.ts +3 -0
- package/dist/core/outline.js +12 -0
- package/dist/core/overview.d.ts +15 -0
- package/dist/core/overview.js +384 -0
- package/dist/core/properties.d.ts +14 -0
- package/dist/core/properties.js +60 -0
- package/dist/core/search.d.ts +17 -0
- package/dist/core/search.js +153 -0
- package/dist/core/tags.d.ts +11 -0
- package/dist/core/tags.js +40 -0
- package/dist/core/tasks.d.ts +35 -0
- package/dist/core/tasks.js +97 -0
- package/dist/core/templates.d.ts +14 -0
- package/dist/core/templates.js +55 -0
- package/dist/core/vault.d.ts +10 -0
- package/dist/core/vault.js +37 -0
- package/dist/core/wordcount.d.ts +5 -0
- package/dist/core/wordcount.js +16 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +1 -0
- package/dist/main.d.ts +2 -0
- package/dist/main.js +715 -0
- package/dist/sdk.d.ts +179 -0
- package/dist/sdk.js +232 -0
- package/dist/templates/coding.d.ts +2 -0
- package/dist/templates/coding.js +104 -0
- package/dist/templates/company.d.ts +2 -0
- package/dist/templates/company.js +121 -0
- package/dist/templates/index.d.ts +4 -0
- package/dist/templates/index.js +15 -0
- package/dist/templates/personal.d.ts +2 -0
- package/dist/templates/personal.js +91 -0
- package/dist/templates/product.d.ts +2 -0
- package/dist/templates/product.js +123 -0
- package/dist/templates/research.d.ts +2 -0
- package/dist/templates/research.js +114 -0
- package/dist/templates/types.d.ts +7 -0
- package/dist/templates/types.js +1 -0
- package/dist/utils/bases.d.ts +61 -0
- package/dist/utils/bases.js +661 -0
- package/dist/utils/config.d.ts +42 -0
- package/dist/utils/config.js +112 -0
- package/dist/utils/exit-codes.d.ts +5 -0
- package/dist/utils/exit-codes.js +5 -0
- package/dist/utils/files.d.ts +135 -0
- package/dist/utils/files.js +299 -0
- package/dist/utils/formula.d.ts +28 -0
- package/dist/utils/formula.js +462 -0
- package/dist/utils/frontmatter.d.ts +17 -0
- package/dist/utils/frontmatter.js +34 -0
- package/dist/utils/markdown.d.ts +31 -0
- package/dist/utils/markdown.js +80 -0
- package/dist/utils/output.d.ts +28 -0
- package/dist/utils/output.js +48 -0
- package/dist/utils/search-cache.d.ts +29 -0
- package/dist/utils/search-cache.js +41 -0
- package/dist/utils/test-helpers.d.ts +13 -0
- package/dist/utils/test-helpers.js +40 -0
- package/dist/utils/vault.d.ts +21 -0
- package/dist/utils/vault.js +144 -0
- package/package.json +76 -0
|
@@ -0,0 +1,462 @@
|
|
|
1
|
+
import Jexl from "jexl";
|
|
2
|
+
// All known transforms (methods that Obsidian calls with dot syntax)
|
|
3
|
+
const TRANSFORMS = new Set([
|
|
4
|
+
// Any
|
|
5
|
+
"isTruthy",
|
|
6
|
+
"isType",
|
|
7
|
+
"toString",
|
|
8
|
+
// Number
|
|
9
|
+
"abs",
|
|
10
|
+
"ceil",
|
|
11
|
+
"floor",
|
|
12
|
+
"round",
|
|
13
|
+
"toFixed",
|
|
14
|
+
"isEmpty",
|
|
15
|
+
// String
|
|
16
|
+
"contains",
|
|
17
|
+
"containsAll",
|
|
18
|
+
"containsAny",
|
|
19
|
+
"startsWith",
|
|
20
|
+
"endsWith",
|
|
21
|
+
"lower",
|
|
22
|
+
"title",
|
|
23
|
+
"trim",
|
|
24
|
+
"replace",
|
|
25
|
+
"repeat",
|
|
26
|
+
"reverse",
|
|
27
|
+
"slice",
|
|
28
|
+
"split",
|
|
29
|
+
// Date
|
|
30
|
+
"format",
|
|
31
|
+
"date",
|
|
32
|
+
"time",
|
|
33
|
+
"relative",
|
|
34
|
+
// List
|
|
35
|
+
"filter",
|
|
36
|
+
"map",
|
|
37
|
+
"reduce",
|
|
38
|
+
"flat",
|
|
39
|
+
"join",
|
|
40
|
+
"sort",
|
|
41
|
+
"unique",
|
|
42
|
+
// File
|
|
43
|
+
"asLink",
|
|
44
|
+
"hasLink",
|
|
45
|
+
"hasTag",
|
|
46
|
+
"hasProperty",
|
|
47
|
+
"inFolder",
|
|
48
|
+
// Link
|
|
49
|
+
"asFile",
|
|
50
|
+
"linksTo",
|
|
51
|
+
// Object
|
|
52
|
+
"keys",
|
|
53
|
+
"values",
|
|
54
|
+
// Regex
|
|
55
|
+
"matches",
|
|
56
|
+
]);
|
|
57
|
+
/**
|
|
58
|
+
* Transform Obsidian expression syntax to jexl syntax.
|
|
59
|
+
* Converts .method(args) to |method(args) for known transforms.
|
|
60
|
+
* Also remaps if() to _if() since if is reserved.
|
|
61
|
+
*/
|
|
62
|
+
export function obsidianToJexl(expr) {
|
|
63
|
+
// Replace if( with _if( — but not inside strings
|
|
64
|
+
let result = expr;
|
|
65
|
+
// Handle if() function calls (not inside quotes)
|
|
66
|
+
result = result.replace(/\bif\s*\(/g, "_if(");
|
|
67
|
+
// Convert .method( to |method( for known transforms
|
|
68
|
+
// Must be careful not to convert property access like file.name
|
|
69
|
+
for (const t of TRANSFORMS) {
|
|
70
|
+
// Match .transform( but not when preceded by a quote (inside string)
|
|
71
|
+
const regex = new RegExp(`\\.${t}\\(`, "g");
|
|
72
|
+
result = result.replace(regex, `|${t}(`);
|
|
73
|
+
}
|
|
74
|
+
// Handle .length (property, not function call) — convert to |_length
|
|
75
|
+
result = result.replace(/\.length\b(?!\s*\()/g, "|_length");
|
|
76
|
+
// Handle .isEmpty() with no args — it's already converted above if it matches
|
|
77
|
+
// Handle .year, .month, .day, .hour, .minute, .second, .millisecond on dates
|
|
78
|
+
for (const field of [
|
|
79
|
+
"year",
|
|
80
|
+
"month",
|
|
81
|
+
"day",
|
|
82
|
+
"hour",
|
|
83
|
+
"minute",
|
|
84
|
+
"second",
|
|
85
|
+
"millisecond",
|
|
86
|
+
"days",
|
|
87
|
+
"hours",
|
|
88
|
+
"minutes",
|
|
89
|
+
"seconds",
|
|
90
|
+
"milliseconds",
|
|
91
|
+
]) {
|
|
92
|
+
const regex = new RegExp(`\\.${field}\\b(?!\\s*\\()`, "g");
|
|
93
|
+
result = result.replace(regex, `|_${field}`);
|
|
94
|
+
}
|
|
95
|
+
return result;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Create a configured jexl instance with all Obsidian Bases functions.
|
|
99
|
+
*/
|
|
100
|
+
export function createFormulaEngine() {
|
|
101
|
+
const jexl = new Jexl.Jexl();
|
|
102
|
+
// === Global functions ===
|
|
103
|
+
jexl.addFunction("_if", (cond, trueVal, falseVal) => cond ? trueVal : (falseVal ?? null));
|
|
104
|
+
jexl.addFunction("now", () => Date.now());
|
|
105
|
+
jexl.addFunction("today", () => {
|
|
106
|
+
const d = new Date();
|
|
107
|
+
d.setHours(0, 0, 0, 0);
|
|
108
|
+
return d.getTime();
|
|
109
|
+
});
|
|
110
|
+
jexl.addFunction("date", (s) => new Date(s).getTime());
|
|
111
|
+
jexl.addFunction("duration", (s) => parseDurationMs(s));
|
|
112
|
+
jexl.addFunction("min", (...args) => Math.min(...args));
|
|
113
|
+
jexl.addFunction("max", (...args) => Math.max(...args));
|
|
114
|
+
jexl.addFunction("number", (v) => {
|
|
115
|
+
if (typeof v === "boolean")
|
|
116
|
+
return v ? 1 : 0;
|
|
117
|
+
return Number(v);
|
|
118
|
+
});
|
|
119
|
+
jexl.addFunction("list", (v) => (Array.isArray(v) ? v : [v]));
|
|
120
|
+
jexl.addFunction("link", (path, display) => display || path);
|
|
121
|
+
jexl.addFunction("icon", (name) => `[${name}]`);
|
|
122
|
+
// === Number transforms ===
|
|
123
|
+
jexl.addTransform("abs", (v) => Math.abs(v));
|
|
124
|
+
jexl.addTransform("ceil", (v) => Math.ceil(v));
|
|
125
|
+
jexl.addTransform("floor", (v) => Math.floor(v));
|
|
126
|
+
jexl.addTransform("round", (v, digits) => {
|
|
127
|
+
const f = 10 ** (digits || 0);
|
|
128
|
+
return Math.round(v * f) / f;
|
|
129
|
+
});
|
|
130
|
+
jexl.addTransform("toFixed", (v, precision) => Number(v).toFixed(precision));
|
|
131
|
+
// === String transforms ===
|
|
132
|
+
jexl.addTransform("contains", (v, sub) => {
|
|
133
|
+
if (Array.isArray(v))
|
|
134
|
+
return v.includes(sub);
|
|
135
|
+
return String(v).includes(String(sub));
|
|
136
|
+
});
|
|
137
|
+
jexl.addTransform("containsAll", (v, ...subs) => {
|
|
138
|
+
if (Array.isArray(v))
|
|
139
|
+
return subs.every((s) => v.includes(s));
|
|
140
|
+
const s = String(v);
|
|
141
|
+
return subs.every((sub) => s.includes(String(sub)));
|
|
142
|
+
});
|
|
143
|
+
jexl.addTransform("containsAny", (v, ...subs) => {
|
|
144
|
+
if (Array.isArray(v))
|
|
145
|
+
return subs.some((s) => v.includes(s));
|
|
146
|
+
const s = String(v);
|
|
147
|
+
return subs.some((sub) => s.includes(String(sub)));
|
|
148
|
+
});
|
|
149
|
+
jexl.addTransform("startsWith", (v, q) => String(v).startsWith(q));
|
|
150
|
+
jexl.addTransform("endsWith", (v, q) => String(v).endsWith(q));
|
|
151
|
+
jexl.addTransform("lower", (v) => String(v).toLowerCase());
|
|
152
|
+
jexl.addTransform("title", (v) => String(v).replace(/\b\w/g, (c) => c.toUpperCase()));
|
|
153
|
+
jexl.addTransform("trim", (v) => String(v).trim());
|
|
154
|
+
jexl.addTransform("replace", (v, pat, rep) => String(v).replace(pat, rep));
|
|
155
|
+
jexl.addTransform("repeat", (v, n) => String(v).repeat(n));
|
|
156
|
+
jexl.addTransform("reverse", (v) => {
|
|
157
|
+
if (Array.isArray(v))
|
|
158
|
+
return [...v].reverse();
|
|
159
|
+
return String(v).split("").reverse().join("");
|
|
160
|
+
});
|
|
161
|
+
jexl.addTransform("slice", (v, start, end) => {
|
|
162
|
+
if (Array.isArray(v))
|
|
163
|
+
return v.slice(start, end);
|
|
164
|
+
return String(v).slice(start, end);
|
|
165
|
+
});
|
|
166
|
+
jexl.addTransform("split", (v, sep, n) => {
|
|
167
|
+
const parts = String(v).split(sep);
|
|
168
|
+
return n ? parts.slice(0, n) : parts;
|
|
169
|
+
});
|
|
170
|
+
jexl.addTransform("toString", (v) => String(v));
|
|
171
|
+
// === Date transforms ===
|
|
172
|
+
jexl.addTransform("format", (v, fmt) => {
|
|
173
|
+
const d = new Date(v);
|
|
174
|
+
return fmt
|
|
175
|
+
.replace("YYYY", String(d.getFullYear()))
|
|
176
|
+
.replace("MM", String(d.getMonth() + 1).padStart(2, "0"))
|
|
177
|
+
.replace("DD", String(d.getDate()).padStart(2, "0"))
|
|
178
|
+
.replace("HH", String(d.getHours()).padStart(2, "0"))
|
|
179
|
+
.replace("mm", String(d.getMinutes()).padStart(2, "0"))
|
|
180
|
+
.replace("ss", String(d.getSeconds()).padStart(2, "0"));
|
|
181
|
+
});
|
|
182
|
+
jexl.addTransform("date", (v) => {
|
|
183
|
+
const d = new Date(v);
|
|
184
|
+
d.setHours(0, 0, 0, 0);
|
|
185
|
+
return d.getTime();
|
|
186
|
+
});
|
|
187
|
+
jexl.addTransform("time", (v) => {
|
|
188
|
+
const d = new Date(v);
|
|
189
|
+
return `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}:${String(d.getSeconds()).padStart(2, "0")}`;
|
|
190
|
+
});
|
|
191
|
+
jexl.addTransform("relative", (v) => {
|
|
192
|
+
const diff = Date.now() - v;
|
|
193
|
+
const abs = Math.abs(diff);
|
|
194
|
+
const ago = diff > 0;
|
|
195
|
+
if (abs < 60000)
|
|
196
|
+
return "just now";
|
|
197
|
+
if (abs < 3600000) {
|
|
198
|
+
const m = Math.floor(abs / 60000);
|
|
199
|
+
return ago
|
|
200
|
+
? `${m} minute${m > 1 ? "s" : ""} ago`
|
|
201
|
+
: `in ${m} minute${m > 1 ? "s" : ""}`;
|
|
202
|
+
}
|
|
203
|
+
if (abs < 86400000) {
|
|
204
|
+
const h = Math.floor(abs / 3600000);
|
|
205
|
+
return ago
|
|
206
|
+
? `${h} hour${h > 1 ? "s" : ""} ago`
|
|
207
|
+
: `in ${h} hour${h > 1 ? "s" : ""}`;
|
|
208
|
+
}
|
|
209
|
+
const d = Math.floor(abs / 86400000);
|
|
210
|
+
return ago
|
|
211
|
+
? `${d} day${d > 1 ? "s" : ""} ago`
|
|
212
|
+
: `in ${d} day${d > 1 ? "s" : ""}`;
|
|
213
|
+
});
|
|
214
|
+
// === Date field transforms (act as property access) ===
|
|
215
|
+
jexl.addTransform("_year", (v) => new Date(v).getFullYear());
|
|
216
|
+
jexl.addTransform("_month", (v) => new Date(v).getMonth() + 1);
|
|
217
|
+
jexl.addTransform("_day", (v) => new Date(v).getDate());
|
|
218
|
+
jexl.addTransform("_hour", (v) => new Date(v).getHours());
|
|
219
|
+
jexl.addTransform("_minute", (v) => new Date(v).getMinutes());
|
|
220
|
+
jexl.addTransform("_second", (v) => new Date(v).getSeconds());
|
|
221
|
+
jexl.addTransform("_millisecond", (v) => new Date(v).getMilliseconds());
|
|
222
|
+
// === Duration field transforms ===
|
|
223
|
+
jexl.addTransform("_days", (v) => v / 86400000);
|
|
224
|
+
jexl.addTransform("_hours", (v) => v / 3600000);
|
|
225
|
+
jexl.addTransform("_minutes", (v) => v / 60000);
|
|
226
|
+
jexl.addTransform("_seconds", (v) => v / 1000);
|
|
227
|
+
jexl.addTransform("_milliseconds", (v) => v);
|
|
228
|
+
// === List transforms ===
|
|
229
|
+
jexl.addTransform("join", (v, sep) => Array.isArray(v) ? v.join(sep) : String(v));
|
|
230
|
+
jexl.addTransform("sort", (v) => Array.isArray(v) ? [...v].sort() : v);
|
|
231
|
+
jexl.addTransform("unique", (v) => Array.isArray(v) ? [...new Set(v)] : v);
|
|
232
|
+
jexl.addTransform("flat", (v) => Array.isArray(v) ? v.flat() : v);
|
|
233
|
+
// === Any transforms ===
|
|
234
|
+
jexl.addTransform("isEmpty", (v) => {
|
|
235
|
+
if (v === null || v === undefined || v === "")
|
|
236
|
+
return true;
|
|
237
|
+
if (Array.isArray(v))
|
|
238
|
+
return v.length === 0;
|
|
239
|
+
if (typeof v === "object")
|
|
240
|
+
return Object.keys(v).length === 0;
|
|
241
|
+
return false;
|
|
242
|
+
});
|
|
243
|
+
jexl.addTransform("isTruthy", (v) => !!v);
|
|
244
|
+
jexl.addTransform("isType", (v, type) => {
|
|
245
|
+
if (type === "string")
|
|
246
|
+
return typeof v === "string";
|
|
247
|
+
if (type === "number")
|
|
248
|
+
return typeof v === "number";
|
|
249
|
+
if (type === "boolean")
|
|
250
|
+
return typeof v === "boolean";
|
|
251
|
+
if (type === "list")
|
|
252
|
+
return Array.isArray(v);
|
|
253
|
+
return false;
|
|
254
|
+
});
|
|
255
|
+
jexl.addTransform("_length", (v) => {
|
|
256
|
+
if (typeof v === "string")
|
|
257
|
+
return v.length;
|
|
258
|
+
if (Array.isArray(v))
|
|
259
|
+
return v.length;
|
|
260
|
+
return 0;
|
|
261
|
+
});
|
|
262
|
+
// === Object transforms ===
|
|
263
|
+
jexl.addTransform("keys", (v) => v && typeof v === "object" && !Array.isArray(v) ? Object.keys(v) : []);
|
|
264
|
+
jexl.addTransform("values", (v) => v && typeof v === "object" && !Array.isArray(v) ? Object.values(v) : []);
|
|
265
|
+
// === File-like transforms (operate on context objects) ===
|
|
266
|
+
jexl.addTransform("hasTag", (file, ...tags) => {
|
|
267
|
+
if (!file?.tags)
|
|
268
|
+
return false;
|
|
269
|
+
return tags.some((t) => file.tags?.some((ft) => ft === t || ft.startsWith(`${t}/`)));
|
|
270
|
+
});
|
|
271
|
+
jexl.addTransform("hasLink", (file, target) => {
|
|
272
|
+
if (!file?.links)
|
|
273
|
+
return false;
|
|
274
|
+
return file.links.includes(target);
|
|
275
|
+
});
|
|
276
|
+
jexl.addTransform("hasProperty", (file, name) => {
|
|
277
|
+
if (!file?.properties)
|
|
278
|
+
return false;
|
|
279
|
+
return name in file.properties;
|
|
280
|
+
});
|
|
281
|
+
jexl.addTransform("inFolder", (file, folder) => {
|
|
282
|
+
if (!file?.folder)
|
|
283
|
+
return false;
|
|
284
|
+
return file.folder === folder || file.folder.startsWith(`${folder}/`);
|
|
285
|
+
});
|
|
286
|
+
jexl.addTransform("asLink", (file, display) => display || file?.name || "");
|
|
287
|
+
// === Regex ===
|
|
288
|
+
jexl.addTransform("matches", (pattern, target) => {
|
|
289
|
+
try {
|
|
290
|
+
return new RegExp(pattern).test(target);
|
|
291
|
+
}
|
|
292
|
+
catch {
|
|
293
|
+
return false;
|
|
294
|
+
}
|
|
295
|
+
});
|
|
296
|
+
return jexl;
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* Build a context object for formula evaluation from a database row.
|
|
300
|
+
*/
|
|
301
|
+
export function buildFormulaContext(columns, row, formulaResults = {}, thisFile) {
|
|
302
|
+
const ctx = {};
|
|
303
|
+
const file = {};
|
|
304
|
+
const note = {};
|
|
305
|
+
for (let i = 0; i < columns.length; i++) {
|
|
306
|
+
const col = columns[i];
|
|
307
|
+
const val = row[i];
|
|
308
|
+
// File metadata columns
|
|
309
|
+
if ([
|
|
310
|
+
"path",
|
|
311
|
+
"name",
|
|
312
|
+
"basename",
|
|
313
|
+
"folder",
|
|
314
|
+
"ext",
|
|
315
|
+
"size",
|
|
316
|
+
"ctime",
|
|
317
|
+
"mtime",
|
|
318
|
+
].includes(col)) {
|
|
319
|
+
file[col] = val;
|
|
320
|
+
}
|
|
321
|
+
else if (col === "tags") {
|
|
322
|
+
try {
|
|
323
|
+
file.tags = JSON.parse(val);
|
|
324
|
+
}
|
|
325
|
+
catch {
|
|
326
|
+
file.tags = [];
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
else if (col === "links") {
|
|
330
|
+
try {
|
|
331
|
+
file.links = JSON.parse(val);
|
|
332
|
+
}
|
|
333
|
+
catch {
|
|
334
|
+
file.links = [];
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
else if (col === "backlinks") {
|
|
338
|
+
try {
|
|
339
|
+
file.backlinks = JSON.parse(val);
|
|
340
|
+
}
|
|
341
|
+
catch {
|
|
342
|
+
file.backlinks = [];
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
else if (col === "embeds") {
|
|
346
|
+
try {
|
|
347
|
+
file.embeds = JSON.parse(val);
|
|
348
|
+
}
|
|
349
|
+
catch {
|
|
350
|
+
file.embeds = [];
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
else if (col === "file_properties") {
|
|
354
|
+
try {
|
|
355
|
+
file.properties = JSON.parse(val);
|
|
356
|
+
}
|
|
357
|
+
catch {
|
|
358
|
+
file.properties = {};
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
else {
|
|
362
|
+
// Frontmatter properties (already stripped of prop_ prefix by queryBase)
|
|
363
|
+
// Try to parse JSON values (lists, objects)
|
|
364
|
+
let parsed = val;
|
|
365
|
+
if (typeof val === "string") {
|
|
366
|
+
try {
|
|
367
|
+
const p = JSON.parse(val);
|
|
368
|
+
if (typeof p === "object")
|
|
369
|
+
parsed = p;
|
|
370
|
+
}
|
|
371
|
+
catch {
|
|
372
|
+
/* keep as string */
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
note[col] = parsed;
|
|
376
|
+
ctx[col] = parsed; // bare property access shorthand
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
// Add formula results
|
|
380
|
+
const formula = { ...formulaResults };
|
|
381
|
+
ctx.formula = formula;
|
|
382
|
+
ctx.file = file;
|
|
383
|
+
ctx.note = note;
|
|
384
|
+
if (thisFile)
|
|
385
|
+
ctx.this = { file: thisFile };
|
|
386
|
+
return ctx;
|
|
387
|
+
}
|
|
388
|
+
/**
|
|
389
|
+
* Evaluate all formulas for a single row.
|
|
390
|
+
* Handles formula dependencies (formula referencing another formula).
|
|
391
|
+
*/
|
|
392
|
+
export async function evaluateFormulas(engine, formulas, columns, row, thisFile) {
|
|
393
|
+
const results = {};
|
|
394
|
+
const remaining = { ...formulas };
|
|
395
|
+
let iterations = 0;
|
|
396
|
+
const maxIterations = Object.keys(formulas).length + 1;
|
|
397
|
+
while (Object.keys(remaining).length > 0 && iterations < maxIterations) {
|
|
398
|
+
iterations++;
|
|
399
|
+
let resolved = false;
|
|
400
|
+
for (const [name, expr] of Object.entries(remaining)) {
|
|
401
|
+
// Check if this formula depends on unresolved formulas
|
|
402
|
+
const deps = Object.keys(remaining).filter((k) => k !== name && expr.includes(`formula.${k}`));
|
|
403
|
+
if (deps.length > 0)
|
|
404
|
+
continue;
|
|
405
|
+
const ctx = buildFormulaContext(columns, row, results, thisFile);
|
|
406
|
+
try {
|
|
407
|
+
const jexlExpr = obsidianToJexl(expr);
|
|
408
|
+
results[name] = await engine.eval(jexlExpr, ctx);
|
|
409
|
+
}
|
|
410
|
+
catch {
|
|
411
|
+
results[name] = null;
|
|
412
|
+
}
|
|
413
|
+
delete remaining[name];
|
|
414
|
+
resolved = true;
|
|
415
|
+
}
|
|
416
|
+
if (!resolved)
|
|
417
|
+
break; // Circular dependency, bail
|
|
418
|
+
}
|
|
419
|
+
// Any remaining (circular deps) get null
|
|
420
|
+
for (const name of Object.keys(remaining)) {
|
|
421
|
+
results[name] = null;
|
|
422
|
+
}
|
|
423
|
+
return results;
|
|
424
|
+
}
|
|
425
|
+
function parseDurationMs(dur) {
|
|
426
|
+
const match = dur.match(/^(\d+)\s*(y|year|years|M|month|months|d|day|days|w|week|weeks|h|hour|hours|m|minute|minutes|s|second|seconds)$/);
|
|
427
|
+
if (!match)
|
|
428
|
+
return 0;
|
|
429
|
+
const n = Number.parseInt(match[1], 10);
|
|
430
|
+
switch (match[2]) {
|
|
431
|
+
case "y":
|
|
432
|
+
case "year":
|
|
433
|
+
case "years":
|
|
434
|
+
return n * 365.25 * 24 * 60 * 60 * 1000;
|
|
435
|
+
case "M":
|
|
436
|
+
case "month":
|
|
437
|
+
case "months":
|
|
438
|
+
return n * 30.44 * 24 * 60 * 60 * 1000;
|
|
439
|
+
case "w":
|
|
440
|
+
case "week":
|
|
441
|
+
case "weeks":
|
|
442
|
+
return n * 7 * 24 * 60 * 60 * 1000;
|
|
443
|
+
case "d":
|
|
444
|
+
case "day":
|
|
445
|
+
case "days":
|
|
446
|
+
return n * 24 * 60 * 60 * 1000;
|
|
447
|
+
case "h":
|
|
448
|
+
case "hour":
|
|
449
|
+
case "hours":
|
|
450
|
+
return n * 60 * 60 * 1000;
|
|
451
|
+
case "m":
|
|
452
|
+
case "minute":
|
|
453
|
+
case "minutes":
|
|
454
|
+
return n * 60 * 1000;
|
|
455
|
+
case "s":
|
|
456
|
+
case "second":
|
|
457
|
+
case "seconds":
|
|
458
|
+
return n * 1000;
|
|
459
|
+
default:
|
|
460
|
+
return 0;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export interface ParsedFrontmatter {
|
|
2
|
+
properties: Record<string, unknown>;
|
|
3
|
+
body: string;
|
|
4
|
+
raw: string;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Parse YAML frontmatter from markdown content.
|
|
8
|
+
*/
|
|
9
|
+
export declare function parseFrontmatter(content: string): ParsedFrontmatter;
|
|
10
|
+
/**
|
|
11
|
+
* Set a property in frontmatter, creating the --- block if needed.
|
|
12
|
+
*/
|
|
13
|
+
export declare function setProperty(content: string, name: string, value: unknown): string;
|
|
14
|
+
/**
|
|
15
|
+
* Remove a property from frontmatter.
|
|
16
|
+
*/
|
|
17
|
+
export declare function removeProperty(content: string, name: string): string;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import matter from "gray-matter";
|
|
2
|
+
/**
|
|
3
|
+
* Parse YAML frontmatter from markdown content.
|
|
4
|
+
*/
|
|
5
|
+
export function parseFrontmatter(content) {
|
|
6
|
+
const result = matter(content);
|
|
7
|
+
return {
|
|
8
|
+
properties: result.data,
|
|
9
|
+
body: result.content,
|
|
10
|
+
raw: result.matter,
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Set a property in frontmatter, creating the --- block if needed.
|
|
15
|
+
*/
|
|
16
|
+
export function setProperty(content, name, value) {
|
|
17
|
+
const result = matter(content);
|
|
18
|
+
const data = { ...result.data, [name]: value };
|
|
19
|
+
return matter.stringify(result.content, data);
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Remove a property from frontmatter.
|
|
23
|
+
*/
|
|
24
|
+
export function removeProperty(content, name) {
|
|
25
|
+
const result = matter(content);
|
|
26
|
+
const data = { ...result.data };
|
|
27
|
+
delete data[name];
|
|
28
|
+
// If no properties left, return just the body
|
|
29
|
+
if (Object.keys(data).length === 0) {
|
|
30
|
+
const body = result.content;
|
|
31
|
+
return body.startsWith("\n") ? body.slice(1) : body;
|
|
32
|
+
}
|
|
33
|
+
return matter.stringify(result.content, data);
|
|
34
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export interface Heading {
|
|
2
|
+
level: number;
|
|
3
|
+
text: string;
|
|
4
|
+
line: number;
|
|
5
|
+
}
|
|
6
|
+
export interface Task {
|
|
7
|
+
line: number;
|
|
8
|
+
status: string;
|
|
9
|
+
text: string;
|
|
10
|
+
done: boolean;
|
|
11
|
+
}
|
|
12
|
+
export interface LinkInfo {
|
|
13
|
+
outgoing: string[];
|
|
14
|
+
wikilinks: string[];
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Extract headings from markdown content.
|
|
18
|
+
*/
|
|
19
|
+
export declare function extractHeadings(content: string): Heading[];
|
|
20
|
+
/**
|
|
21
|
+
* Extract tasks (checkboxes) from markdown content.
|
|
22
|
+
*/
|
|
23
|
+
export declare function extractTasks(content: string): Task[];
|
|
24
|
+
/**
|
|
25
|
+
* Extract tags from markdown content (both inline #tags and frontmatter tags).
|
|
26
|
+
*/
|
|
27
|
+
export declare function extractTags(content: string): string[];
|
|
28
|
+
/**
|
|
29
|
+
* Extract links from markdown content.
|
|
30
|
+
*/
|
|
31
|
+
export declare function extractLinks(content: string): LinkInfo;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extract headings from markdown content.
|
|
3
|
+
*/
|
|
4
|
+
export function extractHeadings(content) {
|
|
5
|
+
const headings = [];
|
|
6
|
+
const lines = content.split("\n");
|
|
7
|
+
for (let i = 0; i < lines.length; i++) {
|
|
8
|
+
const match = lines[i].match(/^(#{1,6})\s+(.+)$/);
|
|
9
|
+
if (match) {
|
|
10
|
+
headings.push({
|
|
11
|
+
level: match[1].length,
|
|
12
|
+
text: match[2].trim(),
|
|
13
|
+
line: i + 1,
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return headings;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Extract tasks (checkboxes) from markdown content.
|
|
21
|
+
*/
|
|
22
|
+
export function extractTasks(content) {
|
|
23
|
+
const tasks = [];
|
|
24
|
+
const lines = content.split("\n");
|
|
25
|
+
for (let i = 0; i < lines.length; i++) {
|
|
26
|
+
const match = lines[i].match(/^[\s]*[-*]\s+\[(.)\]\s+(.*)$/);
|
|
27
|
+
if (match) {
|
|
28
|
+
const status = match[1];
|
|
29
|
+
tasks.push({
|
|
30
|
+
line: i + 1,
|
|
31
|
+
status,
|
|
32
|
+
text: match[2].trim(),
|
|
33
|
+
done: status === "x" || status === "X",
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return tasks;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Extract tags from markdown content (both inline #tags and frontmatter tags).
|
|
41
|
+
*/
|
|
42
|
+
export function extractTags(content) {
|
|
43
|
+
const tags = new Set();
|
|
44
|
+
// Inline tags: #tag (not inside code blocks or links)
|
|
45
|
+
const tagRegex = /(?:^|\s)#([a-zA-Z][\w/-]*)/g;
|
|
46
|
+
for (let match = tagRegex.exec(content); match !== null; match = tagRegex.exec(content)) {
|
|
47
|
+
tags.add(match[1]);
|
|
48
|
+
}
|
|
49
|
+
return [...tags].sort();
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Extract links from markdown content.
|
|
53
|
+
*/
|
|
54
|
+
export function extractLinks(content) {
|
|
55
|
+
const wikilinks = [];
|
|
56
|
+
const outgoing = [];
|
|
57
|
+
// Wikilinks: [[target]] or [[target|alias]]
|
|
58
|
+
const wikiRegex = /\[\[([^\]|]+)(?:\|[^\]]+)?\]\]/g;
|
|
59
|
+
for (let match = wikiRegex.exec(content); match !== null; match = wikiRegex.exec(content)) {
|
|
60
|
+
const target = match[1].trim();
|
|
61
|
+
// Strip heading/block refs
|
|
62
|
+
const clean = target.split("#")[0].trim();
|
|
63
|
+
if (clean) {
|
|
64
|
+
wikilinks.push(clean);
|
|
65
|
+
outgoing.push(clean);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
// Markdown links: [text](url)
|
|
69
|
+
const mdRegex = /\[([^\]]*)\]\(([^)]+)\)/g;
|
|
70
|
+
for (let match = mdRegex.exec(content); match !== null; match = mdRegex.exec(content)) {
|
|
71
|
+
const url = match[2].trim();
|
|
72
|
+
// Only internal links (not http/https/mailto)
|
|
73
|
+
if (!url.match(/^(https?|mailto|obsidian):\/\//)) {
|
|
74
|
+
const clean = decodeURIComponent(url.split("#")[0].trim());
|
|
75
|
+
if (clean)
|
|
76
|
+
outgoing.push(clean);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return { outgoing, wikilinks };
|
|
80
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export interface OutputOptions {
|
|
2
|
+
json?: boolean;
|
|
3
|
+
quiet?: boolean;
|
|
4
|
+
}
|
|
5
|
+
export declare const success: (msg: string) => void;
|
|
6
|
+
export declare const info: (msg: string) => void;
|
|
7
|
+
export declare const warn: (msg: string) => void;
|
|
8
|
+
export declare const error: (msg: string) => void;
|
|
9
|
+
export declare const errorWithHint: (msg: string, hint: string) => void;
|
|
10
|
+
/**
|
|
11
|
+
* Standard "file not found" error with suggestions.
|
|
12
|
+
* Import suggestFile where needed and pass results here.
|
|
13
|
+
*/
|
|
14
|
+
export declare function fileNotFound(ref: string, suggestions?: string[]): void;
|
|
15
|
+
export declare const bold: (s: string) => string;
|
|
16
|
+
export declare const dim: (s: string) => string;
|
|
17
|
+
export declare const cmd: (s: string) => string;
|
|
18
|
+
export declare const bullet: (msg: string) => void;
|
|
19
|
+
export declare const bulletDim: (msg: string) => void;
|
|
20
|
+
export declare const hint: (msg: string) => void;
|
|
21
|
+
export declare const nextStep: (command: string) => void;
|
|
22
|
+
export declare const header: (title: string) => void;
|
|
23
|
+
export declare function jsonOutput(data: object): void;
|
|
24
|
+
export declare function output(options: OutputOptions, handlers: {
|
|
25
|
+
json?: () => object;
|
|
26
|
+
quiet?: () => void;
|
|
27
|
+
human: () => void;
|
|
28
|
+
}): void;
|