@tabnas/transduce 0.1.1 → 0.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.
Files changed (83) hide show
  1. package/README.md +18 -0
  2. package/dist/guard.d.ts +1 -3
  3. package/dist/guard.js +8 -9
  4. package/dist/guard.js.map +1 -1
  5. package/dist/lines.d.ts +1 -2
  6. package/dist/lines.js +46 -89
  7. package/dist/lines.js.map +1 -1
  8. package/dist/matcher.d.ts +1 -3
  9. package/dist/matcher.js +12 -13
  10. package/dist/matcher.js.map +1 -1
  11. package/dist/parser-source.d.ts +1 -2
  12. package/dist/parser-source.js +11 -13
  13. package/dist/parser-source.js.map +1 -1
  14. package/dist/route.d.ts +1 -32
  15. package/dist/route.js +8 -32
  16. package/dist/route.js.map +1 -1
  17. package/dist/routers.d.ts +2 -0
  18. package/dist/routers.js +24 -0
  19. package/dist/routers.js.map +1 -0
  20. package/dist/rule-events.d.ts +1 -4
  21. package/dist/rule-events.js +40 -44
  22. package/dist/rule-events.js.map +1 -1
  23. package/dist/scan.d.ts +1 -10
  24. package/dist/scan.js +4 -17
  25. package/dist/scan.js.map +1 -1
  26. package/dist/source.d.ts +1 -4
  27. package/dist/source.js +20 -22
  28. package/dist/source.js.map +1 -1
  29. package/dist/table-from-json.d.ts +1 -5
  30. package/dist/table-from-json.js +25 -29
  31. package/dist/table-from-json.js.map +1 -1
  32. package/dist/transduce.d.ts +23 -19
  33. package/dist/transduce.js +66 -56
  34. package/dist/transduce.js.map +1 -1
  35. package/dist/tsconfig.tsbuildinfo +1 -1
  36. package/package.json +12 -4
  37. package/src/guard.ts +10 -4
  38. package/src/lines.ts +45 -6
  39. package/src/matcher.ts +1 -6
  40. package/src/parser-source.ts +2 -4
  41. package/src/route.ts +18 -70
  42. package/src/routers.ts +64 -0
  43. package/src/rule-events.ts +17 -7
  44. package/src/scan.ts +1 -19
  45. package/src/source.ts +12 -6
  46. package/src/table-from-json.ts +20 -9
  47. package/src/transduce.ts +36 -19
  48. package/dist/datum.d.ts +0 -64
  49. package/dist/datum.js +0 -391
  50. package/dist/datum.js.map +0 -1
  51. package/dist/error.d.ts +0 -43
  52. package/dist/error.js +0 -169
  53. package/dist/error.js.map +0 -1
  54. package/dist/event.d.ts +0 -56
  55. package/dist/event.js +0 -101
  56. package/dist/event.js.map +0 -1
  57. package/dist/json.d.ts +0 -4
  58. package/dist/json.js +0 -92
  59. package/dist/json.js.map +0 -1
  60. package/dist/limits.d.ts +0 -36
  61. package/dist/limits.js +0 -133
  62. package/dist/limits.js.map +0 -1
  63. package/dist/selector.d.ts +0 -40
  64. package/dist/selector.js +0 -154
  65. package/dist/selector.js.map +0 -1
  66. package/dist/sink.d.ts +0 -29
  67. package/dist/sink.js +0 -184
  68. package/dist/sink.js.map +0 -1
  69. package/dist/table.d.ts +0 -81
  70. package/dist/table.js +0 -141
  71. package/dist/table.js.map +0 -1
  72. package/dist/value.d.ts +0 -16
  73. package/dist/value.js +0 -96
  74. package/dist/value.js.map +0 -1
  75. package/src/datum.ts +0 -407
  76. package/src/error.ts +0 -198
  77. package/src/event.ts +0 -134
  78. package/src/json.ts +0 -82
  79. package/src/limits.ts +0 -165
  80. package/src/selector.ts +0 -191
  81. package/src/sink.ts +0 -199
  82. package/src/table.ts +0 -213
  83. package/src/value.ts +0 -93
package/src/event.ts DELETED
@@ -1,134 +0,0 @@
1
- /* Copyright (c) 2026 tabnas, MIT License */
2
-
3
- // `JsonEvents/1`: the source protocol every transducer consumes.
4
- //
5
- // A source (a tabnas parse, a parsed value, a line-delimited reader) emits
6
- // the events of one document in order: container boundaries, keys, whole
7
- // scalars, and one `end` after the root value. Events are plain frozen
8
- // objects, so a recorder keeps them as they are and a stage that keeps
9
- // anything keeps the event itself.
10
- //
11
- // Scalars arrive whole because the tabnas lexer produces whole tokens. A
12
- // later `JsonEvents/2` may chunk strings; it will be a separately named
13
- // protocol, never a change to this one.
14
-
15
- // One event of `JsonEvents/1`, a tagged union on `type`.
16
- //
17
- // `number` carries the machine value and, when the source could hand it
18
- // over, its `lexeme` (the source text), else `null`; a renderer falls back
19
- // to the value. Keeping both is how `50.25` stays `50.25` and how a number
20
- // beyond a double's exact range keeps its digits.
21
- export type JsonEvent =
22
- | { readonly type: 'object_start' }
23
- | { readonly type: 'object_end' }
24
- | { readonly type: 'array_start' }
25
- | { readonly type: 'array_end' }
26
- | { readonly type: 'key'; readonly key: string }
27
- | { readonly type: 'null' }
28
- | { readonly type: 'bool'; readonly value: boolean }
29
- | { readonly type: 'number'; readonly value: number; readonly lexeme: string | null }
30
- | { readonly type: 'string'; readonly value: string }
31
- | { readonly type: 'end' }
32
-
33
- export type JsonEventType = JsonEvent['type']
34
-
35
- // The events without a payload are shared, so emitting one allocates
36
- // nothing.
37
- const OBJECT_START: JsonEvent = Object.freeze({ type: 'object_start' })
38
- const OBJECT_END: JsonEvent = Object.freeze({ type: 'object_end' })
39
- const ARRAY_START: JsonEvent = Object.freeze({ type: 'array_start' })
40
- const ARRAY_END: JsonEvent = Object.freeze({ type: 'array_end' })
41
- const NULL: JsonEvent = Object.freeze({ type: 'null' })
42
- const TRUE: JsonEvent = Object.freeze({ type: 'bool', value: true })
43
- const FALSE: JsonEvent = Object.freeze({ type: 'bool', value: false })
44
- const END: JsonEvent = Object.freeze({ type: 'end' })
45
-
46
- // Constructors for every event.
47
- export const Ev = Object.freeze({
48
- objectStart: OBJECT_START,
49
- objectEnd: OBJECT_END,
50
- arrayStart: ARRAY_START,
51
- arrayEnd: ARRAY_END,
52
- null: NULL,
53
- end: END,
54
- key(key: string): JsonEvent {
55
- return { type: 'key', key }
56
- },
57
- bool(value: boolean): JsonEvent {
58
- return value ? TRUE : FALSE
59
- },
60
- number(value: number, lexeme?: string | null): JsonEvent {
61
- return { type: 'number', value, lexeme: lexeme ?? null }
62
- },
63
- string(value: string): JsonEvent {
64
- return { type: 'string', value }
65
- },
66
- })
67
-
68
- // Whether the event opens a container.
69
- export function isStart(ev: JsonEvent): boolean {
70
- return 'object_start' === ev.type || 'array_start' === ev.type
71
- }
72
-
73
- // Whether the event closes a container.
74
- export function isEnd(ev: JsonEvent): boolean {
75
- return 'object_end' === ev.type || 'array_end' === ev.type
76
- }
77
-
78
- // Whether the event is a whole scalar value.
79
- export function isScalar(ev: JsonEvent): boolean {
80
- return (
81
- 'null' === ev.type ||
82
- 'bool' === ev.type ||
83
- 'number' === ev.type ||
84
- 'string' === ev.type
85
- )
86
- }
87
-
88
- // Two events are equal: same type and payload. Numbers compare by
89
- // `Object.is` (so `-0` is not `0`) and lexeme.
90
- export function eventEquals(a: JsonEvent, b: JsonEvent): boolean {
91
- if (a.type !== b.type) return false
92
- switch (a.type) {
93
- case 'key':
94
- return a.key === (b as any).key
95
- case 'bool':
96
- case 'string':
97
- return a.value === (b as any).value
98
- case 'number':
99
- return (
100
- (Object.is(a.value, (b as any).value) ||
101
- (Number.isNaN(a.value) && Number.isNaN((b as any).value))) &&
102
- a.lexeme === (b as any).lexeme
103
- )
104
- default:
105
- return true
106
- }
107
- }
108
-
109
- // The event as short text, for messages: `{`, `}`, `[`, `]`, `key "a"`,
110
- // a scalar as JSON, `end`.
111
- export function eventText(ev: JsonEvent): string {
112
- switch (ev.type) {
113
- case 'object_start':
114
- return '{'
115
- case 'object_end':
116
- return '}'
117
- case 'array_start':
118
- return '['
119
- case 'array_end':
120
- return ']'
121
- case 'key':
122
- return 'key ' + JSON.stringify(ev.key)
123
- case 'null':
124
- return 'null'
125
- case 'bool':
126
- return String(ev.value)
127
- case 'number':
128
- return null != ev.lexeme ? ev.lexeme : String(ev.value)
129
- case 'string':
130
- return JSON.stringify(ev.value)
131
- case 'end':
132
- return 'end'
133
- }
134
- }
package/src/json.ts DELETED
@@ -1,82 +0,0 @@
1
- /* Copyright (c) 2026 tabnas, MIT License */
2
-
3
- // JSON text: the escaping and number spelling every runtime shares.
4
-
5
- // `s` as a JSON string literal, escaped as RFC 8259 requires: `"` and `\`
6
- // escaped, the short forms `\b \f \n \r \t`, other control characters as
7
- // `\u00XX` with lowercase hex, and everything else as itself.
8
- export function jsonString(s: string): string {
9
- let out = '"'
10
- let from = 0
11
- for (let i = 0; i < s.length; i++) {
12
- const c = s.charCodeAt(i)
13
- if (c >= 0x20 && c !== 0x22 && c !== 0x5c) continue
14
- if (i > from) out += s.slice(from, i)
15
- from = i + 1
16
- switch (c) {
17
- case 0x22:
18
- out += '\\"'
19
- break
20
- case 0x5c:
21
- out += '\\\\'
22
- break
23
- case 0x08:
24
- out += '\\b'
25
- break
26
- case 0x0c:
27
- out += '\\f'
28
- break
29
- case 0x0a:
30
- out += '\\n'
31
- break
32
- case 0x0d:
33
- out += '\\r'
34
- break
35
- case 0x09:
36
- out += '\\t'
37
- break
38
- default:
39
- out += '\\u' + c.toString(16).padStart(4, '0')
40
- }
41
- }
42
- if (from < s.length) out += s.slice(from)
43
- return out + '"'
44
- }
45
-
46
- // A number with no lexeme, as text: the shortest digits that read back as
47
- // the same double, written positionally, never with an exponent (`1e21` is
48
- // `1000000000000000000000`, `1e-7` is `0.0000001`, `-0` is `-0`). This is
49
- // the spelling the Rust runtime's `f64` `Display` gives, so a container
50
- // cell holding such a number reads the same in every runtime;
51
- // `DIVERGENCE.md` keeps the decision open. A non-finite value has no JSON
52
- // form and is `null`.
53
- export function numberText(value: number): string {
54
- if (!Number.isFinite(value)) return 'null'
55
- if (Object.is(value, -0)) return '-0'
56
- if (0 === value) return '0'
57
- const neg = value < 0
58
- const exp = Math.abs(value).toExponential()
59
- const at = exp.indexOf('e')
60
- const digits = exp.slice(0, at).replace('.', '')
61
- const e = parseInt(exp.slice(at + 1), 10)
62
- let text: string
63
- if (e >= digits.length - 1) {
64
- text = digits + '0'.repeat(e - (digits.length - 1))
65
- } else if (e >= 0) {
66
- text = digits.slice(0, e + 1) + '.' + digits.slice(e + 1)
67
- } else {
68
- text = '0.' + '0'.repeat(-e - 1) + digits
69
- }
70
- return neg ? '-' + text : text
71
- }
72
-
73
- // A number as JSON text: its lexeme when known, else `numberText`.
74
- export function jsonNumber(value: number, lexeme: string | null | undefined): string {
75
- return null != lexeme ? lexeme : numberText(value)
76
- }
77
-
78
- // RFC 8259's number grammar: `-?(0|[1-9][0-9]*)(\.[0-9]+)?([eE][+-]?[0-9]+)?`.
79
- // Only such a lexeme is kept, so a renderer can write it as it stands.
80
- export function isJsonNumber(s: string): boolean {
81
- return /^-?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?$/.test(s)
82
- }
package/src/limits.ts DELETED
@@ -1,165 +0,0 @@
1
- /* Copyright (c) 2026 tabnas, MIT License */
2
-
3
- // Limits, metrics and cancellation.
4
- //
5
- // Limits are part of a plan, not advice: a stage that would exceed one
6
- // fails with `RESOURCE_LIMIT_EXCEEDED` naming the field, rather than
7
- // switching to a slower or larger algorithm. Values are payload bytes
8
- // (UTF-8 lengths of keys and scalars, and the sum of them for a retained
9
- // value plus a fixed per-node allowance), which is a stable and portable
10
- // measure, the same in every runtime; they are not a heap measurement. A
11
- // JavaScript string is UTF-16, so every count here goes through
12
- // `utf8Bytes`, never `length`.
13
-
14
- // Per-run limits. Every field is named in a failure as written here, which
15
- // is why the fields keep the snake_case names every runtime shares.
16
- export type Limits = {
17
- // Container nesting the source may reach.
18
- max_depth: number
19
- // Bytes in one object key.
20
- max_key_bytes: number
21
- // Bytes in one scalar (a string's text, a number's lexeme).
22
- max_scalar_bytes: number
23
- // Bytes retained for metadata (the column descriptors).
24
- max_metadata_bytes: number
25
- // Columns a schema may declare.
26
- max_columns: number
27
- // Bytes retained for one row before projection.
28
- max_record_bytes: number
29
- // Bytes retained for any one materialized capture.
30
- max_capture_bytes: number
31
- // Bytes the run may write; `null` for no limit.
32
- max_output_bytes: number | null
33
- }
34
-
35
- // The names of the `Limits` fields, in declaration order.
36
- export const LIMIT_NAMES: readonly (keyof Limits)[] = Object.freeze([
37
- 'max_depth',
38
- 'max_key_bytes',
39
- 'max_scalar_bytes',
40
- 'max_metadata_bytes',
41
- 'max_columns',
42
- 'max_record_bytes',
43
- 'max_capture_bytes',
44
- 'max_output_bytes',
45
- ])
46
-
47
- export const Limits = Object.freeze({
48
- // The defaults: 256, 64 KiB, 16 MiB, 16 MiB, 10 000, 64 MiB, 64 MiB, none.
49
- default(): Limits {
50
- return {
51
- max_depth: 256,
52
- max_key_bytes: 64 * 1024,
53
- max_scalar_bytes: 16 * 1024 * 1024,
54
- max_metadata_bytes: 16 * 1024 * 1024,
55
- max_columns: 10_000,
56
- max_record_bytes: 64 * 1024 * 1024,
57
- max_capture_bytes: 64 * 1024 * 1024,
58
- max_output_bytes: null,
59
- }
60
- },
61
-
62
- // No limit on anything that can be unlimited, and the largest values
63
- // otherwise. For tests and trusted, measured inputs only.
64
- unlimited(): Limits {
65
- const max = Number.MAX_SAFE_INTEGER
66
- return {
67
- max_depth: max,
68
- max_key_bytes: max,
69
- max_scalar_bytes: max,
70
- max_metadata_bytes: max,
71
- max_columns: max,
72
- max_record_bytes: max,
73
- max_capture_bytes: max,
74
- max_output_bytes: null,
75
- }
76
- },
77
-
78
- // The defaults with `fields` applied over them.
79
- with(fields: Partial<Limits>): Limits {
80
- return { ...Limits.default(), ...fields }
81
- },
82
- })
83
-
84
- // The fixed allowance counted for every retained node (a container, a
85
- // member, an element) on top of its payload bytes.
86
- export const NODE_BYTES = 16
87
-
88
- // The UTF-8 length of a string: `é` is 2, `日` 3, `😀` 4. A lone
89
- // surrogate counts 3, as its replacement character would.
90
- export function utf8Bytes(s: string): number {
91
- const n = s.length
92
- // ASCII fast path: most keys and scalars never leave it.
93
- let i = 0
94
- for (; i < n; i++) {
95
- if (s.charCodeAt(i) >= 0x80) break
96
- }
97
- if (i === n) return n
98
- return Buffer.byteLength(s, 'utf8')
99
- }
100
-
101
- // Counters and high-water marks a run reports, shared between stages.
102
- export class Metrics {
103
- // Source events seen.
104
- events = 0
105
- // Object keys seen.
106
- keys = 0
107
- // Scalars seen.
108
- scalars = 0
109
- // Rows delivered to the table protocol.
110
- rows = 0
111
- // Bytes currently held by materialized captures.
112
- captured_bytes = 0
113
- // The most bytes ever held by captures at once.
114
- captured_bytes_high = 0
115
- // The most bytes ever retained at once across every retaining stage.
116
- retained_bytes_high = 0
117
- // Bytes written to the output.
118
- output_bytes = 0
119
-
120
- // Account `bytes` as captured now, and raise the high-water marks.
121
- capture(bytes: number): void {
122
- this.captured_bytes += bytes
123
- if (this.captured_bytes > this.captured_bytes_high) {
124
- this.captured_bytes_high = this.captured_bytes
125
- }
126
- if (this.captured_bytes > this.retained_bytes_high) {
127
- this.retained_bytes_high = this.captured_bytes
128
- }
129
- }
130
-
131
- // Release `bytes` captured earlier.
132
- release(bytes: number): void {
133
- this.captured_bytes -= bytes
134
- }
135
-
136
- // The metrics as a plain object, one field per counter.
137
- toJSON(): Record<string, number> {
138
- return {
139
- events: this.events,
140
- keys: this.keys,
141
- scalars: this.scalars,
142
- rows: this.rows,
143
- captured_bytes: this.captured_bytes,
144
- captured_bytes_high: this.captured_bytes_high,
145
- retained_bytes_high: this.retained_bytes_high,
146
- output_bytes: this.output_bytes,
147
- }
148
- }
149
- }
150
-
151
- // A cancellation flag shared by the caller, the source and the stages.
152
- //
153
- // The source polls it between parse steps through the engine's parse
154
- // budget and stops with `ABORTED`; long loops in stages poll it too.
155
- export class AbortFlag {
156
- private aborted = false
157
-
158
- abort(): void {
159
- this.aborted = true
160
- }
161
-
162
- isAborted(): boolean {
163
- return this.aborted
164
- }
165
- }
package/src/selector.ts DELETED
@@ -1,191 +0,0 @@
1
- /* Copyright (c) 2026 tabnas, MIT License */
2
-
3
- // Selectors: reusable descriptions of where in a document to look.
4
- //
5
- // A selector is data, never code: it is built from constructors or from
6
- // validated path segments (`Selector.fromSegments`), and a matcher
7
- // interprets it. A concrete `Path` names one location; a `Selector` may
8
- // name many (`each_index`, `each_member`). Both print in jq syntax, which
9
- // is what the rest of the fleet prints and accepts.
10
-
11
- import { jsonString } from './json'
12
-
13
- // One step of a concrete path: a string is a member's key, a non-negative
14
- // integer an element's index.
15
- export type Segment = string | number
16
-
17
- // One key as jq writes it: bare when it is an ASCII identifier, a JSON
18
- // string otherwise.
19
- export function keyText(key: string): string {
20
- return /^[A-Za-z_][A-Za-z0-9_]*$/.test(key) ? '.' + key : '.' + jsonString(key)
21
- }
22
-
23
- // A concrete location in a document.
24
- export class Path {
25
- readonly segments: Segment[]
26
-
27
- constructor(segments: Segment[] = []) {
28
- this.segments = segments
29
- }
30
-
31
- static root(): Path {
32
- return new Path([])
33
- }
34
-
35
- push(seg: Segment): void {
36
- this.segments.push(seg)
37
- }
38
-
39
- pop(): Segment | undefined {
40
- return this.segments.pop()
41
- }
42
-
43
- depth(): number {
44
- return this.segments.length
45
- }
46
-
47
- clone(): Path {
48
- return new Path(this.segments.slice())
49
- }
50
-
51
- // jq syntax: `.a[0]."b-c"`, and `.` for the root.
52
- toString(): string {
53
- return pathText(this.segments)
54
- }
55
- }
56
-
57
- // Segments in jq syntax.
58
- export function pathText(segments: readonly Segment[]): string {
59
- if (0 === segments.length) return '.'
60
- let out = ''
61
- for (const seg of segments) {
62
- out += 'number' === typeof seg ? `[${seg}]` : keyText(seg)
63
- }
64
- return out
65
- }
66
-
67
- // One step of a selector.
68
- export type Step =
69
- // The member with this name, inside an object.
70
- | { readonly type: 'property'; readonly name: string }
71
- // The element at this position, inside an array.
72
- | { readonly type: 'index'; readonly index: number }
73
- // Every element of an array.
74
- | { readonly type: 'each_index' }
75
- // Every member value of an object.
76
- | { readonly type: 'each_member' }
77
-
78
- const EACH_INDEX: Step = Object.freeze({ type: 'each_index' })
79
- const EACH_MEMBER: Step = Object.freeze({ type: 'each_member' })
80
-
81
- // A description of locations: the root, narrowed step by step. Immutable:
82
- // every builder returns a new selector.
83
- export class Selector {
84
- readonly steps: readonly Step[]
85
-
86
- constructor(steps: readonly Step[] = []) {
87
- this.steps = Object.freeze(steps.slice())
88
- }
89
-
90
- // The document itself.
91
- static root(): Selector {
92
- return new Selector([])
93
- }
94
-
95
- property(name: string): Selector {
96
- return new Selector([...this.steps, { type: 'property', name }])
97
- }
98
-
99
- index(index: number): Selector {
100
- if (!Number.isInteger(index) || index < 0) {
101
- throw new TypeError('an index step is a non-negative integer: ' + index)
102
- }
103
- return new Selector([...this.steps, { type: 'index', index }])
104
- }
105
-
106
- eachIndex(): Selector {
107
- return new Selector([...this.steps, EACH_INDEX])
108
- }
109
-
110
- eachMember(): Selector {
111
- return new Selector([...this.steps, EACH_MEMBER])
112
- }
113
-
114
- // `this`, then `other` below every location `this` names.
115
- compose(other: Selector): Selector {
116
- return new Selector([...this.steps, ...other.steps])
117
- }
118
-
119
- // A selector naming exactly one location: `as-path` over data.
120
- static fromSegments(segments: readonly Segment[]): Selector {
121
- let s = Selector.root()
122
- for (const seg of segments) {
123
- s = 'number' === typeof seg ? s.index(seg) : s.property(seg)
124
- }
125
- return s
126
- }
127
-
128
- isRoot(): boolean {
129
- return 0 === this.steps.length
130
- }
131
-
132
- // Whether the selector can name more than one location.
133
- isMulti(): boolean {
134
- return this.steps.some((s) => 'each_index' === s.type || 'each_member' === s.type)
135
- }
136
-
137
- // Whether this selector names a location strictly inside a location
138
- // `other` names, or the same one: the test a router uses to refuse
139
- // overlapping captures.
140
- mayOverlap(other: Selector): boolean {
141
- const [short, long] =
142
- this.steps.length <= other.steps.length ? [this, other] : [other, this]
143
- for (let i = 0; i < short.steps.length; i++) {
144
- if (!stepMayMatchSame(short.steps[i], long.steps[i])) return false
145
- }
146
- return true
147
- }
148
-
149
- equals(other: Selector): boolean {
150
- return this.toString() === other.toString()
151
- }
152
-
153
- // jq syntax: `.response.records[*]`, `."odd key"`, `[3]`, `[]` for
154
- // every member, `.` for the root.
155
- toString(): string {
156
- if (0 === this.steps.length) return '.'
157
- let out = ''
158
- for (const step of this.steps) {
159
- switch (step.type) {
160
- case 'property':
161
- out += keyText(step.name)
162
- break
163
- case 'index':
164
- out += `[${step.index}]`
165
- break
166
- case 'each_index':
167
- out += '[*]'
168
- break
169
- case 'each_member':
170
- out += '[]'
171
- break
172
- }
173
- }
174
- return out
175
- }
176
- }
177
-
178
- function stepMayMatchSame(a: Step, b: Step): boolean {
179
- switch (a.type) {
180
- case 'property':
181
- return (
182
- ('property' === b.type && a.name === b.name) || 'each_member' === b.type
183
- )
184
- case 'each_member':
185
- return 'property' === b.type || 'each_member' === b.type
186
- case 'index':
187
- return ('index' === b.type && a.index === b.index) || 'each_index' === b.type
188
- case 'each_index':
189
- return 'index' === b.type || 'each_index' === b.type
190
- }
191
- }