@ifc-lite/parser 4.3.1 → 5.0.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.
Files changed (176) hide show
  1. package/dist/attribute-helpers.d.ts +38 -4
  2. package/dist/attribute-helpers.d.ts.map +1 -1
  3. package/dist/attribute-helpers.js +71 -4
  4. package/dist/attribute-helpers.js.map +1 -1
  5. package/dist/columnar-parser-attributes.d.ts.map +1 -1
  6. package/dist/columnar-parser-attributes.js +35 -5
  7. package/dist/columnar-parser-attributes.js.map +1 -1
  8. package/dist/columnar-parser-indexes.d.ts.map +1 -1
  9. package/dist/columnar-parser-indexes.js +17 -2
  10. package/dist/columnar-parser-indexes.js.map +1 -1
  11. package/dist/columnar-parser-relationships.d.ts.map +1 -1
  12. package/dist/columnar-parser-relationships.js +6 -1
  13. package/dist/columnar-parser-relationships.js.map +1 -1
  14. package/dist/columnar-parser.d.ts +3 -1
  15. package/dist/columnar-parser.d.ts.map +1 -1
  16. package/dist/columnar-parser.js +27 -76
  17. package/dist/columnar-parser.js.map +1 -1
  18. package/dist/compact-entity-index-transport.d.ts +23 -0
  19. package/dist/compact-entity-index-transport.d.ts.map +1 -0
  20. package/dist/compact-entity-index-transport.js +28 -0
  21. package/dist/compact-entity-index-transport.js.map +1 -0
  22. package/dist/compact-entity-index.d.ts +0 -10
  23. package/dist/compact-entity-index.d.ts.map +1 -1
  24. package/dist/compact-entity-index.js +31 -24
  25. package/dist/compact-entity-index.js.map +1 -1
  26. package/dist/data-store-transport.d.ts +2 -12
  27. package/dist/data-store-transport.d.ts.map +1 -1
  28. package/dist/data-store-transport.js +7 -20
  29. package/dist/data-store-transport.js.map +1 -1
  30. package/dist/entity-extractor.d.ts.map +1 -1
  31. package/dist/entity-extractor.js +101 -6
  32. package/dist/entity-extractor.js.map +1 -1
  33. package/dist/entity-refs-from-index.d.ts.map +1 -1
  34. package/dist/entity-refs-from-index.js +12 -2
  35. package/dist/entity-refs-from-index.js.map +1 -1
  36. package/dist/entity-scanner.d.ts +63 -0
  37. package/dist/entity-scanner.d.ts.map +1 -1
  38. package/dist/entity-scanner.js +79 -2
  39. package/dist/entity-scanner.js.map +1 -1
  40. package/dist/express-id.d.ts +47 -0
  41. package/dist/express-id.d.ts.map +1 -0
  42. package/dist/express-id.js +57 -0
  43. package/dist/express-id.js.map +1 -0
  44. package/dist/generated/entities.d.ts +1 -1
  45. package/dist/generated/entities.d.ts.map +1 -1
  46. package/dist/generated/schema-registry.js +146 -146
  47. package/dist/generated/schema-registry.js.map +1 -1
  48. package/dist/georef-epset.d.ts +25 -0
  49. package/dist/georef-epset.d.ts.map +1 -0
  50. package/dist/georef-epset.js +214 -0
  51. package/dist/georef-epset.js.map +1 -0
  52. package/dist/georef-extractor.d.ts +9 -12
  53. package/dist/georef-extractor.d.ts.map +1 -1
  54. package/dist/georef-extractor.js +66 -304
  55. package/dist/georef-extractor.js.map +1 -1
  56. package/dist/georef-map-conversion.d.ts +29 -0
  57. package/dist/georef-map-conversion.d.ts.map +1 -0
  58. package/dist/georef-map-conversion.js +75 -0
  59. package/dist/georef-map-conversion.js.map +1 -0
  60. package/dist/georef-transform.d.ts +25 -0
  61. package/dist/georef-transform.d.ts.map +1 -0
  62. package/dist/georef-transform.js +97 -0
  63. package/dist/georef-transform.js.map +1 -0
  64. package/dist/ifc-schema.d.ts +4 -2
  65. package/dist/ifc-schema.d.ts.map +1 -1
  66. package/dist/ifc-schema.js +15 -5
  67. package/dist/ifc-schema.js.map +1 -1
  68. package/dist/index.d.ts +5 -2
  69. package/dist/index.d.ts.map +1 -1
  70. package/dist/index.js +5 -2
  71. package/dist/index.js.map +1 -1
  72. package/dist/map-unit-label.d.ts +43 -0
  73. package/dist/map-unit-label.d.ts.map +1 -0
  74. package/dist/map-unit-label.js +140 -0
  75. package/dist/map-unit-label.js.map +1 -0
  76. package/dist/material-extractor.d.ts +2 -17
  77. package/dist/material-extractor.d.ts.map +1 -1
  78. package/dist/material-extractor.js +12 -37
  79. package/dist/material-extractor.js.map +1 -1
  80. package/dist/material-layer-reader.d.ts +45 -0
  81. package/dist/material-layer-reader.d.ts.map +1 -0
  82. package/dist/material-layer-reader.js +63 -0
  83. package/dist/material-layer-reader.js.map +1 -0
  84. package/dist/material-resolver.d.ts.map +1 -1
  85. package/dist/material-resolver.js +14 -14
  86. package/dist/material-resolver.js.map +1 -1
  87. package/dist/measure-unit-scale.d.ts +41 -0
  88. package/dist/measure-unit-scale.d.ts.map +1 -0
  89. package/dist/measure-unit-scale.js +53 -0
  90. package/dist/measure-unit-scale.js.map +1 -0
  91. package/dist/on-demand-cache.d.ts +3 -0
  92. package/dist/on-demand-cache.d.ts.map +1 -0
  93. package/dist/on-demand-cache.js +25 -0
  94. package/dist/on-demand-cache.js.map +1 -0
  95. package/dist/on-demand-extractors.d.ts +5 -21
  96. package/dist/on-demand-extractors.d.ts.map +1 -1
  97. package/dist/on-demand-extractors.js +64 -437
  98. package/dist/on-demand-extractors.js.map +1 -1
  99. package/dist/on-demand-georeferencing.d.ts +19 -0
  100. package/dist/on-demand-georeferencing.d.ts.map +1 -0
  101. package/dist/on-demand-georeferencing.js +139 -0
  102. package/dist/on-demand-georeferencing.js.map +1 -0
  103. package/dist/owning-project.d.ts +54 -0
  104. package/dist/owning-project.d.ts.map +1 -0
  105. package/dist/owning-project.js +76 -0
  106. package/dist/owning-project.js.map +1 -0
  107. package/dist/parser.worker.d.ts +14 -0
  108. package/dist/parser.worker.d.ts.map +1 -1
  109. package/dist/parser.worker.js +2 -0
  110. package/dist/parser.worker.js.map +1 -1
  111. package/dist/project-units-symbols.d.ts +33 -0
  112. package/dist/project-units-symbols.d.ts.map +1 -0
  113. package/dist/project-units-symbols.js +265 -0
  114. package/dist/project-units-symbols.js.map +1 -0
  115. package/dist/project-units.d.ts +11 -19
  116. package/dist/project-units.d.ts.map +1 -1
  117. package/dist/project-units.js +9 -267
  118. package/dist/project-units.js.map +1 -1
  119. package/dist/property-set-merge.d.ts +65 -8
  120. package/dist/property-set-merge.d.ts.map +1 -1
  121. package/dist/property-set-merge.js +130 -29
  122. package/dist/property-set-merge.js.map +1 -1
  123. package/dist/property-value-parser.d.ts +55 -0
  124. package/dist/property-value-parser.d.ts.map +1 -0
  125. package/dist/property-value-parser.js +277 -0
  126. package/dist/property-value-parser.js.map +1 -0
  127. package/dist/quantity-collect.d.ts +135 -0
  128. package/dist/quantity-collect.d.ts.map +1 -0
  129. package/dist/quantity-collect.js +253 -0
  130. package/dist/quantity-collect.js.map +1 -0
  131. package/dist/query-backend-maps.d.ts +5 -8
  132. package/dist/query-backend-maps.d.ts.map +1 -1
  133. package/dist/query-backend-maps.js +159 -15
  134. package/dist/query-backend-maps.js.map +1 -1
  135. package/dist/relationship-extractor.d.ts.map +1 -1
  136. package/dist/relationship-extractor.js +18 -0
  137. package/dist/relationship-extractor.js.map +1 -1
  138. package/dist/scan-entities-balanced.d.ts +38 -0
  139. package/dist/scan-entities-balanced.d.ts.map +1 -0
  140. package/dist/scan-entities-balanced.js +258 -0
  141. package/dist/scan-entities-balanced.js.map +1 -0
  142. package/dist/scan-worker-inline.d.ts +17 -11
  143. package/dist/scan-worker-inline.d.ts.map +1 -1
  144. package/dist/scan-worker-inline.js +9 -235
  145. package/dist/scan-worker-inline.js.map +1 -1
  146. package/dist/scan-worker-source.d.ts +9 -0
  147. package/dist/scan-worker-source.d.ts.map +1 -0
  148. package/dist/scan-worker-source.js +383 -0
  149. package/dist/scan-worker-source.js.map +1 -0
  150. package/dist/source-header.d.ts +23 -1
  151. package/dist/source-header.d.ts.map +1 -1
  152. package/dist/source-header.js +133 -43
  153. package/dist/source-header.js.map +1 -1
  154. package/dist/spatial-hierarchy-builder.js +3 -3
  155. package/dist/spatial-hierarchy-builder.js.map +1 -1
  156. package/dist/step-lexing.d.ts +94 -0
  157. package/dist/step-lexing.d.ts.map +1 -1
  158. package/dist/step-lexing.js +292 -25
  159. package/dist/step-lexing.js.map +1 -1
  160. package/dist/step-trivia.d.ts +60 -0
  161. package/dist/step-trivia.d.ts.map +1 -0
  162. package/dist/step-trivia.js +63 -0
  163. package/dist/step-trivia.js.map +1 -0
  164. package/dist/tokenizer.d.ts +20 -21
  165. package/dist/tokenizer.d.ts.map +1 -1
  166. package/dist/tokenizer.js +164 -203
  167. package/dist/tokenizer.js.map +1 -1
  168. package/dist/unit-extractor.d.ts +56 -1
  169. package/dist/unit-extractor.d.ts.map +1 -1
  170. package/dist/unit-extractor.js +80 -6
  171. package/dist/unit-extractor.js.map +1 -1
  172. package/dist/worker-parser.d.ts +11 -1
  173. package/dist/worker-parser.d.ts.map +1 -1
  174. package/dist/worker-parser.js +16 -2
  175. package/dist/worker-parser.js.map +1 -1
  176. package/package.json +6 -6
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Regex source for ISO 10303-21 "trivia": a run of ASCII whitespace and
3
+ * non-nesting `/* ... *​/` comments, in any order, ANYWHERE whitespace is
4
+ * legal in a record — including between a type name and its `(`.
5
+ *
6
+ * String-side counterpart to `skipTrivia` in `step-lexing.ts`, for callers
7
+ * that already hold a decoded record as a JS string and build a regex around
8
+ * it (entity/typed-value extraction and rewriting), rather than scanning raw
9
+ * bytes. Both mirror `skip_step_trivia` in `rust/core/src/parser/lexical.rs`.
10
+ *
11
+ * Deliberately ASCII-only (`[ \t\n\r\x0b\x0c]`, matching `is_step_space` /
12
+ * `isSpaceByte` in `step-lexing.ts`) rather than `\s`, which also matches
13
+ * U+00A0 and other Unicode space separators Rust's byte scanner does not
14
+ * treat as whitespace — the exact TS/Rust divergence class issue #3733
15
+ * fixed for the byte scanners; a new trivia matcher should not reintroduce
16
+ * it under a different name.
17
+ *
18
+ * ## Why each alternative is shaped the way it is
19
+ *
20
+ * The outer `(?:A|B)*` must have exactly ONE way to partition any input into
21
+ * iterations. Where it has more than one, a failing suffix makes the engine
22
+ * enumerate them, which is exponential in the length of the trivia run. Both
23
+ * alternatives below are written to keep that count at one, and BOTH shapes
24
+ * have been measured — the two axes fail independently, so neither is
25
+ * theoretical:
26
+ *
27
+ * - The comment body is `(?:[^*]|\*(?!/))*`, NOT a lazy `[\s\S]*?`. A lazy
28
+ * body looks unambiguous locally, but when the overall pattern fails past
29
+ * a comment the engine retries it against every later `*​/`, so one comment
30
+ * can absorb the ones after it and the two alternatives start overlapping
31
+ * on the same span. Requiring every `*` in the body to not be followed by
32
+ * `/` gives the body exactly one maximal extent, so a comment can never
33
+ * swallow the next one.
34
+ * - The whitespace alternative is a SINGLE-CHARACTER class, NOT `[...]+`.
35
+ * This is the counter-intuitive half: `+` looks like it collapses a run
36
+ * into one iteration, but the outer `*` can still split an n-character run
37
+ * into any composition of `+` matches (2^(n-1) of them) and walks all of
38
+ * them on failure — the textbook `(?:A+|B)*` blowup. A single-character
39
+ * class has exactly one partition: n iterations of one character each.
40
+ *
41
+ * Both hazards are pinned by `packages/parser/test/step-trivia-redos.test.ts`,
42
+ * one case per axis, and the test comment there carries the measurements.
43
+ * Change either alternative only with that test in front of you.
44
+ *
45
+ * ## Relation to the byte scanners
46
+ *
47
+ * The comment body also brings this pattern into line with
48
+ * `skip_step_trivia`, which stops a comment at its FIRST `*​/`. A lazy body
49
+ * did not: on backtracking it would accept `/* a *​/ *​/` as one comment,
50
+ * where Rust reads a comment followed by junk and refuses the record. The
51
+ * shared vector "reject: comments do not nest, so the trailing `*​/` is junk"
52
+ * in `rust/core/tests/fixtures/type_paren_trivia_vectors.json` pins that
53
+ * agreement, so this is a deliberate narrowing, not an accident.
54
+ *
55
+ * An unpaired, unterminated `/*` still fails to match rather than hanging:
56
+ * the body runs out of input, backtracking is O(1) per position, and the
57
+ * required `\*​/` after it never appears.
58
+ */
59
+ export declare const STEP_TRIVIA = "(?:[ \\t\\n\\r\\x0b\\x0c]|/\\*(?:[^*]|\\*(?!/))*\\*/)*";
60
+ //# sourceMappingURL=step-trivia.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"step-trivia.d.ts","sourceRoot":"","sources":["../src/step-trivia.ts"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyDG;AACH,eAAO,MAAM,WAAW,2DAA2D,CAAC"}
@@ -0,0 +1,63 @@
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
+ * Regex source for ISO 10303-21 "trivia": a run of ASCII whitespace and
6
+ * non-nesting `/* ... *​/` comments, in any order, ANYWHERE whitespace is
7
+ * legal in a record — including between a type name and its `(`.
8
+ *
9
+ * String-side counterpart to `skipTrivia` in `step-lexing.ts`, for callers
10
+ * that already hold a decoded record as a JS string and build a regex around
11
+ * it (entity/typed-value extraction and rewriting), rather than scanning raw
12
+ * bytes. Both mirror `skip_step_trivia` in `rust/core/src/parser/lexical.rs`.
13
+ *
14
+ * Deliberately ASCII-only (`[ \t\n\r\x0b\x0c]`, matching `is_step_space` /
15
+ * `isSpaceByte` in `step-lexing.ts`) rather than `\s`, which also matches
16
+ * U+00A0 and other Unicode space separators Rust's byte scanner does not
17
+ * treat as whitespace — the exact TS/Rust divergence class issue #3733
18
+ * fixed for the byte scanners; a new trivia matcher should not reintroduce
19
+ * it under a different name.
20
+ *
21
+ * ## Why each alternative is shaped the way it is
22
+ *
23
+ * The outer `(?:A|B)*` must have exactly ONE way to partition any input into
24
+ * iterations. Where it has more than one, a failing suffix makes the engine
25
+ * enumerate them, which is exponential in the length of the trivia run. Both
26
+ * alternatives below are written to keep that count at one, and BOTH shapes
27
+ * have been measured — the two axes fail independently, so neither is
28
+ * theoretical:
29
+ *
30
+ * - The comment body is `(?:[^*]|\*(?!/))*`, NOT a lazy `[\s\S]*?`. A lazy
31
+ * body looks unambiguous locally, but when the overall pattern fails past
32
+ * a comment the engine retries it against every later `*​/`, so one comment
33
+ * can absorb the ones after it and the two alternatives start overlapping
34
+ * on the same span. Requiring every `*` in the body to not be followed by
35
+ * `/` gives the body exactly one maximal extent, so a comment can never
36
+ * swallow the next one.
37
+ * - The whitespace alternative is a SINGLE-CHARACTER class, NOT `[...]+`.
38
+ * This is the counter-intuitive half: `+` looks like it collapses a run
39
+ * into one iteration, but the outer `*` can still split an n-character run
40
+ * into any composition of `+` matches (2^(n-1) of them) and walks all of
41
+ * them on failure — the textbook `(?:A+|B)*` blowup. A single-character
42
+ * class has exactly one partition: n iterations of one character each.
43
+ *
44
+ * Both hazards are pinned by `packages/parser/test/step-trivia-redos.test.ts`,
45
+ * one case per axis, and the test comment there carries the measurements.
46
+ * Change either alternative only with that test in front of you.
47
+ *
48
+ * ## Relation to the byte scanners
49
+ *
50
+ * The comment body also brings this pattern into line with
51
+ * `skip_step_trivia`, which stops a comment at its FIRST `*​/`. A lazy body
52
+ * did not: on backtracking it would accept `/* a *​/ *​/` as one comment,
53
+ * where Rust reads a comment followed by junk and refuses the record. The
54
+ * shared vector "reject: comments do not nest, so the trailing `*​/` is junk"
55
+ * in `rust/core/tests/fixtures/type_paren_trivia_vectors.json` pins that
56
+ * agreement, so this is a deliberate narrowing, not an accident.
57
+ *
58
+ * An unpaired, unterminated `/*` still fails to match rather than hanging:
59
+ * the body runs out of input, backtracking is O(1) per position, and the
60
+ * required `\*​/` after it never appears.
61
+ */
62
+ export const STEP_TRIVIA = '(?:[ \\t\\n\\r\\x0b\\x0c]|/\\*(?:[^*]|\\*(?!/))*\\*/)*';
63
+ //# sourceMappingURL=step-trivia.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"step-trivia.js","sourceRoot":"","sources":["../src/step-trivia.ts"],"names":[],"mappings":"AAAA;;+DAE+D;AAE/D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyDG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG,wDAAwD,CAAC"}
@@ -1,33 +1,32 @@
1
+ import { type ScannedEntityRef } from './scan-entities-balanced.js';
1
2
  export declare class StepTokenizer {
2
3
  private buffer;
3
- private position;
4
- private lineNumber;
4
+ private oversizedIds;
5
+ private malformedRecords;
5
6
  constructor(buffer: Uint8Array);
7
+ /** Records the last scan refused for an out-of-contract express id
8
+ * (express-id.ts, #3395). Reset per scan; the caller reports it. */
9
+ get oversizedIdCount(): number;
10
+ /** 0 or 1: whether the last `scanEntitiesFast`/`scanEntities` run stopped
11
+ * early on an unclosed `'` string, an unclosed block comment, or a
12
+ * declaration cut off before its own '(' -- never a count of how many,
13
+ * since the scan has no reliable way to resume past the first one it
14
+ * hits. Reset at the start of every scan; the caller reports it. */
15
+ get malformedRecordCount(): number;
6
16
  /**
7
17
  * Scan for all entity declarations (#EXPRESS_ID = TYPE(...))
8
- * Returns entity references without parsing full content
18
+ * Returns entity references without parsing full content.
19
+ *
20
+ * Closes each record on the ')' balancing its argument list. The scan itself
21
+ * lives in `scan-entities-balanced.ts`; only the refusal count comes back
22
+ * here, and it comes back in a `finally` so an abandoned generator still
23
+ * reports what it refused.
9
24
  */
10
- scanEntities(): Generator<{
11
- expressId: number;
12
- type: string;
13
- offset: number;
14
- length: number;
15
- line: number;
16
- }>;
25
+ scanEntities(): Generator<ScannedEntityRef>;
17
26
  /**
18
27
  * FAST scan - skips to semicolon instead of matching parentheses
19
28
  * ~5-10x faster for large files, yields length=0 (calculate on-demand)
20
29
  */
21
- scanEntitiesFast(): Generator<{
22
- expressId: number;
23
- type: string;
24
- offset: number;
25
- length: number;
26
- line: number;
27
- }>;
28
- private readExpressId;
29
- private readTypeName;
30
- private skipWhitespace;
31
- private findEntityLength;
30
+ scanEntitiesFast(): Generator<ScannedEntityRef>;
32
31
  }
33
32
  //# sourceMappingURL=tokenizer.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"tokenizer.d.ts","sourceRoot":"","sources":["../src/tokenizer.ts"],"names":[],"mappings":"AAaA,qBAAa,aAAa;IACxB,OAAO,CAAC,MAAM,CAAa;IAC3B,OAAO,CAAC,QAAQ,CAAa;IAC7B,OAAO,CAAC,UAAU,CAAa;gBAEnB,MAAM,EAAE,UAAU;IAI9B;;;OAGG;IACF,YAAY,IAAI,SAAS,CAAC;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;IA0F7G;;;OAGG;IACF,gBAAgB,IAAI,SAAS,CAAC;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;IAuKjH,OAAO,CAAC,aAAa;IAqBrB,OAAO,CAAC,YAAY;IA+BpB,OAAO,CAAC,cAAc;IAYtB,OAAO,CAAC,gBAAgB;CA6CzB"}
1
+ {"version":3,"file":"tokenizer.d.ts","sourceRoot":"","sources":["../src/tokenizer.ts"],"names":[],"mappings":"AAUA,OAAO,EAAsB,KAAK,gBAAgB,EAAE,MAAM,6BAA6B,CAAC;AAUxF,qBAAa,aAAa;IACxB,OAAO,CAAC,MAAM,CAAa;IAC3B,OAAO,CAAC,YAAY,CAAa;IACjC,OAAO,CAAC,gBAAgB,CAAa;gBAEzB,MAAM,EAAE,UAAU;IAI9B;yEACqE;IACrE,IAAI,gBAAgB,IAAI,MAAM,CAA8B;IAE5D;;;;yEAIqE;IACrE,IAAI,oBAAoB,IAAI,MAAM,CAAkC;IAEpE;;;;;;;;OAQG;IACF,YAAY,IAAI,SAAS,CAAC,gBAAgB,CAAC;IAY5C;;;OAGG;IACF,gBAAgB,IAAI,SAAS,CAAC,gBAAgB,CAAC;CAqQjD"}
package/dist/tokenizer.js CHANGED
@@ -5,97 +5,44 @@
5
5
  * STEP tokenizer - fast byte-level scanning for entity markers
6
6
  * Leverages Spike 1 approach: ~1,259 MB/s throughput
7
7
  */
8
- import { safeUtf8Decode } from '@ifc-lite/data';
9
- import { countNewlines, opensLiteralOrComment, skipLexical } from './step-lexing.js';
8
+ import { isIndexableExpressId } from './express-id.js';
9
+ import { BalancedEntityScan } from './scan-entities-balanced.js';
10
+ import { countNewlines, opensComment, opensLiteralOrComment, skipComment, skipLexical, skipTrivia, } from './step-lexing.js';
10
11
  export class StepTokenizer {
11
12
  buffer;
12
- position = 0;
13
- lineNumber = 1;
13
+ oversizedIds = 0;
14
+ malformedRecords = 0;
14
15
  constructor(buffer) {
15
16
  this.buffer = buffer;
16
17
  }
18
+ /** Records the last scan refused for an out-of-contract express id
19
+ * (express-id.ts, #3395). Reset per scan; the caller reports it. */
20
+ get oversizedIdCount() { return this.oversizedIds; }
21
+ /** 0 or 1: whether the last `scanEntitiesFast`/`scanEntities` run stopped
22
+ * early on an unclosed `'` string, an unclosed block comment, or a
23
+ * declaration cut off before its own '(' -- never a count of how many,
24
+ * since the scan has no reliable way to resume past the first one it
25
+ * hits. Reset at the start of every scan; the caller reports it. */
26
+ get malformedRecordCount() { return this.malformedRecords; }
17
27
  /**
18
28
  * Scan for all entity declarations (#EXPRESS_ID = TYPE(...))
19
- * Returns entity references without parsing full content
29
+ * Returns entity references without parsing full content.
30
+ *
31
+ * Closes each record on the ')' balancing its argument list. The scan itself
32
+ * lives in `scan-entities-balanced.ts`; only the refusal count comes back
33
+ * here, and it comes back in a `finally` so an abandoned generator still
34
+ * reports what it refused.
20
35
  */
21
36
  *scanEntities() {
22
- this.position = 0;
23
- this.lineNumber = 1;
24
- while (this.position < this.buffer.length) {
25
- // Look for '#' character (entity ID marker)
26
- if (this.buffer[this.position] === 0x23) { // '#'
27
- const startOffset = this.position;
28
- const startLine = this.lineNumber;
29
- // Read express ID
30
- const expressId = this.readExpressId();
31
- if (expressId === null) {
32
- this.position++;
33
- continue;
34
- }
35
- // Skip whitespace
36
- this.skipWhitespace();
37
- // Check for '=' (assignment)
38
- if (this.position >= this.buffer.length || this.buffer[this.position] !== 0x3D) {
39
- this.position++;
40
- continue;
41
- }
42
- this.position++; // Skip '='
43
- // Skip whitespace
44
- this.skipWhitespace();
45
- // Read type name
46
- const type = this.readTypeName();
47
- if (!type) {
48
- this.position++;
49
- continue;
50
- }
51
- // Skip whitespace
52
- this.skipWhitespace();
53
- // Check for '(' (start of parameters)
54
- if (this.position >= this.buffer.length || this.buffer[this.position] !== 0x28) {
55
- this.position++;
56
- continue;
57
- }
58
- // Find matching closing parenthesis to get full entity length
59
- const entityLength = this.findEntityLength(startOffset);
60
- if (entityLength > 0) {
61
- // Step past the whole record, as Rust's next_entity does. Leaving
62
- // `position` at the '(' made this loop re-walk the body, which was
63
- // harmless only while it ignored quotes and comments.
64
- //
65
- // Count from `position`, not from `startOffset`: `position` is on the
66
- // '(' here, and every newline before it was already counted by the
67
- // three skipWhitespace calls above. Counting the whole record instead
68
- // double-counts a newline written between `#1=` and its type name,
69
- // which is ordinary whitespace and legal.
70
- this.lineNumber += countNewlines(this.buffer, this.position, startOffset + entityLength);
71
- this.position = startOffset + entityLength;
72
- yield {
73
- expressId,
74
- type,
75
- offset: startOffset,
76
- length: entityLength,
77
- line: startLine,
78
- };
79
- }
80
- }
81
- else if (this.buffer[this.position] === 0x0A) {
82
- // Newline
83
- this.lineNumber++;
84
- this.position++;
85
- }
86
- else if (opensLiteralOrComment(this.buffer, this.position, this.buffer.length)) {
87
- // A commented-out record satisfies every check above, so a comment has
88
- // to be skipped as a region; a literal has to be skipped so its
89
- // contents cannot look like one. See step-lexing.
90
- const skip = skipLexical(this.buffer, this.position, this.buffer.length);
91
- this.lineNumber += skip.lines;
92
- this.position = skip.next;
93
- if (skip.stop)
94
- return;
95
- }
96
- else {
97
- this.position++;
98
- }
37
+ const scan = new BalancedEntityScan(this.buffer);
38
+ this.oversizedIds = 0;
39
+ this.malformedRecords = 0;
40
+ try {
41
+ yield* scan.run();
42
+ }
43
+ finally {
44
+ this.oversizedIds = scan.oversizedIdCount;
45
+ this.malformedRecords = scan.malformedRecordCount;
99
46
  }
100
47
  }
101
48
  /**
@@ -103,8 +50,8 @@ export class StepTokenizer {
103
50
  * ~5-10x faster for large files, yields length=0 (calculate on-demand)
104
51
  */
105
52
  *scanEntitiesFast() {
106
- this.position = 0;
107
- this.lineNumber = 1;
53
+ this.oversizedIds = 0;
54
+ this.malformedRecords = 0;
108
55
  // Pre-compute common byte codes
109
56
  const HASH = 0x23; // '#'
110
57
  const EQUALS = 0x3D; // '='
@@ -112,6 +59,7 @@ export class StepTokenizer {
112
59
  const SEMICOLON = 0x3B; // ';'
113
60
  const QUOTE = 0x27; // '\''
114
61
  const NEWLINE = 0x0A; // '\n'
62
+ const SLASH = 0x2F; // '/'
115
63
  const buf = this.buffer;
116
64
  const len = buf.length;
117
65
  let pos = 0;
@@ -119,6 +67,22 @@ export class StepTokenizer {
119
67
  // Cache type name strings: IFC files have ~776 unique types repeated
120
68
  // across 8M+ entities. Caching avoids millions of String.fromCharCode allocations.
121
69
  const typeCache = new Map();
70
+ // Set on the way to the single post-loop check at the bottom of this
71
+ // function, not counted at each site: `stopped` for an unclosed string or
72
+ // comment that ran the scan to end of buffer with nothing left to find,
73
+ // `declOpen` while a `#id=TYPE(` header is incomplete. `declOpen` stays
74
+ // armed ONLY when the reason for abandoning is running out of buffer
75
+ // (`pos >= len`); a mismatch with buffer still left (bad byte, oversized
76
+ // id) clears it, because the scan resumes byte-by-byte from wherever it
77
+ // gave up, and a `#ref` token inside the abandoned record's own argument
78
+ // list reads as a fresh, equally incomplete attempt -- one that must not
79
+ // report "cut off" just because nothing later happens to clear it.
80
+ // Per-site increments used to miss whole shapes -- a leading unterminated
81
+ // comment before '=', or a declaration cut off before its own '(' --
82
+ // because each site only knew about its own exit, never the scan's final
83
+ // state.
84
+ let stopped = false;
85
+ let declOpen = false;
122
86
  while (pos < len) {
123
87
  const char = buf[pos];
124
88
  if (char === HASH) {
@@ -141,10 +105,15 @@ export class StepTokenizer {
141
105
  }
142
106
  if (!hasDigits)
143
107
  continue;
144
- // Skip whitespace (inline)
108
+ declOpen = true;
109
+ // Skip whitespace (inline). Kept byte-for-byte in sync with
110
+ // `isSpaceByte` in step-lexing.ts (space, tab, CR, LF, form feed,
111
+ // vertical tab) -- this loop, its two twins below in this method, and
112
+ // the worker's copy in scan-worker-source.ts are the same rule
113
+ // hand-duplicated for speed, not four independent decisions.
145
114
  while (pos < len) {
146
115
  const c = buf[pos];
147
- if (c === 0x20 || c === 0x09 || c === 0x0D) {
116
+ if (c === 0x20 || c === 0x09 || c === 0x0D || c === 0x0C || c === 0x0B) {
148
117
  pos++;
149
118
  }
150
119
  else if (c === NEWLINE) {
@@ -154,14 +123,50 @@ export class StepTokenizer {
154
123
  else
155
124
  break;
156
125
  }
157
- // Check for '='
158
- if (pos >= len || buf[pos] !== EQUALS)
126
+ // 10303-21 allows a comment wherever whitespace is allowed, so
127
+ // `#1 /* was #7 */ =` is a declaration. The inline loop above stays
128
+ // for the common case; this runs only once a comment actually opens,
129
+ // and skipTrivia (step-lexing) then takes the whole run of both.
130
+ if (opensComment(buf, pos, len)) {
131
+ const t = skipTrivia(buf, pos, len);
132
+ line += t.lines;
133
+ pos = t.next;
134
+ if (t.stop) {
135
+ stopped = true;
136
+ break;
137
+ }
138
+ }
139
+ // Check for '='. A byte that is not '=' with buffer left to scan is
140
+ // not a truncation -- clear declOpen so a reference token inside a
141
+ // LATER abandoned record's argument list (see the oversized-id note
142
+ // below) cannot leave it stuck armed with nothing left to clear it.
143
+ if (pos >= len)
144
+ continue;
145
+ if (buf[pos] !== EQUALS) {
146
+ declOpen = false;
159
147
  continue;
148
+ }
160
149
  pos++;
150
+ // Storage contract, not just overflow: see express-id.ts (#3395).
151
+ // Tested only now that `#<digits>[ws]*=` has matched, which is the
152
+ // DECLARATION shape Rust's `EntityScanner` validates before it
153
+ // refuses. Refusing above the '=' check counted references too: the
154
+ // `continue` resumes inside the refused record's argument list
155
+ // (unlike the accepted path, which skips to the terminating ';'), so
156
+ // `#4294967297=IFCWALL(#4294967298,#4294967299,...)` reported three
157
+ // skipped records for the one record actually dropped. A count that
158
+ // overstates is the same class of defect as one that undercounts.
159
+ // Refused for being out of range, not for running out of buffer, so
160
+ // it does not belong to `declOpen`'s "cut off by EOF" story either.
161
+ if (!isIndexableExpressId(expressId)) {
162
+ this.oversizedIds++;
163
+ declOpen = false;
164
+ continue;
165
+ }
161
166
  // Skip whitespace
162
167
  while (pos < len) {
163
168
  const c = buf[pos];
164
- if (c === 0x20 || c === 0x09 || c === 0x0D) {
169
+ if (c === 0x20 || c === 0x09 || c === 0x0D || c === 0x0C || c === 0x0B) {
165
170
  pos++;
166
171
  }
167
172
  else if (c === NEWLINE) {
@@ -171,10 +176,24 @@ export class StepTokenizer {
171
176
  else
172
177
  break;
173
178
  }
174
- // Read type name (inline)
179
+ if (opensComment(buf, pos, len)) {
180
+ const t = skipTrivia(buf, pos, len);
181
+ line += t.lines;
182
+ pos = t.next;
183
+ if (t.stop) {
184
+ stopped = true;
185
+ break;
186
+ }
187
+ }
188
+ // Read type name (inline). Must start A-Z; a bad start byte with
189
+ // buffer left clears declOpen for the same reason as the '=' check.
175
190
  const typeStart = pos;
176
- if (pos >= len || buf[pos] < 0x41 || buf[pos] > 0x5A)
177
- continue; // Must start A-Z
191
+ if (pos >= len)
192
+ continue;
193
+ if (buf[pos] < 0x41 || buf[pos] > 0x5A) {
194
+ declOpen = false;
195
+ continue;
196
+ }
178
197
  while (pos < len) {
179
198
  const c = buf[pos];
180
199
  if ((c >= 0x41 && c <= 0x5A) || (c >= 0x61 && c <= 0x7A) ||
@@ -218,7 +237,7 @@ export class StepTokenizer {
218
237
  // Skip whitespace
219
238
  while (pos < len) {
220
239
  const c = buf[pos];
221
- if (c === 0x20 || c === 0x09 || c === 0x0D) {
240
+ if (c === 0x20 || c === 0x09 || c === 0x0D || c === 0x0C || c === 0x0B) {
222
241
  pos++;
223
242
  }
224
243
  else if (c === NEWLINE) {
@@ -228,11 +247,26 @@ export class StepTokenizer {
228
247
  else
229
248
  break;
230
249
  }
231
- // Check for '('
232
- if (pos >= len || buf[pos] !== LPAREN)
250
+ if (opensComment(buf, pos, len)) {
251
+ const t = skipTrivia(buf, pos, len);
252
+ line += t.lines;
253
+ pos = t.next;
254
+ if (t.stop) {
255
+ stopped = true;
256
+ break;
257
+ }
258
+ }
259
+ // Check for '('. Same EOF-vs-mismatch split as '=' and the type name.
260
+ if (pos >= len)
233
261
  continue;
262
+ if (buf[pos] !== LPAREN) {
263
+ declOpen = false;
264
+ continue;
265
+ }
266
+ declOpen = false; // Header complete: '(' found.
234
267
  // FAST: Skip to semicolon (handling strings)
235
268
  let inString = false;
269
+ let foundTerminator = false;
236
270
  while (pos < len) {
237
271
  const c = buf[pos];
238
272
  if (c === QUOTE) {
@@ -242,11 +276,29 @@ export class StepTokenizer {
242
276
  }
243
277
  inString = !inString;
244
278
  }
279
+ else if (c === SLASH && !inString && opensComment(buf, pos, len)) {
280
+ // The ';' that ends a record can be preceded by a comment holding
281
+ // its own ';'. Take the comment whole -- which also makes the
282
+ // quotes and parens inside it text, the other half of the rule the
283
+ // literal skip above provides in the opposite direction.
284
+ const end = skipComment(buf, pos, len);
285
+ if (end < 0) {
286
+ // Unterminated: this record has no terminator, and neither has
287
+ // anything after it. Drop it and stop, which is the None Rust's
288
+ // find_entity_end returns on the same input.
289
+ pos = len;
290
+ break;
291
+ }
292
+ line += countNewlines(buf, pos, end);
293
+ pos = end;
294
+ continue;
295
+ }
245
296
  else if (c === SEMICOLON && !inString) {
246
297
  // Found end of entity
247
298
  const entityLength = pos - startOffset + 1; // Include semicolon
248
299
  yield { expressId, type, offset: startOffset, length: entityLength, line: startLine };
249
300
  pos++;
301
+ foundTerminator = true;
250
302
  break;
251
303
  }
252
304
  else if (c === NEWLINE) {
@@ -254,6 +306,14 @@ export class StepTokenizer {
254
306
  }
255
307
  pos++;
256
308
  }
309
+ // Ran off the end without an unquoted ';' — usually an unescaped `'`
310
+ // left open, or the unterminated-comment break above; `pos` is
311
+ // already `len`, ending the scan here. Not resynced: with no known
312
+ // terminator, guessing a resume point risks fabricating entities from
313
+ // misaligned bytes. Recorded in `stopped`, not incremented here --
314
+ // see the post-loop check below.
315
+ if (!foundTerminator)
316
+ stopped = true;
257
317
  }
258
318
  else if (char === NEWLINE) {
259
319
  line++;
@@ -268,120 +328,21 @@ export class StepTokenizer {
268
328
  line += skip.lines;
269
329
  pos = skip.next;
270
330
  if (skip.stop) {
271
- this.position = len;
272
- this.lineNumber = line;
273
- return;
274
- }
275
- }
276
- else {
277
- pos++;
278
- }
279
- }
280
- this.position = pos;
281
- this.lineNumber = line;
282
- }
283
- readExpressId() {
284
- let id = 0;
285
- let digits = 0;
286
- let pos = this.position + 1; // Skip '#'
287
- while (pos < this.buffer.length) {
288
- const char = this.buffer[pos];
289
- if (char >= 0x30 && char <= 0x39) { // '0'-'9'
290
- id = id * 10 + (char - 0x30);
291
- digits++;
292
- pos++;
293
- }
294
- else {
295
- break;
296
- }
297
- }
298
- if (digits === 0)
299
- return null;
300
- this.position = pos;
301
- return id;
302
- }
303
- readTypeName() {
304
- let start = this.position;
305
- let end = start;
306
- // Type names start with uppercase letter
307
- if (this.position >= this.buffer.length || this.buffer[this.position] < 0x41 || this.buffer[this.position] > 0x5A) {
308
- return null;
309
- }
310
- while (end < this.buffer.length) {
311
- const char = this.buffer[end];
312
- // Allow letters, numbers, and underscore
313
- if ((char >= 0x41 && char <= 0x5A) || // A-Z
314
- (char >= 0x61 && char <= 0x7A) || // a-z
315
- (char >= 0x30 && char <= 0x39) || // 0-9
316
- char === 0x5F // _
317
- ) {
318
- end++;
319
- }
320
- else {
321
- break;
322
- }
323
- }
324
- if (end === start)
325
- return null;
326
- const typeName = safeUtf8Decode(this.buffer, start, end);
327
- this.position = end;
328
- return typeName;
329
- }
330
- skipWhitespace() {
331
- while (this.position < this.buffer.length) {
332
- const char = this.buffer[this.position];
333
- if (char === 0x20 || char === 0x09 || char === 0x0D || char === 0x0A) { // space, tab, CR, LF
334
- if (char === 0x0A)
335
- this.lineNumber++;
336
- this.position++;
337
- }
338
- else {
339
- break;
340
- }
341
- }
342
- }
343
- findEntityLength(startOffset) {
344
- let pos = this.position;
345
- let depth = 0;
346
- let inString = false;
347
- while (pos < this.buffer.length) {
348
- const char = this.buffer[pos];
349
- if (char === 0x27) { // Single quote (string delimiter)
350
- if (inString) {
351
- // Check for escaped quote ('') - STEP uses doubled quotes
352
- if (pos + 1 < this.buffer.length && this.buffer[pos + 1] === 0x27) {
353
- pos += 2; // Skip escaped quote
354
- continue;
355
- }
356
- inString = false;
357
- }
358
- else {
359
- inString = true;
360
- }
361
- pos++;
362
- continue;
363
- }
364
- if (inString) {
365
- pos++;
366
- continue;
367
- }
368
- if (char === 0x28) { // '('
369
- depth++;
370
- pos++;
371
- }
372
- else if (char === 0x29) { // ')'
373
- depth--;
374
- pos++;
375
- if (depth === 0) {
376
- // Found matching closing parenthesis
377
- return pos - startOffset;
331
+ stopped = true;
332
+ break;
378
333
  }
379
334
  }
380
335
  else {
381
336
  pos++;
382
337
  }
383
338
  }
384
- return 0; // No matching closing parenthesis found
339
+ // ONE post-loop check, not an increment at every exit site above: the
340
+ // scan stopped early if it hit an explicit "no terminator" boundary
341
+ // (`stopped`), or the last `#id=TYPE(` header was cut short before its
342
+ // '(' was found (`declOpen`). Always 0 or 1 -- the scan stops at the
343
+ // first one, so there is nothing further to accumulate.
344
+ if (stopped || declOpen)
345
+ this.malformedRecords = 1;
385
346
  }
386
347
  }
387
348
  //# sourceMappingURL=tokenizer.js.map