@mintlify/common 1.0.1312 → 1.0.1314

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.
@@ -0,0 +1,604 @@
1
+ import { formulaFilterEngine } from './formula/filter.js';
2
+ import { columnReference, quoteString } from './formula/format.js';
3
+ import { FORMULA_LIMITS } from './formula/limits.js';
4
+ import { parseFormula } from './formula/parse.js';
5
+ import { referencedColumns, renameColumn, toDisplayFormula, toStoredFormula, } from './formula/references.js';
6
+ import { isKeyword, tokenize } from './formula/tokenize.js';
7
+ import { typecheck } from './formula/typecheck.js';
8
+ import { evaluateQuery } from './query/evaluate.js';
9
+ import { QUERY_LIMITS } from './query/types.js';
10
+ import { normalizeDate } from './query/values.js';
11
+ export { columnReference } from './formula/format.js';
12
+ export const DATABASE_VIEWS = ['table', 'board'];
13
+ export const DATABASE_FILTER_OPS = [
14
+ 'eq',
15
+ 'neq',
16
+ 'lt',
17
+ 'lte',
18
+ 'gt',
19
+ 'gte',
20
+ 'contains',
21
+ 'not_contains',
22
+ 'empty',
23
+ 'not_empty',
24
+ 'any_of',
25
+ 'contains_any',
26
+ ];
27
+ const FILTER_KINDS = {
28
+ text: 'text',
29
+ url: 'text',
30
+ email: 'text',
31
+ number: 'number',
32
+ select: 'select',
33
+ multi_select: 'multi',
34
+ boolean: 'checkbox',
35
+ date: 'date',
36
+ };
37
+ export const filterKindOf = (type) => { var _a; return (_a = FILTER_KINDS[type]) !== null && _a !== void 0 ? _a : null; };
38
+ export const isViewableColumn = (column) => filterKindOf(column.type) !== null;
39
+ export const isSortableColumn = (column) => isViewableColumn(column) && column.type !== 'multi_select';
40
+ export const isGroupableColumn = (column) => column.type === 'select';
41
+ const OPS_BY_KIND = {
42
+ text: ['contains', 'not_contains', 'eq', 'neq', 'empty', 'not_empty'],
43
+ number: ['eq', 'neq', 'lt', 'lte', 'gt', 'gte', 'empty', 'not_empty'],
44
+ select: ['eq', 'neq', 'any_of', 'empty', 'not_empty'],
45
+ multi: ['contains', 'not_contains', 'contains_any', 'empty', 'not_empty'],
46
+ checkbox: ['eq', 'empty', 'not_empty'],
47
+ date: ['lt', 'gt', 'eq', 'empty', 'not_empty'],
48
+ };
49
+ export const filterOpsFor = (type) => {
50
+ const kind = filterKindOf(type);
51
+ return kind ? OPS_BY_KIND[kind] : [];
52
+ };
53
+ export const isKnownView = (value) => DATABASE_VIEWS.some((view) => view === value);
54
+ export const resolveView = (value) => (isKnownView(value) ? value : 'table');
55
+ export const normalizeColumnName = (name) => name.trim();
56
+ const isStringArray = (value) => Array.isArray(value) && value.every((entry) => typeof entry === 'string');
57
+ const isDateValue = (value) => typeof value === 'object' &&
58
+ value !== null &&
59
+ !Array.isArray(value) &&
60
+ 'date' in value &&
61
+ typeof value.date === 'string';
62
+ const isListOp = (op) => op === 'any_of' || op === 'contains_any';
63
+ export const isEmptinessOp = (op) => op === 'empty' || op === 'not_empty';
64
+ export const isCompleteRule = ({ op, value }) => {
65
+ if (isEmptinessOp(op))
66
+ return true;
67
+ if (isListOp(op))
68
+ return isStringArray(value) && value.length > 0;
69
+ if (typeof value === 'number')
70
+ return Number.isFinite(value);
71
+ if (isDateValue(value))
72
+ return value.date !== '';
73
+ return (typeof value === 'string' && value !== '') || typeof value === 'boolean';
74
+ };
75
+ export const isInvalidDateRule = ({ value }) => isDateValue(value) && value.date !== '' && normalizeDate(value.date) === null;
76
+ const safeTokens = (source) => {
77
+ try {
78
+ return tokenize(source);
79
+ }
80
+ catch (_a) {
81
+ return null;
82
+ }
83
+ };
84
+ const readName = (tokens, index) => {
85
+ const token = tokens[index];
86
+ if ((token === null || token === void 0 ? void 0 : token.type) === 'string')
87
+ return { name: token.value, next: index + 1 };
88
+ if ((token === null || token === void 0 ? void 0 : token.type) !== 'identifier')
89
+ return null;
90
+ const open = tokens[index + 1];
91
+ if (token.value === 'prop') {
92
+ const name = tokens[index + 2];
93
+ const close = tokens[index + 3];
94
+ if ((open === null || open === void 0 ? void 0 : open.value) !== '(' || (name === null || name === void 0 ? void 0 : name.type) !== 'string' || (close === null || close === void 0 ? void 0 : close.value) !== ')')
95
+ return null;
96
+ return { name: name.value, next: index + 4 };
97
+ }
98
+ if (isKeyword(token.value) || ((open === null || open === void 0 ? void 0 : open.type) === 'symbol' && open.value === '('))
99
+ return null;
100
+ return { name: token.value, next: index + 1 };
101
+ };
102
+ const splitSortKeys = (source) => {
103
+ const keys = [];
104
+ let quote = null;
105
+ let start = 0;
106
+ for (let index = 0; index < source.length; index++) {
107
+ const char = source.charAt(index);
108
+ if (quote) {
109
+ if (char === '\\')
110
+ index++;
111
+ else if (char === quote)
112
+ quote = null;
113
+ }
114
+ else if (char === '"' || char === "'") {
115
+ quote = char;
116
+ }
117
+ else if (char === ',') {
118
+ keys.push(source.slice(start, index));
119
+ start = index + 1;
120
+ }
121
+ }
122
+ keys.push(source.slice(start));
123
+ return keys;
124
+ };
125
+ const parseSortKey = (source) => {
126
+ var _a;
127
+ const tokens = safeTokens(source);
128
+ if (!tokens)
129
+ return null;
130
+ const minus = tokens[0];
131
+ const descending = (minus === null || minus === void 0 ? void 0 : minus.type) === 'symbol' && minus.value === '-';
132
+ const ref = readName(tokens, descending ? 1 : 0);
133
+ if (!ref || ((_a = tokens[ref.next]) === null || _a === void 0 ? void 0 : _a.type) !== 'eof')
134
+ return null;
135
+ return { column: ref.name, direction: descending ? 'desc' : 'asc' };
136
+ };
137
+ export const parseSortKeys = (source) => (source === null || source === void 0 ? void 0 : source.trim())
138
+ ? splitSortKeys(source).map((key) => ({ source: key.trim(), rule: parseSortKey(key) }))
139
+ : [];
140
+ export const parseSort = (source) => {
141
+ const rules = [];
142
+ for (const { rule } of parseSortKeys(source)) {
143
+ if (!rule)
144
+ return null;
145
+ rules.push(rule);
146
+ }
147
+ return rules;
148
+ };
149
+ export const serializeSort = (rules) => rules.length > 0
150
+ ? rules
151
+ .map(({ column, direction }) => `${direction === 'desc' ? '-' : ''}${columnReference(column)}`)
152
+ .join(', ')
153
+ : null;
154
+ const COMPARE_OPS = {
155
+ '==': 'eq',
156
+ '!=': 'neq',
157
+ '<': 'lt',
158
+ '<=': 'lte',
159
+ '>': 'gt',
160
+ '>=': 'gte',
161
+ };
162
+ const MIRRORED = {
163
+ eq: 'eq',
164
+ neq: 'neq',
165
+ lt: 'gt',
166
+ lte: 'gte',
167
+ gt: 'lt',
168
+ gte: 'lte',
169
+ };
170
+ const OP_SOURCE = {
171
+ eq: '==',
172
+ neq: '!=',
173
+ lt: '<',
174
+ lte: '<=',
175
+ gt: '>',
176
+ gte: '>=',
177
+ };
178
+ const refName = (node) => (node === null || node === void 0 ? void 0 : node.kind) === 'ref' && node.form !== 'id' ? node.name : null;
179
+ const literalOf = (node) => {
180
+ switch (node.kind) {
181
+ case 'string':
182
+ case 'number':
183
+ case 'boolean':
184
+ return node.value;
185
+ case 'unary':
186
+ return node.operator === '-' && node.operand.kind === 'number'
187
+ ? -node.operand.value
188
+ : undefined;
189
+ case 'call': {
190
+ const [arg] = node.args;
191
+ return node.name === 'date' && node.args.length === 1 && (arg === null || arg === void 0 ? void 0 : arg.kind) === 'string'
192
+ ? { date: arg.value }
193
+ : undefined;
194
+ }
195
+ default:
196
+ return undefined;
197
+ }
198
+ };
199
+ const flatten = (node, operator) => node.kind === 'binary' && node.operator === operator
200
+ ? [...flatten(node.left, operator), ...flatten(node.right, operator)]
201
+ : [node];
202
+ const containsOf = (node) => {
203
+ if (node.kind !== 'call' || node.name !== 'contains' || node.args.length !== 2)
204
+ return null;
205
+ const [subject, needle] = node.args;
206
+ const column = refName(subject);
207
+ return column !== null && (needle === null || needle === void 0 ? void 0 : needle.kind) === 'string' ? { column, value: needle.value } : null;
208
+ };
209
+ const BLANK_OPS = {
210
+ eq: 'empty',
211
+ neq: 'not_empty',
212
+ };
213
+ const compareRule = (column, op, value) => {
214
+ const blank = value === '' ? BLANK_OPS[op] : undefined;
215
+ return blank ? { column, op: blank, value: null } : { column, op, value };
216
+ };
217
+ const emptyOf = (node) => {
218
+ if (node.kind !== 'call' || node.name !== 'empty' || node.args.length !== 1)
219
+ return null;
220
+ return refName(node.args[0]);
221
+ };
222
+ const atomRule = (node) => {
223
+ if (node.kind === 'binary') {
224
+ const op = COMPARE_OPS[node.operator];
225
+ if (!op)
226
+ return null;
227
+ const left = refName(node.left);
228
+ const right = refName(node.right);
229
+ if (left !== null && right === null) {
230
+ const value = literalOf(node.right);
231
+ return value === undefined ? null : compareRule(left, op, value);
232
+ }
233
+ if (right !== null && left === null) {
234
+ const value = literalOf(node.left);
235
+ const mirrored = MIRRORED[op];
236
+ return value === undefined || !mirrored ? null : compareRule(right, mirrored, value);
237
+ }
238
+ return null;
239
+ }
240
+ if (node.kind === 'unary' && node.operator === 'not') {
241
+ const inner = containsOf(node.operand);
242
+ if (inner)
243
+ return { column: inner.column, op: 'not_contains', value: inner.value };
244
+ const blank = emptyOf(node.operand);
245
+ return blank === null ? null : { column: blank, op: 'not_empty', value: null };
246
+ }
247
+ const contains = containsOf(node);
248
+ if (contains)
249
+ return { column: contains.column, op: 'contains', value: contains.value };
250
+ const blank = emptyOf(node);
251
+ return blank === null ? null : { column: blank, op: 'empty', value: null };
252
+ };
253
+ const groupRule = (atoms) => {
254
+ const rules = atoms.map(atomRule);
255
+ const [first] = rules;
256
+ if (!first || rules.some((rule) => (rule === null || rule === void 0 ? void 0 : rule.column) !== first.column))
257
+ return null;
258
+ const values = rules.map((rule) => rule === null || rule === void 0 ? void 0 : rule.value);
259
+ if (!isStringArray(values))
260
+ return null;
261
+ if (rules.every((rule) => (rule === null || rule === void 0 ? void 0 : rule.op) === 'eq')) {
262
+ return { column: first.column, op: 'any_of', value: values };
263
+ }
264
+ if (rules.every((rule) => (rule === null || rule === void 0 ? void 0 : rule.op) === 'contains')) {
265
+ return { column: first.column, op: 'contains_any', value: values };
266
+ }
267
+ return null;
268
+ };
269
+ const rulesOf = (ast) => {
270
+ const rules = [];
271
+ for (const clause of flatten(ast, 'and')) {
272
+ const atoms = flatten(clause, 'or');
273
+ const rule = atoms.length === 1 ? atomRule(clause) : groupRule(atoms);
274
+ if (!rule)
275
+ return null;
276
+ rules.push(rule);
277
+ }
278
+ return rules;
279
+ };
280
+ export const parseFilter = (source) => {
281
+ const trimmed = source === null || source === void 0 ? void 0 : source.trim();
282
+ if (!trimmed)
283
+ return { kind: 'none' };
284
+ const parsed = parseFormula(trimmed);
285
+ const rules = 'ast' in parsed && parsed.ast.kind !== 'empty' ? rulesOf(parsed.ast) : null;
286
+ return rules ? { kind: 'rules', rules } : { kind: 'custom', source: trimmed };
287
+ };
288
+ const literalSource = (value) => {
289
+ if (typeof value === 'string')
290
+ return quoteString(value);
291
+ if (typeof value === 'number' || typeof value === 'boolean')
292
+ return String(value);
293
+ if (isDateValue(value))
294
+ return `date(${quoteString(value.date)})`;
295
+ return '""';
296
+ };
297
+ const ruleSource = ({ column, op, value }) => {
298
+ var _a;
299
+ const reference = columnReference(column);
300
+ const values = isStringArray(value) ? value : [];
301
+ const group = (parts) => { var _a; return parts.length === 1 ? ((_a = parts[0]) !== null && _a !== void 0 ? _a : '') : `(${parts.join(' or ')})`; };
302
+ switch (op) {
303
+ case 'empty':
304
+ return `empty(${reference})`;
305
+ case 'not_empty':
306
+ return `not empty(${reference})`;
307
+ case 'contains':
308
+ return `contains(${reference}, ${literalSource(value)})`;
309
+ case 'not_contains':
310
+ return `not contains(${reference}, ${literalSource(value)})`;
311
+ case 'any_of':
312
+ return group(values.map((entry) => `${reference} == ${quoteString(entry)}`));
313
+ case 'contains_any':
314
+ return group(values.map((entry) => `contains(${reference}, ${quoteString(entry)})`));
315
+ default:
316
+ return `${reference} ${(_a = OP_SOURCE[op]) !== null && _a !== void 0 ? _a : '=='} ${literalSource(value)}`;
317
+ }
318
+ };
319
+ const rulesSource = (rules) => {
320
+ const parts = rules.filter(isCompleteRule).map(ruleSource);
321
+ return parts.length > 0 ? parts.join(' and ') : null;
322
+ };
323
+ export const serializeFilter = (filter) => {
324
+ if (filter.kind === 'custom')
325
+ return filter.source.trim() || null;
326
+ if (filter.kind === 'none')
327
+ return null;
328
+ return rulesSource(filter.rules);
329
+ };
330
+ const PLACEHOLDER = 'view-rename-';
331
+ const placeholderId = (index) => `${PLACEHOLDER}${index}`;
332
+ const leaksPlaceholder = (source) => {
333
+ const parsed = parseFormula(source, { maxBytes: FORMULA_LIMITS.draftBytes });
334
+ return ('ast' in parsed && referencedColumns(parsed.ast).some((ref) => { var _a; return (_a = ref.id) === null || _a === void 0 ? void 0 : _a.startsWith(PLACEHOLDER); }));
335
+ };
336
+ const renameFormula = (source, renames) => {
337
+ const entries = [...renames].filter(([from, to]) => from !== to);
338
+ if (entries.length === 0)
339
+ return source;
340
+ const [only] = entries;
341
+ if (entries.length === 1 && only)
342
+ return renameColumn(source, only[0], only[1]);
343
+ const targets = [...new Set(entries.map(([, to]) => to))];
344
+ const idOf = (name) => placeholderId(targets.indexOf(name));
345
+ const stored = toStoredFormula(source, entries.map(([from, to]) => ({ id: idOf(to), name: from })));
346
+ if ('error' in stored)
347
+ return source;
348
+ const renamed = toDisplayFormula(stored.source, targets.map((name) => ({ id: idOf(name), name })));
349
+ return leaksPlaceholder(renamed) ? source : renamed;
350
+ };
351
+ export const resolveColumnName = (written, candidates) => {
352
+ const name = normalizeColumnName(written);
353
+ const exact = candidates.find((candidate) => normalizeColumnName(candidate.name) === name);
354
+ if (exact)
355
+ return { column: exact };
356
+ const folded = name.toLowerCase();
357
+ const loose = candidates.filter((candidate) => normalizeColumnName(candidate.name).toLowerCase() === folded);
358
+ const [first] = loose;
359
+ if (loose.length === 1 && first)
360
+ return { column: first };
361
+ return { column: null, reason: loose.length > 1 ? 'ambiguous' : 'unknown' };
362
+ };
363
+ export const matchColumnName = (written, candidates) => { var _a; return (_a = resolveColumnName(written, candidates).column) !== null && _a !== void 0 ? _a : undefined; };
364
+ const renamedGroupBy = (written, renames) => { var _a; return (_a = matchColumnName(written, [...renames].map(([name, to]) => ({ name, to })))) === null || _a === void 0 ? void 0 : _a.to; };
365
+ export const renameColumnsInViewProps = (props, renames) => {
366
+ const changes = {};
367
+ if (renames.size === 0)
368
+ return changes;
369
+ const keys = parseSortKeys(props.sort);
370
+ const renamedKey = ({ source, rule }) => {
371
+ const to = rule && renames.get(rule.column);
372
+ return rule && to ? serializeSort([Object.assign(Object.assign({}, rule), { column: to })]) : source;
373
+ };
374
+ const filter = props.filter ? renameFormula(props.filter, renames) : props.filter;
375
+ const groupBy = props.groupBy
376
+ ? renamedGroupBy(normalizeColumnName(props.groupBy), renames)
377
+ : undefined;
378
+ if (keys.some(({ rule }) => rule && renames.has(rule.column))) {
379
+ changes.sort = keys.map(renamedKey).join(', ');
380
+ }
381
+ if (filter !== props.filter)
382
+ changes.filter = filter;
383
+ if (groupBy !== undefined)
384
+ changes.groupBy = groupBy;
385
+ return changes;
386
+ };
387
+ export const renameColumnInViewProps = (props, from, to) => renameColumnsInViewProps(props, new Map([[from, to]]));
388
+ const aliasTargets = (columns) => {
389
+ var _a, _b;
390
+ const targets = new Map();
391
+ for (const column of columns) {
392
+ for (const alias of (_a = column.aliases) !== null && _a !== void 0 ? _a : []) {
393
+ if (resolveColumnName(alias, columns).column)
394
+ continue;
395
+ const claimed = (_b = targets.get(alias)) !== null && _b !== void 0 ? _b : [];
396
+ if (!claimed.some((entry) => entry.id === column.id))
397
+ claimed.push(column);
398
+ targets.set(alias, claimed);
399
+ }
400
+ }
401
+ return targets;
402
+ };
403
+ export const aliasRenames = (columns) => {
404
+ const renames = new Map();
405
+ for (const [alias, [column, ...others]] of aliasTargets(columns)) {
406
+ if (column && others.length === 0)
407
+ renames.set(alias, column.name);
408
+ }
409
+ return renames;
410
+ };
411
+ export const ambiguousAliases = (columns) => new Set([...aliasTargets(columns)].flatMap(([alias, claimed]) => (claimed.length > 1 ? [alias] : [])));
412
+ const nameWarning = (prop, column, reason, columns) => reason === 'ambiguous' || ambiguousAliases(columns).has(column)
413
+ ? { kind: 'ambiguous_column', prop, column }
414
+ : { kind: 'unknown_column', prop, column };
415
+ export const resolveGroupBy = (written, columns) => {
416
+ var _a;
417
+ const name = written ? normalizeColumnName(written) : '';
418
+ if (!name)
419
+ return { column: (_a = columns.find(isGroupableColumn)) !== null && _a !== void 0 ? _a : null, warnings: [] };
420
+ const match = resolveColumnName(name, columns);
421
+ if (!match.column) {
422
+ return { column: null, warnings: [nameWarning('groupBy', name, match.reason, columns)] };
423
+ }
424
+ const { column } = match;
425
+ if (!isGroupableColumn(column)) {
426
+ return {
427
+ column: null,
428
+ warnings: [{ kind: 'group_by_unsupported', column: column.name, type: column.type }],
429
+ };
430
+ }
431
+ return { column, warnings: [] };
432
+ };
433
+ const ALL_ROWS = () => true;
434
+ const NO_ROWS = () => false;
435
+ const canonicalNames = (source, columns, warn) => {
436
+ const parsed = parseFormula(source);
437
+ if (!('ast' in parsed))
438
+ return source;
439
+ const written = new Set(referencedColumns(parsed.ast).flatMap((ref) => (ref.id === null && ref.name ? [ref.name] : [])));
440
+ let canonical = source;
441
+ for (const name of written) {
442
+ if (columns.some((column) => column.name === name))
443
+ continue;
444
+ const match = resolveColumnName(name, columns);
445
+ if (match.column)
446
+ canonical = renameColumn(canonical, name, match.column.name);
447
+ else
448
+ warn(normalizeColumnName(name), match.reason);
449
+ }
450
+ return canonical;
451
+ };
452
+ export const resolveViewProps = (written, columns, { onInvalidFilter = 'show-all' } = {}) => {
453
+ var _a, _b;
454
+ var _c, _d;
455
+ const healed = renameColumnsInViewProps(written, aliasRenames(columns));
456
+ if (written.groupBy && matchColumnName(written.groupBy, columns))
457
+ delete healed.groupBy;
458
+ const props = Object.assign(Object.assign({}, written), healed);
459
+ const warnings = [];
460
+ let filterInvalid = false;
461
+ const known = (prop, name) => {
462
+ const match = resolveColumnName(name, columns);
463
+ if (match.column)
464
+ return match.column;
465
+ warnings.push(nameWarning(prop, normalizeColumnName(name), match.reason, columns));
466
+ if (prop === 'filter')
467
+ filterInvalid = true;
468
+ return undefined;
469
+ };
470
+ const rawView = ((_a = props.view) === null || _a === void 0 ? void 0 : _a.trim()) || null;
471
+ if (rawView && !isKnownView(rawView))
472
+ warnings.push({ kind: 'unknown_view', view: rawView });
473
+ const sort = [];
474
+ const querySort = [];
475
+ for (const key of parseSortKeys(props.sort)) {
476
+ if (!key.rule) {
477
+ warnings.push({ kind: 'sort_invalid', key: key.source });
478
+ continue;
479
+ }
480
+ const column = known('sort', key.rule.column);
481
+ const rule = { column: (_c = column === null || column === void 0 ? void 0 : column.name) !== null && _c !== void 0 ? _c : key.rule.column, direction: key.rule.direction };
482
+ sort.push(rule);
483
+ if (!column)
484
+ continue;
485
+ if (!isSortableColumn(column)) {
486
+ warnings.push({ kind: 'unsortable', column: column.name, type: column.type });
487
+ }
488
+ else if (querySort.some((entry) => entry.columnId === column.id)) {
489
+ warnings.push({ kind: 'sort_ignored', column: column.name, reason: 'duplicate' });
490
+ }
491
+ else if (querySort.length >= QUERY_LIMITS.sortKeys) {
492
+ warnings.push({ kind: 'sort_ignored', column: column.name, reason: 'limit' });
493
+ }
494
+ else {
495
+ querySort.push({ columnId: column.id, direction: rule.direction });
496
+ }
497
+ }
498
+ const parsedFilter = parseFilter(props.filter);
499
+ const filter = parsedFilter.kind === 'rules'
500
+ ? {
501
+ kind: 'rules',
502
+ rules: parsedFilter.rules.map((rule) => { var _a; var _b; return (Object.assign(Object.assign({}, rule), { column: (_b = (_a = resolveColumnName(rule.column, columns).column) === null || _a === void 0 ? void 0 : _a.name) !== null && _b !== void 0 ? _b : rule.column })); }),
503
+ }
504
+ : parsedFilter;
505
+ let filterSource = null;
506
+ if (filter.kind === 'rules') {
507
+ const active = filter.rules.flatMap((rule) => {
508
+ const column = known('filter', rule.column);
509
+ if (!column)
510
+ return [];
511
+ if (isInvalidDateRule(rule) && isDateValue(rule.value)) {
512
+ warnings.push({ kind: 'invalid_date', column: column.name, value: rule.value.date });
513
+ filterInvalid = true;
514
+ return [];
515
+ }
516
+ return [Object.assign(Object.assign({}, rule), { column: column.name })];
517
+ });
518
+ filterSource = rulesSource(active);
519
+ }
520
+ else if (filter.kind === 'custom') {
521
+ filterSource = canonicalNames(filter.source, columns, (column, reason) => {
522
+ warnings.push(nameWarning('filter', column, reason, columns));
523
+ filterInvalid = true;
524
+ });
525
+ }
526
+ let matches = ALL_ROWS;
527
+ if (filterSource) {
528
+ const compiled = formulaFilterEngine(filterSource, columns);
529
+ if ('error' in compiled) {
530
+ if (!filterInvalid) {
531
+ warnings.push({ kind: 'filter_error', source: filterSource, message: compiled.error });
532
+ }
533
+ filterInvalid = true;
534
+ }
535
+ else {
536
+ matches = compiled.test;
537
+ }
538
+ }
539
+ if (filterInvalid && onInvalidFilter === 'hide-all')
540
+ matches = NO_ROWS;
541
+ const stored = filterSource ? toStoredFormula(filterSource, columns) : null;
542
+ const group = resolveGroupBy(props.groupBy, columns);
543
+ warnings.push(...group.warnings);
544
+ return {
545
+ view: resolveView(rawView),
546
+ sort,
547
+ filter,
548
+ groupBy: (_d = (_b = group.column) === null || _b === void 0 ? void 0 : _b.name) !== null && _d !== void 0 ? _d : null,
549
+ warnings,
550
+ healed,
551
+ filterSource: matches === ALL_ROWS || matches === NO_ROWS ? null : filterSource,
552
+ filterInvalid,
553
+ querySort,
554
+ rulesKey: JSON.stringify([
555
+ querySort,
556
+ stored && 'source' in stored ? stored.source : filterSource,
557
+ ]),
558
+ matches,
559
+ sortRows: (rows) => {
560
+ const result = evaluateQuery({ sort: querySort }, rows, columns);
561
+ if ('error' in result)
562
+ return [...rows];
563
+ const byId = new Map(rows.map((row) => [row.id, row]));
564
+ return result.ids.flatMap((id) => {
565
+ const row = byId.get(id);
566
+ return row ? [row] : [];
567
+ });
568
+ },
569
+ };
570
+ };
571
+ export const filterDiagnostic = (source, columns) => {
572
+ const parsed = parseFormula(source);
573
+ if ('error' in parsed) {
574
+ const { description, start, end } = parsed.error;
575
+ return { message: description, start, end };
576
+ }
577
+ const named = [...columns];
578
+ for (const ref of referencedColumns(parsed.ast)) {
579
+ if (ref.id !== null || ref.name === null)
580
+ continue;
581
+ const match = resolveColumnName(ref.name, columns);
582
+ const name = normalizeColumnName(ref.name);
583
+ if (match.column) {
584
+ named.push(Object.assign(Object.assign({}, match.column), { name: ref.name }));
585
+ continue;
586
+ }
587
+ const message = match.reason === 'ambiguous' || ambiguousAliases(columns).has(name)
588
+ ? `"${name}" matches more than one property`
589
+ : `No property named "${name}"`;
590
+ return { message, start: ref.start, end: ref.end };
591
+ }
592
+ const { type, diagnostics } = typecheck(parsed.ast, named);
593
+ const failure = diagnostics.find((diagnostic) => diagnostic.severity === 'error');
594
+ if (failure)
595
+ return { message: failure.description, start: failure.start, end: failure.end };
596
+ if (type !== 'boolean') {
597
+ return { message: 'A filter must be true or false', start: 0, end: source.length };
598
+ }
599
+ return null;
600
+ };
601
+ export const applyView = (columns, rows, props, { keep, onInvalidFilter } = {}) => {
602
+ const { matches, sortRows } = resolveViewProps(props, columns, { onInvalidFilter });
603
+ return sortRows(rows.filter((row) => (keep === null || keep === void 0 ? void 0 : keep(row)) || matches(row)));
604
+ };
@@ -7,7 +7,7 @@ const MDX_HEADING_ID_RE = /^(\s*)(#{1,6})\s+(.+?)\s*\{\s*#([^}]+?)\s*\}\s*$/;
7
7
  // `<MDX>` or `<MDX className="x">`; not `<MDX />` (no body) and not `<MDXProvider>`
8
8
  const MDX_OPEN_RE = /<MDX(?:\s[^>]*[^/>\s])?\s*>/g;
9
9
  const MDX_CLOSE_RE = /<\/MDX\s*>/g;
10
- const COMMENT_MASK_RE = /(`+).*?\1|"[^"\n]*"/g;
10
+ const COMMENT_MASK_RE = /(`+).*?\1|=\s*'[^'\n]*'|"[^"\n]*"/g;
11
11
  const UNSAFE_HTML_ATTR_CHARS = /[<>"]/g;
12
12
  /**
13
13
  * Characters to strip from custom heading IDs in the default (non-editor) path.
@@ -179,4 +179,11 @@ describe('customHeadingIdSourceLines', () => {
179
179
  expect(sourceLines).toEqual([1, 1, 1, 2, 3, 4, 5, 6, 6, 6]);
180
180
  expect(sourceLines).toHaveLength(preprocessCustomHeadingIds(content).split('\n').length);
181
181
  });
182
+ it('ignores comment markers inside single-quoted attribute values', () => {
183
+ const input = `<Database filter='Name == "a\\"{/*"' />\n\n## Heading {#custom}`;
184
+ expect(preprocessCustomHeadingIds(input)).toContain('<h2 id="custom">');
185
+ });
186
+ it('still treats a comment after an apostrophe in prose as a comment', () => {
187
+ expect(preprocessCustomHeadingIds("It's {/* hidden\n## Heading {#custom}\n*/}")).toContain('## Heading {#custom}');
188
+ });
182
189
  });