@bitcobblers/wod-wiki-wql 0.1.0
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.cjs +1956 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +896 -0
- package/dist/index.d.ts +896 -0
- package/dist/index.mjs +1882 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +70 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,1956 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var lr = require('@lezer/lr');
|
|
4
|
+
var language = require('@codemirror/language');
|
|
5
|
+
var autocomplete = require('@codemirror/autocomplete');
|
|
6
|
+
var highlight = require('@lezer/highlight');
|
|
7
|
+
var wodWikiCore = require('@bitcobblers/wod-wiki-core');
|
|
8
|
+
|
|
9
|
+
var __defProp = Object.defineProperty;
|
|
10
|
+
var __export = (target, all) => {
|
|
11
|
+
for (var name in all)
|
|
12
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
// src/vocabulary.ts
|
|
16
|
+
var WQL_METRIC_FAMILIES = ["reps", "distance", "resistance", "elapsed", "power", "pace"];
|
|
17
|
+
var WQL_METRIC_AGGREGATES = ["totalVolume", "totalDistance", "tis", "sessionLoad"];
|
|
18
|
+
var WQL_AGGREGATORS = ["sum", "avg", "min", "max", "count", "last", "delta"];
|
|
19
|
+
var WQL_COMPARISON_OPS = [">", ">=", "<", "<=", "==", "!="];
|
|
20
|
+
var WQL_TAG_KEYS = [
|
|
21
|
+
"effort",
|
|
22
|
+
"discipline",
|
|
23
|
+
"intensity",
|
|
24
|
+
"note",
|
|
25
|
+
"page",
|
|
26
|
+
"origin",
|
|
27
|
+
"grain",
|
|
28
|
+
"metric",
|
|
29
|
+
"block",
|
|
30
|
+
"result",
|
|
31
|
+
"tags"
|
|
32
|
+
];
|
|
33
|
+
var WQL_VIRTUAL_DIMS = ["day", "week", "session", "round"];
|
|
34
|
+
var WQL_CALC_TARGETS = [
|
|
35
|
+
"calc.metMinutes",
|
|
36
|
+
"calc.acwr",
|
|
37
|
+
"calc.monotony",
|
|
38
|
+
"calc.strain",
|
|
39
|
+
"calc.e1rm",
|
|
40
|
+
"calc.ctl",
|
|
41
|
+
"calc.atl",
|
|
42
|
+
"calc.tsb",
|
|
43
|
+
// Wellness + capture metrics (seeds in seeds.ts): soreness/sleep/hrv are
|
|
44
|
+
// user-captured passthroughs; readiness is their composite; mvcBw is
|
|
45
|
+
// hang/%BW; ef is running pace ÷ HR; adherence is sessions ÷ planned;
|
|
46
|
+
// pct1rm is the Epley-inverse intensity; sends count climbing sends.
|
|
47
|
+
"calc.soreness",
|
|
48
|
+
"calc.sleep",
|
|
49
|
+
"calc.hrv",
|
|
50
|
+
"calc.readiness",
|
|
51
|
+
"calc.mvcBw",
|
|
52
|
+
"calc.ef",
|
|
53
|
+
"calc.adherence",
|
|
54
|
+
"calc.pct1rm",
|
|
55
|
+
"calc.sends"
|
|
56
|
+
];
|
|
57
|
+
var WQL_INTENSITY_TIERS = ["low", "moderate", "high"];
|
|
58
|
+
var WQL_GRAINS = ["segment", "summary", "rollup"];
|
|
59
|
+
var WQL_FIND_TARGETS = ["note", "block", "effort"];
|
|
60
|
+
var WQL_SCOPES = ["journal", "collections", "feeds", "all"];
|
|
61
|
+
var WQL_CONTENT_FILTER_KEYS = ["type", "text", "has", "source", "catalog", ...WQL_TAG_KEYS];
|
|
62
|
+
var WQL_SOURCES = ["journal", "collections", "feeds", "notes", "blocks", "efforts", "metrics"];
|
|
63
|
+
var WQL_ROLLUP_PERIODS = ["1d", "1w"];
|
|
64
|
+
var WQL_DISPLAY_UNITS = ["kg", "lb"];
|
|
65
|
+
var WQL_KEYWORDS = [
|
|
66
|
+
"find",
|
|
67
|
+
"rows",
|
|
68
|
+
"where",
|
|
69
|
+
"group",
|
|
70
|
+
"by",
|
|
71
|
+
"order",
|
|
72
|
+
"limit",
|
|
73
|
+
"sum",
|
|
74
|
+
"avg",
|
|
75
|
+
"min",
|
|
76
|
+
"max",
|
|
77
|
+
"count",
|
|
78
|
+
"last",
|
|
79
|
+
"delta"
|
|
80
|
+
];
|
|
81
|
+
var parser = lr.LRParser.deserialize({
|
|
82
|
+
version: 14,
|
|
83
|
+
states: "(jOVQPOOOOQO'#C_'#C_O[QPO'#C^QaQQOOOlQPO,58xOqQPO'#CbO|QPO'#CjO!RQPO'#CmQOQPOOQ!WQPOOQ!]QQOOO!eQQO'#CaOOQO1G.d1G.dOOQO'#Ce'#CeO!vQPO'#CcO!{QPO'#CcOOQO,58|,58|O#QQPO,58|O#YQPO,59UO#bQQO,59XO#gQPO'#CpO#lQQO,58{O#}QPO,58}O$VQPO,58}O$[QPO'#CrO$dQPO1G.hOOQO1G.h1G.hOOQO'#Cl'#ClOOQO1G.p1G.pO$lQPO1G.pO$tQPO1G.sOOQO,59[,59[OOQO-E6n-E6nO$yQPO'#CgO%[QPO'#CgO%jQPO'#CfOOQO1G.i1G.iO#}QPO1G.iOOQO,59^,59^OOQO-E6p-E6pOOQO7+$S7+$SO%uQPO'#CsO%zQPO7+$[OOQO7+$[7+$[O&SQPO7+$_OOQO,59R,59RO&XQPO,59RO#}QPO'#CqO&^QPO,59QOOQO7+$T7+$TOOQO,59_,59_OOQO-E6q-E6qOOQO<<Gv<<GvOOQO<<Gy<<GyO&iQPO1G.mOOQO,59],59]OOQO-E6o-E6oOOQO7+$X7+$X",
|
|
84
|
+
stateData: "&w~OjOS~OSPO~OkSO~O_UObVOmTO~OSZO~OS]OW_Op`O~OmbO~OqcO~ObVO~O_UObVO~OldO_TXbTXhTXmTX~OkfO~OS]O~OohOpjO~OSkOplO~OcnO~OSoO~OldO_TabTahTamTa~OSqO]rO~OkuO~OS]OW_O~OohOpxO~OoyOp{O~OS|O~O[}Ok!OOnZXoZXpZX~O[}OnZXoZXpZX~On!POoYXpYX~OSkO~OoyOp!UO~Or!VO~OS!WO~On!POoYapYa~O[!ZOnZioZipZi~O",
|
|
85
|
+
goto: "#YhPPilPoruP{!S!YPP!aP!e!kPP!p!v!|#SRRORQOR[SRYRQaTRvhS^ThRg_QtfR!RuSsfuR!X!PTXRYQmbR!SyVWRXYQeZRpeQ!QsR!Y!QQiaRwiQzmR!Tz",
|
|
86
|
+
nodeNames: "\u26A0 Query Head Aggregator Word Metric Filters Filter Negate TagKey TagValue Value Star Quoted GroupBy By Dimension Rollup RollupDot Int",
|
|
87
|
+
maxTerm: 34,
|
|
88
|
+
skippedNodes: [0],
|
|
89
|
+
repeatNodeCount: 4,
|
|
90
|
+
tokenData: "'a~RfXY!gYZ!g]^!gpq!gqr!xrs!}xy#lyz#qz{#v|}#{}!O$Q!O!P$f!Q![%c![!]%y!c!}$Q#R#S$Q#T#U$Q#U#V&O#V#o$Q#o#p'Q#p#q'V#q#r'[~!lSj~XY!gYZ!g]^!gpq!g~!}OW~~#QTOr!}rs#as;'S!};'S;=`#f<%lO!}~#fO]~~#iP;=`<%l!}~#qOq~~#vOr~~#{O[~~$QOo~P$VTSP}!O$Q!Q![$Q!c!}$Q#R#S$Q#T#o$Q~$kPl~#f#g$n~$qP#c#d$t~$wP#`#a$z~$}P#`#a%Q~%TP#i#j%W~%ZP#d#e%^~%cOb~R%jTcQSP}!O$Q!Q![%c!c!}$Q#R#S$Q#T#o$Q~&OOk~R&TVSP}!O$Q!Q![$Q!c!}$Q#R#S$Q#T#m$Q#m#n&j#n#o$QR&qT_QSP}!O$Q!Q![$Q!c!}$Q#R#S$Q#T#o$Q~'VOm~~'[On~~'aOp~",
|
|
91
|
+
tokenizers: [0, 1],
|
|
92
|
+
topRules: { "Query": [0, 1] },
|
|
93
|
+
tokenPrec: 0
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
// src/grammar/wql.parser.terms.ts
|
|
97
|
+
var wql_parser_terms_exports = {};
|
|
98
|
+
__export(wql_parser_terms_exports, {
|
|
99
|
+
Aggregator: () => Aggregator,
|
|
100
|
+
By: () => By,
|
|
101
|
+
Dimension: () => Dimension,
|
|
102
|
+
Filter: () => Filter,
|
|
103
|
+
Filters: () => Filters,
|
|
104
|
+
GroupBy: () => GroupBy,
|
|
105
|
+
Head: () => Head,
|
|
106
|
+
Int: () => Int,
|
|
107
|
+
Metric: () => Metric,
|
|
108
|
+
Negate: () => Negate,
|
|
109
|
+
Query: () => Query,
|
|
110
|
+
Quoted: () => Quoted,
|
|
111
|
+
Rollup: () => Rollup,
|
|
112
|
+
RollupDot: () => RollupDot,
|
|
113
|
+
Star: () => Star,
|
|
114
|
+
TagKey: () => TagKey,
|
|
115
|
+
TagValue: () => TagValue,
|
|
116
|
+
Value: () => Value,
|
|
117
|
+
Word: () => Word
|
|
118
|
+
});
|
|
119
|
+
var Query = 1;
|
|
120
|
+
var Head = 2;
|
|
121
|
+
var Aggregator = 3;
|
|
122
|
+
var Word = 4;
|
|
123
|
+
var Metric = 5;
|
|
124
|
+
var Filters = 6;
|
|
125
|
+
var Filter = 7;
|
|
126
|
+
var Negate = 8;
|
|
127
|
+
var TagKey = 9;
|
|
128
|
+
var TagValue = 10;
|
|
129
|
+
var Value = 11;
|
|
130
|
+
var Star = 12;
|
|
131
|
+
var Quoted = 13;
|
|
132
|
+
var GroupBy = 14;
|
|
133
|
+
var By = 15;
|
|
134
|
+
var Dimension = 16;
|
|
135
|
+
var Rollup = 17;
|
|
136
|
+
var RollupDot = 18;
|
|
137
|
+
var Int = 19;
|
|
138
|
+
|
|
139
|
+
// src/wqlSuffix.ts
|
|
140
|
+
function splitAtWhere(text) {
|
|
141
|
+
let depth = 0;
|
|
142
|
+
for (let i = 0; i < text.length; i++) {
|
|
143
|
+
const c = text[i];
|
|
144
|
+
if (c === "{") depth++;
|
|
145
|
+
else if (c === "}") depth = Math.max(0, depth - 1);
|
|
146
|
+
else if (depth === 0 && c === "w" && text.slice(i, i + 5) === "where" && (i === 0 || /\s/.test(text[i - 1])) && (i + 5 >= text.length || /\s/.test(text[i + 5]))) {
|
|
147
|
+
return { primary: text.slice(0, i).trim(), where: text.slice(i + 5).trim() };
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return { primary: text.trim() };
|
|
151
|
+
}
|
|
152
|
+
var DISPLAY_UNIT_RE = /\s+in\s+([a-zA-Z0-9_-]+)\s*$/;
|
|
153
|
+
var ROLLUP_RE = /\.rollup\((\d+)?([a-zA-Z]*)\)\s*$/;
|
|
154
|
+
var BY_RE = /\s+by\s+\{([^}]*)\}\s*$/;
|
|
155
|
+
var LAST_RE = /\s+last\s+(\d+)([dw])\s*$/i;
|
|
156
|
+
var IN_SCOPE_RE = /\s+in\s+(\w+)\s*$/;
|
|
157
|
+
function parseWqlSuffixes(raw) {
|
|
158
|
+
const { primary, where } = splitAtWhere(raw);
|
|
159
|
+
let text = primary.trim();
|
|
160
|
+
let displayUnit;
|
|
161
|
+
let rollup;
|
|
162
|
+
let groupBy;
|
|
163
|
+
let last;
|
|
164
|
+
let scope;
|
|
165
|
+
const isFind = text.startsWith("find:");
|
|
166
|
+
const isRows = /^rows(?=[:{]|\s|$)/.test(text);
|
|
167
|
+
if (isFind || isRows) {
|
|
168
|
+
const lastMatch = LAST_RE.exec(text);
|
|
169
|
+
if (lastMatch) {
|
|
170
|
+
last = {
|
|
171
|
+
size: parseInt(lastMatch[1], 10),
|
|
172
|
+
unit: lastMatch[2].toLowerCase(),
|
|
173
|
+
raw: lastMatch[0].trim()
|
|
174
|
+
};
|
|
175
|
+
text = text.slice(0, lastMatch.index).trim();
|
|
176
|
+
}
|
|
177
|
+
if (isFind) {
|
|
178
|
+
const scopeMatch = IN_SCOPE_RE.exec(text);
|
|
179
|
+
if (scopeMatch) {
|
|
180
|
+
scope = scopeMatch[1];
|
|
181
|
+
text = text.slice(0, scopeMatch.index).trim();
|
|
182
|
+
}
|
|
183
|
+
} else {
|
|
184
|
+
const rollupMatch = ROLLUP_RE.exec(text);
|
|
185
|
+
if (rollupMatch) {
|
|
186
|
+
const rawRollup = rollupMatch[1] + rollupMatch[2];
|
|
187
|
+
rollup = { size: rollupMatch[1] ? parseInt(rollupMatch[1], 10) : 1, unit: rollupMatch[2] || "", raw: rawRollup };
|
|
188
|
+
text = text.slice(0, rollupMatch.index).trim();
|
|
189
|
+
}
|
|
190
|
+
const byMatch = BY_RE.exec(text);
|
|
191
|
+
if (byMatch) {
|
|
192
|
+
groupBy = byMatch[1].split(",").map((d) => d.trim()).filter(Boolean);
|
|
193
|
+
text = text.slice(0, byMatch.index).trim();
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
} else {
|
|
197
|
+
const unitMatch = DISPLAY_UNIT_RE.exec(text);
|
|
198
|
+
if (unitMatch) {
|
|
199
|
+
displayUnit = unitMatch[1];
|
|
200
|
+
text = text.slice(0, unitMatch.index).trim();
|
|
201
|
+
}
|
|
202
|
+
const rollupMatch = ROLLUP_RE.exec(text);
|
|
203
|
+
if (rollupMatch) {
|
|
204
|
+
const rawRollup = rollupMatch[1] + rollupMatch[2];
|
|
205
|
+
const size = rollupMatch[1] ? parseInt(rollupMatch[1], 10) : 1;
|
|
206
|
+
const unit = rollupMatch[2] || "";
|
|
207
|
+
rollup = { size, unit, raw: rawRollup };
|
|
208
|
+
text = text.slice(0, rollupMatch.index).trim();
|
|
209
|
+
}
|
|
210
|
+
const byMatch = BY_RE.exec(text);
|
|
211
|
+
if (byMatch) {
|
|
212
|
+
groupBy = byMatch[1].split(",").map((d) => d.trim()).filter(Boolean);
|
|
213
|
+
text = text.slice(0, byMatch.index).trim();
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return {
|
|
217
|
+
where,
|
|
218
|
+
displayUnit,
|
|
219
|
+
rollup,
|
|
220
|
+
groupBy,
|
|
221
|
+
last,
|
|
222
|
+
scope,
|
|
223
|
+
primaryText: text
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// src/wql.ts
|
|
228
|
+
function isFindQuery(parsed) {
|
|
229
|
+
return "target" in parsed;
|
|
230
|
+
}
|
|
231
|
+
function isRowsQuery(parsed) {
|
|
232
|
+
return parsed.family === "rows";
|
|
233
|
+
}
|
|
234
|
+
var AGGS = WQL_AGGREGATORS;
|
|
235
|
+
function cannotParse(text) {
|
|
236
|
+
return `Cannot parse "${text}". Expected agg:metric{filters} by {dims} .rollup(period)`;
|
|
237
|
+
}
|
|
238
|
+
var CMP_RE = /^(.+?)\s*(>=|<=|!=|==|>|<)\s*(-?\d+(?:\.\d+)?)\s*$/;
|
|
239
|
+
function cannotParseJoin(text) {
|
|
240
|
+
return `Cannot parse join "${text}". Expected find:target{filters} or agg:metric{filters} <op> <number>`;
|
|
241
|
+
}
|
|
242
|
+
function parseJoinClause(where) {
|
|
243
|
+
if (where.trimStart().startsWith("find:")) {
|
|
244
|
+
const fp = parseFindQuery(where);
|
|
245
|
+
if (fp.error) return { error: fp.error };
|
|
246
|
+
return { find: { target: fp.target, filters: fp.filters, scope: fp.scope, last: fp.last } };
|
|
247
|
+
}
|
|
248
|
+
const m = CMP_RE.exec(where.trim());
|
|
249
|
+
if (!m) return { error: cannotParseJoin(where) };
|
|
250
|
+
const head = parseAnalyticsQuery(m[1].trim());
|
|
251
|
+
if (head.error) return { error: head.error };
|
|
252
|
+
return {
|
|
253
|
+
metric: {
|
|
254
|
+
agg: head.agg,
|
|
255
|
+
metric: head.metric,
|
|
256
|
+
filters: head.filters,
|
|
257
|
+
operator: m[2],
|
|
258
|
+
threshold: parseFloat(m[3])
|
|
259
|
+
}
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
function parseQuery(raw) {
|
|
263
|
+
const trimmed = raw.trimStart();
|
|
264
|
+
if (trimmed.startsWith("find:")) {
|
|
265
|
+
return parseFindQuery(raw);
|
|
266
|
+
}
|
|
267
|
+
if (/^rows(?=[:{]|\s|$)/.test(trimmed)) {
|
|
268
|
+
return parseRowsQuery(raw);
|
|
269
|
+
}
|
|
270
|
+
return parseAnalyticsQuery(raw);
|
|
271
|
+
}
|
|
272
|
+
function cannotParseRows(text) {
|
|
273
|
+
return `Cannot parse "${text}". Expected rows:{result:\u2026|block:\u2026|note:\u2026} or rows:segment{\u2026} last 8w`;
|
|
274
|
+
}
|
|
275
|
+
function parseRowsQuery(raw) {
|
|
276
|
+
const suffixes = parseWqlSuffixes(raw);
|
|
277
|
+
const { where: whereText, last, groupBy, rollup, primaryText } = suffixes;
|
|
278
|
+
const result = {
|
|
279
|
+
raw,
|
|
280
|
+
family: "rows",
|
|
281
|
+
filters: [],
|
|
282
|
+
last: last ? { size: last.size, unit: last.unit } : void 0
|
|
283
|
+
};
|
|
284
|
+
if (whereText || groupBy || rollup) {
|
|
285
|
+
result.error = `Rows queries return raw statements \u2014 no where / by / rollup. Got "${primaryText.trim()}"`;
|
|
286
|
+
return result;
|
|
287
|
+
}
|
|
288
|
+
const text = primaryText.trim();
|
|
289
|
+
const brace = text.indexOf("{");
|
|
290
|
+
const head = (brace === -1 ? text : text.slice(0, brace)).trim();
|
|
291
|
+
const filterText = brace === -1 ? "" : text.slice(brace).trim();
|
|
292
|
+
if (head === "rows" || head === "rows:") ; else if (head.startsWith("rows:")) {
|
|
293
|
+
const target = head.slice("rows:".length).trim();
|
|
294
|
+
if (!target || /\s/.test(target)) {
|
|
295
|
+
result.error = cannotParseRows(text);
|
|
296
|
+
return result;
|
|
297
|
+
}
|
|
298
|
+
result.outputType = target;
|
|
299
|
+
} else {
|
|
300
|
+
result.error = cannotParseRows(text);
|
|
301
|
+
return result;
|
|
302
|
+
}
|
|
303
|
+
if (filterText) {
|
|
304
|
+
if (!/^\{.*\}$/.test(filterText)) {
|
|
305
|
+
result.error = cannotParseRows(text);
|
|
306
|
+
return result;
|
|
307
|
+
}
|
|
308
|
+
const synthetic = `find:_${filterText}`;
|
|
309
|
+
const tree = parser.parse(synthetic);
|
|
310
|
+
let syntaxError = false;
|
|
311
|
+
tree.iterate({ enter(node) {
|
|
312
|
+
if (node.type.isError) syntaxError = true;
|
|
313
|
+
} });
|
|
314
|
+
if (syntaxError) {
|
|
315
|
+
result.error = cannotParseRows(text);
|
|
316
|
+
return result;
|
|
317
|
+
}
|
|
318
|
+
result.filters = extractFilters(tree.topNode, synthetic);
|
|
319
|
+
}
|
|
320
|
+
return result;
|
|
321
|
+
}
|
|
322
|
+
function extractFilters(query, text) {
|
|
323
|
+
const out = [];
|
|
324
|
+
const filters = query.getChild(Filters);
|
|
325
|
+
if (!filters) return out;
|
|
326
|
+
for (const filter of filters.getChildren(Filter)) {
|
|
327
|
+
const keyNode = filter.getChild(TagKey);
|
|
328
|
+
const valueNode = filter.getChild(TagValue);
|
|
329
|
+
if (!keyNode || !valueNode) continue;
|
|
330
|
+
const values = [];
|
|
331
|
+
for (const valueChild of valueNode.getChildren(Value)) {
|
|
332
|
+
const raw = text.slice(valueChild.from, valueChild.to);
|
|
333
|
+
const quoted = valueChild.getChild(Quoted) !== null;
|
|
334
|
+
let value = quoted ? raw.slice(1, -1) : raw;
|
|
335
|
+
const wildcard = quoted ? false : value.endsWith("*");
|
|
336
|
+
if (wildcard) value = value.slice(0, -1);
|
|
337
|
+
if (!value) continue;
|
|
338
|
+
values.push({ value, wildcard });
|
|
339
|
+
}
|
|
340
|
+
if (values.length === 0) continue;
|
|
341
|
+
out.push({
|
|
342
|
+
key: text.slice(keyNode.from, keyNode.to),
|
|
343
|
+
negate: filter.getChild(Negate) !== null,
|
|
344
|
+
values
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
return out;
|
|
348
|
+
}
|
|
349
|
+
function parseAnalyticsQuery(raw) {
|
|
350
|
+
const suffixes = parseWqlSuffixes(raw);
|
|
351
|
+
const { where: whereText, displayUnit, groupBy, rollup, primaryText: text } = suffixes;
|
|
352
|
+
const base = {
|
|
353
|
+
raw,
|
|
354
|
+
agg: "sum",
|
|
355
|
+
metric: "",
|
|
356
|
+
filters: [],
|
|
357
|
+
groupBy: groupBy ?? [],
|
|
358
|
+
displayUnit,
|
|
359
|
+
rollup: rollup ? { size: rollup.size, unit: rollup.unit } : void 0
|
|
360
|
+
};
|
|
361
|
+
if (rollup && rollup.unit !== "d" && rollup.unit !== "w") {
|
|
362
|
+
base.error = cannotParse(text);
|
|
363
|
+
return base;
|
|
364
|
+
}
|
|
365
|
+
const tree = parser.parse(text);
|
|
366
|
+
let syntaxError = false;
|
|
367
|
+
tree.iterate({ enter(node) {
|
|
368
|
+
if (node.type.isError) syntaxError = true;
|
|
369
|
+
} });
|
|
370
|
+
if (syntaxError) {
|
|
371
|
+
base.error = cannotParse(text);
|
|
372
|
+
return base;
|
|
373
|
+
}
|
|
374
|
+
const query = tree.topNode;
|
|
375
|
+
const head = query.getChild(Head);
|
|
376
|
+
const aggNode = head?.getChild(Aggregator);
|
|
377
|
+
const metricNode = head?.getChild(Metric);
|
|
378
|
+
if (!head || !aggNode || !metricNode) {
|
|
379
|
+
base.error = cannotParse(text);
|
|
380
|
+
return base;
|
|
381
|
+
}
|
|
382
|
+
const aggText = text.slice(aggNode.from, aggNode.to);
|
|
383
|
+
if (!AGGS.includes(aggText)) {
|
|
384
|
+
base.error = `Unknown aggregator "${aggText}". Try: ${AGGS.join(", ")}`;
|
|
385
|
+
return base;
|
|
386
|
+
}
|
|
387
|
+
base.agg = aggText;
|
|
388
|
+
base.metric = text.slice(metricNode.from, metricNode.to);
|
|
389
|
+
base.filters = extractFilters(query, text);
|
|
390
|
+
if (whereText) {
|
|
391
|
+
const join = parseJoinClause(whereText);
|
|
392
|
+
if (join.error) {
|
|
393
|
+
base.error = join.error;
|
|
394
|
+
return base;
|
|
395
|
+
}
|
|
396
|
+
if (!join.find) {
|
|
397
|
+
base.error = `Cross-store join on an analytics query must be find:\u2026, got "${whereText}"`;
|
|
398
|
+
return base;
|
|
399
|
+
}
|
|
400
|
+
base.join = join.find;
|
|
401
|
+
}
|
|
402
|
+
return base;
|
|
403
|
+
}
|
|
404
|
+
function cannotParseFind(text) {
|
|
405
|
+
return `Cannot parse "${text}". Expected find:target{filters} in scope last 8w`;
|
|
406
|
+
}
|
|
407
|
+
function parseFindQuery(raw) {
|
|
408
|
+
const suffixes = parseWqlSuffixes(raw);
|
|
409
|
+
const { where: whereText, last, scope, primaryText: text } = suffixes;
|
|
410
|
+
const result = {
|
|
411
|
+
raw,
|
|
412
|
+
target: "",
|
|
413
|
+
filters: [],
|
|
414
|
+
scope,
|
|
415
|
+
last: last ? { size: last.size, unit: last.unit } : void 0
|
|
416
|
+
};
|
|
417
|
+
const tree = parser.parse(text);
|
|
418
|
+
let syntaxError = false;
|
|
419
|
+
tree.iterate({ enter(node) {
|
|
420
|
+
if (node.type.isError) syntaxError = true;
|
|
421
|
+
} });
|
|
422
|
+
if (syntaxError) {
|
|
423
|
+
result.error = cannotParseFind(text);
|
|
424
|
+
return result;
|
|
425
|
+
}
|
|
426
|
+
const query = tree.topNode;
|
|
427
|
+
const head = query.getChild(Head);
|
|
428
|
+
const aggNode = head?.getChild(Aggregator);
|
|
429
|
+
const metricNode = head?.getChild(Metric);
|
|
430
|
+
if (!head || !aggNode || !metricNode) {
|
|
431
|
+
result.error = cannotParseFind(text);
|
|
432
|
+
return result;
|
|
433
|
+
}
|
|
434
|
+
const aggText = text.slice(aggNode.from, aggNode.to);
|
|
435
|
+
if (aggText !== "find") {
|
|
436
|
+
result.error = `Expected "find:" but got "${aggText}:"`;
|
|
437
|
+
return result;
|
|
438
|
+
}
|
|
439
|
+
result.target = text.slice(metricNode.from, metricNode.to);
|
|
440
|
+
result.filters = extractFilters(query, text);
|
|
441
|
+
if (whereText) {
|
|
442
|
+
const join = parseJoinClause(whereText);
|
|
443
|
+
if (join.error) {
|
|
444
|
+
result.error = join.error;
|
|
445
|
+
return result;
|
|
446
|
+
}
|
|
447
|
+
if (!join.metric) {
|
|
448
|
+
result.error = `Cross-store join on a find query must be agg:metric{} <op> <number>, got "${whereText}"`;
|
|
449
|
+
return result;
|
|
450
|
+
}
|
|
451
|
+
result.join = join.metric;
|
|
452
|
+
}
|
|
453
|
+
return result;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// src/disciplines.ts
|
|
457
|
+
var EFFORT_DISCIPLINES = [
|
|
458
|
+
"bodyweight",
|
|
459
|
+
"cycling",
|
|
460
|
+
"gymnastics",
|
|
461
|
+
"kettlebell",
|
|
462
|
+
"recovery",
|
|
463
|
+
"rowing",
|
|
464
|
+
"running",
|
|
465
|
+
"strength",
|
|
466
|
+
"swimming",
|
|
467
|
+
"walking"
|
|
468
|
+
];
|
|
469
|
+
function isEffortDiscipline(value) {
|
|
470
|
+
return typeof value === "string" && EFFORT_DISCIPLINES.includes(value.toLowerCase());
|
|
471
|
+
}
|
|
472
|
+
var DISCIPLINE_FACTORS = {
|
|
473
|
+
strength: 1.2,
|
|
474
|
+
kettlebell: 1.2,
|
|
475
|
+
gymnastics: 1.2,
|
|
476
|
+
recovery: 0.9,
|
|
477
|
+
bodyweight: 1,
|
|
478
|
+
cycling: 1,
|
|
479
|
+
rowing: 1,
|
|
480
|
+
running: 1,
|
|
481
|
+
swimming: 1,
|
|
482
|
+
walking: 1
|
|
483
|
+
};
|
|
484
|
+
function disciplineFactorFor(discipline) {
|
|
485
|
+
if (!discipline) return 1;
|
|
486
|
+
const key = discipline.toLowerCase();
|
|
487
|
+
return isEffortDiscipline(key) ? DISCIPLINE_FACTORS[key] : 1;
|
|
488
|
+
}
|
|
489
|
+
var wqlLanguage = language.LRLanguage.define({
|
|
490
|
+
parser: parser.configure({
|
|
491
|
+
props: [
|
|
492
|
+
highlight.styleTags({
|
|
493
|
+
// Rule styles color stray punctuation; the Word tokens inside carry
|
|
494
|
+
// the visible styling (unstyled children reset the parent class).
|
|
495
|
+
Aggregator: highlight.tags.keyword,
|
|
496
|
+
"Aggregator/Word": highlight.tags.keyword,
|
|
497
|
+
Metric: highlight.tags.variableName,
|
|
498
|
+
"Metric/Word": highlight.tags.variableName,
|
|
499
|
+
TagKey: highlight.tags.propertyName,
|
|
500
|
+
"TagKey/Word": highlight.tags.propertyName,
|
|
501
|
+
TagValue: highlight.tags.string,
|
|
502
|
+
"TagValue/Value/Word": highlight.tags.string,
|
|
503
|
+
"TagValue/Word": highlight.tags.string,
|
|
504
|
+
Negate: highlight.tags.operator,
|
|
505
|
+
Star: highlight.tags.operator,
|
|
506
|
+
Dimension: highlight.tags.attributeName,
|
|
507
|
+
"Dimension/Word": highlight.tags.attributeName,
|
|
508
|
+
By: highlight.tags.keyword,
|
|
509
|
+
RollupDot: highlight.tags.keyword,
|
|
510
|
+
Int: highlight.tags.number,
|
|
511
|
+
"Rollup/Word": highlight.tags.unit,
|
|
512
|
+
braceOpen: highlight.tags.bracket,
|
|
513
|
+
braceClose: highlight.tags.bracket,
|
|
514
|
+
parenOpen: highlight.tags.bracket,
|
|
515
|
+
parenClose: highlight.tags.bracket,
|
|
516
|
+
colon: highlight.tags.punctuation,
|
|
517
|
+
comma: highlight.tags.punctuation,
|
|
518
|
+
pipe: highlight.tags.punctuation,
|
|
519
|
+
dot: highlight.tags.punctuation
|
|
520
|
+
})
|
|
521
|
+
]
|
|
522
|
+
}),
|
|
523
|
+
languageData: {
|
|
524
|
+
closeBrackets: { brackets: ["{", "("] }
|
|
525
|
+
}
|
|
526
|
+
});
|
|
527
|
+
function ancestor(node, ...names) {
|
|
528
|
+
for (let n = node; n; n = n.parent) {
|
|
529
|
+
if (names.includes(n.name)) return n;
|
|
530
|
+
}
|
|
531
|
+
return null;
|
|
532
|
+
}
|
|
533
|
+
function options(labels) {
|
|
534
|
+
return labels.map((label) => typeof label === "string" ? { label } : label);
|
|
535
|
+
}
|
|
536
|
+
function wqlCompletionSource(options_ = {}) {
|
|
537
|
+
const { effortNames } = options_;
|
|
538
|
+
const metricOptions = () => {
|
|
539
|
+
const efforts = effortNames?.() ?? [];
|
|
540
|
+
return [
|
|
541
|
+
...options(WQL_METRIC_AGGREGATES).map((c) => ({ ...c, type: "constant" })),
|
|
542
|
+
...options(WQL_METRIC_FAMILIES).map((c) => ({ ...c, type: "variable" })),
|
|
543
|
+
...options(efforts.flatMap((slug) => WQL_METRIC_FAMILIES.map((family) => `${slug}.${family}`))).map((c) => ({ ...c, type: "variable" })),
|
|
544
|
+
...options(WQL_CALC_TARGETS).map((c) => ({ ...c, type: "namespace" })),
|
|
545
|
+
autocomplete.snippetCompletion("calc.${target}", { label: "calc.", detail: "calculated target", type: "namespace" })
|
|
546
|
+
];
|
|
547
|
+
};
|
|
548
|
+
const tagValueOptions = (key) => {
|
|
549
|
+
switch (key) {
|
|
550
|
+
case "effort":
|
|
551
|
+
return options(effortNames?.() ?? []);
|
|
552
|
+
case "discipline":
|
|
553
|
+
return options(EFFORT_DISCIPLINES);
|
|
554
|
+
case "intensity":
|
|
555
|
+
return options(WQL_INTENSITY_TIERS);
|
|
556
|
+
case "grain":
|
|
557
|
+
return options(WQL_GRAINS);
|
|
558
|
+
case "metric":
|
|
559
|
+
return metricOptions();
|
|
560
|
+
default:
|
|
561
|
+
return null;
|
|
562
|
+
}
|
|
563
|
+
};
|
|
564
|
+
function source(context) {
|
|
565
|
+
const word = context.matchBefore(/[\w.*-]*/);
|
|
566
|
+
if (!word || word.from === word.to && !context.explicit) return null;
|
|
567
|
+
const tree = language.syntaxTree(context.state);
|
|
568
|
+
const node = tree.resolveInner(context.pos, -1);
|
|
569
|
+
const filter = ancestor(node, "Filter");
|
|
570
|
+
if (filter) {
|
|
571
|
+
const filterText = context.state.sliceDoc(filter.from, context.pos);
|
|
572
|
+
const colonIndex = filterText.indexOf(":");
|
|
573
|
+
if (colonIndex === -1) {
|
|
574
|
+
const keyWord = context.matchBefore(/[\w-]*/);
|
|
575
|
+
return { from: keyWord.from, options: options(WQL_TAG_KEYS).map((c) => ({ ...c, type: "property" })), validFor: /^[\w-]*$/ };
|
|
576
|
+
}
|
|
577
|
+
const key = filterText.slice(0, colonIndex).replace(/^!/, "").trim();
|
|
578
|
+
const valueOptions = tagValueOptions(key);
|
|
579
|
+
if (!valueOptions) return null;
|
|
580
|
+
return { from: word.from, options: valueOptions, validFor: /^[\w*-]*$/ };
|
|
581
|
+
}
|
|
582
|
+
if (ancestor(node, "Filters")) {
|
|
583
|
+
const keyWord = context.matchBefore(/[\w-]*/);
|
|
584
|
+
return { from: keyWord.from, options: options(WQL_TAG_KEYS).map((c) => ({ ...c, type: "property" })), validFor: /^[\w-]*$/ };
|
|
585
|
+
}
|
|
586
|
+
if (ancestor(node, "GroupBy")) {
|
|
587
|
+
const dimWord = context.matchBefore(/[\w-]*/);
|
|
588
|
+
return {
|
|
589
|
+
from: dimWord.from,
|
|
590
|
+
options: [
|
|
591
|
+
...options(WQL_VIRTUAL_DIMS).map((c) => ({ ...c, type: "atom" })),
|
|
592
|
+
...options(WQL_TAG_KEYS).map((c) => ({ ...c, type: "property" }))
|
|
593
|
+
],
|
|
594
|
+
validFor: /^[\w-]*$/
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
const head = tree.topNode.getChild("Head");
|
|
598
|
+
const insideHead = ancestor(node, "Head") !== null;
|
|
599
|
+
const inMetric = node.name === "Metric" || node.name === "Word" && node.parent?.name === "Metric";
|
|
600
|
+
if (inMetric) {
|
|
601
|
+
return { from: word.from, options: metricOptions(), validFor: /^[\w.-]*$/ };
|
|
602
|
+
}
|
|
603
|
+
if (insideHead && head) {
|
|
604
|
+
const afterColon = context.state.sliceDoc(head.from, context.pos).includes(":");
|
|
605
|
+
const metricWord = head.getChild("Metric")?.getChild("Word");
|
|
606
|
+
const typedPastMetric = metricWord !== null && metricWord !== void 0 && /\s/.test(context.state.sliceDoc(metricWord.to, context.pos));
|
|
607
|
+
if (afterColon && !typedPastMetric) {
|
|
608
|
+
return { from: word.from, options: metricOptions(), validFor: /^[\w.-]*$/ };
|
|
609
|
+
}
|
|
610
|
+
if (!afterColon) {
|
|
611
|
+
return { from: word.from, options: options(WQL_AGGREGATORS).map((c) => ({ ...c, type: "keyword" })), validFor: /^[\w-]*$/ };
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
if (ancestor(node, "Rollup")) {
|
|
615
|
+
return {
|
|
616
|
+
from: word.from,
|
|
617
|
+
options: options(WQL_ROLLUP_PERIODS).map((c) => ({ ...c, type: "constant" })),
|
|
618
|
+
validFor: /^[\w]*$/
|
|
619
|
+
};
|
|
620
|
+
}
|
|
621
|
+
if (!head) {
|
|
622
|
+
return { from: word.from, options: options(WQL_AGGREGATORS).map((c) => ({ ...c, type: "keyword" })), validFor: /^[\w-]*$/ };
|
|
623
|
+
}
|
|
624
|
+
return {
|
|
625
|
+
from: word.from,
|
|
626
|
+
options: [
|
|
627
|
+
autocomplete.snippetCompletion("by {${dimension}}", { label: "by {}", detail: "group by dimensions", type: "keyword" }),
|
|
628
|
+
autocomplete.snippetCompletion(".rollup(${1}${d|w})", { label: ".rollup()", detail: "bucket period", type: "keyword" })
|
|
629
|
+
],
|
|
630
|
+
validFor: /^[\w.-]*$/
|
|
631
|
+
};
|
|
632
|
+
}
|
|
633
|
+
return source;
|
|
634
|
+
}
|
|
635
|
+
function wqlCompletion(options2) {
|
|
636
|
+
return autocomplete.autocompletion({ override: [wqlCompletionSource(options2)] });
|
|
637
|
+
}
|
|
638
|
+
var wqlHighlightStyle = language.HighlightStyle.define([
|
|
639
|
+
{ tag: highlight.tags.keyword, color: "hsl(var(--primary))" },
|
|
640
|
+
{ tag: highlight.tags.variableName, color: "hsl(var(--accent-foreground))", fontWeight: "600" },
|
|
641
|
+
{ tag: highlight.tags.propertyName, color: "hsl(var(--secondary-foreground))" },
|
|
642
|
+
{ tag: highlight.tags.string, color: "hsl(var(--success))" },
|
|
643
|
+
{ tag: highlight.tags.attributeName, color: "hsl(var(--warning))" },
|
|
644
|
+
{ tag: highlight.tags.number, color: "hsl(var(--primary))" },
|
|
645
|
+
{ tag: highlight.tags.unit, color: "hsl(var(--muted-foreground))" },
|
|
646
|
+
{ tag: highlight.tags.operator, color: "hsl(var(--destructive))" },
|
|
647
|
+
{ tag: [highlight.tags.bracket, highlight.tags.punctuation], color: "hsl(var(--muted-foreground))" }
|
|
648
|
+
]);
|
|
649
|
+
function wql(completionOptions) {
|
|
650
|
+
return new language.LanguageSupport(wqlLanguage, [
|
|
651
|
+
wqlCompletion(completionOptions),
|
|
652
|
+
language.syntaxHighlighting(wqlHighlightStyle)
|
|
653
|
+
]);
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
// src/units.ts
|
|
657
|
+
var KG_PER_LB = 0.45359237;
|
|
658
|
+
var UNIT_FAMILIES = {
|
|
659
|
+
mass: {
|
|
660
|
+
base: "kg",
|
|
661
|
+
units: {
|
|
662
|
+
kg: 1,
|
|
663
|
+
lb: KG_PER_LB
|
|
664
|
+
}
|
|
665
|
+
},
|
|
666
|
+
distance: {
|
|
667
|
+
base: "m",
|
|
668
|
+
units: {
|
|
669
|
+
m: 1,
|
|
670
|
+
km: 1e3
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
};
|
|
674
|
+
function getUnitFamily(unit) {
|
|
675
|
+
if (!unit) return void 0;
|
|
676
|
+
for (const [family, def] of Object.entries(UNIT_FAMILIES)) {
|
|
677
|
+
if (def.units[unit] !== void 0) return family;
|
|
678
|
+
}
|
|
679
|
+
return void 0;
|
|
680
|
+
}
|
|
681
|
+
function convert(value, from, to) {
|
|
682
|
+
if (from === to || from === void 0 || to === void 0) return value;
|
|
683
|
+
const family = getUnitFamily(from);
|
|
684
|
+
if (!family || family !== getUnitFamily(to)) return value;
|
|
685
|
+
const def = UNIT_FAMILIES[family];
|
|
686
|
+
const fromFactor = def.units[from];
|
|
687
|
+
const toFactor = def.units[to];
|
|
688
|
+
if (fromFactor === void 0 || toFactor === void 0) return value;
|
|
689
|
+
return value * fromFactor / toFactor;
|
|
690
|
+
}
|
|
691
|
+
function resolveDisplayUnit(facts, {
|
|
692
|
+
directive,
|
|
693
|
+
preferred
|
|
694
|
+
}) {
|
|
695
|
+
const sourceUnits = facts.map((row) => row.unit ?? row.metricUnit).filter((u) => Boolean(u));
|
|
696
|
+
const firstSource = sourceUnits[0];
|
|
697
|
+
if (directive) {
|
|
698
|
+
const directiveFamily = getUnitFamily(directive);
|
|
699
|
+
const applicable = directiveFamily !== void 0 && sourceUnits.some((u) => getUnitFamily(u) === directiveFamily);
|
|
700
|
+
return { unit: directive, convert: applicable };
|
|
701
|
+
}
|
|
702
|
+
if (preferred) {
|
|
703
|
+
const preferredFamily = getUnitFamily(preferred);
|
|
704
|
+
if (preferredFamily && sourceUnits.some((u) => getUnitFamily(u) === preferredFamily)) {
|
|
705
|
+
return { unit: preferred, convert: true };
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
return { unit: firstSource, convert: false };
|
|
709
|
+
}
|
|
710
|
+
function resolveCanonicalMetricKey(projectionName) {
|
|
711
|
+
const words = projectionName.trim().split(/[^a-zA-Z0-9]+/).filter(Boolean);
|
|
712
|
+
return words.map(
|
|
713
|
+
(word, index) => index === 0 ? word.toLowerCase() : word.charAt(0).toUpperCase() + word.slice(1)
|
|
714
|
+
).join("");
|
|
715
|
+
}
|
|
716
|
+
function metadataString(metadata, key) {
|
|
717
|
+
const value = metadata?.[key];
|
|
718
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
719
|
+
}
|
|
720
|
+
function readGroupTags(metadata) {
|
|
721
|
+
const tags = metadata?.groupTags;
|
|
722
|
+
if (!tags || typeof tags !== "object") return void 0;
|
|
723
|
+
const entries = Object.entries(tags).filter((pair) => typeof pair[1] === "string" && pair[1].length > 0).sort(([a], [b]) => a < b ? -1 : 1);
|
|
724
|
+
return entries.length > 0 ? Object.fromEntries(entries) : void 0;
|
|
725
|
+
}
|
|
726
|
+
function normalizeSummaryFacts(logs, identity) {
|
|
727
|
+
const now = Date.now();
|
|
728
|
+
const rowsByKey = /* @__PURE__ */ new Map();
|
|
729
|
+
for (const output of logs) {
|
|
730
|
+
if (output.outputType !== "analytics") continue;
|
|
731
|
+
const label = output.metrics.find((m) => m.type === wodWikiCore.MetricType.Label);
|
|
732
|
+
const value = output.metrics.find((m) => m.type !== wodWikiCore.MetricType.Label && typeof m.value === "number");
|
|
733
|
+
if (!label || !value) continue;
|
|
734
|
+
const projectionName = String(label.value ?? label.image ?? "");
|
|
735
|
+
if (!projectionName) continue;
|
|
736
|
+
const metricKey = metadataString(value.metadata, "canonicalKey") ?? resolveCanonicalMetricKey(projectionName);
|
|
737
|
+
const effortSlug = metadataString(value.metadata, "effortSlug");
|
|
738
|
+
const discipline = metadataString(value.metadata, "effortDiscipline");
|
|
739
|
+
const intensityTier = metadataString(value.metadata, "effortIntensityTier");
|
|
740
|
+
const grade = metadataString(value.metadata, "grade");
|
|
741
|
+
const groupTags = readGroupTags(value.metadata) ?? (effortSlug ? { effort: effortSlug } : void 0);
|
|
742
|
+
const rowKey = groupTags ? `${metricKey}:${Object.entries(groupTags).map(([k, v]) => `${k}=${v}`).join(":")}` : metricKey;
|
|
743
|
+
rowsByKey.set(rowKey, {
|
|
744
|
+
id: `${identity.resultId}-${rowKey}-${now}`,
|
|
745
|
+
noteId: identity.noteId,
|
|
746
|
+
blockContentId: identity.blockContentId,
|
|
747
|
+
origin: identity.origin,
|
|
748
|
+
pageId: identity.pageId,
|
|
749
|
+
grain: "summary",
|
|
750
|
+
segmentId: identity.segmentId ?? "",
|
|
751
|
+
segmentVersion: identity.segmentVersion ?? 0,
|
|
752
|
+
resultId: identity.resultId,
|
|
753
|
+
type: metricKey,
|
|
754
|
+
value: value.value,
|
|
755
|
+
unit: value.unit,
|
|
756
|
+
label: projectionName,
|
|
757
|
+
metricKey,
|
|
758
|
+
metricLabel: projectionName,
|
|
759
|
+
metricUnit: value.unit,
|
|
760
|
+
...effortSlug ? { effortSlug } : {},
|
|
761
|
+
...discipline ? { discipline } : {},
|
|
762
|
+
...intensityTier ? { intensityTier } : {},
|
|
763
|
+
...grade ? { grade } : {},
|
|
764
|
+
timestamp: identity.workoutTimestamp ?? output.timeSpan?.started ?? now,
|
|
765
|
+
createdAt: now
|
|
766
|
+
});
|
|
767
|
+
}
|
|
768
|
+
return Array.from(rowsByKey.values());
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
// src/dashboard/frontmatter.ts
|
|
772
|
+
var FRONTMATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/;
|
|
773
|
+
function unquote(value) {
|
|
774
|
+
return value.replace(
|
|
775
|
+
/^(['"])(.*)\1$/,
|
|
776
|
+
(_match, quote, inner) => quote === '"' ? inner.replace(/\\(["\\])/g, "$1") : inner
|
|
777
|
+
);
|
|
778
|
+
}
|
|
779
|
+
function parseFrontmatter(raw) {
|
|
780
|
+
const match = raw.match(FRONTMATTER_RE);
|
|
781
|
+
if (!match) return { meta: {}, body: raw };
|
|
782
|
+
return { meta: parseMetaLines(match[1].split(/\r?\n/)), body: raw.slice(match[0].length) };
|
|
783
|
+
}
|
|
784
|
+
function parseMetaLines(lines) {
|
|
785
|
+
const meta = {};
|
|
786
|
+
for (let i = 0; i < lines.length; i++) {
|
|
787
|
+
const keyMatch = lines[i].match(/^([A-Za-z][\w.-]*)\s*:(.*)$/);
|
|
788
|
+
if (!keyMatch) continue;
|
|
789
|
+
const key = keyMatch[1];
|
|
790
|
+
const rawVal = keyMatch[2].trim();
|
|
791
|
+
if (rawVal === "") {
|
|
792
|
+
const items = [];
|
|
793
|
+
while (i + 1 < lines.length) {
|
|
794
|
+
const itemMatch = lines[i + 1].match(/^\s+-\s+(.+)$/);
|
|
795
|
+
if (!itemMatch) break;
|
|
796
|
+
items.push(itemMatch[1].trim());
|
|
797
|
+
i++;
|
|
798
|
+
}
|
|
799
|
+
meta[key] = items.length > 0 ? items : "";
|
|
800
|
+
} else {
|
|
801
|
+
const wasQuoted = /^(['"])(.*)\1$/.test(rawVal);
|
|
802
|
+
const value = unquote(rawVal);
|
|
803
|
+
const num = Number(value);
|
|
804
|
+
meta[key] = !wasQuoted && value !== "" && !isNaN(num) ? num : value;
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
return meta;
|
|
808
|
+
}
|
|
809
|
+
function parseFrontmatterBody(innerContent) {
|
|
810
|
+
return parseMetaLines(innerContent.split(/\r?\n/));
|
|
811
|
+
}
|
|
812
|
+
function quoteYamlScalar(value) {
|
|
813
|
+
if (value === "") return '""';
|
|
814
|
+
const looksNumeric = !isNaN(Number(value));
|
|
815
|
+
const looksKeyword = /^(true|false|null|yes|no|on|off)$/i.test(value);
|
|
816
|
+
if (/[":'\n#{}[\\],&*?|<>=%!@`]/.test(value) || value !== value.trim() || value.startsWith("-") || looksNumeric || looksKeyword) {
|
|
817
|
+
return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
818
|
+
}
|
|
819
|
+
return value;
|
|
820
|
+
}
|
|
821
|
+
function serializeFrontmatter(meta) {
|
|
822
|
+
const lines = [];
|
|
823
|
+
for (const [key, value] of Object.entries(meta)) {
|
|
824
|
+
if (Array.isArray(value)) {
|
|
825
|
+
if (value.length === 0) {
|
|
826
|
+
lines.push(`${key}: ""`);
|
|
827
|
+
continue;
|
|
828
|
+
}
|
|
829
|
+
lines.push(`${key}:`);
|
|
830
|
+
for (const item of value) {
|
|
831
|
+
lines.push(` - ${quoteYamlScalar(item)}`);
|
|
832
|
+
}
|
|
833
|
+
} else if (typeof value === "number") {
|
|
834
|
+
lines.push(`${key}: ${value}`);
|
|
835
|
+
} else {
|
|
836
|
+
lines.push(`${key}: ${quoteYamlScalar(value)}`);
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
return lines.join("\n");
|
|
840
|
+
}
|
|
841
|
+
function stripFrontmatter(raw) {
|
|
842
|
+
return parseFrontmatter(raw).body;
|
|
843
|
+
}
|
|
844
|
+
function getScalar(meta, key) {
|
|
845
|
+
const value = meta[key];
|
|
846
|
+
return typeof value === "string" || typeof value === "number" ? value : void 0;
|
|
847
|
+
}
|
|
848
|
+
function getList(meta, key) {
|
|
849
|
+
const value = meta[key];
|
|
850
|
+
return Array.isArray(value) ? value : [];
|
|
851
|
+
}
|
|
852
|
+
function parseFrontmatterCategories(raw) {
|
|
853
|
+
return getList(parseFrontmatter(raw).meta, "category").map((c) => c.toLowerCase());
|
|
854
|
+
}
|
|
855
|
+
function extractFrontmatterTags(raw) {
|
|
856
|
+
const meta = FRONTMATTER_RE.test(raw) ? parseFrontmatter(raw).meta : parseFrontmatterBody(raw);
|
|
857
|
+
const tags = meta["tags"];
|
|
858
|
+
const list = Array.isArray(tags) ? tags : typeof tags === "string" && tags.trim() ? tags.trim().startsWith("[") && tags.trim().endsWith("]") ? tags.trim().slice(1, -1).split(",") : [tags] : [];
|
|
859
|
+
return [...new Set(list.map((tag) => tag.trim()).filter(Boolean))];
|
|
860
|
+
}
|
|
861
|
+
function parseFlatProperties(innerContent) {
|
|
862
|
+
const props = {};
|
|
863
|
+
for (const line of innerContent.split(/\r?\n/)) {
|
|
864
|
+
const match = line.match(/^([^:]+):\s*(.*)$/);
|
|
865
|
+
if (match) {
|
|
866
|
+
props[match[1].trim()] = match[2].trim();
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
return props;
|
|
870
|
+
}
|
|
871
|
+
function parseFrontmatterProps(lines) {
|
|
872
|
+
const props = {};
|
|
873
|
+
for (const line of lines) {
|
|
874
|
+
const match = line.match(/^([^:]+):\s*(.*)$/);
|
|
875
|
+
if (match) {
|
|
876
|
+
props[match[1].trim()] = match[2].trim();
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
return props;
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
// src/static.ts
|
|
883
|
+
function staticNotesFromBlocks(blocks) {
|
|
884
|
+
const map = /* @__PURE__ */ new Map();
|
|
885
|
+
for (const block of blocks) {
|
|
886
|
+
if (!map.has(block.noteId)) {
|
|
887
|
+
map.set(block.noteId, {
|
|
888
|
+
id: block.noteId,
|
|
889
|
+
title: block.noteTitle,
|
|
890
|
+
createdAt: block.createdAt,
|
|
891
|
+
type: "note",
|
|
892
|
+
sourceId: block.sourceId,
|
|
893
|
+
catalog: (block.noteId.startsWith("feeds/") ? block.noteId.slice("feeds/".length) : block.noteId).split("/")[0]
|
|
894
|
+
});
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
return Array.from(map.values());
|
|
898
|
+
}
|
|
899
|
+
function staticTagIndexFromBlocks(blocks) {
|
|
900
|
+
const index = /* @__PURE__ */ new Map();
|
|
901
|
+
for (const block of blocks) {
|
|
902
|
+
if (block.dataType !== "frontmatter") continue;
|
|
903
|
+
for (const tag of extractFrontmatterTags(block.rawContent)) {
|
|
904
|
+
const ids = index.get(tag);
|
|
905
|
+
if (ids) ids.add(block.noteId);
|
|
906
|
+
else index.set(tag, /* @__PURE__ */ new Set([block.noteId]));
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
return index;
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
// src/QueryService.ts
|
|
913
|
+
var DAY = 864e5;
|
|
914
|
+
function localDateString(ts) {
|
|
915
|
+
return new Date(ts).toLocaleDateString(void 0, { year: "numeric", month: "short", day: "numeric" });
|
|
916
|
+
}
|
|
917
|
+
function catalogOfItem(item) {
|
|
918
|
+
if (item.catalog) return item.catalog;
|
|
919
|
+
const raw = item.sourceId ? item.sourceId.replace(/^(collection|feed):/, "") : item.noteId || item.id || "";
|
|
920
|
+
if (!raw) return void 0;
|
|
921
|
+
const clean = raw.startsWith("feeds/") ? raw.slice("feeds/".length) : raw;
|
|
922
|
+
return clean.split("/")[0];
|
|
923
|
+
}
|
|
924
|
+
function sourceMatches(sourceId, kind) {
|
|
925
|
+
if (kind === "journal") return !sourceId || sourceId === "journal";
|
|
926
|
+
if (kind === "collection" || kind === "feed") {
|
|
927
|
+
return !!sourceId && sourceId.startsWith(`${kind}:`);
|
|
928
|
+
}
|
|
929
|
+
return sourceId === kind;
|
|
930
|
+
}
|
|
931
|
+
function applySourceFilter(items, filters) {
|
|
932
|
+
for (const filter of filters) {
|
|
933
|
+
if (filter.key !== "source") continue;
|
|
934
|
+
const wants = filter.values.map((v) => v.value);
|
|
935
|
+
items = items.filter((item) => {
|
|
936
|
+
const match = wants.some((w) => sourceMatches(item.sourceId, w));
|
|
937
|
+
return filter.negate ? !match : match;
|
|
938
|
+
});
|
|
939
|
+
}
|
|
940
|
+
return items;
|
|
941
|
+
}
|
|
942
|
+
function effectiveTimeWindow(createdAt, last, range, anchorNow) {
|
|
943
|
+
if (range) return createdAt >= range.start && createdAt <= range.end;
|
|
944
|
+
if (last) {
|
|
945
|
+
const cutoff = (anchorNow ?? Date.now()) - last.size * (last.unit === "w" ? 7 : 1) * DAY;
|
|
946
|
+
return createdAt >= cutoff;
|
|
947
|
+
}
|
|
948
|
+
return true;
|
|
949
|
+
}
|
|
950
|
+
function latestActivity(rows) {
|
|
951
|
+
let max = 0;
|
|
952
|
+
for (const r of rows) if (r.createdAt > max) max = r.createdAt;
|
|
953
|
+
return max;
|
|
954
|
+
}
|
|
955
|
+
function windowAnchor(selected, parsed, options2) {
|
|
956
|
+
if (options2.anchorNow !== void 0) return options2.anchorNow;
|
|
957
|
+
if (options2.anchor === "latest-activity" && parsed.last) return latestActivity(selected);
|
|
958
|
+
return void 0;
|
|
959
|
+
}
|
|
960
|
+
var defaultFactStore = {
|
|
961
|
+
getFactsByMetric: async () => [],
|
|
962
|
+
getFactsByTimeRange: async () => [],
|
|
963
|
+
getNoteTagLabels: async () => []
|
|
964
|
+
};
|
|
965
|
+
var defaultNoteStore = {
|
|
966
|
+
getAllNotes: async () => [],
|
|
967
|
+
getNoteIdsForTag: async () => /* @__PURE__ */ new Set()
|
|
968
|
+
};
|
|
969
|
+
var defaultBlockStore = {
|
|
970
|
+
getAllBlocks: async () => []
|
|
971
|
+
};
|
|
972
|
+
var defaultResultStore = {
|
|
973
|
+
getResultsByContentId: async () => [],
|
|
974
|
+
getResultById: async () => void 0,
|
|
975
|
+
getResultsForNote: async () => []
|
|
976
|
+
};
|
|
977
|
+
var defaultEffortStore = {
|
|
978
|
+
getAllEfforts: async () => []
|
|
979
|
+
};
|
|
980
|
+
function factTagValue(row, key, noteTags) {
|
|
981
|
+
switch (key) {
|
|
982
|
+
case "effort":
|
|
983
|
+
return row.effortSlug;
|
|
984
|
+
case "discipline":
|
|
985
|
+
return row.discipline;
|
|
986
|
+
case "grade":
|
|
987
|
+
return row.grade;
|
|
988
|
+
case "intensity":
|
|
989
|
+
return row.intensityTier;
|
|
990
|
+
case "note":
|
|
991
|
+
return row.noteId;
|
|
992
|
+
case "page":
|
|
993
|
+
return row.pageId;
|
|
994
|
+
case "origin":
|
|
995
|
+
return row.origin;
|
|
996
|
+
case "grain":
|
|
997
|
+
return row.grain;
|
|
998
|
+
case "metric":
|
|
999
|
+
return row.metricKey;
|
|
1000
|
+
case "block":
|
|
1001
|
+
return row.blockContentId;
|
|
1002
|
+
case "result":
|
|
1003
|
+
return row.resultId;
|
|
1004
|
+
case "tags":
|
|
1005
|
+
return noteTags.get(row.noteId) ?? [];
|
|
1006
|
+
default:
|
|
1007
|
+
return void 0;
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
function matchesFilters(row, filters, noteTags) {
|
|
1011
|
+
const groups = /* @__PURE__ */ new Map();
|
|
1012
|
+
for (const f of filters) {
|
|
1013
|
+
const groupKey = `${f.negate ? "!" : ""}${f.key}`;
|
|
1014
|
+
const bucket = groups.get(groupKey);
|
|
1015
|
+
if (bucket) bucket.push(f);
|
|
1016
|
+
else groups.set(groupKey, [f]);
|
|
1017
|
+
}
|
|
1018
|
+
return [...groups.values()].every((group) => {
|
|
1019
|
+
const key = group[0].key;
|
|
1020
|
+
const negate = group[0].negate;
|
|
1021
|
+
const raw = factTagValue(row, key, noteTags);
|
|
1022
|
+
const rowValues = Array.isArray(raw) ? raw : raw !== void 0 ? [raw] : [];
|
|
1023
|
+
const hit = group.some(
|
|
1024
|
+
(f) => f.values.some(
|
|
1025
|
+
(a) => rowValues.some((v) => a.wildcard ? v.startsWith(a.value) : v === a.value)
|
|
1026
|
+
)
|
|
1027
|
+
);
|
|
1028
|
+
return negate ? !hit : hit;
|
|
1029
|
+
});
|
|
1030
|
+
}
|
|
1031
|
+
function dimValue(row, dim, noteTags) {
|
|
1032
|
+
if (dim === "day") return localDateString(row.timestamp);
|
|
1033
|
+
if (dim === "week") {
|
|
1034
|
+
const d = new Date(row.timestamp);
|
|
1035
|
+
const monday = new Date(row.timestamp - (d.getDay() + 6) % 7 * DAY);
|
|
1036
|
+
return `w/${monday.toISOString().slice(5, 10)}`;
|
|
1037
|
+
}
|
|
1038
|
+
if (dim === "session") return row.resultId;
|
|
1039
|
+
const raw = factTagValue(row, dim, noteTags);
|
|
1040
|
+
if (raw === void 0) return "(none)";
|
|
1041
|
+
if (typeof raw === "string") return raw;
|
|
1042
|
+
return raw.length ? raw.join(",") : "(none)";
|
|
1043
|
+
}
|
|
1044
|
+
function toDisplayValue(value, unit, targetUnit) {
|
|
1045
|
+
if (!targetUnit || unit === targetUnit) return value;
|
|
1046
|
+
return convert(value, unit, targetUnit);
|
|
1047
|
+
}
|
|
1048
|
+
function aggregate(values, agg, points, targetUnit) {
|
|
1049
|
+
if (agg === "count") return points.length;
|
|
1050
|
+
if (values.length === 0) return 0;
|
|
1051
|
+
switch (agg) {
|
|
1052
|
+
case "sum":
|
|
1053
|
+
return values.reduce((a, b) => a + b, 0);
|
|
1054
|
+
case "avg":
|
|
1055
|
+
return values.reduce((a, b) => a + b, 0) / values.length;
|
|
1056
|
+
case "min":
|
|
1057
|
+
return Math.min(...values);
|
|
1058
|
+
case "max":
|
|
1059
|
+
return Math.max(...values);
|
|
1060
|
+
case "last": {
|
|
1061
|
+
const latest = [...points].sort((a, b) => b.timestamp - a.timestamp)[0];
|
|
1062
|
+
return toDisplayValue(latest.value, latest.unit ?? latest.metricUnit, targetUnit);
|
|
1063
|
+
}
|
|
1064
|
+
case "delta":
|
|
1065
|
+
return values[values.length - 1] - values[0];
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
function compareOp(value, op, threshold) {
|
|
1069
|
+
switch (op) {
|
|
1070
|
+
case ">":
|
|
1071
|
+
return value > threshold;
|
|
1072
|
+
case ">=":
|
|
1073
|
+
return value >= threshold;
|
|
1074
|
+
case "<":
|
|
1075
|
+
return value < threshold;
|
|
1076
|
+
case "<=":
|
|
1077
|
+
return value <= threshold;
|
|
1078
|
+
case "==":
|
|
1079
|
+
return value === threshold;
|
|
1080
|
+
case "!=":
|
|
1081
|
+
return value !== threshold;
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
var QueryService = class {
|
|
1085
|
+
store;
|
|
1086
|
+
noteStore;
|
|
1087
|
+
blockStore;
|
|
1088
|
+
resultStore;
|
|
1089
|
+
effortStore;
|
|
1090
|
+
staticNoteStore;
|
|
1091
|
+
staticBlockStore;
|
|
1092
|
+
constructor(storesOrFactStore, noteStore, blockStore, resultStore, effortStore, staticNoteStore, staticBlockStore) {
|
|
1093
|
+
if (storesOrFactStore && typeof storesOrFactStore === "object" && ("factStore" in storesOrFactStore || "noteStore" in storesOrFactStore || "blockStore" in storesOrFactStore || "resultStore" in storesOrFactStore || "effortStore" in storesOrFactStore || "staticNoteStore" in storesOrFactStore || "staticBlockStore" in storesOrFactStore)) {
|
|
1094
|
+
const stores = storesOrFactStore;
|
|
1095
|
+
this.store = stores.factStore ?? defaultFactStore;
|
|
1096
|
+
this.noteStore = stores.noteStore ?? defaultNoteStore;
|
|
1097
|
+
this.blockStore = stores.blockStore ?? defaultBlockStore;
|
|
1098
|
+
this.resultStore = stores.resultStore ?? defaultResultStore;
|
|
1099
|
+
this.effortStore = stores.effortStore ?? defaultEffortStore;
|
|
1100
|
+
this.staticNoteStore = stores.staticNoteStore;
|
|
1101
|
+
this.staticBlockStore = stores.staticBlockStore;
|
|
1102
|
+
} else {
|
|
1103
|
+
this.store = storesOrFactStore ?? defaultFactStore;
|
|
1104
|
+
this.noteStore = noteStore ?? defaultNoteStore;
|
|
1105
|
+
this.blockStore = blockStore ?? defaultBlockStore;
|
|
1106
|
+
this.resultStore = resultStore ?? defaultResultStore;
|
|
1107
|
+
this.effortStore = effortStore ?? defaultEffortStore;
|
|
1108
|
+
this.staticNoteStore = staticNoteStore;
|
|
1109
|
+
this.staticBlockStore = staticBlockStore;
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
async getFactsByTimeRange(start, end) {
|
|
1113
|
+
return this.store.getFactsByTimeRange(start, end);
|
|
1114
|
+
}
|
|
1115
|
+
async runQuery(raw, options2 = {}) {
|
|
1116
|
+
const parsed = parseQuery(raw);
|
|
1117
|
+
if (isFindQuery(parsed)) {
|
|
1118
|
+
return {
|
|
1119
|
+
parsed: { raw, agg: "count", metric: parsed.target, filters: [], groupBy: [] },
|
|
1120
|
+
series: [],
|
|
1121
|
+
stages: { selected: 0, buckets: 0, aggregated: 0, groups: 0 },
|
|
1122
|
+
matched: []
|
|
1123
|
+
};
|
|
1124
|
+
}
|
|
1125
|
+
if (isRowsQuery(parsed)) {
|
|
1126
|
+
return {
|
|
1127
|
+
parsed: { raw, agg: "count", metric: "rows", filters: [], groupBy: [] },
|
|
1128
|
+
series: [],
|
|
1129
|
+
stages: { selected: 0, buckets: 0, aggregated: 0, groups: 0 },
|
|
1130
|
+
matched: []
|
|
1131
|
+
};
|
|
1132
|
+
}
|
|
1133
|
+
return this.run(parsed, options2);
|
|
1134
|
+
}
|
|
1135
|
+
/**
|
|
1136
|
+
* Execute a rows query (rows:{…}, #949) — the session results table plane.
|
|
1137
|
+
* Re-derives output statements from raw WorkoutResult logs through the
|
|
1138
|
+
* ResultLogStore seam ("logs win" — facts are never read for this path).
|
|
1139
|
+
*/
|
|
1140
|
+
async runRows(parsed, options2 = {}) {
|
|
1141
|
+
const empty = { parsed, runs: [] };
|
|
1142
|
+
if (parsed.error) return { ...empty, error: parsed.error };
|
|
1143
|
+
const ROWS_SCOPE_KEYS = /* @__PURE__ */ new Set(["result", "block", "note"]);
|
|
1144
|
+
const unsupported = parsed.filters.filter(
|
|
1145
|
+
(f) => !ROWS_SCOPE_KEYS.has(f.key) || f.negate || f.values.some((v) => v.wildcard)
|
|
1146
|
+
);
|
|
1147
|
+
if (unsupported.length > 0) {
|
|
1148
|
+
return {
|
|
1149
|
+
...empty,
|
|
1150
|
+
error: `Unsupported rows filter(s): ${unsupported.map((f) => (f.negate ? "!" : "") + f.key).join(", ")}. Rows queries support exact result:, block:, note: values.`
|
|
1151
|
+
};
|
|
1152
|
+
}
|
|
1153
|
+
const scopeValues = (key) => parsed.filters.filter((f) => f.key === key).flatMap((f) => f.values.map((v) => v.value));
|
|
1154
|
+
const resultIds = scopeValues("result");
|
|
1155
|
+
const blockIds = scopeValues("block");
|
|
1156
|
+
const noteIds = scopeValues("note");
|
|
1157
|
+
if (resultIds.length + blockIds.length + noteIds.length === 0) {
|
|
1158
|
+
return { ...empty, error: "Rows query needs a scope: result:, block:, or note:." };
|
|
1159
|
+
}
|
|
1160
|
+
const byId = /* @__PURE__ */ new Map();
|
|
1161
|
+
for (const id of resultIds) {
|
|
1162
|
+
const r = await this.resultStore.getResultById(id);
|
|
1163
|
+
if (r) byId.set(r.id, r);
|
|
1164
|
+
}
|
|
1165
|
+
for (const blockContentId of blockIds) {
|
|
1166
|
+
for (const r of await this.resultStore.getResultsByContentId(blockContentId)) byId.set(r.id, r);
|
|
1167
|
+
}
|
|
1168
|
+
for (const noteId of noteIds) {
|
|
1169
|
+
for (const r of await this.resultStore.getResultsForNote(noteId)) byId.set(r.id, r);
|
|
1170
|
+
}
|
|
1171
|
+
let results = [...byId.values()].filter((r) => r.data?.logs?.length);
|
|
1172
|
+
if (parsed.last) {
|
|
1173
|
+
results = results.filter(
|
|
1174
|
+
(r) => effectiveTimeWindow(r.data.endTime ?? r.createdAt ?? 0, parsed.last, void 0, options2.anchorNow)
|
|
1175
|
+
);
|
|
1176
|
+
}
|
|
1177
|
+
results.sort((a, b) => (b.data.endTime ?? b.createdAt) - (a.data.endTime ?? a.createdAt));
|
|
1178
|
+
const runs = results.map((result) => ({
|
|
1179
|
+
result,
|
|
1180
|
+
logs: parsed.outputType ? result.data.logs.filter((l) => l.outputType === parsed.outputType) : result.data.logs
|
|
1181
|
+
})).filter((run) => run.logs.length > 0);
|
|
1182
|
+
return { parsed, runs };
|
|
1183
|
+
}
|
|
1184
|
+
/**
|
|
1185
|
+
* Execute a content-discovery query (find:note). Naive in-memory filtering
|
|
1186
|
+
* per the tracer-bullet scope (#797): load all notes, then apply tag/text/
|
|
1187
|
+
* time filters.
|
|
1188
|
+
*/
|
|
1189
|
+
async runFind(parsed, options2 = {}) {
|
|
1190
|
+
if (parsed.error) {
|
|
1191
|
+
return { parsed, notes: [], blocks: [], stages: { selected: 0, matched: 0 } };
|
|
1192
|
+
}
|
|
1193
|
+
if (parsed.target === "block") {
|
|
1194
|
+
return this.runFindBlock(parsed, options2);
|
|
1195
|
+
}
|
|
1196
|
+
if (parsed.target === "effort") {
|
|
1197
|
+
return this.runFindEffort(parsed);
|
|
1198
|
+
}
|
|
1199
|
+
let notes = [];
|
|
1200
|
+
const scope = parsed.scope || "journal";
|
|
1201
|
+
if (scope === "journal" || scope === "all") {
|
|
1202
|
+
notes = notes.concat(await this.noteStore.getAllNotes());
|
|
1203
|
+
}
|
|
1204
|
+
if (scope === "collections" || scope === "feeds" || scope === "all") {
|
|
1205
|
+
let sNotes = this.staticNoteStore ? await this.staticNoteStore.getAllNotes() : [];
|
|
1206
|
+
if (scope === "collections") sNotes = sNotes.filter((n) => n.sourceId?.startsWith("collection:"));
|
|
1207
|
+
if (scope === "feeds") sNotes = sNotes.filter((n) => n.sourceId?.startsWith("feed:"));
|
|
1208
|
+
notes = notes.concat(sNotes);
|
|
1209
|
+
}
|
|
1210
|
+
const selectedCount = notes.length;
|
|
1211
|
+
const anchorNow = windowAnchor(notes, parsed, options2);
|
|
1212
|
+
notes = applySourceFilter(notes, parsed.filters);
|
|
1213
|
+
for (const filter of parsed.filters) {
|
|
1214
|
+
if (filter.key === "tags" && !filter.negate) {
|
|
1215
|
+
const matchingIds = /* @__PURE__ */ new Set();
|
|
1216
|
+
for (const v of filter.values) {
|
|
1217
|
+
const ids = await this.noteStore.getNoteIdsForTag(v.value);
|
|
1218
|
+
const sIds = this.staticNoteStore ? await this.staticNoteStore.getNoteIdsForTag(v.value) : /* @__PURE__ */ new Set();
|
|
1219
|
+
ids.forEach((id) => matchingIds.add(id));
|
|
1220
|
+
sIds.forEach((id) => matchingIds.add(id));
|
|
1221
|
+
}
|
|
1222
|
+
notes = notes.filter((n) => matchingIds.has(n.id));
|
|
1223
|
+
} else if (filter.key === "tags" && filter.negate) {
|
|
1224
|
+
for (const v of filter.values) {
|
|
1225
|
+
const ids = await this.noteStore.getNoteIdsForTag(v.value);
|
|
1226
|
+
const sIds = this.staticNoteStore ? await this.staticNoteStore.getNoteIdsForTag(v.value) : /* @__PURE__ */ new Set();
|
|
1227
|
+
notes = notes.filter((n) => !ids.has(n.id) && !sIds.has(n.id));
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
for (const filter of parsed.filters) {
|
|
1232
|
+
if (filter.key === "text" && !filter.negate) {
|
|
1233
|
+
const search = filter.values.map((v) => v.value).join(" ").toLowerCase();
|
|
1234
|
+
notes = notes.filter((n) => n.title.toLowerCase().includes(search));
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
for (const filter of parsed.filters) {
|
|
1238
|
+
if (filter.key === "type" && !filter.negate) {
|
|
1239
|
+
const wanted = new Set(filter.values.map((v) => v.value));
|
|
1240
|
+
notes = notes.filter((n) => n.type && wanted.has(n.type));
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
for (const filter of parsed.filters) {
|
|
1244
|
+
if (filter.key === "catalog") {
|
|
1245
|
+
const wanted = new Set(filter.values.map((v) => v.value));
|
|
1246
|
+
notes = notes.filter((n) => {
|
|
1247
|
+
const cat = catalogOfItem(n);
|
|
1248
|
+
if (!cat) return filter.negate;
|
|
1249
|
+
const hit = wanted.has(cat);
|
|
1250
|
+
return filter.negate ? !hit : hit;
|
|
1251
|
+
});
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
if (parsed.last || options2.range) {
|
|
1255
|
+
notes = notes.filter((n) => effectiveTimeWindow(n.createdAt, parsed.last, options2.range, anchorNow));
|
|
1256
|
+
}
|
|
1257
|
+
if (parsed.join) {
|
|
1258
|
+
const joined = await this.applyMetricJoin(parsed, notes, []);
|
|
1259
|
+
notes = joined.notes;
|
|
1260
|
+
}
|
|
1261
|
+
return { parsed, notes, blocks: [], stages: { selected: selectedCount, matched: notes.length } };
|
|
1262
|
+
}
|
|
1263
|
+
/**
|
|
1264
|
+
* Execute a find:block query against the derived block_index store.
|
|
1265
|
+
*/
|
|
1266
|
+
async runFindBlock(parsed, options2 = {}) {
|
|
1267
|
+
let blocks = [];
|
|
1268
|
+
const scope = parsed.scope || "journal";
|
|
1269
|
+
if (scope === "journal" || scope === "all") {
|
|
1270
|
+
blocks = blocks.concat(await this.blockStore.getAllBlocks());
|
|
1271
|
+
}
|
|
1272
|
+
if (scope === "collections" || scope === "feeds" || scope === "all") {
|
|
1273
|
+
let sBlocks = this.staticBlockStore ? await this.staticBlockStore.getAllBlocks() : [];
|
|
1274
|
+
if (scope === "collections") sBlocks = sBlocks.filter((b) => b.sourceId?.startsWith("collection:"));
|
|
1275
|
+
if (scope === "feeds") sBlocks = sBlocks.filter((b) => b.sourceId?.startsWith("feed:"));
|
|
1276
|
+
blocks = blocks.concat(sBlocks);
|
|
1277
|
+
}
|
|
1278
|
+
const selectedCount = blocks.length;
|
|
1279
|
+
const anchorNow = windowAnchor(blocks, parsed, options2);
|
|
1280
|
+
blocks = applySourceFilter(blocks, parsed.filters);
|
|
1281
|
+
for (const filter of parsed.filters) {
|
|
1282
|
+
if (filter.key === "text" && !filter.negate) {
|
|
1283
|
+
const search = filter.values.map((v) => v.value).join(" ").toLowerCase();
|
|
1284
|
+
blocks = blocks.filter((b) => b.rawContent.toLowerCase().includes(search));
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
for (const filter of parsed.filters) {
|
|
1288
|
+
if (filter.key === "type" && !filter.negate) {
|
|
1289
|
+
const wanted = new Set(filter.values.map((v) => v.value));
|
|
1290
|
+
blocks = blocks.filter((b) => wanted.has(b.dataType));
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
for (const filter of parsed.filters) {
|
|
1294
|
+
if (filter.key === "catalog") {
|
|
1295
|
+
const wanted = new Set(filter.values.map((v) => v.value));
|
|
1296
|
+
blocks = blocks.filter((b) => {
|
|
1297
|
+
const cat = catalogOfItem(b);
|
|
1298
|
+
if (!cat) return filter.negate;
|
|
1299
|
+
const hit = wanted.has(cat);
|
|
1300
|
+
return filter.negate ? !hit : hit;
|
|
1301
|
+
});
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
for (const filter of parsed.filters) {
|
|
1305
|
+
if (filter.key === "tags" && !filter.negate) {
|
|
1306
|
+
const matchingNoteIds = /* @__PURE__ */ new Set();
|
|
1307
|
+
for (const v of filter.values) {
|
|
1308
|
+
const ids = await this.noteStore.getNoteIdsForTag(v.value);
|
|
1309
|
+
const sIds = this.staticNoteStore ? await this.staticNoteStore.getNoteIdsForTag(v.value) : /* @__PURE__ */ new Set();
|
|
1310
|
+
ids.forEach((id) => matchingNoteIds.add(id));
|
|
1311
|
+
sIds.forEach((id) => matchingNoteIds.add(id));
|
|
1312
|
+
}
|
|
1313
|
+
blocks = blocks.filter((b) => matchingNoteIds.has(b.noteId));
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
if (parsed.last || options2.range) {
|
|
1317
|
+
blocks = blocks.filter((b) => effectiveTimeWindow(b.createdAt, parsed.last, options2.range, anchorNow));
|
|
1318
|
+
}
|
|
1319
|
+
if (parsed.join) {
|
|
1320
|
+
const joined = await this.applyMetricJoin(parsed, [], blocks);
|
|
1321
|
+
blocks = joined.blocks;
|
|
1322
|
+
}
|
|
1323
|
+
return { parsed, notes: [], blocks, stages: { selected: selectedCount, matched: blocks.length } };
|
|
1324
|
+
}
|
|
1325
|
+
/**
|
|
1326
|
+
* Execute a find:effort query against the effort registry.
|
|
1327
|
+
*/
|
|
1328
|
+
async runFindEffort(parsed) {
|
|
1329
|
+
const all = await this.effortStore.getAllEfforts();
|
|
1330
|
+
const selectedCount = all.length;
|
|
1331
|
+
const matches = (effort, key, value, wildcard) => {
|
|
1332
|
+
const needle = value.toLowerCase();
|
|
1333
|
+
switch (key) {
|
|
1334
|
+
case "effort":
|
|
1335
|
+
if (wildcard) {
|
|
1336
|
+
return effort.slug.includes(needle) || effort.label.toLowerCase().includes(needle) || effort.aliases.some((a) => a.toLowerCase().includes(needle));
|
|
1337
|
+
}
|
|
1338
|
+
return effort.slug === value || effort.label.toLowerCase() === needle || effort.aliases.some((a) => a.toLowerCase() === needle);
|
|
1339
|
+
case "discipline":
|
|
1340
|
+
return effort.baseAttributes.discipline === value;
|
|
1341
|
+
case "intensity":
|
|
1342
|
+
return effort.baseAttributes.intensityTier === value;
|
|
1343
|
+
case "origin":
|
|
1344
|
+
return effort.registrySource === value;
|
|
1345
|
+
case "text":
|
|
1346
|
+
return effort.label.toLowerCase().includes(needle) || effort.slug.includes(needle) || effort.aliases.some((a) => a.toLowerCase().includes(needle));
|
|
1347
|
+
default:
|
|
1348
|
+
return true;
|
|
1349
|
+
}
|
|
1350
|
+
};
|
|
1351
|
+
let efforts = all;
|
|
1352
|
+
for (const filter of parsed.filters) {
|
|
1353
|
+
if (!["effort", "discipline", "intensity", "origin", "text"].includes(filter.key)) continue;
|
|
1354
|
+
efforts = efforts.filter((effort) => {
|
|
1355
|
+
const hit = filter.values.some((v) => matches(effort, filter.key, v.value, v.wildcard));
|
|
1356
|
+
return filter.negate ? !hit : hit;
|
|
1357
|
+
});
|
|
1358
|
+
}
|
|
1359
|
+
return { parsed, notes: [], blocks: [], efforts, stages: { selected: selectedCount, matched: efforts.length } };
|
|
1360
|
+
}
|
|
1361
|
+
async run(parsed, options2 = {}) {
|
|
1362
|
+
const empty = {
|
|
1363
|
+
parsed,
|
|
1364
|
+
series: [],
|
|
1365
|
+
stages: { selected: 0, buckets: 0, aggregated: 0, groups: 0 },
|
|
1366
|
+
matched: []
|
|
1367
|
+
};
|
|
1368
|
+
if (parsed.error) return empty;
|
|
1369
|
+
if (parsed.join) return this.runJoined(parsed, options2);
|
|
1370
|
+
const byMetric = await this.store.getFactsByMetric(parsed.metric);
|
|
1371
|
+
let candidates = byMetric;
|
|
1372
|
+
if (options2.rangeStart !== void 0 || options2.rangeEnd !== void 0) {
|
|
1373
|
+
const start = options2.rangeStart ?? 0;
|
|
1374
|
+
const end = options2.rangeEnd ?? Number.MAX_SAFE_INTEGER;
|
|
1375
|
+
const inRange = new Set((await this.store.getFactsByTimeRange(start, end)).map((row) => row.id));
|
|
1376
|
+
candidates = candidates.filter((row) => inRange.has(row.id));
|
|
1377
|
+
}
|
|
1378
|
+
const touchesTags = parsed.filters.some((f) => f.key === "tags") || parsed.groupBy.includes("tags");
|
|
1379
|
+
const noteTags = await this.loadNoteTags(candidates, touchesTags);
|
|
1380
|
+
const matched = this.applyEffortScope(
|
|
1381
|
+
candidates.filter((row) => matchesFilters(row, parsed.filters, noteTags)),
|
|
1382
|
+
parsed
|
|
1383
|
+
);
|
|
1384
|
+
return this.buildResult(matched, parsed, options2, noteTags);
|
|
1385
|
+
}
|
|
1386
|
+
/**
|
|
1387
|
+
* Stages 2–4 — BUCKET → GROUP → AGGREGATE over an already-selected +
|
|
1388
|
+
* filtered `matched` set.
|
|
1389
|
+
*/
|
|
1390
|
+
buildResult(matched, parsed, options2, noteTags) {
|
|
1391
|
+
const timeDim = parsed.groupBy.find((d) => d === "day" || d === "week");
|
|
1392
|
+
const tagDims = parsed.groupBy.filter((d) => d !== "day" && d !== "week");
|
|
1393
|
+
const bucketMs = timeDim ? (timeDim === "week" ? 7 : 1) * DAY : parsed.rollup ? parsed.rollup.size * (parsed.rollup.unit === "w" ? 7 : 1) * DAY : null;
|
|
1394
|
+
const bucketKey = (ts) => bucketMs ? Math.floor(ts / bucketMs) : 0;
|
|
1395
|
+
const bucketCount = bucketMs ? new Set(matched.map((p) => bucketKey(p.timestamp))).size : matched.length ? 1 : 0;
|
|
1396
|
+
const { unit: targetUnit, convert: shouldConvert } = resolveDisplayUnit(matched, {
|
|
1397
|
+
directive: parsed.displayUnit,
|
|
1398
|
+
preferred: options2.preferredUnit
|
|
1399
|
+
});
|
|
1400
|
+
const groups = /* @__PURE__ */ new Map();
|
|
1401
|
+
for (const row of matched) {
|
|
1402
|
+
const key = tagDims.length ? tagDims.map((d) => dimValue(row, d, noteTags)).join(" \xB7 ") : parsed.metric;
|
|
1403
|
+
const bucket = groups.get(key);
|
|
1404
|
+
if (bucket) bucket.push(row);
|
|
1405
|
+
else groups.set(key, [row]);
|
|
1406
|
+
}
|
|
1407
|
+
const series = [...groups.entries()].map(([key, rows]) => {
|
|
1408
|
+
const byBucket = /* @__PURE__ */ new Map();
|
|
1409
|
+
for (const row of rows) {
|
|
1410
|
+
const b = bucketKey(row.timestamp);
|
|
1411
|
+
const members = byBucket.get(b);
|
|
1412
|
+
if (members) members.push(row);
|
|
1413
|
+
else byBucket.set(b, [row]);
|
|
1414
|
+
}
|
|
1415
|
+
const points = [...byBucket.entries()].sort(([a], [b]) => a - b).map(([b, members]) => {
|
|
1416
|
+
const values = members.map(
|
|
1417
|
+
(m) => toDisplayValue(m.value, m.unit ?? m.metricUnit, shouldConvert ? targetUnit : void 0)
|
|
1418
|
+
);
|
|
1419
|
+
return {
|
|
1420
|
+
ts: bucketMs ? b * bucketMs + bucketMs / 2 : Math.min(...members.map((m) => m.timestamp)),
|
|
1421
|
+
value: Math.round(aggregate(values, parsed.agg, members, shouldConvert ? targetUnit : void 0) * 100) / 100
|
|
1422
|
+
};
|
|
1423
|
+
});
|
|
1424
|
+
const seriesUnit = shouldConvert ? targetUnit : rows[0]?.unit ?? rows[0]?.metricUnit;
|
|
1425
|
+
return { key, label: key, points, unit: seriesUnit };
|
|
1426
|
+
});
|
|
1427
|
+
const aggregated = series.reduce((n, s) => n + s.points.length, 0);
|
|
1428
|
+
const scalar = series.length === 1 && series[0].points.length === 1 ? series[0].points[0].value : void 0;
|
|
1429
|
+
const resultUnit = series.length > 0 ? series[0].unit : void 0;
|
|
1430
|
+
return {
|
|
1431
|
+
parsed,
|
|
1432
|
+
series,
|
|
1433
|
+
stages: { selected: matched.length, buckets: bucketCount, aggregated, groups: series.length },
|
|
1434
|
+
matched,
|
|
1435
|
+
scalar,
|
|
1436
|
+
unit: resultUnit
|
|
1437
|
+
};
|
|
1438
|
+
}
|
|
1439
|
+
/** Direction 2 — re-derive the metric from raw logs, restricted to the
|
|
1440
|
+
* blockContentIds owned by the find predicate's content matches. */
|
|
1441
|
+
async runJoined(parsed, options2) {
|
|
1442
|
+
const empty = {
|
|
1443
|
+
parsed,
|
|
1444
|
+
series: [],
|
|
1445
|
+
stages: { selected: 0, buckets: 0, aggregated: 0, groups: 0 },
|
|
1446
|
+
matched: []
|
|
1447
|
+
};
|
|
1448
|
+
const join = parsed.join;
|
|
1449
|
+
const findResult = await this.runFind({
|
|
1450
|
+
raw: "",
|
|
1451
|
+
target: join.target,
|
|
1452
|
+
filters: join.filters,
|
|
1453
|
+
scope: join.scope,
|
|
1454
|
+
last: join.last
|
|
1455
|
+
});
|
|
1456
|
+
const contentIds = await this.contentIdsFromFindResult(findResult);
|
|
1457
|
+
if (contentIds.size === 0) return empty;
|
|
1458
|
+
let facts = await this.deriveMetricFacts(contentIds, parsed.metric);
|
|
1459
|
+
if (facts.length === 0) return empty;
|
|
1460
|
+
if (options2.rangeStart !== void 0 || options2.rangeEnd !== void 0) {
|
|
1461
|
+
const start = options2.rangeStart ?? 0;
|
|
1462
|
+
const end = options2.rangeEnd ?? Number.MAX_SAFE_INTEGER;
|
|
1463
|
+
facts = facts.filter((f) => f.timestamp >= start && f.timestamp <= end);
|
|
1464
|
+
}
|
|
1465
|
+
const touchesTags = parsed.filters.some((f) => f.key === "tags") || parsed.groupBy.includes("tags");
|
|
1466
|
+
const noteTags = await this.loadNoteTags(facts, touchesTags);
|
|
1467
|
+
const matched = this.applyEffortScope(
|
|
1468
|
+
facts.filter((f) => matchesFilters(f, parsed.filters, noteTags)),
|
|
1469
|
+
parsed
|
|
1470
|
+
);
|
|
1471
|
+
return this.buildResult(matched, parsed, options2, noteTags);
|
|
1472
|
+
}
|
|
1473
|
+
/** Direction 1 — keep only content owning a wod block whose raw-log metric
|
|
1474
|
+
* aggregate satisfies the predicate. `notes` for find:note, `blocks` for
|
|
1475
|
+
* find:block. */
|
|
1476
|
+
async applyMetricJoin(parsed, notes, blocks) {
|
|
1477
|
+
const join = parsed.join;
|
|
1478
|
+
const noteToContent = await this.noteContentMap(new Set(notes.map((n) => n.id)));
|
|
1479
|
+
const candidateIds = /* @__PURE__ */ new Set();
|
|
1480
|
+
for (const set of noteToContent.values()) for (const id of set) candidateIds.add(id);
|
|
1481
|
+
for (const b of blocks) if (b.dataType === "wod" && b.blockContentId) candidateIds.add(b.blockContentId);
|
|
1482
|
+
const passing = await this.contentIdsSatisfying(candidateIds, join);
|
|
1483
|
+
blocks = blocks.filter((b) => b.dataType === "wod" && !!b.blockContentId && passing.has(b.blockContentId));
|
|
1484
|
+
notes = notes.filter((n) => {
|
|
1485
|
+
const cids = noteToContent.get(n.id);
|
|
1486
|
+
return !!cids && [...cids].some((id) => passing.has(id));
|
|
1487
|
+
});
|
|
1488
|
+
return { notes, blocks };
|
|
1489
|
+
}
|
|
1490
|
+
/** Re-derive the metric from raw logs for each content id, aggregate per id,
|
|
1491
|
+
* and return the ids whose aggregate satisfies the join predicate. */
|
|
1492
|
+
async contentIdsSatisfying(contentIds, join) {
|
|
1493
|
+
const facts = await this.deriveMetricFacts(contentIds, join.metric);
|
|
1494
|
+
const noteTags = await this.loadNoteTags(facts, join.filters.some((f) => f.key === "tags"));
|
|
1495
|
+
const filtered = this.applyEffortScope(
|
|
1496
|
+
facts.filter((f) => matchesFilters(f, join.filters, noteTags)),
|
|
1497
|
+
{ filters: join.filters, groupBy: [] }
|
|
1498
|
+
);
|
|
1499
|
+
const byContent = /* @__PURE__ */ new Map();
|
|
1500
|
+
for (const f of filtered) {
|
|
1501
|
+
const cid = f.blockContentId ?? "";
|
|
1502
|
+
const arr = byContent.get(cid);
|
|
1503
|
+
if (arr) arr.push(f);
|
|
1504
|
+
else byContent.set(cid, [f]);
|
|
1505
|
+
}
|
|
1506
|
+
const passing = /* @__PURE__ */ new Set();
|
|
1507
|
+
for (const [cid, rows] of byContent) {
|
|
1508
|
+
const values = rows.map((r) => r.value);
|
|
1509
|
+
if (compareOp(aggregate(values, join.agg, rows, void 0), join.operator, join.threshold)) passing.add(cid);
|
|
1510
|
+
}
|
|
1511
|
+
return passing;
|
|
1512
|
+
}
|
|
1513
|
+
/** Filter per-effort vs un-attributed overall summary rows to avoid
|
|
1514
|
+
* double-counting. */
|
|
1515
|
+
applyEffortScope(matched, scope) {
|
|
1516
|
+
if (scope.groupBy.includes("effort") || scope.filters.some((f) => f.key === "effort")) {
|
|
1517
|
+
return matched.some((r) => r.effortSlug !== void 0) ? matched.filter((r) => r.effortSlug !== void 0) : matched;
|
|
1518
|
+
}
|
|
1519
|
+
return matched.some((r) => r.effortSlug === void 0) ? matched.filter((r) => r.effortSlug === void 0) : matched;
|
|
1520
|
+
}
|
|
1521
|
+
/** Re-derive summary facts for one Canonical Metric Key from RAW WorkoutResult
|
|
1522
|
+
* logs across the given content ids — the cross-store join source. One fact
|
|
1523
|
+
* row per result (normalizeSummaryFacts dedupes within a result). */
|
|
1524
|
+
async deriveMetricFacts(contentIds, metricKey) {
|
|
1525
|
+
const out = [];
|
|
1526
|
+
const ids = [...new Set(contentIds)];
|
|
1527
|
+
await Promise.all(ids.map(async (blockContentId) => {
|
|
1528
|
+
const results = await this.resultStore.getResultsByContentId(blockContentId);
|
|
1529
|
+
for (const result of results) {
|
|
1530
|
+
const facts = normalizeSummaryFacts(result.data.logs ?? [], {
|
|
1531
|
+
noteId: result.noteId,
|
|
1532
|
+
resultId: result.id,
|
|
1533
|
+
segmentId: result.segmentId,
|
|
1534
|
+
segmentVersion: result.segmentVersion,
|
|
1535
|
+
blockContentId,
|
|
1536
|
+
origin: result.origin,
|
|
1537
|
+
pageId: result.pageId,
|
|
1538
|
+
workoutTimestamp: result.createdAt
|
|
1539
|
+
});
|
|
1540
|
+
for (const f of facts) if (f.metricKey === metricKey) out.push(f);
|
|
1541
|
+
}
|
|
1542
|
+
}));
|
|
1543
|
+
return out;
|
|
1544
|
+
}
|
|
1545
|
+
/** All content blocks across the journal + static corpus. */
|
|
1546
|
+
async allContentBlocks() {
|
|
1547
|
+
const blocks = await this.blockStore.getAllBlocks();
|
|
1548
|
+
const staticBlocks = this.staticBlockStore ? await this.staticBlockStore.getAllBlocks() : [];
|
|
1549
|
+
return blocks.concat(staticBlocks);
|
|
1550
|
+
}
|
|
1551
|
+
/** Map each note id to the wod blockContentIds it owns. */
|
|
1552
|
+
async noteContentMap(noteIds) {
|
|
1553
|
+
const map = /* @__PURE__ */ new Map();
|
|
1554
|
+
if (!noteIds.size) return map;
|
|
1555
|
+
const allBlocks = await this.allContentBlocks();
|
|
1556
|
+
for (const b of allBlocks) {
|
|
1557
|
+
if (b.dataType !== "wod" || !b.blockContentId || !noteIds.has(b.noteId)) continue;
|
|
1558
|
+
let set = map.get(b.noteId);
|
|
1559
|
+
if (!set) {
|
|
1560
|
+
set = /* @__PURE__ */ new Set();
|
|
1561
|
+
map.set(b.noteId, set);
|
|
1562
|
+
}
|
|
1563
|
+
set.add(b.blockContentId);
|
|
1564
|
+
}
|
|
1565
|
+
return map;
|
|
1566
|
+
}
|
|
1567
|
+
/** Collect wod blockContentIds owned by a find query's content matches. */
|
|
1568
|
+
async contentIdsFromFindResult(findResult) {
|
|
1569
|
+
const ids = /* @__PURE__ */ new Set();
|
|
1570
|
+
for (const b of findResult.blocks) if (b.dataType === "wod" && b.blockContentId) ids.add(b.blockContentId);
|
|
1571
|
+
if (findResult.notes.length) {
|
|
1572
|
+
const noteIds = new Set(findResult.notes.map((n) => n.id));
|
|
1573
|
+
const noteMap = await this.noteContentMap(noteIds);
|
|
1574
|
+
for (const set of noteMap.values()) for (const id of set) ids.add(id);
|
|
1575
|
+
}
|
|
1576
|
+
return ids;
|
|
1577
|
+
}
|
|
1578
|
+
/** Load note tag labels only when the query touches 'tags'. */
|
|
1579
|
+
async loadNoteTags(rows, touchesTags) {
|
|
1580
|
+
const noteTags = /* @__PURE__ */ new Map();
|
|
1581
|
+
if (!touchesTags) return noteTags;
|
|
1582
|
+
const noteIds = [...new Set(rows.map((r) => r.noteId))];
|
|
1583
|
+
await Promise.all(noteIds.map(async (id) => noteTags.set(id, await this.store.getNoteTagLabels(id))));
|
|
1584
|
+
return noteTags;
|
|
1585
|
+
}
|
|
1586
|
+
};
|
|
1587
|
+
|
|
1588
|
+
// src/dashboard/model.ts
|
|
1589
|
+
var DASHBOARD_WIDGET_TYPES = [
|
|
1590
|
+
"table",
|
|
1591
|
+
"value",
|
|
1592
|
+
"timeseries",
|
|
1593
|
+
"bar",
|
|
1594
|
+
"toplist",
|
|
1595
|
+
"stacked-bar",
|
|
1596
|
+
"goal-rings",
|
|
1597
|
+
"zone-distribution"
|
|
1598
|
+
];
|
|
1599
|
+
function isDashboardWidgetType(type) {
|
|
1600
|
+
return DASHBOARD_WIDGET_TYPES.includes(type);
|
|
1601
|
+
}
|
|
1602
|
+
function resolveWidgetType(type) {
|
|
1603
|
+
return type == null || type === "" ? "table" : type;
|
|
1604
|
+
}
|
|
1605
|
+
function unknownWidgetTypeMessage(type) {
|
|
1606
|
+
return `unknown widget type "${type}"`;
|
|
1607
|
+
}
|
|
1608
|
+
function unknownTokensMessage(missing) {
|
|
1609
|
+
return `unknown token${missing.length > 1 ? "s" : ""}: ${missing.map((m) => `$${m}`).join(", ")}`;
|
|
1610
|
+
}
|
|
1611
|
+
var PLANNED_WIDGET_TYPES = [];
|
|
1612
|
+
var KNOWN_CALC_METRICS = new Set(WQL_CALC_TARGETS);
|
|
1613
|
+
function isProposedMetric(metricKey) {
|
|
1614
|
+
if (!metricKey) return false;
|
|
1615
|
+
if (!metricKey.startsWith("calc.")) return false;
|
|
1616
|
+
return !KNOWN_CALC_METRICS.has(metricKey);
|
|
1617
|
+
}
|
|
1618
|
+
var DASHBOARD_GRID_MAX_COLS = 4;
|
|
1619
|
+
function parseQueryWidgetSuffix(suffix) {
|
|
1620
|
+
if (suffix === "") return { type: "", error: 'missing widget type after "query:"' };
|
|
1621
|
+
let rest = suffix.toLowerCase();
|
|
1622
|
+
let spanCols;
|
|
1623
|
+
let spanFull;
|
|
1624
|
+
if (rest.endsWith("-full")) {
|
|
1625
|
+
spanFull = true;
|
|
1626
|
+
rest = rest.slice(0, -"-full".length);
|
|
1627
|
+
} else {
|
|
1628
|
+
const spanMatch = rest.match(/-(\d+)$/);
|
|
1629
|
+
if (spanMatch) {
|
|
1630
|
+
spanCols = Number(spanMatch[1]);
|
|
1631
|
+
rest = rest.slice(0, -spanMatch[0].length);
|
|
1632
|
+
}
|
|
1633
|
+
}
|
|
1634
|
+
if (rest === "" || !/^[a-z][a-z0-9-]*$/.test(rest)) {
|
|
1635
|
+
return { type: rest, spanCols, spanFull, error: `malformed widget type "${suffix}"` };
|
|
1636
|
+
}
|
|
1637
|
+
if (/-(\d+|full)$/.test(rest)) {
|
|
1638
|
+
return { type: rest, spanCols, spanFull, error: `malformed widget suffix "${suffix}"` };
|
|
1639
|
+
}
|
|
1640
|
+
if (spanCols != null && (spanCols < 1 || spanCols > DASHBOARD_GRID_MAX_COLS)) {
|
|
1641
|
+
return { type: rest, spanCols, spanFull, error: `span ${spanCols} outside 1..${DASHBOARD_GRID_MAX_COLS}` };
|
|
1642
|
+
}
|
|
1643
|
+
return { type: rest, spanCols, spanFull };
|
|
1644
|
+
}
|
|
1645
|
+
var TOKEN_PREFIX = "dashboard.";
|
|
1646
|
+
function isDashboardMeta(meta) {
|
|
1647
|
+
return meta["dashboard"] === "true" || meta["dashboard"] === true;
|
|
1648
|
+
}
|
|
1649
|
+
function extractDashboardTokens(meta) {
|
|
1650
|
+
const tokens = [];
|
|
1651
|
+
for (const [key, value] of Object.entries(meta)) {
|
|
1652
|
+
if (!key.startsWith(TOKEN_PREFIX) || key === TOKEN_PREFIX) continue;
|
|
1653
|
+
const name = key.slice(TOKEN_PREFIX.length);
|
|
1654
|
+
if (name === "") continue;
|
|
1655
|
+
if (Array.isArray(value)) {
|
|
1656
|
+
if (value.length > 0) tokens.push({ name, values: value.map(String), isList: true });
|
|
1657
|
+
} else {
|
|
1658
|
+
tokens.push({ name, values: [String(value)], isList: false });
|
|
1659
|
+
}
|
|
1660
|
+
}
|
|
1661
|
+
return tokens;
|
|
1662
|
+
}
|
|
1663
|
+
function defaultTokenValues(tokens) {
|
|
1664
|
+
const values = {};
|
|
1665
|
+
for (const token of tokens) values[token.name] = token.values[0];
|
|
1666
|
+
return values;
|
|
1667
|
+
}
|
|
1668
|
+
function setDashboardTokenValue(meta, name, value) {
|
|
1669
|
+
const key = `${TOKEN_PREFIX}${name}`;
|
|
1670
|
+
const current = meta[key];
|
|
1671
|
+
const next = { ...meta };
|
|
1672
|
+
if (Array.isArray(current)) {
|
|
1673
|
+
if (!current.includes(value)) return meta;
|
|
1674
|
+
next[key] = [value, ...current.filter((v) => v !== value)];
|
|
1675
|
+
} else if (current != null) {
|
|
1676
|
+
next[key] = typeof current === "number" && !isNaN(Number(value)) ? Number(value) : value;
|
|
1677
|
+
} else {
|
|
1678
|
+
next[key] = value;
|
|
1679
|
+
}
|
|
1680
|
+
return next;
|
|
1681
|
+
}
|
|
1682
|
+
var TOKEN_REF_RE = /\$([A-Za-z][\w-]*)/g;
|
|
1683
|
+
function substituteTokens(query, values) {
|
|
1684
|
+
const missing = /* @__PURE__ */ new Set();
|
|
1685
|
+
const substituted = query.replace(TOKEN_REF_RE, (raw, name) => {
|
|
1686
|
+
const value = values[name];
|
|
1687
|
+
if (value == null) {
|
|
1688
|
+
missing.add(name);
|
|
1689
|
+
return raw;
|
|
1690
|
+
}
|
|
1691
|
+
return value;
|
|
1692
|
+
});
|
|
1693
|
+
return { query: substituted, missing: [...missing] };
|
|
1694
|
+
}
|
|
1695
|
+
function referencedTokens(query) {
|
|
1696
|
+
const names = /* @__PURE__ */ new Set();
|
|
1697
|
+
for (const match of query.matchAll(TOKEN_REF_RE)) names.add(match[1]);
|
|
1698
|
+
return [...names];
|
|
1699
|
+
}
|
|
1700
|
+
function splitWidgetBody(body) {
|
|
1701
|
+
const trimmed = body.trim();
|
|
1702
|
+
const sep = trimmed.indexOf(" / ");
|
|
1703
|
+
if (sep === -1) return { query: trimmed, params: [] };
|
|
1704
|
+
const query = trimmed.slice(0, sep).trim();
|
|
1705
|
+
const params = trimmed.slice(sep + 3).split(/\s+/).map((p) => p.trim()).filter((p) => p.length > 0);
|
|
1706
|
+
return { query, params };
|
|
1707
|
+
}
|
|
1708
|
+
function headingText(content) {
|
|
1709
|
+
const line = content.split("\n", 1)[0].replace(/^#{1,6}\s*/, "").trim();
|
|
1710
|
+
return line === "" ? void 0 : line;
|
|
1711
|
+
}
|
|
1712
|
+
function paragraphText(content) {
|
|
1713
|
+
const text = content.replace(/\s*\n\s*/g, " ").trim();
|
|
1714
|
+
return text === "" ? void 0 : text;
|
|
1715
|
+
}
|
|
1716
|
+
function buildDashboardDocument(sections, meta) {
|
|
1717
|
+
const widgets = [];
|
|
1718
|
+
sections.forEach((section, i) => {
|
|
1719
|
+
if (section.type !== "query") return;
|
|
1720
|
+
const bodyLine = section.content.split("\n").map((l) => l.trim()).find((l) => l !== "" && !l.startsWith("#")) ?? "";
|
|
1721
|
+
const { query, params } = splitWidgetBody(bodyLine);
|
|
1722
|
+
let title;
|
|
1723
|
+
let question;
|
|
1724
|
+
const above = sections[i - 1];
|
|
1725
|
+
if (above?.type === "markdown" && above.subtype === "paragraph") {
|
|
1726
|
+
question = paragraphText(above.content);
|
|
1727
|
+
const heading = sections[i - 2];
|
|
1728
|
+
if (heading?.type === "markdown" && heading.subtype === "heading") {
|
|
1729
|
+
title = headingText(heading.content);
|
|
1730
|
+
}
|
|
1731
|
+
} else if (above?.type === "markdown" && above.subtype === "heading") {
|
|
1732
|
+
title = headingText(above.content);
|
|
1733
|
+
}
|
|
1734
|
+
widgets.push({
|
|
1735
|
+
key: `w${widgets.length}`,
|
|
1736
|
+
type: section.widgetType ?? "",
|
|
1737
|
+
spanCols: section.spanCols,
|
|
1738
|
+
spanFull: section.spanFull,
|
|
1739
|
+
widgetError: section.widgetError,
|
|
1740
|
+
body: bodyLine,
|
|
1741
|
+
query,
|
|
1742
|
+
params,
|
|
1743
|
+
title,
|
|
1744
|
+
question
|
|
1745
|
+
});
|
|
1746
|
+
});
|
|
1747
|
+
const titleMeta = meta["title"];
|
|
1748
|
+
return {
|
|
1749
|
+
isDashboard: isDashboardMeta(meta),
|
|
1750
|
+
title: typeof titleMeta === "string" && titleMeta !== "" ? titleMeta : void 0,
|
|
1751
|
+
tokens: extractDashboardTokens(meta),
|
|
1752
|
+
widgets
|
|
1753
|
+
};
|
|
1754
|
+
}
|
|
1755
|
+
|
|
1756
|
+
// src/dashboard/parser.ts
|
|
1757
|
+
function parseDashboardNote(rawContent) {
|
|
1758
|
+
const { meta } = parseFrontmatter(rawContent);
|
|
1759
|
+
const lines = rawContent.split(/\r?\n/);
|
|
1760
|
+
const sections = [];
|
|
1761
|
+
let i = 0;
|
|
1762
|
+
if (lines.length > 0 && lines[0].trim() === "---") {
|
|
1763
|
+
i = 1;
|
|
1764
|
+
while (i < lines.length && lines[i].trim() !== "---") {
|
|
1765
|
+
i++;
|
|
1766
|
+
}
|
|
1767
|
+
i++;
|
|
1768
|
+
}
|
|
1769
|
+
while (i < lines.length) {
|
|
1770
|
+
const line = lines[i];
|
|
1771
|
+
const trimmed = line.trim();
|
|
1772
|
+
if (/^```query/.test(trimmed)) {
|
|
1773
|
+
const tag = trimmed.slice(3).split(/[\s\t]/)[0].toLowerCase();
|
|
1774
|
+
let widgetType;
|
|
1775
|
+
let spanCols;
|
|
1776
|
+
let spanFull;
|
|
1777
|
+
let widgetError;
|
|
1778
|
+
if (tag.startsWith("query:")) {
|
|
1779
|
+
const spec = parseQueryWidgetSuffix(tag.slice("query:".length));
|
|
1780
|
+
widgetType = spec.type || void 0;
|
|
1781
|
+
spanCols = spec.spanCols;
|
|
1782
|
+
spanFull = spec.spanFull;
|
|
1783
|
+
widgetError = spec.error;
|
|
1784
|
+
}
|
|
1785
|
+
const startLine = i;
|
|
1786
|
+
i++;
|
|
1787
|
+
const bodyLines = [];
|
|
1788
|
+
while (i < lines.length && !/^```[\w\d_:-]*$/.test(lines[i].trim())) {
|
|
1789
|
+
bodyLines.push(lines[i]);
|
|
1790
|
+
i++;
|
|
1791
|
+
}
|
|
1792
|
+
const endLine = i;
|
|
1793
|
+
sections.push({
|
|
1794
|
+
type: "query",
|
|
1795
|
+
content: bodyLines.join("\n"),
|
|
1796
|
+
widgetType,
|
|
1797
|
+
spanCols,
|
|
1798
|
+
spanFull,
|
|
1799
|
+
widgetError,
|
|
1800
|
+
startLine,
|
|
1801
|
+
endLine
|
|
1802
|
+
});
|
|
1803
|
+
i++;
|
|
1804
|
+
continue;
|
|
1805
|
+
}
|
|
1806
|
+
if (/^```[\w\d_:-]*$/.test(trimmed)) {
|
|
1807
|
+
const startLine = i;
|
|
1808
|
+
const fenceLines = [lines[i]];
|
|
1809
|
+
i++;
|
|
1810
|
+
while (i < lines.length && !/^```[\w\d_:-]*$/.test(lines[i].trim())) {
|
|
1811
|
+
fenceLines.push(lines[i]);
|
|
1812
|
+
i++;
|
|
1813
|
+
}
|
|
1814
|
+
if (i < lines.length) {
|
|
1815
|
+
fenceLines.push(lines[i]);
|
|
1816
|
+
i++;
|
|
1817
|
+
}
|
|
1818
|
+
sections.push({
|
|
1819
|
+
type: "markdown",
|
|
1820
|
+
subtype: "paragraph",
|
|
1821
|
+
content: fenceLines.join("\n"),
|
|
1822
|
+
startLine,
|
|
1823
|
+
endLine: i - 1
|
|
1824
|
+
});
|
|
1825
|
+
continue;
|
|
1826
|
+
}
|
|
1827
|
+
if (trimmed.startsWith("#")) {
|
|
1828
|
+
sections.push({
|
|
1829
|
+
type: "markdown",
|
|
1830
|
+
subtype: "heading",
|
|
1831
|
+
content: trimmed,
|
|
1832
|
+
startLine: i,
|
|
1833
|
+
endLine: i
|
|
1834
|
+
});
|
|
1835
|
+
i++;
|
|
1836
|
+
continue;
|
|
1837
|
+
}
|
|
1838
|
+
if (trimmed !== "") {
|
|
1839
|
+
const startLine = i;
|
|
1840
|
+
const paraLines = [];
|
|
1841
|
+
while (i < lines.length && lines[i].trim() !== "" && !lines[i].trim().startsWith("#") && !/^```[\w\d_:-]*$/.test(lines[i].trim())) {
|
|
1842
|
+
paraLines.push(lines[i]);
|
|
1843
|
+
i++;
|
|
1844
|
+
}
|
|
1845
|
+
sections.push({
|
|
1846
|
+
type: "markdown",
|
|
1847
|
+
subtype: "paragraph",
|
|
1848
|
+
content: paraLines.join("\n"),
|
|
1849
|
+
startLine,
|
|
1850
|
+
endLine: i - 1
|
|
1851
|
+
});
|
|
1852
|
+
continue;
|
|
1853
|
+
}
|
|
1854
|
+
i++;
|
|
1855
|
+
}
|
|
1856
|
+
return { meta, sections };
|
|
1857
|
+
}
|
|
1858
|
+
|
|
1859
|
+
// src/dashboard/scaffold.ts
|
|
1860
|
+
var DEFAULT_DASHBOARD_TITLE = "New Dashboard";
|
|
1861
|
+
function dashboardSlug(title) {
|
|
1862
|
+
return title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "") || "dashboard";
|
|
1863
|
+
}
|
|
1864
|
+
function buildDashboardScaffold(title = DEFAULT_DASHBOARD_TITLE) {
|
|
1865
|
+
return `---
|
|
1866
|
+
dashboard: true
|
|
1867
|
+
dashboard.active: true
|
|
1868
|
+
title: ${title}
|
|
1869
|
+
slug: ${dashboardSlug(title)}
|
|
1870
|
+
dashboard.metric: [totalVolume, totalReps]
|
|
1871
|
+
---
|
|
1872
|
+
|
|
1873
|
+
## Weekly Trend
|
|
1874
|
+
How does $metric trend week over week?
|
|
1875
|
+
|
|
1876
|
+
\`\`\`query:timeseries
|
|
1877
|
+
sum:$metric{} rollup:1w
|
|
1878
|
+
\`\`\`
|
|
1879
|
+
`;
|
|
1880
|
+
}
|
|
1881
|
+
|
|
1882
|
+
exports.DASHBOARD_GRID_MAX_COLS = DASHBOARD_GRID_MAX_COLS;
|
|
1883
|
+
exports.DASHBOARD_WIDGET_TYPES = DASHBOARD_WIDGET_TYPES;
|
|
1884
|
+
exports.DEFAULT_DASHBOARD_TITLE = DEFAULT_DASHBOARD_TITLE;
|
|
1885
|
+
exports.DISCIPLINE_FACTORS = DISCIPLINE_FACTORS;
|
|
1886
|
+
exports.EFFORT_DISCIPLINES = EFFORT_DISCIPLINES;
|
|
1887
|
+
exports.KG_PER_LB = KG_PER_LB;
|
|
1888
|
+
exports.PLANNED_WIDGET_TYPES = PLANNED_WIDGET_TYPES;
|
|
1889
|
+
exports.QueryService = QueryService;
|
|
1890
|
+
exports.UNIT_FAMILIES = UNIT_FAMILIES;
|
|
1891
|
+
exports.WQL_AGGREGATORS = WQL_AGGREGATORS;
|
|
1892
|
+
exports.WQL_CALC_TARGETS = WQL_CALC_TARGETS;
|
|
1893
|
+
exports.WQL_COMPARISON_OPS = WQL_COMPARISON_OPS;
|
|
1894
|
+
exports.WQL_CONTENT_FILTER_KEYS = WQL_CONTENT_FILTER_KEYS;
|
|
1895
|
+
exports.WQL_DISPLAY_UNITS = WQL_DISPLAY_UNITS;
|
|
1896
|
+
exports.WQL_FIND_TARGETS = WQL_FIND_TARGETS;
|
|
1897
|
+
exports.WQL_GRAINS = WQL_GRAINS;
|
|
1898
|
+
exports.WQL_INTENSITY_TIERS = WQL_INTENSITY_TIERS;
|
|
1899
|
+
exports.WQL_KEYWORDS = WQL_KEYWORDS;
|
|
1900
|
+
exports.WQL_METRIC_AGGREGATES = WQL_METRIC_AGGREGATES;
|
|
1901
|
+
exports.WQL_METRIC_FAMILIES = WQL_METRIC_FAMILIES;
|
|
1902
|
+
exports.WQL_ROLLUP_PERIODS = WQL_ROLLUP_PERIODS;
|
|
1903
|
+
exports.WQL_SCOPES = WQL_SCOPES;
|
|
1904
|
+
exports.WQL_SOURCES = WQL_SOURCES;
|
|
1905
|
+
exports.WQL_TAG_KEYS = WQL_TAG_KEYS;
|
|
1906
|
+
exports.WQL_VIRTUAL_DIMS = WQL_VIRTUAL_DIMS;
|
|
1907
|
+
exports.buildDashboardDocument = buildDashboardDocument;
|
|
1908
|
+
exports.buildDashboardScaffold = buildDashboardScaffold;
|
|
1909
|
+
exports.convert = convert;
|
|
1910
|
+
exports.dashboardSlug = dashboardSlug;
|
|
1911
|
+
exports.defaultTokenValues = defaultTokenValues;
|
|
1912
|
+
exports.disciplineFactorFor = disciplineFactorFor;
|
|
1913
|
+
exports.extractDashboardTokens = extractDashboardTokens;
|
|
1914
|
+
exports.extractFrontmatterTags = extractFrontmatterTags;
|
|
1915
|
+
exports.getList = getList;
|
|
1916
|
+
exports.getScalar = getScalar;
|
|
1917
|
+
exports.getUnitFamily = getUnitFamily;
|
|
1918
|
+
exports.isDashboardMeta = isDashboardMeta;
|
|
1919
|
+
exports.isDashboardWidgetType = isDashboardWidgetType;
|
|
1920
|
+
exports.isEffortDiscipline = isEffortDiscipline;
|
|
1921
|
+
exports.isFindQuery = isFindQuery;
|
|
1922
|
+
exports.isProposedMetric = isProposedMetric;
|
|
1923
|
+
exports.isRowsQuery = isRowsQuery;
|
|
1924
|
+
exports.normalizeSummaryFacts = normalizeSummaryFacts;
|
|
1925
|
+
exports.parseDashboardNote = parseDashboardNote;
|
|
1926
|
+
exports.parseFlatProperties = parseFlatProperties;
|
|
1927
|
+
exports.parseFrontmatter = parseFrontmatter;
|
|
1928
|
+
exports.parseFrontmatterBody = parseFrontmatterBody;
|
|
1929
|
+
exports.parseFrontmatterCategories = parseFrontmatterCategories;
|
|
1930
|
+
exports.parseFrontmatterProps = parseFrontmatterProps;
|
|
1931
|
+
exports.parseQuery = parseQuery;
|
|
1932
|
+
exports.parseQueryWidgetSuffix = parseQueryWidgetSuffix;
|
|
1933
|
+
exports.parseWqlSuffixes = parseWqlSuffixes;
|
|
1934
|
+
exports.referencedTokens = referencedTokens;
|
|
1935
|
+
exports.resolveCanonicalMetricKey = resolveCanonicalMetricKey;
|
|
1936
|
+
exports.resolveDisplayUnit = resolveDisplayUnit;
|
|
1937
|
+
exports.resolveWidgetType = resolveWidgetType;
|
|
1938
|
+
exports.serializeFrontmatter = serializeFrontmatter;
|
|
1939
|
+
exports.setDashboardTokenValue = setDashboardTokenValue;
|
|
1940
|
+
exports.splitAtWhere = splitAtWhere;
|
|
1941
|
+
exports.splitWidgetBody = splitWidgetBody;
|
|
1942
|
+
exports.staticNotesFromBlocks = staticNotesFromBlocks;
|
|
1943
|
+
exports.staticTagIndexFromBlocks = staticTagIndexFromBlocks;
|
|
1944
|
+
exports.stripFrontmatter = stripFrontmatter;
|
|
1945
|
+
exports.substituteTokens = substituteTokens;
|
|
1946
|
+
exports.unknownTokensMessage = unknownTokensMessage;
|
|
1947
|
+
exports.unknownWidgetTypeMessage = unknownWidgetTypeMessage;
|
|
1948
|
+
exports.wql = wql;
|
|
1949
|
+
exports.wqlCompletion = wqlCompletion;
|
|
1950
|
+
exports.wqlCompletionSource = wqlCompletionSource;
|
|
1951
|
+
exports.wqlHighlightStyle = wqlHighlightStyle;
|
|
1952
|
+
exports.wqlLanguage = wqlLanguage;
|
|
1953
|
+
exports.wqlParser = parser;
|
|
1954
|
+
exports.wqlTerms = wql_parser_terms_exports;
|
|
1955
|
+
//# sourceMappingURL=index.cjs.map
|
|
1956
|
+
//# sourceMappingURL=index.cjs.map
|