@ifc-lite/query 2.0.0 → 2.2.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/duckdb-integration.d.ts.map +1 -1
- package/dist/duckdb-integration.js +10 -15
- package/dist/duckdb-integration.js.map +1 -1
- package/dist/entity-node.d.ts.map +1 -1
- package/dist/entity-node.js +25 -6
- package/dist/entity-node.js.map +1 -1
- package/dist/filter-predicate.d.ts +35 -0
- package/dist/filter-predicate.d.ts.map +1 -0
- package/dist/filter-predicate.js +47 -0
- package/dist/filter-predicate.js.map +1 -0
- package/dist/ifc-query.d.ts.map +1 -1
- package/dist/ifc-query.js +7 -3
- package/dist/ifc-query.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/property-table.d.ts +10 -1
- package/dist/property-table.d.ts.map +1 -1
- package/dist/property-table.js +21 -2
- package/dist/property-table.js.map +1 -1
- package/dist/pset-lookup.d.ts +63 -0
- package/dist/pset-lookup.d.ts.map +1 -0
- package/dist/pset-lookup.js +73 -0
- package/dist/pset-lookup.js.map +1 -0
- package/dist/selector/ast.d.ts +106 -0
- package/dist/selector/ast.d.ts.map +1 -0
- package/dist/selector/ast.js +5 -0
- package/dist/selector/ast.js.map +1 -0
- package/dist/selector/parse.d.ts +20 -0
- package/dist/selector/parse.d.ts.map +1 -0
- package/dist/selector/parse.js +227 -0
- package/dist/selector/parse.js.map +1 -0
- package/dist/selector/tokenize.d.ts +47 -0
- package/dist/selector/tokenize.d.ts.map +1 -0
- package/dist/selector/tokenize.js +147 -0
- package/dist/selector/tokenize.js.map +1 -0
- package/package.json +6 -6
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
|
4
|
+
/**
|
|
5
|
+
* Find a property by (setName, propName) across every property set
|
|
6
|
+
* carrying that name, first match across the sequence wins. Returns
|
|
7
|
+
* `undefined` when no same-named set carries a property with that name.
|
|
8
|
+
*/
|
|
9
|
+
export function findPropertyInSets(sets, setName, propName) {
|
|
10
|
+
for (const set of sets) {
|
|
11
|
+
if (set.name !== setName)
|
|
12
|
+
continue;
|
|
13
|
+
const match = set.properties.find((p) => p.name === propName);
|
|
14
|
+
if (match)
|
|
15
|
+
return match;
|
|
16
|
+
}
|
|
17
|
+
return undefined;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Find a quantity by (setName, quantityName) across every quantity set
|
|
21
|
+
* carrying that name, first match across the sequence wins. Returns
|
|
22
|
+
* `undefined` when no same-named set carries a quantity with that name.
|
|
23
|
+
*/
|
|
24
|
+
export function findQuantityInSets(sets, setName, quantityName) {
|
|
25
|
+
for (const set of sets) {
|
|
26
|
+
if (set.name !== setName)
|
|
27
|
+
continue;
|
|
28
|
+
const match = set.quantities.find((q) => q.name === quantityName);
|
|
29
|
+
if (match)
|
|
30
|
+
return match;
|
|
31
|
+
}
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Find EVERY property named `propName`, across EVERY same-named
|
|
36
|
+
* (setName) property set — not just the first. For a caller evaluating a
|
|
37
|
+
* predicate ("does this entity have a set with this name and value?")
|
|
38
|
+
* first-match is wrong: an entity can carry two distinct `IfcPropertySet`s
|
|
39
|
+
* named e.g. "Pset_WallCommon" (one from the type, one from the
|
|
40
|
+
* occurrence), and the wanted value may live on the second one. Use this
|
|
41
|
+
* instead of `findPropertyInSets` when the result feeds a `.some(...)`
|
|
42
|
+
* predicate rather than a single value read; keep `findPropertyInSets` for
|
|
43
|
+
* value extraction (export/aggregation/display), where picking one member
|
|
44
|
+
* is the documented, correct behaviour.
|
|
45
|
+
*/
|
|
46
|
+
export function findAllPropertiesInSets(sets, setName, propName) {
|
|
47
|
+
const matches = [];
|
|
48
|
+
for (const set of sets) {
|
|
49
|
+
if (set.name !== setName)
|
|
50
|
+
continue;
|
|
51
|
+
for (const p of set.properties) {
|
|
52
|
+
if (p.name === propName)
|
|
53
|
+
matches.push(p);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return matches;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Quantity counterpart to `findAllPropertiesInSets` — see its doc comment.
|
|
60
|
+
*/
|
|
61
|
+
export function findAllQuantitiesInSets(sets, setName, quantityName) {
|
|
62
|
+
const matches = [];
|
|
63
|
+
for (const set of sets) {
|
|
64
|
+
if (set.name !== setName)
|
|
65
|
+
continue;
|
|
66
|
+
for (const q of set.quantities) {
|
|
67
|
+
if (q.name === quantityName)
|
|
68
|
+
matches.push(q);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return matches;
|
|
72
|
+
}
|
|
73
|
+
//# sourceMappingURL=pset-lookup.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pset-lookup.js","sourceRoot":"","sources":["../src/pset-lookup.ts"],"names":[],"mappings":"AAAA;;+DAE+D;AAsC/D;;;;GAIG;AACH,MAAM,UAAU,kBAAkB,CAChC,IAAmC,EACnC,OAAe,EACf,QAAgB;IAEhB,KAAK,MAAM,GAAG,IAAI,IAAI,EAAE,CAAC;QACvB,IAAI,GAAG,CAAC,IAAI,KAAK,OAAO;YAAE,SAAS;QACnC,MAAM,KAAK,GAAG,GAAG,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC;QAC9D,IAAI,KAAK;YAAE,OAAO,KAAK,CAAC;IAC1B,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,kBAAkB,CAChC,IAAmC,EACnC,OAAe,EACf,YAAoB;IAEpB,KAAK,MAAM,GAAG,IAAI,IAAI,EAAE,CAAC;QACvB,IAAI,GAAG,CAAC,IAAI,KAAK,OAAO;YAAE,SAAS;QACnC,MAAM,KAAK,GAAG,GAAG,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,YAAY,CAAC,CAAC;QAClE,IAAI,KAAK;YAAE,OAAO,KAAK,CAAC;IAC1B,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,uBAAuB,CACrC,IAAmC,EACnC,OAAe,EACf,QAAgB;IAEhB,MAAM,OAAO,GAAQ,EAAE,CAAC;IACxB,KAAK,MAAM,GAAG,IAAI,IAAI,EAAE,CAAC;QACvB,IAAI,GAAG,CAAC,IAAI,KAAK,OAAO;YAAE,SAAS;QACnC,KAAK,MAAM,CAAC,IAAI,GAAG,CAAC,UAAU,EAAE,CAAC;YAC/B,IAAI,CAAC,CAAC,IAAI,KAAK,QAAQ;gBAAE,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAC3C,CAAC;IACH,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,uBAAuB,CACrC,IAAmC,EACnC,OAAe,EACf,YAAoB;IAEpB,MAAM,OAAO,GAAQ,EAAE,CAAC;IACxB,KAAK,MAAM,GAAG,IAAI,IAAI,EAAE,CAAC;QACvB,IAAI,GAAG,CAAC,IAAI,KAAK,OAAO;YAAE,SAAS;QACnC,KAAK,MAAM,CAAC,IAAI,GAAG,CAAC,UAAU,EAAE,CAAC;YAC/B,IAAI,CAAC,CAAC,IAAI,KAAK,YAAY;gBAAE,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAC/C,CAAC;IACH,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC"}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AST for the IfcOpenShell selector (filter) syntax.
|
|
3
|
+
*
|
|
4
|
+
* The AST is the seam: {@link parseSelector} understands the whole grammar,
|
|
5
|
+
* and each surface (the viewer's rule builder today, the CLI/MCP/SDK query
|
|
6
|
+
* descriptor next) is an adapter from this tree onto its own representation.
|
|
7
|
+
* The parser therefore accepts constructs no adapter can evaluate yet — an
|
|
8
|
+
* adapter reports those back rather than dropping them, which is why every
|
|
9
|
+
* node carries {@link SelectorFilterBase.text}, the exact source substring the
|
|
10
|
+
* user typed.
|
|
11
|
+
*/
|
|
12
|
+
/** Comparison operators the grammar defines. `*=` is "contains". */
|
|
13
|
+
export type SelectorOp = '=' | '!=' | '>' | '>=' | '<' | '<=' | '*=' | '!*=';
|
|
14
|
+
/** A literal name or value: a plain string, or a `/…/` regular expression. */
|
|
15
|
+
export type SelectorText = {
|
|
16
|
+
kind: 'string';
|
|
17
|
+
text: string;
|
|
18
|
+
} | {
|
|
19
|
+
kind: 'regex';
|
|
20
|
+
source: string;
|
|
21
|
+
};
|
|
22
|
+
/** A comparison right-hand side. Bare `NULL` (any case) is the null literal. */
|
|
23
|
+
export type SelectorValue = SelectorText | {
|
|
24
|
+
kind: 'null';
|
|
25
|
+
};
|
|
26
|
+
/** Keyword filters that compare one derived attribute of an element. */
|
|
27
|
+
export type SelectorKeywordKind = 'type' | 'material' | 'classification' | 'location' | 'parent';
|
|
28
|
+
interface SelectorFilterBase {
|
|
29
|
+
/** The exact source substring this filter was parsed from. */
|
|
30
|
+
text: string;
|
|
31
|
+
}
|
|
32
|
+
/** `IfcWall` / `! IfcWall` — the class and all its subclasses. */
|
|
33
|
+
interface SelectorClassFilter extends SelectorFilterBase {
|
|
34
|
+
kind: 'class';
|
|
35
|
+
name: string;
|
|
36
|
+
negate: boolean;
|
|
37
|
+
}
|
|
38
|
+
/** `325Q7Fhnf67OZC$$r43uzK` / `! 325Q7Fhnf67OZC$$r43uzK` — one element. */
|
|
39
|
+
interface SelectorGlobalIdFilter extends SelectorFilterBase {
|
|
40
|
+
kind: 'globalId';
|
|
41
|
+
id: string;
|
|
42
|
+
negate: boolean;
|
|
43
|
+
}
|
|
44
|
+
/** `Name=Foo` — an IFC attribute of the element itself. */
|
|
45
|
+
interface SelectorAttributeFilter extends SelectorFilterBase {
|
|
46
|
+
kind: 'attribute';
|
|
47
|
+
name: string;
|
|
48
|
+
op: SelectorOp;
|
|
49
|
+
value: SelectorValue;
|
|
50
|
+
}
|
|
51
|
+
/** `Pset_WallCommon.FireRating=2HR` — a property in a property set. */
|
|
52
|
+
interface SelectorPropertyFilter extends SelectorFilterBase {
|
|
53
|
+
kind: 'property';
|
|
54
|
+
pset: SelectorText;
|
|
55
|
+
prop: SelectorText;
|
|
56
|
+
op: SelectorOp;
|
|
57
|
+
value: SelectorValue;
|
|
58
|
+
}
|
|
59
|
+
/** `type=WT01`, `material=concrete`, `location="Level 3"`, … */
|
|
60
|
+
interface SelectorKeywordFilter extends SelectorFilterBase {
|
|
61
|
+
kind: SelectorKeywordKind;
|
|
62
|
+
op: SelectorOp;
|
|
63
|
+
value: SelectorValue;
|
|
64
|
+
}
|
|
65
|
+
/** `query:types.count=0` — a value-query key path. */
|
|
66
|
+
interface SelectorQueryFilter extends SelectorFilterBase {
|
|
67
|
+
kind: 'query';
|
|
68
|
+
keys: string;
|
|
69
|
+
op: SelectorOp;
|
|
70
|
+
value: SelectorValue;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Every filter the grammar can express. The member interfaces stay
|
|
74
|
+
* module-internal: a consumer narrows on `filter.kind` off this union rather
|
|
75
|
+
* than naming one, and an exported type nothing consumes is semver liability
|
|
76
|
+
* with no reader.
|
|
77
|
+
*/
|
|
78
|
+
export type SelectorFilter = SelectorClassFilter | SelectorGlobalIdFilter | SelectorAttributeFilter | SelectorPropertyFilter | SelectorKeywordFilter | SelectorQueryFilter;
|
|
79
|
+
/**
|
|
80
|
+
* Filters chained with `,`. They narrow left to right (AND).
|
|
81
|
+
*
|
|
82
|
+
* Not re-exported from the package index: a caller reaches one through
|
|
83
|
+
* {@link SelectorQuery.groups}, and an export nothing consumes is permanent
|
|
84
|
+
* semver liability.
|
|
85
|
+
*/
|
|
86
|
+
export interface SelectorGroup {
|
|
87
|
+
filters: SelectorFilter[];
|
|
88
|
+
}
|
|
89
|
+
/** Groups joined with `+`. Their results are unioned. */
|
|
90
|
+
export interface SelectorQuery {
|
|
91
|
+
groups: SelectorGroup[];
|
|
92
|
+
}
|
|
93
|
+
export interface SelectorParseError {
|
|
94
|
+
message: string;
|
|
95
|
+
/** 0-based character offset into the input where the problem starts. */
|
|
96
|
+
offset: number;
|
|
97
|
+
}
|
|
98
|
+
export type SelectorParseResult = {
|
|
99
|
+
ok: true;
|
|
100
|
+
query: SelectorQuery;
|
|
101
|
+
} | {
|
|
102
|
+
ok: false;
|
|
103
|
+
error: SelectorParseError;
|
|
104
|
+
};
|
|
105
|
+
export {};
|
|
106
|
+
//# sourceMappingURL=ast.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ast.d.ts","sourceRoot":"","sources":["../../src/selector/ast.ts"],"names":[],"mappings":"AAIA;;;;;;;;;;GAUG;AAEH,oEAAoE;AACpE,MAAM,MAAM,UAAU,GAAG,GAAG,GAAG,IAAI,GAAG,GAAG,GAAG,IAAI,GAAG,GAAG,GAAG,IAAI,GAAG,IAAI,GAAG,KAAK,CAAC;AAE7E,8EAA8E;AAC9E,MAAM,MAAM,YAAY,GACpB;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAChC;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAEtC,gFAAgF;AAChF,MAAM,MAAM,aAAa,GAAG,YAAY,GAAG;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC;AAE5D,wEAAwE;AACxE,MAAM,MAAM,mBAAmB,GAC3B,MAAM,GACN,UAAU,GACV,gBAAgB,GAChB,UAAU,GACV,QAAQ,CAAC;AAEb,UAAU,kBAAkB;IAC1B,8DAA8D;IAC9D,IAAI,EAAE,MAAM,CAAC;CACd;AAED,kEAAkE;AAClE,UAAU,mBAAoB,SAAQ,kBAAkB;IACtD,IAAI,EAAE,OAAO,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,OAAO,CAAC;CACjB;AAED,2EAA2E;AAC3E,UAAU,sBAAuB,SAAQ,kBAAkB;IACzD,IAAI,EAAE,UAAU,CAAC;IACjB,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,OAAO,CAAC;CACjB;AAED,2DAA2D;AAC3D,UAAU,uBAAwB,SAAQ,kBAAkB;IAC1D,IAAI,EAAE,WAAW,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,UAAU,CAAC;IACf,KAAK,EAAE,aAAa,CAAC;CACtB;AAED,uEAAuE;AACvE,UAAU,sBAAuB,SAAQ,kBAAkB;IACzD,IAAI,EAAE,UAAU,CAAC;IACjB,IAAI,EAAE,YAAY,CAAC;IACnB,IAAI,EAAE,YAAY,CAAC;IACnB,EAAE,EAAE,UAAU,CAAC;IACf,KAAK,EAAE,aAAa,CAAC;CACtB;AAED,gEAAgE;AAChE,UAAU,qBAAsB,SAAQ,kBAAkB;IACxD,IAAI,EAAE,mBAAmB,CAAC;IAC1B,EAAE,EAAE,UAAU,CAAC;IACf,KAAK,EAAE,aAAa,CAAC;CACtB;AAED,sDAAsD;AACtD,UAAU,mBAAoB,SAAQ,kBAAkB;IACtD,IAAI,EAAE,OAAO,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,UAAU,CAAC;IACf,KAAK,EAAE,aAAa,CAAC;CACtB;AAED;;;;;GAKG;AACH,MAAM,MAAM,cAAc,GACtB,mBAAmB,GACnB,sBAAsB,GACtB,uBAAuB,GACvB,sBAAsB,GACtB,qBAAqB,GACrB,mBAAmB,CAAC;AAExB;;;;;;GAMG;AACH,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,cAAc,EAAE,CAAC;CAC3B;AAED,yDAAyD;AACzD,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,aAAa,EAAE,CAAC;CACzB;AAED,MAAM,WAAW,kBAAkB;IACjC,OAAO,EAAE,MAAM,CAAC;IAChB,wEAAwE;IACxE,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,MAAM,mBAAmB,GAC3B;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,aAAa,CAAA;CAAE,GAClC;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,kBAAkB,CAAA;CAAE,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ast.js","sourceRoot":"","sources":["../../src/selector/ast.ts"],"names":[],"mappings":"AAAA;;+DAE+D"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Recursive-descent parser for the IfcOpenShell selector syntax.
|
|
3
|
+
*
|
|
4
|
+
* query := group ("+" group)* groups are unioned
|
|
5
|
+
* group := filter ("," filter)* filters narrow left to right
|
|
6
|
+
* filter := "!"? (class | globalId)
|
|
7
|
+
* | word ("." name)? op value
|
|
8
|
+
* | (string | regex) "." name op value
|
|
9
|
+
* | word "." (string | regex) op value
|
|
10
|
+
*
|
|
11
|
+
* The parser is deliberately more capable than any single adapter: it accepts
|
|
12
|
+
* every construct the page documents, so an adapter can name what it cannot
|
|
13
|
+
* evaluate instead of silently matching nothing — the defect behind #4091.
|
|
14
|
+
*
|
|
15
|
+
* See {@link tokenizeSelector} for the lexical rules and where they are ifc-lite's
|
|
16
|
+
* reading rather than the page's words.
|
|
17
|
+
*/
|
|
18
|
+
import type { SelectorParseResult } from './ast.js';
|
|
19
|
+
export declare function parseSelector(text: string): SelectorParseResult;
|
|
20
|
+
//# sourceMappingURL=parse.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"parse.d.ts","sourceRoot":"","sources":["../../src/selector/parse.ts"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,KAAK,EAMV,mBAAmB,EAGpB,MAAM,UAAU,CAAC;AAqBlB,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,mBAAmB,CAe/D"}
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
|
4
|
+
import { tokenizeSelector } from './tokenize.js';
|
|
5
|
+
const KEYWORDS = new Map([
|
|
6
|
+
['type', 'type'],
|
|
7
|
+
['material', 'material'],
|
|
8
|
+
['classification', 'classification'],
|
|
9
|
+
['location', 'location'],
|
|
10
|
+
['parent', 'parent'],
|
|
11
|
+
]);
|
|
12
|
+
const QUERY_PREFIX = 'query:';
|
|
13
|
+
const GLOBAL_ID = /^[0-9A-Za-z_$]{22}$/;
|
|
14
|
+
class ParseFailure extends Error {
|
|
15
|
+
error;
|
|
16
|
+
constructor(error) {
|
|
17
|
+
super(error.message);
|
|
18
|
+
this.error = error;
|
|
19
|
+
this.name = 'ParseFailure';
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export function parseSelector(text) {
|
|
23
|
+
const lexed = tokenizeSelector(text);
|
|
24
|
+
if (!lexed.ok)
|
|
25
|
+
return { ok: false, error: lexed.error };
|
|
26
|
+
if (lexed.tokens.length === 0) {
|
|
27
|
+
return {
|
|
28
|
+
ok: false,
|
|
29
|
+
error: { message: 'empty selector: type a class name such as IfcWall', offset: 0 },
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
try {
|
|
33
|
+
return { ok: true, query: { groups: new Parser(text, lexed.tokens).parseQuery() } };
|
|
34
|
+
}
|
|
35
|
+
catch (err) {
|
|
36
|
+
if (err instanceof ParseFailure)
|
|
37
|
+
return { ok: false, error: err.error };
|
|
38
|
+
throw err;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
class Parser {
|
|
42
|
+
source;
|
|
43
|
+
tokens;
|
|
44
|
+
pos = 0;
|
|
45
|
+
constructor(source, tokens) {
|
|
46
|
+
this.source = source;
|
|
47
|
+
this.tokens = tokens;
|
|
48
|
+
}
|
|
49
|
+
parseQuery() {
|
|
50
|
+
const groups = [this.parseGroup()];
|
|
51
|
+
while (this.peek()?.kind === 'plus') {
|
|
52
|
+
this.advance();
|
|
53
|
+
groups.push(this.parseGroup());
|
|
54
|
+
}
|
|
55
|
+
const trailing = this.peek();
|
|
56
|
+
if (trailing)
|
|
57
|
+
this.fail(`unexpected "${trailing.value}"`, trailing.start);
|
|
58
|
+
return groups;
|
|
59
|
+
}
|
|
60
|
+
parseGroup() {
|
|
61
|
+
const filters = [this.parseFilter()];
|
|
62
|
+
while (this.peek()?.kind === 'comma') {
|
|
63
|
+
this.advance();
|
|
64
|
+
filters.push(this.parseFilter());
|
|
65
|
+
}
|
|
66
|
+
return { filters };
|
|
67
|
+
}
|
|
68
|
+
parseFilter() {
|
|
69
|
+
const first = this.require('expected a filter');
|
|
70
|
+
let negate = false;
|
|
71
|
+
let head = first;
|
|
72
|
+
if (first.kind === 'bang') {
|
|
73
|
+
negate = true;
|
|
74
|
+
this.advance();
|
|
75
|
+
head = this.require('expected a class name or GlobalId after "!"');
|
|
76
|
+
}
|
|
77
|
+
if (head.kind !== 'word' && head.kind !== 'string' && head.kind !== 'regex') {
|
|
78
|
+
this.fail(`unexpected "${head.value}"`, head.start);
|
|
79
|
+
}
|
|
80
|
+
this.advance();
|
|
81
|
+
const after = this.peek();
|
|
82
|
+
if (after?.kind === 'dot') {
|
|
83
|
+
if (negate)
|
|
84
|
+
this.failNegate(first);
|
|
85
|
+
this.advance();
|
|
86
|
+
return this.finishProperty(first, nameOf(head));
|
|
87
|
+
}
|
|
88
|
+
// `Pset_BeamCommon."IsExternal"` — a bare-word property set keeps its '.'
|
|
89
|
+
// inside the word (that is what keeps a decimal like `1.5` one token), so
|
|
90
|
+
// the separator never reaches the branch above and the quoted property
|
|
91
|
+
// name arrives as the NEXT token. Without this, only the regex spelling
|
|
92
|
+
// `/Pset_.*Common/."IsExternal"` parsed, and the literal one failed with a
|
|
93
|
+
// message about a missing operator (#4091).
|
|
94
|
+
if (head.kind === 'word' &&
|
|
95
|
+
head.value.length > 1 &&
|
|
96
|
+
head.value.endsWith('.') &&
|
|
97
|
+
(after?.kind === 'string' || after?.kind === 'regex')) {
|
|
98
|
+
if (negate)
|
|
99
|
+
this.failNegate(first);
|
|
100
|
+
return this.finishProperty(first, { kind: 'string', text: head.value.slice(0, -1) });
|
|
101
|
+
}
|
|
102
|
+
if (after?.kind === 'op') {
|
|
103
|
+
if (negate)
|
|
104
|
+
this.failNegate(first);
|
|
105
|
+
if (head.kind !== 'word') {
|
|
106
|
+
this.fail('a quoted name or regular expression is only valid as a property-set or property name, e.g. /Pset_.*Common/.FireRating=2HR', head.start);
|
|
107
|
+
}
|
|
108
|
+
return this.finishKeyed(first, head);
|
|
109
|
+
}
|
|
110
|
+
return this.finishBare(first, head, negate);
|
|
111
|
+
}
|
|
112
|
+
/** `<pset> . <prop> <op> <value>`, the pset already consumed. */
|
|
113
|
+
finishProperty(first, pset) {
|
|
114
|
+
const propTok = this.require('expected a property name after "."');
|
|
115
|
+
if (propTok.kind !== 'word' && propTok.kind !== 'string' && propTok.kind !== 'regex') {
|
|
116
|
+
this.fail(`expected a property name after "." but found "${propTok.value}"`, propTok.start);
|
|
117
|
+
}
|
|
118
|
+
this.advance();
|
|
119
|
+
const op = this.requireOp();
|
|
120
|
+
const value = this.requireValue(op);
|
|
121
|
+
return {
|
|
122
|
+
kind: 'property',
|
|
123
|
+
pset,
|
|
124
|
+
prop: nameOf(propTok),
|
|
125
|
+
op: op.value,
|
|
126
|
+
value,
|
|
127
|
+
text: this.spanFrom(first),
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
/** `<word> <op> <value>` — property with an inline dot, keyword, query or attribute. */
|
|
131
|
+
finishKeyed(first, head) {
|
|
132
|
+
const key = head.value;
|
|
133
|
+
const op = this.requireOp();
|
|
134
|
+
const value = this.requireValue(op);
|
|
135
|
+
const text = this.spanFrom(first);
|
|
136
|
+
if (key.toLowerCase().startsWith(QUERY_PREFIX)) {
|
|
137
|
+
const keys = key.slice(QUERY_PREFIX.length);
|
|
138
|
+
if (keys.length === 0)
|
|
139
|
+
this.fail('expected a key path after "query:"', head.start);
|
|
140
|
+
return { kind: 'query', keys, op: op.value, value, text };
|
|
141
|
+
}
|
|
142
|
+
const dot = key.indexOf('.');
|
|
143
|
+
if (dot >= 0) {
|
|
144
|
+
const pset = key.slice(0, dot);
|
|
145
|
+
const prop = key.slice(dot + 1);
|
|
146
|
+
if (pset.length === 0 || prop.length === 0) {
|
|
147
|
+
this.fail(`"${key}" is not a property set and property name, e.g. Pset_WallCommon.FireRating`, head.start);
|
|
148
|
+
}
|
|
149
|
+
return {
|
|
150
|
+
kind: 'property',
|
|
151
|
+
pset: { kind: 'string', text: pset },
|
|
152
|
+
prop: { kind: 'string', text: prop },
|
|
153
|
+
op: op.value,
|
|
154
|
+
value,
|
|
155
|
+
text,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
const keyword = KEYWORDS.get(key.toLowerCase());
|
|
159
|
+
if (keyword)
|
|
160
|
+
return { kind: keyword, op: op.value, value, text };
|
|
161
|
+
return { kind: 'attribute', name: key, op: op.value, value, text };
|
|
162
|
+
}
|
|
163
|
+
/** A filter with no operator: a class name or a GlobalId. */
|
|
164
|
+
finishBare(first, head, negate) {
|
|
165
|
+
if (head.kind !== 'word') {
|
|
166
|
+
this.fail(`"${head.value}" is not a filter: a quoted value or regular expression needs an attribute or property to compare, e.g. Name=${head.kind === 'regex' ? `/${head.value}/` : `"${head.value}"`}`, head.start);
|
|
167
|
+
}
|
|
168
|
+
const text = this.spanFrom(first);
|
|
169
|
+
if (head.value.includes('.')) {
|
|
170
|
+
this.fail(`"${head.value}" reads as a property set and property name, so it needs one of the operators = != > >= < <= *= !*= and a value`, head.start);
|
|
171
|
+
}
|
|
172
|
+
if (head.value.toLowerCase().startsWith('ifc')) {
|
|
173
|
+
return { kind: 'class', name: head.value, negate, text };
|
|
174
|
+
}
|
|
175
|
+
if (GLOBAL_ID.test(head.value)) {
|
|
176
|
+
return { kind: 'globalId', id: head.value, negate, text };
|
|
177
|
+
}
|
|
178
|
+
this.fail(`"${head.value}" is not an IFC class name (those start with "Ifc"), a 22-character GlobalId, or a comparison such as Name=${head.value}`, head.start);
|
|
179
|
+
}
|
|
180
|
+
// ── Token helpers ───────────────────────────────────────────────────────
|
|
181
|
+
peek() {
|
|
182
|
+
return this.tokens[this.pos];
|
|
183
|
+
}
|
|
184
|
+
advance() {
|
|
185
|
+
this.pos += 1;
|
|
186
|
+
}
|
|
187
|
+
require(message) {
|
|
188
|
+
const tok = this.peek();
|
|
189
|
+
if (!tok || tok.kind === 'comma' || tok.kind === 'plus') {
|
|
190
|
+
this.fail(message, tok ? tok.start : this.source.length);
|
|
191
|
+
}
|
|
192
|
+
return tok;
|
|
193
|
+
}
|
|
194
|
+
requireOp() {
|
|
195
|
+
const tok = this.peek();
|
|
196
|
+
if (!tok || tok.kind !== 'op') {
|
|
197
|
+
this.fail('expected one of the operators = != > >= < <= *= !*=', tok ? tok.start : this.source.length);
|
|
198
|
+
}
|
|
199
|
+
this.advance();
|
|
200
|
+
return tok;
|
|
201
|
+
}
|
|
202
|
+
requireValue(op) {
|
|
203
|
+
const tok = this.peek();
|
|
204
|
+
if (!tok || (tok.kind !== 'word' && tok.kind !== 'string' && tok.kind !== 'regex')) {
|
|
205
|
+
this.fail(`expected a value after "${op.value}"`, tok ? tok.start : this.source.length);
|
|
206
|
+
}
|
|
207
|
+
this.advance();
|
|
208
|
+
if (tok.kind === 'word' && tok.value.toUpperCase() === 'NULL')
|
|
209
|
+
return { kind: 'null' };
|
|
210
|
+
return nameOf(tok);
|
|
211
|
+
}
|
|
212
|
+
/** The source text from `first` through the token just consumed. */
|
|
213
|
+
spanFrom(first) {
|
|
214
|
+
const last = this.tokens[this.pos - 1];
|
|
215
|
+
return this.source.slice(first.start, last.end);
|
|
216
|
+
}
|
|
217
|
+
failNegate(first) {
|
|
218
|
+
this.fail('"!" may only negate a class name or a GlobalId', first.start);
|
|
219
|
+
}
|
|
220
|
+
fail(message, offset) {
|
|
221
|
+
throw new ParseFailure({ message, offset });
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
function nameOf(tok) {
|
|
225
|
+
return tok.kind === 'regex' ? { kind: 'regex', source: tok.value } : { kind: 'string', text: tok.value };
|
|
226
|
+
}
|
|
227
|
+
//# sourceMappingURL=parse.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"parse.js","sourceRoot":"","sources":["../../src/selector/parse.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lexer for the IfcOpenShell selector syntax.
|
|
3
|
+
*
|
|
4
|
+
* The rules below are ifc-lite's reading of
|
|
5
|
+
* <https://docs.ifcopenshell.org/ifcopenshell-python/selector_syntax.html>,
|
|
6
|
+
* stated here because the page describes the language in prose rather than a
|
|
7
|
+
* grammar:
|
|
8
|
+
*
|
|
9
|
+
* - Whitespace is insignificant outside quotes.
|
|
10
|
+
* - `,` separates filters within a group, `+` separates groups.
|
|
11
|
+
* - `!` immediately followed by `=` is the `!=` operator, followed by `*=` is
|
|
12
|
+
* `!*=`, and otherwise negates the class or GlobalId that follows.
|
|
13
|
+
* - `*` is only ever part of `*=`. A trailing `*` is NOT a glob in this
|
|
14
|
+
* grammar, so `IfcWall*` is a lexical error rather than a silent no-match —
|
|
15
|
+
* the exact shape reported in #4091.
|
|
16
|
+
* - `"…"` quotes a value; `\"` and `\\` escape inside it.
|
|
17
|
+
* - `/…/` is a regular expression; `\/` escapes a slash inside the body. No
|
|
18
|
+
* trailing flags: the grammar has none, so `/foo/i` is a lexical error at
|
|
19
|
+
* the `i` rather than a silently different match.
|
|
20
|
+
* - A bare word runs until whitespace or one of `,+=!<>*"/`, and may not start
|
|
21
|
+
* with `.`; a `.` in that leading position is the property-set separator, so
|
|
22
|
+
* both `Pset_WallCommon.FireRating` (one word, split by the parser) and
|
|
23
|
+
* `/Pset_.*Common/.FireRating` (regex, dot, word) read correctly while a
|
|
24
|
+
* decimal value such as `1.5` stays one word.
|
|
25
|
+
*/
|
|
26
|
+
import type { SelectorParseError } from './ast.js';
|
|
27
|
+
/** Lexeme classes. Not exported: `Token` is the only shape callers name. */
|
|
28
|
+
type TokenKind = 'word' | 'string' | 'regex' | 'op' | 'comma' | 'plus' | 'bang' | 'dot';
|
|
29
|
+
export interface Token {
|
|
30
|
+
kind: TokenKind;
|
|
31
|
+
/** Word text, string/regex body, or the operator spelling. */
|
|
32
|
+
value: string;
|
|
33
|
+
/** 0-based offset of the token's first character. */
|
|
34
|
+
start: number;
|
|
35
|
+
/** 0-based offset one past the token's last character. */
|
|
36
|
+
end: number;
|
|
37
|
+
}
|
|
38
|
+
export type TokenizeResult = {
|
|
39
|
+
ok: true;
|
|
40
|
+
tokens: Token[];
|
|
41
|
+
} | {
|
|
42
|
+
ok: false;
|
|
43
|
+
error: SelectorParseError;
|
|
44
|
+
};
|
|
45
|
+
export declare function tokenizeSelector(text: string): TokenizeResult;
|
|
46
|
+
export {};
|
|
47
|
+
//# sourceMappingURL=tokenize.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tokenize.d.ts","sourceRoot":"","sources":["../../src/selector/tokenize.ts"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,UAAU,CAAC;AAEnD,4EAA4E;AAC5E,KAAK,SAAS,GACV,MAAM,GACN,QAAQ,GACR,OAAO,GACP,IAAI,GACJ,OAAO,GACP,MAAM,GACN,MAAM,GACN,KAAK,CAAC;AAEV,MAAM,WAAW,KAAK;IACpB,IAAI,EAAE,SAAS,CAAC;IAChB,8DAA8D;IAC9D,KAAK,EAAE,MAAM,CAAC;IACd,qDAAqD;IACrD,KAAK,EAAE,MAAM,CAAC;IACd,0DAA0D;IAC1D,GAAG,EAAE,MAAM,CAAC;CACb;AAED,MAAM,MAAM,cAAc,GACtB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,MAAM,EAAE,KAAK,EAAE,CAAA;CAAE,GAC7B;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,kBAAkB,CAAA;CAAE,CAAC;AAa7C,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,cAAc,CA2E7D"}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
|
4
|
+
/** Characters that end a bare word. */
|
|
5
|
+
const WORD_BREAK = new Set([',', '+', '=', '!', '<', '>', '*', '"', '/']);
|
|
6
|
+
function isSpace(ch) {
|
|
7
|
+
return ch === ' ' || ch === '\t' || ch === '\n' || ch === '\r' || ch === '\f' || ch === '\v';
|
|
8
|
+
}
|
|
9
|
+
function fail(message, offset) {
|
|
10
|
+
return { ok: false, error: { message, offset } };
|
|
11
|
+
}
|
|
12
|
+
export function tokenizeSelector(text) {
|
|
13
|
+
const tokens = [];
|
|
14
|
+
let i = 0;
|
|
15
|
+
while (i < text.length) {
|
|
16
|
+
const ch = text[i];
|
|
17
|
+
if (isSpace(ch)) {
|
|
18
|
+
i += 1;
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
if (ch === ',') {
|
|
22
|
+
tokens.push({ kind: 'comma', value: ',', start: i, end: i + 1 });
|
|
23
|
+
i += 1;
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
if (ch === '+') {
|
|
27
|
+
tokens.push({ kind: 'plus', value: '+', start: i, end: i + 1 });
|
|
28
|
+
i += 1;
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
if (ch === '.') {
|
|
32
|
+
tokens.push({ kind: 'dot', value: '.', start: i, end: i + 1 });
|
|
33
|
+
i += 1;
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
if (ch === '!') {
|
|
37
|
+
if (text[i + 1] === '=') {
|
|
38
|
+
tokens.push({ kind: 'op', value: '!=', start: i, end: i + 2 });
|
|
39
|
+
i += 2;
|
|
40
|
+
}
|
|
41
|
+
else if (text[i + 1] === '*' && text[i + 2] === '=') {
|
|
42
|
+
tokens.push({ kind: 'op', value: '!*=', start: i, end: i + 3 });
|
|
43
|
+
i += 3;
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
tokens.push({ kind: 'bang', value: '!', start: i, end: i + 1 });
|
|
47
|
+
i += 1;
|
|
48
|
+
}
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
if (ch === '*') {
|
|
52
|
+
if (text[i + 1] !== '=') {
|
|
53
|
+
return fail(`"*" is only valid as part of the "*=" (contains) operator; this grammar has no "*" wildcard. Use a regular expression such as /Ifc.*Wall/ instead.`, i);
|
|
54
|
+
}
|
|
55
|
+
tokens.push({ kind: 'op', value: '*=', start: i, end: i + 2 });
|
|
56
|
+
i += 2;
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
if (ch === '=') {
|
|
60
|
+
tokens.push({ kind: 'op', value: '=', start: i, end: i + 1 });
|
|
61
|
+
i += 1;
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
if (ch === '<' || ch === '>') {
|
|
65
|
+
const two = text[i + 1] === '=';
|
|
66
|
+
const value = two ? `${ch}=` : ch;
|
|
67
|
+
tokens.push({ kind: 'op', value, start: i, end: i + value.length });
|
|
68
|
+
i += value.length;
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
if (ch === '"') {
|
|
72
|
+
const quoted = readQuoted(text, i);
|
|
73
|
+
if (!quoted.ok)
|
|
74
|
+
return quoted;
|
|
75
|
+
tokens.push(quoted.token);
|
|
76
|
+
i = quoted.token.end;
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
if (ch === '/') {
|
|
80
|
+
const regex = readRegex(text, i);
|
|
81
|
+
if (!regex.ok)
|
|
82
|
+
return regex;
|
|
83
|
+
tokens.push(regex.token);
|
|
84
|
+
i = regex.token.end;
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
const start = i;
|
|
88
|
+
while (i < text.length) {
|
|
89
|
+
const c = text[i];
|
|
90
|
+
if (isSpace(c) || WORD_BREAK.has(c))
|
|
91
|
+
break;
|
|
92
|
+
i += 1;
|
|
93
|
+
}
|
|
94
|
+
tokens.push({ kind: 'word', value: text.slice(start, i), start, end: i });
|
|
95
|
+
}
|
|
96
|
+
return { ok: true, tokens };
|
|
97
|
+
}
|
|
98
|
+
/** `"foo \"bar\" baz"` — the token value is the unescaped body. */
|
|
99
|
+
function readQuoted(text, start) {
|
|
100
|
+
let out = '';
|
|
101
|
+
let i = start + 1;
|
|
102
|
+
while (i < text.length) {
|
|
103
|
+
const c = text[i];
|
|
104
|
+
if (c === '\\') {
|
|
105
|
+
const next = text[i + 1];
|
|
106
|
+
if (next === undefined)
|
|
107
|
+
break;
|
|
108
|
+
out += next;
|
|
109
|
+
i += 2;
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
if (c === '"') {
|
|
113
|
+
return { ok: true, token: { kind: 'string', value: out, start, end: i + 1 } };
|
|
114
|
+
}
|
|
115
|
+
out += c;
|
|
116
|
+
i += 1;
|
|
117
|
+
}
|
|
118
|
+
return { ok: false, error: { message: 'unterminated quoted value: no closing "', offset: start } };
|
|
119
|
+
}
|
|
120
|
+
/** `/foo\/bar/` — the token value is the regex source with `\/` unescaped. */
|
|
121
|
+
function readRegex(text, start) {
|
|
122
|
+
let out = '';
|
|
123
|
+
let i = start + 1;
|
|
124
|
+
while (i < text.length) {
|
|
125
|
+
const c = text[i];
|
|
126
|
+
if (c === '\\') {
|
|
127
|
+
const next = text[i + 1];
|
|
128
|
+
if (next === undefined)
|
|
129
|
+
break;
|
|
130
|
+
// Only `\/` is about the delimiter; every other escape belongs to the
|
|
131
|
+
// regular expression and is handed through untouched.
|
|
132
|
+
out += next === '/' ? '/' : `\\${next}`;
|
|
133
|
+
i += 2;
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
if (c === '/') {
|
|
137
|
+
if (out.length === 0) {
|
|
138
|
+
return { ok: false, error: { message: 'empty regular expression: // matches nothing', offset: start } };
|
|
139
|
+
}
|
|
140
|
+
return { ok: true, token: { kind: 'regex', value: out, start, end: i + 1 } };
|
|
141
|
+
}
|
|
142
|
+
out += c;
|
|
143
|
+
i += 1;
|
|
144
|
+
}
|
|
145
|
+
return { ok: false, error: { message: 'unterminated regular expression: no closing /', offset: start } };
|
|
146
|
+
}
|
|
147
|
+
//# sourceMappingURL=tokenize.js.map
|