@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.
- package/README.md +18 -0
- package/dist/guard.d.ts +1 -3
- package/dist/guard.js +8 -9
- package/dist/guard.js.map +1 -1
- package/dist/lines.d.ts +1 -2
- package/dist/lines.js +46 -89
- package/dist/lines.js.map +1 -1
- package/dist/matcher.d.ts +1 -3
- package/dist/matcher.js +12 -13
- package/dist/matcher.js.map +1 -1
- package/dist/parser-source.d.ts +1 -2
- package/dist/parser-source.js +11 -13
- package/dist/parser-source.js.map +1 -1
- package/dist/route.d.ts +1 -32
- package/dist/route.js +8 -32
- package/dist/route.js.map +1 -1
- package/dist/routers.d.ts +2 -0
- package/dist/routers.js +24 -0
- package/dist/routers.js.map +1 -0
- package/dist/rule-events.d.ts +1 -4
- package/dist/rule-events.js +40 -44
- package/dist/rule-events.js.map +1 -1
- package/dist/scan.d.ts +1 -10
- package/dist/scan.js +4 -17
- package/dist/scan.js.map +1 -1
- package/dist/source.d.ts +1 -4
- package/dist/source.js +20 -22
- package/dist/source.js.map +1 -1
- package/dist/table-from-json.d.ts +1 -5
- package/dist/table-from-json.js +25 -29
- package/dist/table-from-json.js.map +1 -1
- package/dist/transduce.d.ts +23 -19
- package/dist/transduce.js +66 -56
- package/dist/transduce.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +12 -4
- package/src/guard.ts +10 -4
- package/src/lines.ts +45 -6
- package/src/matcher.ts +1 -6
- package/src/parser-source.ts +2 -4
- package/src/route.ts +18 -70
- package/src/routers.ts +64 -0
- package/src/rule-events.ts +17 -7
- package/src/scan.ts +1 -19
- package/src/source.ts +12 -6
- package/src/table-from-json.ts +20 -9
- package/src/transduce.ts +36 -19
- package/dist/datum.d.ts +0 -64
- package/dist/datum.js +0 -391
- package/dist/datum.js.map +0 -1
- package/dist/error.d.ts +0 -43
- package/dist/error.js +0 -169
- package/dist/error.js.map +0 -1
- package/dist/event.d.ts +0 -56
- package/dist/event.js +0 -101
- package/dist/event.js.map +0 -1
- package/dist/json.d.ts +0 -4
- package/dist/json.js +0 -92
- package/dist/json.js.map +0 -1
- package/dist/limits.d.ts +0 -36
- package/dist/limits.js +0 -133
- package/dist/limits.js.map +0 -1
- package/dist/selector.d.ts +0 -40
- package/dist/selector.js +0 -154
- package/dist/selector.js.map +0 -1
- package/dist/sink.d.ts +0 -29
- package/dist/sink.js +0 -184
- package/dist/sink.js.map +0 -1
- package/dist/table.d.ts +0 -81
- package/dist/table.js +0 -141
- package/dist/table.js.map +0 -1
- package/dist/value.d.ts +0 -16
- package/dist/value.js +0 -96
- package/dist/value.js.map +0 -1
- package/src/datum.ts +0 -407
- package/src/error.ts +0 -198
- package/src/event.ts +0 -134
- package/src/json.ts +0 -82
- package/src/limits.ts +0 -165
- package/src/selector.ts +0 -191
- package/src/sink.ts +0 -199
- package/src/table.ts +0 -213
- 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
|
-
}
|