From 106a38afa55cbe018842b02abdc8249d2d8db8dc Mon Sep 17 00:00:00 2001 From: Sergey Kolesnik Date: Mon, 15 Jul 2024 21:21:07 +0300 Subject: [PATCH] feat(query language): .valueType filter --- docs/changelog.md | 6 +++ docs/reference__query_language.md | 49 +++++++++++++++++++--- src/context.ts | 2 +- src/query.ts | 69 +++++++++++++++++++++++++++++++ 4 files changed, 120 insertions(+), 6 deletions(-) diff --git a/docs/changelog.md b/docs/changelog.md index 78a959f..b33e2ee 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,3 +1,9 @@ +## NEXT + +- **Query language for pages**: [`.valueType`](reference__query_language.md#filter-value-type) filter with `.onlyStrings` & `.onlyNumbers` shortcuts to filter properties values by type. + + + ## v4.0 :id=v40 ### Set cursor position diff --git a/docs/reference__query_language.md b/docs/reference__query_language.md index 15ee23c..17a6978 100644 --- a/docs/reference__query_language.md +++ b/docs/reference__query_language.md @@ -373,13 +373,13 @@ Filter by page empty (or non-empty) property value. *Note*: the page may have no #### `.integerValue` -Filter by page property value. *Note*: this filter treats properties values as an integer numbers. +Filter by page property **integer** value. ?> This filter must be preceded by [`.property`](#filter-property) as it interacts with property's value -!> Property values could be a mix of integers and strings, which introduces some caveats: \ -For `=` and `!=` operations, all string values (including empty strings) will be ignored. \ -For other comparison operations, all string values will be considered greater than integer values, and there is no way to filter out string values. +!> Property values could be a mix of integers or strings (or sets of references), which introduces comparison caveat: **any string values will be considered greater than integer values**. \ +\ +If you need to filter out some values types, use [`.valueType`](#filter-value-type) filter. - `.integerValue(number)` — shortcut for `.integerValue('=', number)` - `.integerValue(operation, number)` @@ -405,10 +405,14 @@ For other comparison operations, all string values will be considered greater th #### `.value` :id=filter-value -Filter by page property value. *Note*: this filter treats properties values as strings. +Filter by page property **string** value. ?> This filter must be preceded by [`.property`](#filter-property) as it interacts with property's value +!> Property values could be a mix of integers or strings (or sets of references), which introduces comparison caveat: **any string values will be considered greater than integer values**. \ +\ +If you need to filter out some values types, use [`.valueType`](#filter-value-type) filter. + !> Note for comparison operations: empty string value is less than any other string - `.value(value)` — shortcut for `.value('=', value)` @@ -439,6 +443,41 @@ after 2020: 10 (0 empty) + +#### `.valueType` & `.string` & `.number` :id=filter-value-type +Filter by page property value type. + +?> This filter must be preceded by [`.property`](#filter-property) as it interacts with property's value + +- `.onlyStrings()` — shortcut for `.valueType(['string'])` +- `.onlyNumbers()` — shortcut for `.valueType(['number'])` +- `.valueType(choices)` +- `.valueType(choices, false)` — inversion form + - `choices`: array of strings: `string`, `number` or `set` + + +#### ***Template*** +```javascript +``{ +var all = query.pages() + .property('year') + .nonEmpty() +var withNumberYear = all.onlyNumbers().get() +var withStringYear = all.onlyStrings().get() +_}`` + +year as number count: ``withNumberYear.length`` +year as string: ``withStringYear.map(p => p.props.year)`` +``` + +#### ***Rendered*** +number years count: 92 +string years: 1947-1977 (набор эссе), 1955-1963, 1991 & 2017 + + + + + #### `.reference` & `.tags` & `.noTags` Filter by reference in page property value. *Note*: this filter searches references within properties values. And ignores all other text content. diff --git a/src/context.ts b/src/context.ts index 0854146..8d8dda7 100644 --- a/src/context.ts +++ b/src/context.ts @@ -208,7 +208,7 @@ export class PageContext extends Context { prefix: parts.slice(0, -1).join('/'), suffix: parts.at(-1), pages: parts.slice(0, -1).reduce( - (r, i) => r.concat(parts.slice(0, r.length + 1).join('/')), + (r) => r.concat(parts.slice(0, r.length + 1).join('/')), [] as Array ), }) as unknown as PageContext['namespace'] diff --git a/src/query.ts b/src/query.ts index 4ee2919..8d79e0e 100644 --- a/src/query.ts +++ b/src/query.ts @@ -214,6 +214,58 @@ class ValueFilter extends Filter { } +class ValueTypeFilter extends Filter { + choices: ('number' | 'string' | 'set')[] + + bindingVars = (prop) => [ + [`?ptype-${prop}`, `[(type ?p-${prop}) ?ptype-${prop}]`], + + [`?type-number`, `[(type 1) ?type-number]`], + [`?type-string`, `[(type "x") ?type-string]`], + [`?type-set`, `[(type #{}) ?type-set]`], + ] + + constructor(choices: ('number' | 'string' | 'set')[]) { + super('') + this.choices = choices + } + checkArgs(builder: PagesQueryBuilder): string | null { + if (builder.lastState === null) + return 'Preceding property filter is required' + if (this.choices.length === 0) + return 'At least one type is required' + + const unknown = this.choices.filter((t) => !['number', 'string', 'set'].includes(t)) + if (unknown.length !== 0) + return `Unknown property types: ${unknown.join(', ')}` + + return null + } + getPredicate(builder: PagesQueryBuilder): string { + const propertyName = builder.lastState + + const lines: string[] = [] + for (const choice of this.choices) { + let predicate: string + + if (choice === 'string') + predicate = `[(= ?ptype-${propertyName} ?type-string)]` + else if (choice === 'number') + predicate = `[(= ?ptype-${propertyName} ?type-number)]` + else if (choice === 'set') + predicate = `[(= ?ptype-${propertyName} ?type-set)]` + else return '' + + lines.push(predicate) + } + + if (lines.length === 1) + return lines[0] + return `(or ${lines.join('\n')} )` + } +} + + class ReferenceFilter extends Filter { values: string[] operation: string @@ -352,12 +404,14 @@ export class PagesQueryBuilder { noProperty(name: string) { return this._filter(new PropertyFilter(name), false) } + empty() { return this._filter(new EmptyFilter()) } nonEmpty() { return this._filter(new EmptyFilter(), false) } + integerValue(operation: string, value: string = '') { if (value === '') { value = operation @@ -374,6 +428,20 @@ export class PagesQueryBuilder { value = value.toString() return this._filter(new ValueFilter(value, operation), nonInverted) } + + valueType(choices: ('number' | 'string' | 'set')[] = [], nonInverted: boolean = true) { + choices = choices.map(x => x.toString() as ('number' | 'string' | 'set')) + if (choices.length === 0) + return this + return this._filter(new ValueTypeFilter(choices), nonInverted) + } + onlyStrings(nonInverted: boolean = true) { + return this.valueType(['string'], nonInverted) + } + onlyNumbers(nonInverted: boolean = true) { + return this.valueType(['number'], nonInverted) + } + reference(operation: string, value: string | string[] = '', nonInverted: boolean = true) { if (value === '') { value = operation @@ -389,6 +457,7 @@ export class PagesQueryBuilder { value = value.toString() return this._filter(new ReferenceCountFilter(value, operation), nonInverted) } + tags(names: string | string[] = '', only: boolean = false) { const cloned = this._filter(new PropertyFilter('tags')) return cloned._filter(new ReferenceFilter(names, only ? 'includes only' : 'includes'))