@bcts/envelope-pattern 1.0.0-alpha.12

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 (53) hide show
  1. package/LICENSE +48 -0
  2. package/README.md +13 -0
  3. package/dist/index.cjs +6781 -0
  4. package/dist/index.cjs.map +1 -0
  5. package/dist/index.d.cts +2628 -0
  6. package/dist/index.d.cts.map +1 -0
  7. package/dist/index.d.mts +2628 -0
  8. package/dist/index.d.mts.map +1 -0
  9. package/dist/index.iife.js +6781 -0
  10. package/dist/index.iife.js.map +1 -0
  11. package/dist/index.mjs +6545 -0
  12. package/dist/index.mjs.map +1 -0
  13. package/package.json +77 -0
  14. package/src/error.ts +262 -0
  15. package/src/format.ts +375 -0
  16. package/src/index.ts +27 -0
  17. package/src/parse/index.ts +923 -0
  18. package/src/parse/token.ts +906 -0
  19. package/src/parse/utils.ts +339 -0
  20. package/src/pattern/index.ts +719 -0
  21. package/src/pattern/leaf/array-pattern.ts +273 -0
  22. package/src/pattern/leaf/bool-pattern.ts +140 -0
  23. package/src/pattern/leaf/byte-string-pattern.ts +172 -0
  24. package/src/pattern/leaf/cbor-pattern.ts +355 -0
  25. package/src/pattern/leaf/date-pattern.ts +178 -0
  26. package/src/pattern/leaf/index.ts +280 -0
  27. package/src/pattern/leaf/known-value-pattern.ts +192 -0
  28. package/src/pattern/leaf/map-pattern.ts +152 -0
  29. package/src/pattern/leaf/null-pattern.ts +110 -0
  30. package/src/pattern/leaf/number-pattern.ts +248 -0
  31. package/src/pattern/leaf/tagged-pattern.ts +228 -0
  32. package/src/pattern/leaf/text-pattern.ts +165 -0
  33. package/src/pattern/matcher.ts +88 -0
  34. package/src/pattern/meta/and-pattern.ts +109 -0
  35. package/src/pattern/meta/any-pattern.ts +81 -0
  36. package/src/pattern/meta/capture-pattern.ts +111 -0
  37. package/src/pattern/meta/group-pattern.ts +110 -0
  38. package/src/pattern/meta/index.ts +269 -0
  39. package/src/pattern/meta/not-pattern.ts +91 -0
  40. package/src/pattern/meta/or-pattern.ts +146 -0
  41. package/src/pattern/meta/search-pattern.ts +201 -0
  42. package/src/pattern/meta/traverse-pattern.ts +146 -0
  43. package/src/pattern/structure/assertions-pattern.ts +244 -0
  44. package/src/pattern/structure/digest-pattern.ts +225 -0
  45. package/src/pattern/structure/index.ts +272 -0
  46. package/src/pattern/structure/leaf-structure-pattern.ts +85 -0
  47. package/src/pattern/structure/node-pattern.ts +188 -0
  48. package/src/pattern/structure/object-pattern.ts +149 -0
  49. package/src/pattern/structure/obscured-pattern.ts +159 -0
  50. package/src/pattern/structure/predicate-pattern.ts +151 -0
  51. package/src/pattern/structure/subject-pattern.ts +152 -0
  52. package/src/pattern/structure/wrapped-pattern.ts +195 -0
  53. package/src/pattern/vm.ts +1021 -0
package/package.json ADDED
@@ -0,0 +1,77 @@
1
+ {
2
+ "name": "@bcts/envelope-pattern",
3
+ "version": "1.0.0-alpha.12",
4
+ "type": "module",
5
+ "description": "Pattern matching for Gordian Envelope structures",
6
+ "license": "BSD-2-Clause-Patent",
7
+ "contributors": [
8
+ {
9
+ "name": "Leonardo Custodio",
10
+ "email": "leonardo.custodio@parity.io",
11
+ "url": "https://github.com/leonardocustodio"
12
+ },
13
+ {
14
+ "name": "Karim Jedda",
15
+ "email": "karim@parity.io",
16
+ "url": "https://github.com/KarimJedda"
17
+ }
18
+ ],
19
+ "homepage": "https://bcts.dev",
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "git+https://github.com/leonardocustodio/bcts.git",
23
+ "directory": "packages/envelope-pattern"
24
+ },
25
+ "bugs": {
26
+ "url": "https://github.com/leonardocustodio/bcts/issues"
27
+ },
28
+ "main": "dist/index.cjs",
29
+ "module": "dist/index.mjs",
30
+ "types": "dist/index.d.mts",
31
+ "browser": "dist/index.iife.js",
32
+ "exports": {
33
+ ".": {
34
+ "types": "./dist/index.d.mts",
35
+ "import": "./dist/index.mjs",
36
+ "require": "./dist/index.cjs",
37
+ "default": "./dist/index.mjs"
38
+ }
39
+ },
40
+ "files": [
41
+ "dist",
42
+ "src",
43
+ "README.md"
44
+ ],
45
+ "scripts": {
46
+ "build": "tsdown",
47
+ "dev": "tsdown --watch",
48
+ "test": "vitest run",
49
+ "test:watch": "vitest",
50
+ "lint": "eslint 'src/**/*.ts' 'tests/**/*.ts'",
51
+ "lint:fix": "eslint 'src/**/*.ts' 'tests/**/*.ts' --fix",
52
+ "typecheck": "tsc --noEmit",
53
+ "clean": "rm -rf dist",
54
+ "docs": "typedoc",
55
+ "prepublishOnly": "npm run clean && npm run build && npm test"
56
+ },
57
+ "engines": {
58
+ "node": ">=18.0.0"
59
+ },
60
+ "devDependencies": {
61
+ "@bcts/eslint": "workspace:*",
62
+ "@bcts/tsconfig": "workspace:*",
63
+ "eslint": "^9.39.2",
64
+ "tsdown": "^0.18.3",
65
+ "typedoc": "^0.28.15",
66
+ "typescript": "^5.9.3",
67
+ "vitest": "^4.0.16"
68
+ },
69
+ "dependencies": {
70
+ "@bcts/dcbor": "workspace:*",
71
+ "@bcts/dcbor-pattern": "workspace:*",
72
+ "@bcts/envelope": "workspace:*",
73
+ "@bcts/components": "workspace:*",
74
+ "@bcts/tags": "workspace:*",
75
+ "@bcts/known-values": "workspace:*"
76
+ }
77
+ }
package/src/error.ts ADDED
@@ -0,0 +1,262 @@
1
+ /**
2
+ * @bcts/envelope-pattern - Error types for envelope pattern parsing
3
+ *
4
+ * This is a 1:1 TypeScript port of bc-envelope-pattern-rust error.rs
5
+ *
6
+ * @module envelope-pattern/error
7
+ */
8
+
9
+ import type { Token } from "./parse/token";
10
+
11
+ /**
12
+ * Span represents a range in the source input.
13
+ */
14
+ export interface Span {
15
+ readonly start: number;
16
+ readonly end: number;
17
+ }
18
+
19
+ /**
20
+ * Error types that can occur during parsing of Envelope patterns.
21
+ *
22
+ * Corresponds to the Rust `Error` enum in error.rs
23
+ */
24
+ export type EnvelopePatternError =
25
+ | { readonly type: "EmptyInput" }
26
+ | { readonly type: "UnexpectedEndOfInput" }
27
+ | { readonly type: "ExtraData"; readonly span: Span }
28
+ | { readonly type: "UnexpectedToken"; readonly token: Token; readonly span: Span }
29
+ | { readonly type: "UnrecognizedToken"; readonly span: Span }
30
+ | { readonly type: "InvalidRegex"; readonly span: Span }
31
+ | { readonly type: "UnterminatedRegex"; readonly span: Span }
32
+ | { readonly type: "InvalidRange"; readonly span: Span }
33
+ | { readonly type: "InvalidHexString"; readonly span: Span }
34
+ | { readonly type: "InvalidDateFormat"; readonly span: Span }
35
+ | { readonly type: "InvalidNumberFormat"; readonly span: Span }
36
+ | { readonly type: "InvalidUr"; readonly message: string; readonly span: Span }
37
+ | { readonly type: "ExpectedOpenParen"; readonly span: Span }
38
+ | { readonly type: "ExpectedCloseParen"; readonly span: Span }
39
+ | { readonly type: "ExpectedOpenBracket"; readonly span: Span }
40
+ | { readonly type: "ExpectedCloseBracket"; readonly span: Span }
41
+ | { readonly type: "ExpectedPattern"; readonly span: Span }
42
+ | { readonly type: "UnmatchedParentheses"; readonly span: Span }
43
+ | { readonly type: "UnmatchedBraces"; readonly span: Span }
44
+ | { readonly type: "InvalidCaptureGroupName"; readonly name: string; readonly span: Span }
45
+ | { readonly type: "InvalidPattern"; readonly span: Span }
46
+ | { readonly type: "Unknown" }
47
+ | { readonly type: "DCBORPatternError"; readonly error: unknown };
48
+
49
+ /**
50
+ * Result type specialized for envelope pattern parsing.
51
+ */
52
+ export type Result<T> =
53
+ | { readonly ok: true; readonly value: T }
54
+ | { readonly ok: false; readonly error: EnvelopePatternError };
55
+
56
+ /**
57
+ * Creates a successful result.
58
+ */
59
+ export function ok<T>(value: T): Result<T> {
60
+ return { ok: true, value };
61
+ }
62
+
63
+ /**
64
+ * Creates a failed result.
65
+ */
66
+ export function err<T>(error: EnvelopePatternError): Result<T> {
67
+ return { ok: false, error };
68
+ }
69
+
70
+ /**
71
+ * Type guard for successful results.
72
+ */
73
+ export function isOk<T>(result: Result<T>): result is { readonly ok: true; readonly value: T } {
74
+ return result.ok;
75
+ }
76
+
77
+ /**
78
+ * Type guard for failed results.
79
+ */
80
+ export function isErr<T>(
81
+ result: Result<T>,
82
+ ): result is { readonly ok: false; readonly error: EnvelopePatternError } {
83
+ return !result.ok;
84
+ }
85
+
86
+ /**
87
+ * Unwraps a successful result or throws the error.
88
+ */
89
+ export function unwrap<T>(result: Result<T>): T {
90
+ if (result.ok) {
91
+ return result.value;
92
+ }
93
+ throw new Error(formatError(result.error));
94
+ }
95
+
96
+ /**
97
+ * Unwraps a successful result or returns a default value.
98
+ */
99
+ export function unwrapOr<T>(result: Result<T>, defaultValue: T): T {
100
+ if (result.ok) {
101
+ return result.value;
102
+ }
103
+ return defaultValue;
104
+ }
105
+
106
+ /**
107
+ * Maps a successful result value.
108
+ */
109
+ export function map<T, U>(result: Result<T>, fn: (value: T) => U): Result<U> {
110
+ if (result.ok) {
111
+ return ok(fn(result.value));
112
+ }
113
+ return result as Result<U>;
114
+ }
115
+
116
+ /**
117
+ * Formats an error for display.
118
+ */
119
+ export function formatError(error: EnvelopePatternError): string {
120
+ switch (error.type) {
121
+ case "EmptyInput":
122
+ return "Empty input";
123
+ case "UnexpectedEndOfInput":
124
+ return "Unexpected end of input";
125
+ case "ExtraData":
126
+ return `Extra data at end of input at position ${error.span.start}-${error.span.end}`;
127
+ case "UnexpectedToken":
128
+ return `Unexpected token ${JSON.stringify(error.token)} at position ${error.span.start}-${error.span.end}`;
129
+ case "UnrecognizedToken":
130
+ return `Unrecognized token at position ${error.span.start}-${error.span.end}`;
131
+ case "InvalidRegex":
132
+ return `Invalid regex pattern at position ${error.span.start}-${error.span.end}`;
133
+ case "UnterminatedRegex":
134
+ return `Unterminated regex pattern at position ${error.span.start}-${error.span.end}`;
135
+ case "InvalidRange":
136
+ return `Invalid range at position ${error.span.start}-${error.span.end}`;
137
+ case "InvalidHexString":
138
+ return `Invalid hex string at position ${error.span.start}-${error.span.end}`;
139
+ case "InvalidDateFormat":
140
+ return `Invalid date format at position ${error.span.start}-${error.span.end}`;
141
+ case "InvalidNumberFormat":
142
+ return `Invalid number format at position ${error.span.start}-${error.span.end}`;
143
+ case "InvalidUr":
144
+ return `Invalid UR: ${error.message} at position ${error.span.start}-${error.span.end}`;
145
+ case "ExpectedOpenParen":
146
+ return `Expected opening parenthesis at position ${error.span.start}-${error.span.end}`;
147
+ case "ExpectedCloseParen":
148
+ return `Expected closing parenthesis at position ${error.span.start}-${error.span.end}`;
149
+ case "ExpectedOpenBracket":
150
+ return `Expected opening bracket at position ${error.span.start}-${error.span.end}`;
151
+ case "ExpectedCloseBracket":
152
+ return `Expected closing bracket at position ${error.span.start}-${error.span.end}`;
153
+ case "ExpectedPattern":
154
+ return `Expected pattern after operator at position ${error.span.start}-${error.span.end}`;
155
+ case "UnmatchedParentheses":
156
+ return `Unmatched parentheses at position ${error.span.start}-${error.span.end}`;
157
+ case "UnmatchedBraces":
158
+ return `Unmatched braces at position ${error.span.start}-${error.span.end}`;
159
+ case "InvalidCaptureGroupName":
160
+ return `Invalid capture group name '${error.name}' at position ${error.span.start}-${error.span.end}`;
161
+ case "InvalidPattern":
162
+ return `Invalid pattern at position ${error.span.start}-${error.span.end}`;
163
+ case "Unknown":
164
+ return "Unknown error";
165
+ case "DCBORPatternError":
166
+ return `DCBOR pattern error: ${String(error.error)}`;
167
+ }
168
+ }
169
+
170
+ // Error factory functions for convenience
171
+
172
+ export function emptyInput(): EnvelopePatternError {
173
+ return { type: "EmptyInput" };
174
+ }
175
+
176
+ export function unexpectedEndOfInput(): EnvelopePatternError {
177
+ return { type: "UnexpectedEndOfInput" };
178
+ }
179
+
180
+ export function extraData(span: Span): EnvelopePatternError {
181
+ return { type: "ExtraData", span };
182
+ }
183
+
184
+ export function unexpectedToken(token: Token, span: Span): EnvelopePatternError {
185
+ return { type: "UnexpectedToken", token, span };
186
+ }
187
+
188
+ export function unrecognizedToken(span: Span): EnvelopePatternError {
189
+ return { type: "UnrecognizedToken", span };
190
+ }
191
+
192
+ export function invalidRegex(span: Span): EnvelopePatternError {
193
+ return { type: "InvalidRegex", span };
194
+ }
195
+
196
+ export function unterminatedRegex(span: Span): EnvelopePatternError {
197
+ return { type: "UnterminatedRegex", span };
198
+ }
199
+
200
+ export function invalidRange(span: Span): EnvelopePatternError {
201
+ return { type: "InvalidRange", span };
202
+ }
203
+
204
+ export function invalidHexString(span: Span): EnvelopePatternError {
205
+ return { type: "InvalidHexString", span };
206
+ }
207
+
208
+ export function invalidDateFormat(span: Span): EnvelopePatternError {
209
+ return { type: "InvalidDateFormat", span };
210
+ }
211
+
212
+ export function invalidNumberFormat(span: Span): EnvelopePatternError {
213
+ return { type: "InvalidNumberFormat", span };
214
+ }
215
+
216
+ export function invalidUr(message: string, span: Span): EnvelopePatternError {
217
+ return { type: "InvalidUr", message, span };
218
+ }
219
+
220
+ export function expectedOpenParen(span: Span): EnvelopePatternError {
221
+ return { type: "ExpectedOpenParen", span };
222
+ }
223
+
224
+ export function expectedCloseParen(span: Span): EnvelopePatternError {
225
+ return { type: "ExpectedCloseParen", span };
226
+ }
227
+
228
+ export function expectedOpenBracket(span: Span): EnvelopePatternError {
229
+ return { type: "ExpectedOpenBracket", span };
230
+ }
231
+
232
+ export function expectedCloseBracket(span: Span): EnvelopePatternError {
233
+ return { type: "ExpectedCloseBracket", span };
234
+ }
235
+
236
+ export function expectedPattern(span: Span): EnvelopePatternError {
237
+ return { type: "ExpectedPattern", span };
238
+ }
239
+
240
+ export function unmatchedParentheses(span: Span): EnvelopePatternError {
241
+ return { type: "UnmatchedParentheses", span };
242
+ }
243
+
244
+ export function unmatchedBraces(span: Span): EnvelopePatternError {
245
+ return { type: "UnmatchedBraces", span };
246
+ }
247
+
248
+ export function invalidCaptureGroupName(name: string, span: Span): EnvelopePatternError {
249
+ return { type: "InvalidCaptureGroupName", name, span };
250
+ }
251
+
252
+ export function invalidPattern(span: Span): EnvelopePatternError {
253
+ return { type: "InvalidPattern", span };
254
+ }
255
+
256
+ export function unknown(): EnvelopePatternError {
257
+ return { type: "Unknown" };
258
+ }
259
+
260
+ export function dcborPatternError(error: unknown): EnvelopePatternError {
261
+ return { type: "DCBORPatternError", error };
262
+ }
package/src/format.ts ADDED
@@ -0,0 +1,375 @@
1
+ /**
2
+ * @bcts/envelope-pattern - Path formatting utilities
3
+ *
4
+ * This is a 1:1 TypeScript port of bc-envelope-pattern-rust format.rs
5
+ *
6
+ * @module envelope-pattern/format
7
+ */
8
+
9
+ import type { Envelope } from "@bcts/envelope";
10
+
11
+ /**
12
+ * A path is a sequence of envelopes from root to a matched element.
13
+ */
14
+ export type Path = Envelope[];
15
+
16
+ /**
17
+ * Format options for each path element.
18
+ *
19
+ * Corresponds to the Rust `PathElementFormat` enum in format.rs
20
+ */
21
+ export type PathElementFormat =
22
+ | { readonly type: "Summary"; readonly maxLength?: number }
23
+ | { readonly type: "EnvelopeUR" }
24
+ | { readonly type: "DigestUR" };
25
+
26
+ /**
27
+ * Creates a Summary format.
28
+ */
29
+ export function summaryFormat(maxLength?: number): PathElementFormat {
30
+ if (maxLength !== undefined) {
31
+ return { type: "Summary", maxLength };
32
+ }
33
+ return { type: "Summary" };
34
+ }
35
+
36
+ /**
37
+ * Creates an EnvelopeUR format.
38
+ */
39
+ export function envelopeURFormat(): PathElementFormat {
40
+ return { type: "EnvelopeUR" };
41
+ }
42
+
43
+ /**
44
+ * Creates a DigestUR format.
45
+ */
46
+ export function digestURFormat(): PathElementFormat {
47
+ return { type: "DigestUR" };
48
+ }
49
+
50
+ /**
51
+ * Default path element format.
52
+ */
53
+ export function defaultPathElementFormat(): PathElementFormat {
54
+ return summaryFormat(undefined);
55
+ }
56
+
57
+ /**
58
+ * Options for formatting paths.
59
+ *
60
+ * Corresponds to the Rust `FormatPathsOpts` struct in format.rs
61
+ */
62
+ export interface FormatPathsOpts {
63
+ /**
64
+ * Whether to indent each path element.
65
+ * If true, each element will be indented by 4 spaces per level.
66
+ * Default: true
67
+ */
68
+ readonly indent: boolean;
69
+
70
+ /**
71
+ * Format for each path element.
72
+ * Default: Summary(None)
73
+ */
74
+ readonly elementFormat: PathElementFormat;
75
+
76
+ /**
77
+ * If true, only the last element of each path will be formatted.
78
+ * This is useful for displaying only the final destination of a path.
79
+ * If false, all elements will be formatted.
80
+ * Default: false
81
+ */
82
+ readonly lastElementOnly: boolean;
83
+ }
84
+
85
+ /**
86
+ * Creates default formatting options.
87
+ */
88
+ export function defaultFormatPathsOpts(): FormatPathsOpts {
89
+ return {
90
+ indent: true,
91
+ elementFormat: defaultPathElementFormat(),
92
+ lastElementOnly: false,
93
+ };
94
+ }
95
+
96
+ /**
97
+ * Builder for FormatPathsOpts.
98
+ */
99
+ export class FormatPathsOptsBuilder {
100
+ #indent = true;
101
+ #elementFormat: PathElementFormat = defaultPathElementFormat();
102
+ #lastElementOnly = false;
103
+
104
+ /**
105
+ * Sets whether to indent each path element.
106
+ */
107
+ indent(indent: boolean): this {
108
+ this.#indent = indent;
109
+ return this;
110
+ }
111
+
112
+ /**
113
+ * Sets the format for each path element.
114
+ */
115
+ elementFormat(format: PathElementFormat): this {
116
+ this.#elementFormat = format;
117
+ return this;
118
+ }
119
+
120
+ /**
121
+ * Sets whether to format only the last element of each path.
122
+ */
123
+ lastElementOnly(lastElementOnly: boolean): this {
124
+ this.#lastElementOnly = lastElementOnly;
125
+ return this;
126
+ }
127
+
128
+ /**
129
+ * Builds the FormatPathsOpts.
130
+ */
131
+ build(): FormatPathsOpts {
132
+ return {
133
+ indent: this.#indent,
134
+ elementFormat: this.#elementFormat,
135
+ lastElementOnly: this.#lastElementOnly,
136
+ };
137
+ }
138
+ }
139
+
140
+ /**
141
+ * Creates a new FormatPathsOptsBuilder.
142
+ */
143
+ export function formatPathsOpts(): FormatPathsOptsBuilder {
144
+ return new FormatPathsOptsBuilder();
145
+ }
146
+
147
+ /**
148
+ * Gets a summary of an envelope for display.
149
+ *
150
+ * @param env - The envelope to summarize
151
+ * @returns A string summary of the envelope
152
+ */
153
+ export function envelopeSummary(env: Envelope): string {
154
+ const id = env.shortId("short");
155
+ const c = env.case();
156
+
157
+ let summary: string;
158
+ switch (c.type) {
159
+ case "node":
160
+ summary = `NODE ${env.summary(Number.MAX_SAFE_INTEGER)}`;
161
+ break;
162
+ case "leaf":
163
+ summary = `LEAF ${env.summary(Number.MAX_SAFE_INTEGER)}`;
164
+ break;
165
+ case "wrapped":
166
+ summary = `WRAPPED ${env.summary(Number.MAX_SAFE_INTEGER)}`;
167
+ break;
168
+ case "assertion":
169
+ summary = `ASSERTION ${env.summary(Number.MAX_SAFE_INTEGER)}`;
170
+ break;
171
+ case "elided":
172
+ summary = "ELIDED";
173
+ break;
174
+ case "knownValue":
175
+ summary = `KNOWN_VALUE '${c.value.name()}'`;
176
+ break;
177
+ case "encrypted":
178
+ summary = "ENCRYPTED";
179
+ break;
180
+ case "compressed":
181
+ summary = "COMPRESSED";
182
+ break;
183
+ default:
184
+ summary = "UNKNOWN";
185
+ }
186
+
187
+ return `${id} ${summary}`;
188
+ }
189
+
190
+ /**
191
+ * Truncates a string to the specified maximum length, appending an ellipsis if truncated.
192
+ *
193
+ * @param s - The string to truncate
194
+ * @param maxLength - Optional maximum length
195
+ * @returns The truncated string
196
+ */
197
+ function truncateWithEllipsis(s: string, maxLength?: number): string {
198
+ if (maxLength === undefined) {
199
+ return s;
200
+ }
201
+ if (s.length > maxLength) {
202
+ if (maxLength > 1) {
203
+ return `${s.substring(0, maxLength - 1)}…`;
204
+ }
205
+ return "…";
206
+ }
207
+ return s;
208
+ }
209
+
210
+ /**
211
+ * Format a single path element on its own line with custom options.
212
+ *
213
+ * @param path - The path to format
214
+ * @param opts - Formatting options
215
+ * @returns The formatted path string
216
+ */
217
+ export function formatPathOpt(
218
+ path: Path,
219
+ opts: FormatPathsOpts = defaultFormatPathsOpts(),
220
+ ): string {
221
+ if (opts.lastElementOnly) {
222
+ // Only format the last element, no indentation
223
+ const element = path[path.length - 1];
224
+ if (element === undefined) {
225
+ return "";
226
+ }
227
+
228
+ switch (opts.elementFormat.type) {
229
+ case "Summary": {
230
+ const summary = envelopeSummary(element);
231
+ return truncateWithEllipsis(summary, opts.elementFormat.maxLength);
232
+ }
233
+ case "EnvelopeUR":
234
+ // TODO: Implement proper UR string format when available
235
+ return element.digest().toString();
236
+ case "DigestUR":
237
+ return element.digest().toString();
238
+ }
239
+ }
240
+
241
+ switch (opts.elementFormat.type) {
242
+ case "Summary": {
243
+ // Multi-line output with indentation for summaries
244
+ const lines: string[] = [];
245
+ for (let index = 0; index < path.length; index++) {
246
+ const element = path[index];
247
+ if (element === undefined) continue;
248
+
249
+ const indent = opts.indent ? " ".repeat(index * 4) : "";
250
+ const summary = envelopeSummary(element);
251
+ const content = truncateWithEllipsis(summary, opts.elementFormat.maxLength);
252
+ lines.push(`${indent}${content}`);
253
+ }
254
+ return lines.join("\n");
255
+ }
256
+ case "EnvelopeUR":
257
+ // TODO: Implement proper UR string format when available
258
+ return path.map((element) => element.digest().toString()).join(" ");
259
+ case "DigestUR":
260
+ // Single-line, space-separated digest strings
261
+ return path.map((element) => element.digest().toString()).join(" ");
262
+ }
263
+ }
264
+
265
+ /**
266
+ * Format a single path with default options.
267
+ *
268
+ * @param path - The path to format
269
+ * @returns The formatted path string
270
+ */
271
+ export function formatPath(path: Path): string {
272
+ return formatPathOpt(path, defaultFormatPathsOpts());
273
+ }
274
+
275
+ /**
276
+ * Format multiple paths with captures and custom options.
277
+ *
278
+ * Captures come first, sorted lexicographically by name, with their name
279
+ * prefixed by '@'. Regular paths follow after all captures.
280
+ *
281
+ * @param paths - The paths to format
282
+ * @param captures - Map of capture name to captured paths
283
+ * @param opts - Formatting options
284
+ * @returns The formatted string
285
+ */
286
+ export function formatPathsWithCapturesOpt(
287
+ paths: Path[],
288
+ captures: Map<string, Path[]>,
289
+ opts: FormatPathsOpts = defaultFormatPathsOpts(),
290
+ ): string {
291
+ const result: string[] = [];
292
+
293
+ // First, format all captures, sorted lexicographically by name
294
+ const captureNames = Array.from(captures.keys()).sort();
295
+
296
+ for (const captureName of captureNames) {
297
+ const capturePaths = captures.get(captureName);
298
+ if (capturePaths === undefined) continue;
299
+
300
+ result.push(`@${captureName}`);
301
+ for (const path of capturePaths) {
302
+ const formattedPath = formatPathOpt(path, opts);
303
+ // Add indentation to each line of the formatted path
304
+ for (const line of formattedPath.split("\n")) {
305
+ if (line.length > 0) {
306
+ result.push(` ${line}`);
307
+ }
308
+ }
309
+ }
310
+ }
311
+
312
+ // Then, format all regular paths
313
+ switch (opts.elementFormat.type) {
314
+ case "EnvelopeUR":
315
+ case "DigestUR": {
316
+ // For UR formats, join paths with spaces on same line
317
+ if (paths.length > 0) {
318
+ const formattedPaths = paths.map((path) => formatPathOpt(path, opts)).join(" ");
319
+ if (formattedPaths.length > 0) {
320
+ result.push(formattedPaths);
321
+ }
322
+ }
323
+ break;
324
+ }
325
+ case "Summary": {
326
+ // For summary format, format each path separately
327
+ for (const path of paths) {
328
+ const formattedPath = formatPathOpt(path, opts);
329
+ for (const line of formattedPath.split("\n")) {
330
+ if (line.length > 0) {
331
+ result.push(line);
332
+ }
333
+ }
334
+ }
335
+ break;
336
+ }
337
+ }
338
+
339
+ return result.join("\n");
340
+ }
341
+
342
+ /**
343
+ * Format multiple paths with captures using default options.
344
+ *
345
+ * @param paths - The paths to format
346
+ * @param captures - Map of capture name to captured paths
347
+ * @returns The formatted string
348
+ */
349
+ export function formatPathsWithCaptures(paths: Path[], captures: Map<string, Path[]>): string {
350
+ return formatPathsWithCapturesOpt(paths, captures, defaultFormatPathsOpts());
351
+ }
352
+
353
+ /**
354
+ * Format multiple paths with custom options.
355
+ *
356
+ * @param paths - The paths to format
357
+ * @param opts - Formatting options
358
+ * @returns The formatted string
359
+ */
360
+ export function formatPathsOpt(
361
+ paths: Path[],
362
+ opts: FormatPathsOpts = defaultFormatPathsOpts(),
363
+ ): string {
364
+ return formatPathsWithCapturesOpt(paths, new Map(), opts);
365
+ }
366
+
367
+ /**
368
+ * Format multiple paths with default options.
369
+ *
370
+ * @param paths - The paths to format
371
+ * @returns The formatted string
372
+ */
373
+ export function formatPaths(paths: Path[]): string {
374
+ return formatPathsOpt(paths, defaultFormatPathsOpts());
375
+ }