@kaiord/tcx 4.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.
- package/LICENSE +21 -0
- package/README.md +77 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +723 -0
- package/dist/index.js.map +1 -0
- package/package.json +60 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Pablo Albaladejo
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the “Software”), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# @kaiord/tcx
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@kaiord/tcx)
|
|
4
|
+
[](https://opensource.org/licenses/MIT)
|
|
5
|
+
|
|
6
|
+
TCX format adapter for Kaiord workout data conversion. Provides reading, writing, and XSD validation of Garmin Training Center XML files.
|
|
7
|
+
|
|
8
|
+
## Installation
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
pnpm add @kaiord/core @kaiord/tcx
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Usage
|
|
15
|
+
|
|
16
|
+
### With Core Providers (Recommended)
|
|
17
|
+
|
|
18
|
+
```typescript
|
|
19
|
+
import { createDefaultProviders } from "@kaiord/core";
|
|
20
|
+
import { createTcxProviders } from "@kaiord/tcx";
|
|
21
|
+
|
|
22
|
+
const providers = createDefaultProviders({
|
|
23
|
+
tcx: createTcxProviders(),
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
// TCX to KRD
|
|
27
|
+
const krd = await providers.convertTcxToKrd!({ tcxString });
|
|
28
|
+
|
|
29
|
+
// KRD to TCX
|
|
30
|
+
const tcxString = await providers.convertKrdToTcx!({ krd });
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### Standalone Adapter Access
|
|
34
|
+
|
|
35
|
+
```typescript
|
|
36
|
+
import {
|
|
37
|
+
createFastXmlTcxReader,
|
|
38
|
+
createFastXmlTcxWriter,
|
|
39
|
+
createXsdTcxValidator,
|
|
40
|
+
} from "@kaiord/tcx";
|
|
41
|
+
import { createConsoleLogger } from "@kaiord/core";
|
|
42
|
+
|
|
43
|
+
const logger = createConsoleLogger();
|
|
44
|
+
const reader = createFastXmlTcxReader(logger);
|
|
45
|
+
const writer = createFastXmlTcxWriter(logger, createXsdTcxValidator(logger));
|
|
46
|
+
const validator = createXsdTcxValidator(logger);
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## API
|
|
50
|
+
|
|
51
|
+
### `createTcxProviders(logger?: Logger): TcxProviders`
|
|
52
|
+
|
|
53
|
+
Creates TCX adapter instances for use with `createDefaultProviders()`.
|
|
54
|
+
|
|
55
|
+
### `createFastXmlTcxReader(logger: Logger): TcxReader`
|
|
56
|
+
|
|
57
|
+
Creates a TCX file reader using fast-xml-parser.
|
|
58
|
+
|
|
59
|
+
### `createFastXmlTcxWriter(logger: Logger, validator: TcxValidator): TcxWriter`
|
|
60
|
+
|
|
61
|
+
Creates a TCX file writer using fast-xml-parser.
|
|
62
|
+
|
|
63
|
+
### `createXsdTcxValidator(logger: Logger): TcxValidator`
|
|
64
|
+
|
|
65
|
+
Creates an XSD schema validator for TCX files.
|
|
66
|
+
|
|
67
|
+
## Supported TCX Features
|
|
68
|
+
|
|
69
|
+
- Workout definitions with structured steps
|
|
70
|
+
- Heart rate, speed, and cadence targets
|
|
71
|
+
- Time-based and distance-based durations
|
|
72
|
+
- Repeat blocks (intervals)
|
|
73
|
+
- Multiple sport types
|
|
74
|
+
|
|
75
|
+
## License
|
|
76
|
+
|
|
77
|
+
MIT
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Logger, TcxReader, TcxWriter, TcxValidator } from '@kaiord/core';
|
|
2
|
+
|
|
3
|
+
type TcxProviders = {
|
|
4
|
+
tcxReader: TcxReader;
|
|
5
|
+
tcxWriter: TcxWriter;
|
|
6
|
+
tcxValidator: TcxValidator;
|
|
7
|
+
};
|
|
8
|
+
declare const createTcxProviders: (logger?: Logger) => TcxProviders;
|
|
9
|
+
|
|
10
|
+
declare const createFastXmlTcxReader: (logger: Logger) => TcxReader;
|
|
11
|
+
declare const createFastXmlTcxWriter: (logger: Logger, validator: TcxValidator) => TcxWriter;
|
|
12
|
+
|
|
13
|
+
declare const createXsdTcxValidator: (logger: Logger) => TcxValidator;
|
|
14
|
+
|
|
15
|
+
export { type TcxProviders, createFastXmlTcxReader, createFastXmlTcxWriter, createTcxProviders, createXsdTcxValidator };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,723 @@
|
|
|
1
|
+
import { createTcxParsingError, createTcxValidationError, createConsoleLogger } from '@kaiord/core';
|
|
2
|
+
import { XMLParser, XMLBuilder, XMLValidator } from 'fast-xml-parser';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
|
|
5
|
+
// src/providers.ts
|
|
6
|
+
|
|
7
|
+
// src/adapters/workout/metadata-extractor.ts
|
|
8
|
+
var extractKaiordMetadata = (trainingCenterDatabase, workout) => {
|
|
9
|
+
const metadata = {
|
|
10
|
+
created: trainingCenterDatabase["@_kaiord:timeCreated"] || (/* @__PURE__ */ new Date()).toISOString(),
|
|
11
|
+
sport: workout.sport,
|
|
12
|
+
subSport: workout.subSport
|
|
13
|
+
};
|
|
14
|
+
if (trainingCenterDatabase["@_kaiord:manufacturer"]) {
|
|
15
|
+
metadata.manufacturer = trainingCenterDatabase["@_kaiord:manufacturer"];
|
|
16
|
+
}
|
|
17
|
+
if (trainingCenterDatabase["@_kaiord:product"]) {
|
|
18
|
+
metadata.product = trainingCenterDatabase["@_kaiord:product"];
|
|
19
|
+
}
|
|
20
|
+
if (trainingCenterDatabase["@_kaiord:serialNumber"]) {
|
|
21
|
+
const serialNumber = trainingCenterDatabase["@_kaiord:serialNumber"];
|
|
22
|
+
metadata.serialNumber = typeof serialNumber === "number" ? String(serialNumber) : serialNumber;
|
|
23
|
+
}
|
|
24
|
+
return metadata;
|
|
25
|
+
};
|
|
26
|
+
var tcxSportSchema = z.enum(["Running", "Biking", "Other"]);
|
|
27
|
+
var TCX_TO_KRD_SPORT = {
|
|
28
|
+
Running: "running",
|
|
29
|
+
Biking: "cycling",
|
|
30
|
+
Other: "generic"
|
|
31
|
+
};
|
|
32
|
+
var KRD_TO_TCX_SPORT = {
|
|
33
|
+
running: "Running",
|
|
34
|
+
cycling: "Biking",
|
|
35
|
+
swimming: "Other",
|
|
36
|
+
generic: "Other"
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// src/adapters/duration/duration-kaiord-restorer.ts
|
|
40
|
+
var restoreHeartRateLessThan = (tcxDuration, logger) => {
|
|
41
|
+
const bpm = tcxDuration["@_kaiord:originalDurationBpm"];
|
|
42
|
+
if (typeof bpm === "number") {
|
|
43
|
+
logger.debug("Restoring heart_rate_less_than from kaiord attributes", {
|
|
44
|
+
bpm
|
|
45
|
+
});
|
|
46
|
+
return { type: "heart_rate_less_than", bpm };
|
|
47
|
+
}
|
|
48
|
+
return null;
|
|
49
|
+
};
|
|
50
|
+
var restorePowerLessThan = (tcxDuration, logger) => {
|
|
51
|
+
const watts = tcxDuration["@_kaiord:originalDurationWatts"];
|
|
52
|
+
if (typeof watts === "number") {
|
|
53
|
+
logger.debug("Restoring power_less_than from kaiord attributes", {
|
|
54
|
+
watts
|
|
55
|
+
});
|
|
56
|
+
return { type: "power_less_than", watts };
|
|
57
|
+
}
|
|
58
|
+
return null;
|
|
59
|
+
};
|
|
60
|
+
var restorePowerGreaterThan = (tcxDuration, logger) => {
|
|
61
|
+
const watts = tcxDuration["@_kaiord:originalDurationWatts"];
|
|
62
|
+
if (typeof watts === "number") {
|
|
63
|
+
logger.debug("Restoring power_greater_than from kaiord attributes", {
|
|
64
|
+
watts
|
|
65
|
+
});
|
|
66
|
+
return { type: "power_greater_than", watts };
|
|
67
|
+
}
|
|
68
|
+
return null;
|
|
69
|
+
};
|
|
70
|
+
var restoreCalories = (tcxDuration, logger) => {
|
|
71
|
+
const calories = tcxDuration["@_kaiord:originalDurationCalories"];
|
|
72
|
+
if (typeof calories === "number") {
|
|
73
|
+
logger.debug("Restoring calories from kaiord attributes", {
|
|
74
|
+
calories
|
|
75
|
+
});
|
|
76
|
+
return { type: "calories", calories };
|
|
77
|
+
}
|
|
78
|
+
return null;
|
|
79
|
+
};
|
|
80
|
+
var restoreKaiordDuration = (tcxDuration, logger) => {
|
|
81
|
+
const originalDurationType = tcxDuration["@_kaiord:originalDurationType"];
|
|
82
|
+
if (originalDurationType === "heart_rate_less_than") {
|
|
83
|
+
return restoreHeartRateLessThan(tcxDuration, logger);
|
|
84
|
+
}
|
|
85
|
+
if (originalDurationType === "power_less_than") {
|
|
86
|
+
return restorePowerLessThan(tcxDuration, logger);
|
|
87
|
+
}
|
|
88
|
+
if (originalDurationType === "power_greater_than") {
|
|
89
|
+
return restorePowerGreaterThan(tcxDuration, logger);
|
|
90
|
+
}
|
|
91
|
+
if (originalDurationType === "calories") {
|
|
92
|
+
return restoreCalories(tcxDuration, logger);
|
|
93
|
+
}
|
|
94
|
+
return null;
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
// src/adapters/duration/duration-standard-converter.ts
|
|
98
|
+
var convertStandardTcxDuration = (tcxDuration) => {
|
|
99
|
+
const durationType = tcxDuration["@_xsi:type"];
|
|
100
|
+
if (durationType === "Time_t") {
|
|
101
|
+
const seconds = tcxDuration.Seconds;
|
|
102
|
+
if (typeof seconds === "number" && seconds > 0) {
|
|
103
|
+
return { type: "time", seconds };
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
if (durationType === "Distance_t") {
|
|
107
|
+
const meters = tcxDuration.Meters;
|
|
108
|
+
if (typeof meters === "number" && meters > 0) {
|
|
109
|
+
return { type: "distance", meters };
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (durationType === "LapButton_t") {
|
|
113
|
+
return { type: "open" };
|
|
114
|
+
}
|
|
115
|
+
return null;
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
// src/adapters/duration/duration.mapper.ts
|
|
119
|
+
var convertTcxDuration = (tcxDuration, logger) => {
|
|
120
|
+
if (!tcxDuration) {
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
const kaiordDuration = restoreKaiordDuration(tcxDuration, logger);
|
|
124
|
+
if (kaiordDuration) {
|
|
125
|
+
return kaiordDuration;
|
|
126
|
+
}
|
|
127
|
+
const standardDuration = convertStandardTcxDuration(tcxDuration);
|
|
128
|
+
if (standardDuration) {
|
|
129
|
+
return standardDuration;
|
|
130
|
+
}
|
|
131
|
+
const durationType = tcxDuration["@_xsi:type"];
|
|
132
|
+
logger.warn("Unsupported duration type", { durationType });
|
|
133
|
+
return null;
|
|
134
|
+
};
|
|
135
|
+
var convertHeartRateTarget = (heartRateZone) => {
|
|
136
|
+
if (!heartRateZone) return null;
|
|
137
|
+
const zoneType = heartRateZone["@_xsi:type"];
|
|
138
|
+
if (zoneType === "PredefinedHeartRateZone_t") {
|
|
139
|
+
const zoneNumber = heartRateZone.Number;
|
|
140
|
+
if (typeof zoneNumber === "number") {
|
|
141
|
+
return {
|
|
142
|
+
type: "heart_rate",
|
|
143
|
+
value: { unit: "zone", value: zoneNumber }
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
if (zoneType === "CustomHeartRateZone_t") {
|
|
148
|
+
const low = heartRateZone.Low;
|
|
149
|
+
const high = heartRateZone.High;
|
|
150
|
+
if (typeof low === "number" && typeof high === "number") {
|
|
151
|
+
return {
|
|
152
|
+
type: "heart_rate",
|
|
153
|
+
value: { unit: "range", min: low, max: high }
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return null;
|
|
158
|
+
};
|
|
159
|
+
var convertTcxTarget = (tcxTarget, logger) => {
|
|
160
|
+
if (!tcxTarget) {
|
|
161
|
+
return { type: "open" };
|
|
162
|
+
}
|
|
163
|
+
const targetType = tcxTarget["@_xsi:type"];
|
|
164
|
+
if (targetType === "None_t") {
|
|
165
|
+
return { type: "open" };
|
|
166
|
+
}
|
|
167
|
+
if (targetType === "HeartRate_t") {
|
|
168
|
+
const heartRateZone = tcxTarget.HeartRateZone;
|
|
169
|
+
const result = convertHeartRateTarget(heartRateZone);
|
|
170
|
+
if (result) return result;
|
|
171
|
+
}
|
|
172
|
+
logger.warn("Unsupported target type", { targetType });
|
|
173
|
+
return { type: "open" };
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
// src/adapters/workout/step-helpers.ts
|
|
177
|
+
var extractIntensity = (tcxStep) => {
|
|
178
|
+
const raw = tcxStep.Intensity;
|
|
179
|
+
const value = raw?.toLowerCase();
|
|
180
|
+
switch (value) {
|
|
181
|
+
case "warmup":
|
|
182
|
+
case "active":
|
|
183
|
+
case "cooldown":
|
|
184
|
+
case "rest":
|
|
185
|
+
return value;
|
|
186
|
+
case "resting":
|
|
187
|
+
return "rest";
|
|
188
|
+
default:
|
|
189
|
+
return void 0;
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
var extractPowerFromExtensions = (extensions, logger) => {
|
|
193
|
+
if (extensions.TPX) {
|
|
194
|
+
const tpx = extensions.TPX;
|
|
195
|
+
if (typeof tpx.Watts === "number") {
|
|
196
|
+
logger.debug("Found power data in TCX extensions", {
|
|
197
|
+
watts: tpx.Watts
|
|
198
|
+
});
|
|
199
|
+
return tpx.Watts;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
if (extensions.Power && typeof extensions.Power === "number") {
|
|
203
|
+
logger.debug("Found power data in TCX extensions", {
|
|
204
|
+
watts: extensions.Power
|
|
205
|
+
});
|
|
206
|
+
return extensions.Power;
|
|
207
|
+
}
|
|
208
|
+
return void 0;
|
|
209
|
+
};
|
|
210
|
+
var extractExtensions = (tcxStep, logger) => {
|
|
211
|
+
const extensions = tcxStep.Extensions;
|
|
212
|
+
if (!extensions) {
|
|
213
|
+
return void 0;
|
|
214
|
+
}
|
|
215
|
+
logger.debug("Extracting TCX extensions from step");
|
|
216
|
+
return { ...extensions };
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
// src/adapters/workout/step.converter.ts
|
|
220
|
+
var convertTargetWithExtensions = (tcxStep, extensions, logger) => {
|
|
221
|
+
const target = convertTcxTarget(
|
|
222
|
+
tcxStep.Target,
|
|
223
|
+
logger
|
|
224
|
+
);
|
|
225
|
+
if (!target) return null;
|
|
226
|
+
if (target.type === "open" && extensions) {
|
|
227
|
+
const powerWatts = extractPowerFromExtensions(extensions, logger);
|
|
228
|
+
if (powerWatts !== void 0) {
|
|
229
|
+
logger.debug("Converting open target to power target from extensions", {
|
|
230
|
+
watts: powerWatts
|
|
231
|
+
});
|
|
232
|
+
return {
|
|
233
|
+
type: "power",
|
|
234
|
+
value: {
|
|
235
|
+
unit: "watts",
|
|
236
|
+
value: powerWatts
|
|
237
|
+
}
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
return target;
|
|
242
|
+
};
|
|
243
|
+
var buildWorkoutStep = (stepIndex, name, duration, target, tcxStep, extensions) => {
|
|
244
|
+
const step = {
|
|
245
|
+
stepIndex,
|
|
246
|
+
name,
|
|
247
|
+
durationType: duration.type,
|
|
248
|
+
duration,
|
|
249
|
+
targetType: target.type,
|
|
250
|
+
target,
|
|
251
|
+
intensity: extractIntensity(tcxStep)
|
|
252
|
+
};
|
|
253
|
+
return extensions ? { ...step, extensions: { tcx: extensions } } : step;
|
|
254
|
+
};
|
|
255
|
+
var convertTcxStep = (tcxStep, stepIndex, logger) => {
|
|
256
|
+
logger.debug("Converting TCX step", { stepIndex });
|
|
257
|
+
const stepType = tcxStep["@_xsi:type"];
|
|
258
|
+
if (stepType === "Repeat_t") {
|
|
259
|
+
logger.warn("Repetition blocks not yet supported", { stepIndex });
|
|
260
|
+
return null;
|
|
261
|
+
}
|
|
262
|
+
const duration = convertTcxDuration(
|
|
263
|
+
tcxStep.Duration,
|
|
264
|
+
logger
|
|
265
|
+
);
|
|
266
|
+
if (!duration) {
|
|
267
|
+
logger.warn("Step has no valid duration, skipping", { stepIndex });
|
|
268
|
+
return null;
|
|
269
|
+
}
|
|
270
|
+
const extensions = extractExtensions(tcxStep, logger);
|
|
271
|
+
const target = convertTargetWithExtensions(tcxStep, extensions, logger);
|
|
272
|
+
if (!target) {
|
|
273
|
+
logger.warn("Step has no valid target, skipping", { stepIndex });
|
|
274
|
+
return null;
|
|
275
|
+
}
|
|
276
|
+
return buildWorkoutStep(
|
|
277
|
+
stepIndex,
|
|
278
|
+
tcxStep.Name,
|
|
279
|
+
duration,
|
|
280
|
+
target,
|
|
281
|
+
tcxStep,
|
|
282
|
+
extensions
|
|
283
|
+
);
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
// src/adapters/workout/workout.converter.ts
|
|
287
|
+
var extractWorkoutExtensions = (tcxWorkout, logger) => {
|
|
288
|
+
const extensions = tcxWorkout.Extensions;
|
|
289
|
+
if (!extensions) {
|
|
290
|
+
return void 0;
|
|
291
|
+
}
|
|
292
|
+
logger.debug("Extracting TCX extensions from workout");
|
|
293
|
+
return { ...extensions };
|
|
294
|
+
};
|
|
295
|
+
var convertSteps = (tcxSteps, logger) => {
|
|
296
|
+
const steps = [];
|
|
297
|
+
if (!tcxSteps) return steps;
|
|
298
|
+
const stepArray = Array.isArray(tcxSteps) ? tcxSteps : [tcxSteps];
|
|
299
|
+
let stepIndex = 0;
|
|
300
|
+
for (const tcxStep of stepArray) {
|
|
301
|
+
const step = convertTcxStep(
|
|
302
|
+
tcxStep,
|
|
303
|
+
stepIndex,
|
|
304
|
+
logger
|
|
305
|
+
);
|
|
306
|
+
if (step) {
|
|
307
|
+
steps.push(step);
|
|
308
|
+
stepIndex++;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
return steps;
|
|
312
|
+
};
|
|
313
|
+
var convertTcxWorkout = (tcxWorkout, logger) => {
|
|
314
|
+
logger.debug("Converting TCX workout");
|
|
315
|
+
const sportAttr = tcxWorkout["@_Sport"];
|
|
316
|
+
const sportResult = tcxSportSchema.safeParse(sportAttr);
|
|
317
|
+
const sport = sportResult.success ? TCX_TO_KRD_SPORT[sportResult.data] : "generic";
|
|
318
|
+
const name = tcxWorkout.Name;
|
|
319
|
+
const steps = convertSteps(tcxWorkout.Step, logger);
|
|
320
|
+
const extensions = extractWorkoutExtensions(tcxWorkout, logger);
|
|
321
|
+
const workout = {
|
|
322
|
+
name,
|
|
323
|
+
sport,
|
|
324
|
+
steps
|
|
325
|
+
};
|
|
326
|
+
if (extensions) {
|
|
327
|
+
return {
|
|
328
|
+
...workout,
|
|
329
|
+
extensions: {
|
|
330
|
+
tcx: extensions
|
|
331
|
+
}
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
return workout;
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
// src/adapters/workout/krd.converter.ts
|
|
338
|
+
var extractTcxExtensions = (trainingCenterDatabase, logger) => {
|
|
339
|
+
const extensions = trainingCenterDatabase.Extensions;
|
|
340
|
+
if (!extensions) {
|
|
341
|
+
return void 0;
|
|
342
|
+
}
|
|
343
|
+
logger.debug("Extracting TCX extensions from TrainingCenterDatabase");
|
|
344
|
+
return { ...extensions };
|
|
345
|
+
};
|
|
346
|
+
var extractWorkoutData = (trainingCenterDatabase) => {
|
|
347
|
+
const workouts = trainingCenterDatabase.Workouts;
|
|
348
|
+
if (!workouts) {
|
|
349
|
+
throw createTcxParsingError("No workouts found in TCX file");
|
|
350
|
+
}
|
|
351
|
+
const workoutArray = Array.isArray(workouts.Workout) ? workouts.Workout : [workouts.Workout];
|
|
352
|
+
if (workoutArray.length === 0) {
|
|
353
|
+
throw createTcxParsingError("No workout data found in TCX file");
|
|
354
|
+
}
|
|
355
|
+
return workoutArray[0];
|
|
356
|
+
};
|
|
357
|
+
var convertTcxToKRD = (tcxData, logger) => {
|
|
358
|
+
logger.debug("Converting TCX to KRD");
|
|
359
|
+
const trainingCenterDatabase = tcxData.TrainingCenterDatabase;
|
|
360
|
+
const tcxWorkout = extractWorkoutData(trainingCenterDatabase);
|
|
361
|
+
const workout = convertTcxWorkout(tcxWorkout, logger);
|
|
362
|
+
const tcxExtensions = extractTcxExtensions(trainingCenterDatabase, logger);
|
|
363
|
+
const metadata = extractKaiordMetadata(trainingCenterDatabase, workout);
|
|
364
|
+
const krd = {
|
|
365
|
+
version: "1.0",
|
|
366
|
+
type: "structured_workout",
|
|
367
|
+
metadata,
|
|
368
|
+
extensions: {
|
|
369
|
+
structured_workout: workout,
|
|
370
|
+
...tcxExtensions && { tcx: tcxExtensions }
|
|
371
|
+
}
|
|
372
|
+
};
|
|
373
|
+
logger.debug("TCX to KRD conversion complete");
|
|
374
|
+
return krd;
|
|
375
|
+
};
|
|
376
|
+
|
|
377
|
+
// src/adapters/workout/metadata-builder.ts
|
|
378
|
+
var addKaiordMetadata = (trainingCenterDatabase, krd) => {
|
|
379
|
+
if (krd.metadata.created) {
|
|
380
|
+
trainingCenterDatabase["@_kaiord:timeCreated"] = krd.metadata.created;
|
|
381
|
+
}
|
|
382
|
+
if (krd.metadata.manufacturer) {
|
|
383
|
+
trainingCenterDatabase["@_kaiord:manufacturer"] = krd.metadata.manufacturer;
|
|
384
|
+
}
|
|
385
|
+
if (krd.metadata.product) {
|
|
386
|
+
trainingCenterDatabase["@_kaiord:product"] = krd.metadata.product;
|
|
387
|
+
}
|
|
388
|
+
if (krd.metadata.serialNumber) {
|
|
389
|
+
trainingCenterDatabase["@_kaiord:serialNumber"] = krd.metadata.serialNumber;
|
|
390
|
+
}
|
|
391
|
+
};
|
|
392
|
+
|
|
393
|
+
// src/adapters/workout/duration-to-tcx-encoder.ts
|
|
394
|
+
var addKaiordAttributes = (tcxDuration, duration) => {
|
|
395
|
+
if (duration.type === "heart_rate_less_than" && duration.bpm !== void 0) {
|
|
396
|
+
tcxDuration["@_kaiord:originalDurationType"] = "heart_rate_less_than";
|
|
397
|
+
tcxDuration["@_kaiord:originalDurationBpm"] = duration.bpm;
|
|
398
|
+
} else if (duration.type === "power_less_than" && duration.watts !== void 0) {
|
|
399
|
+
tcxDuration["@_kaiord:originalDurationType"] = "power_less_than";
|
|
400
|
+
tcxDuration["@_kaiord:originalDurationWatts"] = duration.watts;
|
|
401
|
+
} else if (duration.type === "power_greater_than" && duration.watts !== void 0) {
|
|
402
|
+
tcxDuration["@_kaiord:originalDurationType"] = "power_greater_than";
|
|
403
|
+
tcxDuration["@_kaiord:originalDurationWatts"] = duration.watts;
|
|
404
|
+
} else if (duration.type === "calories" && duration.calories !== void 0) {
|
|
405
|
+
tcxDuration["@_kaiord:originalDurationType"] = "calories";
|
|
406
|
+
tcxDuration["@_kaiord:originalDurationCalories"] = duration.calories;
|
|
407
|
+
}
|
|
408
|
+
};
|
|
409
|
+
var convertDurationToTcx = (step) => {
|
|
410
|
+
const duration = step.duration;
|
|
411
|
+
if (duration.type === "time") {
|
|
412
|
+
return {
|
|
413
|
+
"@_xsi:type": "Time_t",
|
|
414
|
+
Seconds: duration.seconds
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
if (duration.type === "distance") {
|
|
418
|
+
return {
|
|
419
|
+
"@_xsi:type": "Distance_t",
|
|
420
|
+
Meters: duration.meters
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
const tcxDuration = {
|
|
424
|
+
"@_xsi:type": "LapButton_t"
|
|
425
|
+
};
|
|
426
|
+
addKaiordAttributes(tcxDuration, duration);
|
|
427
|
+
return tcxDuration;
|
|
428
|
+
};
|
|
429
|
+
|
|
430
|
+
// src/adapters/workout/target-to-tcx.converter.ts
|
|
431
|
+
var convertHeartRateToTcx = (value) => {
|
|
432
|
+
if (value.unit === "zone") {
|
|
433
|
+
return {
|
|
434
|
+
"@_xsi:type": "HeartRate_t",
|
|
435
|
+
HeartRateZone: {
|
|
436
|
+
"@_xsi:type": "PredefinedHeartRateZone_t",
|
|
437
|
+
Number: value.value
|
|
438
|
+
}
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
if (value.unit === "bpm" || value.unit === "range") {
|
|
442
|
+
const min = "min" in value ? value.min : value.value;
|
|
443
|
+
const max = "max" in value ? value.max : value.value;
|
|
444
|
+
return {
|
|
445
|
+
"@_xsi:type": "HeartRate_t",
|
|
446
|
+
HeartRateZone: {
|
|
447
|
+
"@_xsi:type": "CustomHeartRateZone_t",
|
|
448
|
+
Low: min,
|
|
449
|
+
High: max
|
|
450
|
+
}
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
return { "@_xsi:type": "None_t" };
|
|
454
|
+
};
|
|
455
|
+
var convertPaceToTcx = (value) => {
|
|
456
|
+
if (value.unit === "meters_per_second" || value.unit === "range") {
|
|
457
|
+
const min = "min" in value ? value.min : value.value;
|
|
458
|
+
const max = "max" in value ? value.max : value.value;
|
|
459
|
+
return {
|
|
460
|
+
"@_xsi:type": "Speed_t",
|
|
461
|
+
SpeedZone: {
|
|
462
|
+
"@_xsi:type": "CustomSpeedZone_t",
|
|
463
|
+
LowInMetersPerSecond: min,
|
|
464
|
+
HighInMetersPerSecond: max
|
|
465
|
+
}
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
return { "@_xsi:type": "None_t" };
|
|
469
|
+
};
|
|
470
|
+
var convertCadenceToTcx = (value) => {
|
|
471
|
+
if (value.unit === "rpm" || value.unit === "range") {
|
|
472
|
+
const min = "min" in value ? value.min : value.value;
|
|
473
|
+
const max = "max" in value ? value.max : value.value;
|
|
474
|
+
return {
|
|
475
|
+
"@_xsi:type": "Cadence_t",
|
|
476
|
+
CadenceZone: {
|
|
477
|
+
"@_xsi:type": "CustomCadenceZone_t",
|
|
478
|
+
Low: min,
|
|
479
|
+
High: max
|
|
480
|
+
}
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
return { "@_xsi:type": "None_t" };
|
|
484
|
+
};
|
|
485
|
+
var convertTargetToTcx = (step) => {
|
|
486
|
+
if (step.target.type === "open") {
|
|
487
|
+
return { "@_xsi:type": "None_t" };
|
|
488
|
+
}
|
|
489
|
+
if (step.target.type === "heart_rate") {
|
|
490
|
+
const value = step.target.value;
|
|
491
|
+
if (value && "unit" in value) {
|
|
492
|
+
return convertHeartRateToTcx(value);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
if (step.target.type === "pace") {
|
|
496
|
+
const value = step.target.value;
|
|
497
|
+
if (value && "unit" in value) {
|
|
498
|
+
return convertPaceToTcx(value);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
if (step.target.type === "cadence") {
|
|
502
|
+
const value = step.target.value;
|
|
503
|
+
if (value && "unit" in value) {
|
|
504
|
+
return convertCadenceToTcx(value);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
return { "@_xsi:type": "None_t" };
|
|
508
|
+
};
|
|
509
|
+
|
|
510
|
+
// src/adapters/workout/step-to-tcx.converter.ts
|
|
511
|
+
var capitalizeFirst = (str) => {
|
|
512
|
+
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
513
|
+
};
|
|
514
|
+
var addPowerExtensions = (step, tcxStep, logger) => {
|
|
515
|
+
if (step.target.type === "power") {
|
|
516
|
+
logger.debug("Encoding power target to TCX extensions", {
|
|
517
|
+
stepIndex: step.stepIndex
|
|
518
|
+
});
|
|
519
|
+
const powerValue = step.target.value;
|
|
520
|
+
if (powerValue && "unit" in powerValue && powerValue.unit === "watts") {
|
|
521
|
+
tcxStep.Extensions = {
|
|
522
|
+
TPX: {
|
|
523
|
+
"@_xmlns": "http://www.garmin.com/xmlschemas/ActivityExtension/v2",
|
|
524
|
+
Watts: powerValue.value
|
|
525
|
+
}
|
|
526
|
+
};
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
};
|
|
530
|
+
var convertStepToTcx = (step, index, logger) => {
|
|
531
|
+
logger.debug("Converting step to TCX", { stepIndex: step.stepIndex });
|
|
532
|
+
const tcxStep = {
|
|
533
|
+
"@_xsi:type": "Step_t",
|
|
534
|
+
StepId: index + 1
|
|
535
|
+
};
|
|
536
|
+
if (step.name) {
|
|
537
|
+
tcxStep.Name = step.name;
|
|
538
|
+
}
|
|
539
|
+
tcxStep.Duration = convertDurationToTcx(step);
|
|
540
|
+
if (step.intensity) {
|
|
541
|
+
tcxStep.Intensity = capitalizeFirst(step.intensity);
|
|
542
|
+
}
|
|
543
|
+
tcxStep.Target = convertTargetToTcx(step);
|
|
544
|
+
if (step.extensions?.tcx) {
|
|
545
|
+
logger.debug("Restoring step-level TCX extensions", {
|
|
546
|
+
stepIndex: step.stepIndex
|
|
547
|
+
});
|
|
548
|
+
tcxStep.Extensions = step.extensions.tcx;
|
|
549
|
+
} else {
|
|
550
|
+
addPowerExtensions(step, tcxStep, logger);
|
|
551
|
+
}
|
|
552
|
+
return tcxStep;
|
|
553
|
+
};
|
|
554
|
+
|
|
555
|
+
// src/adapters/workout/tcx.converter.ts
|
|
556
|
+
var buildTcxWorkout = (workout, logger) => {
|
|
557
|
+
const tcxSport = KRD_TO_TCX_SPORT[workout.sport] || "Other";
|
|
558
|
+
const tcxSteps = workout.steps.map(
|
|
559
|
+
(step, index) => convertStepToTcx(step, index, logger)
|
|
560
|
+
);
|
|
561
|
+
const tcxWorkout = {
|
|
562
|
+
"@_Sport": tcxSport,
|
|
563
|
+
Name: workout.name,
|
|
564
|
+
Step: tcxSteps
|
|
565
|
+
};
|
|
566
|
+
if (workout.extensions?.tcx) {
|
|
567
|
+
logger.debug("Restoring workout-level TCX extensions");
|
|
568
|
+
tcxWorkout.Extensions = workout.extensions.tcx;
|
|
569
|
+
}
|
|
570
|
+
return tcxWorkout;
|
|
571
|
+
};
|
|
572
|
+
var buildTrainingCenterDatabase = (tcxWorkout, krd, logger) => {
|
|
573
|
+
const trainingCenterDatabase = {
|
|
574
|
+
"@_xmlns": "http://www.garmin.com/xmlschemas/TrainingCenterDatabase/v2",
|
|
575
|
+
"@_xmlns:xsi": "http://www.w3.org/2001/XMLSchema-instance",
|
|
576
|
+
"@_xmlns:kaiord": "http://kaiord.dev/tcx-extensions/1.0",
|
|
577
|
+
Workouts: {
|
|
578
|
+
Workout: tcxWorkout
|
|
579
|
+
}
|
|
580
|
+
};
|
|
581
|
+
addKaiordMetadata(trainingCenterDatabase, krd);
|
|
582
|
+
if (krd.extensions?.tcx) {
|
|
583
|
+
logger.debug("Restoring TrainingCenterDatabase-level TCX extensions");
|
|
584
|
+
trainingCenterDatabase.Extensions = krd.extensions.tcx;
|
|
585
|
+
}
|
|
586
|
+
return trainingCenterDatabase;
|
|
587
|
+
};
|
|
588
|
+
var convertKRDToTcx = (krd, logger) => {
|
|
589
|
+
logger.debug("Converting KRD to TCX structure");
|
|
590
|
+
if (!krd.extensions?.structured_workout) {
|
|
591
|
+
throw createTcxParsingError(
|
|
592
|
+
"KRD does not contain workout data in extensions"
|
|
593
|
+
);
|
|
594
|
+
}
|
|
595
|
+
const workout = krd.extensions.structured_workout;
|
|
596
|
+
const tcxWorkout = buildTcxWorkout(workout, logger);
|
|
597
|
+
const trainingCenterDatabase = buildTrainingCenterDatabase(
|
|
598
|
+
tcxWorkout,
|
|
599
|
+
krd,
|
|
600
|
+
logger
|
|
601
|
+
);
|
|
602
|
+
return {
|
|
603
|
+
TrainingCenterDatabase: trainingCenterDatabase
|
|
604
|
+
};
|
|
605
|
+
};
|
|
606
|
+
|
|
607
|
+
// src/adapters/fast-xml-parser.ts
|
|
608
|
+
var createFastXmlTcxReader = (logger) => async (xmlString) => {
|
|
609
|
+
logger.debug("Parsing TCX file", { xmlLength: xmlString.length });
|
|
610
|
+
let tcxData;
|
|
611
|
+
try {
|
|
612
|
+
const parser = new XMLParser({
|
|
613
|
+
ignoreAttributes: false,
|
|
614
|
+
attributeNamePrefix: "@_",
|
|
615
|
+
parseAttributeValue: true
|
|
616
|
+
});
|
|
617
|
+
tcxData = parser.parse(xmlString);
|
|
618
|
+
} catch (error) {
|
|
619
|
+
logger.error("Failed to parse TCX XML", { error });
|
|
620
|
+
throw createTcxParsingError("Failed to parse TCX file", error);
|
|
621
|
+
}
|
|
622
|
+
if (!tcxData || typeof tcxData !== "object" || !("TrainingCenterDatabase" in tcxData)) {
|
|
623
|
+
const error = createTcxParsingError(
|
|
624
|
+
"Invalid TCX format: missing TrainingCenterDatabase element"
|
|
625
|
+
);
|
|
626
|
+
logger.error("Invalid TCX structure", { error });
|
|
627
|
+
throw error;
|
|
628
|
+
}
|
|
629
|
+
logger.info("TCX file parsed successfully");
|
|
630
|
+
return convertTcxToKRD(tcxData, logger);
|
|
631
|
+
};
|
|
632
|
+
var createFastXmlTcxWriter = (logger, validator) => async (krd) => {
|
|
633
|
+
logger.debug("Encoding KRD to TCX");
|
|
634
|
+
let tcxData;
|
|
635
|
+
try {
|
|
636
|
+
tcxData = convertKRDToTcx2(krd, logger);
|
|
637
|
+
} catch (error) {
|
|
638
|
+
logger.error("Failed to convert KRD to TCX structure", { error });
|
|
639
|
+
throw createTcxParsingError("Failed to convert KRD to TCX", error);
|
|
640
|
+
}
|
|
641
|
+
let xmlString;
|
|
642
|
+
try {
|
|
643
|
+
const builder = new XMLBuilder({
|
|
644
|
+
ignoreAttributes: false,
|
|
645
|
+
attributeNamePrefix: "@_",
|
|
646
|
+
format: true,
|
|
647
|
+
indentBy: " "
|
|
648
|
+
});
|
|
649
|
+
xmlString = builder.build(tcxData);
|
|
650
|
+
} catch (error) {
|
|
651
|
+
logger.error("Failed to build TCX XML", { error });
|
|
652
|
+
throw createTcxParsingError("Failed to build TCX XML", error);
|
|
653
|
+
}
|
|
654
|
+
logger.debug("Validating generated TCX against XSD");
|
|
655
|
+
const validationResult = await validator(xmlString);
|
|
656
|
+
if (!validationResult.valid) {
|
|
657
|
+
logger.error("Generated TCX does not conform to XSD schema", {
|
|
658
|
+
errors: validationResult.errors
|
|
659
|
+
});
|
|
660
|
+
throw createTcxValidationError(
|
|
661
|
+
"Generated TCX file does not conform to XSD schema",
|
|
662
|
+
validationResult.errors.map((err) => ({
|
|
663
|
+
field: err.path,
|
|
664
|
+
message: err.message
|
|
665
|
+
}))
|
|
666
|
+
);
|
|
667
|
+
}
|
|
668
|
+
logger.info("KRD encoded to TCX successfully");
|
|
669
|
+
return xmlString;
|
|
670
|
+
};
|
|
671
|
+
var convertKRDToTcx2 = (krd, logger) => {
|
|
672
|
+
return convertKRDToTcx(krd, logger);
|
|
673
|
+
};
|
|
674
|
+
var createXsdTcxValidator = (logger) => async (xmlString) => {
|
|
675
|
+
try {
|
|
676
|
+
logger.debug("Validating TCX XML structure");
|
|
677
|
+
const xmlValidation = XMLValidator.validate(xmlString, {
|
|
678
|
+
allowBooleanAttributes: true
|
|
679
|
+
});
|
|
680
|
+
if (xmlValidation !== true) {
|
|
681
|
+
logger.warn("TCX XML validation failed", {
|
|
682
|
+
error: xmlValidation.err
|
|
683
|
+
});
|
|
684
|
+
return {
|
|
685
|
+
valid: false,
|
|
686
|
+
errors: [
|
|
687
|
+
{
|
|
688
|
+
path: `line ${xmlValidation.err.line}`,
|
|
689
|
+
message: `XML validation failed: ${xmlValidation.err.msg}`
|
|
690
|
+
}
|
|
691
|
+
]
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
logger.info("TCX XML structure is valid");
|
|
695
|
+
return { valid: true, errors: [] };
|
|
696
|
+
} catch (error) {
|
|
697
|
+
logger.error("TCX validation failed", { error });
|
|
698
|
+
return {
|
|
699
|
+
valid: false,
|
|
700
|
+
errors: [
|
|
701
|
+
{
|
|
702
|
+
path: "root",
|
|
703
|
+
message: error instanceof Error ? error.message : "Unknown error"
|
|
704
|
+
}
|
|
705
|
+
]
|
|
706
|
+
};
|
|
707
|
+
}
|
|
708
|
+
};
|
|
709
|
+
|
|
710
|
+
// src/providers.ts
|
|
711
|
+
var createTcxProviders = (logger) => {
|
|
712
|
+
const log = logger || createConsoleLogger();
|
|
713
|
+
const tcxValidator = createXsdTcxValidator(log);
|
|
714
|
+
return {
|
|
715
|
+
tcxReader: createFastXmlTcxReader(log),
|
|
716
|
+
tcxWriter: createFastXmlTcxWriter(log, tcxValidator),
|
|
717
|
+
tcxValidator
|
|
718
|
+
};
|
|
719
|
+
};
|
|
720
|
+
|
|
721
|
+
export { createFastXmlTcxReader, createFastXmlTcxWriter, createTcxProviders, createXsdTcxValidator };
|
|
722
|
+
//# sourceMappingURL=index.js.map
|
|
723
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/adapters/workout/metadata-extractor.ts","../src/adapters/schemas/tcx-sport.ts","../src/adapters/duration/duration-kaiord-restorer.ts","../src/adapters/duration/duration-standard-converter.ts","../src/adapters/duration/duration.mapper.ts","../src/adapters/target/tcx-to-krd.mapper.ts","../src/adapters/workout/step-helpers.ts","../src/adapters/workout/step.converter.ts","../src/adapters/workout/workout.converter.ts","../src/adapters/workout/krd.converter.ts","../src/adapters/workout/metadata-builder.ts","../src/adapters/workout/duration-to-tcx-encoder.ts","../src/adapters/workout/target-to-tcx.converter.ts","../src/adapters/workout/step-to-tcx.converter.ts","../src/adapters/workout/tcx.converter.ts","../src/adapters/fast-xml-parser.ts","../src/adapters/xsd-validator.ts","../src/providers.ts"],"names":["createTcxParsingError","convertKRDToTcx"],"mappings":";;;;;;;AAGO,IAAM,qBAAA,GAAwB,CACnC,sBAAA,EACA,OAAA,KACoB;AACpB,EAAA,MAAM,QAAA,GAA4B;AAAA,IAChC,SACG,sBAAA,CAAuB,sBAAsB,sBAC9C,IAAI,IAAA,IAAO,WAAA,EAAY;AAAA,IACzB,OAAO,OAAA,CAAQ,KAAA;AAAA,IACf,UAAU,OAAA,CAAQ;AAAA,GACpB;AAEA,EAAA,IAAI,sBAAA,CAAuB,uBAAuB,CAAA,EAAG;AACnD,IAAA,QAAA,CAAS,YAAA,GAAe,uBACtB,uBACF,CAAA;AAAA,EACF;AACA,EAAA,IAAI,sBAAA,CAAuB,kBAAkB,CAAA,EAAG;AAC9C,IAAA,QAAA,CAAS,OAAA,GAAU,uBAAuB,kBAAkB,CAAA;AAAA,EAC9D;AACA,EAAA,IAAI,sBAAA,CAAuB,uBAAuB,CAAA,EAAG;AACnD,IAAA,MAAM,YAAA,GAAe,uBAAuB,uBAAuB,CAAA;AAEnE,IAAA,QAAA,CAAS,eACP,OAAO,YAAA,KAAiB,QAAA,GACpB,MAAA,CAAO,YAAY,CAAA,GAClB,YAAA;AAAA,EACT;AAEA,EAAA,OAAO,QAAA;AACT,CAAA;AC9BO,IAAM,iBAAiB,CAAA,CAAE,IAAA,CAAK,CAAC,SAAA,EAAW,QAAA,EAAU,OAAO,CAAC,CAAA;AAI5D,IAAM,gBAAA,GAA4C;AAAA,EACvD,OAAA,EAAS,SAAA;AAAA,EACT,MAAA,EAAQ,SAAA;AAAA,EACR,KAAA,EAAO;AACT,CAAA;AAEO,IAAM,gBAAA,GAA4C;AAAA,EACvD,OAAA,EAAS,SAAA;AAAA,EACT,OAAA,EAAS,QAAA;AAAA,EACT,QAAA,EAAU,OAAA;AAAA,EACV,OAAA,EAAS;AACX,CAAA;;;ACfA,IAAM,wBAAA,GAA2B,CAC/B,WAAA,EACA,MAAA,KACoB;AACpB,EAAA,MAAM,GAAA,GAAM,YAAY,8BAA8B,CAAA;AACtD,EAAA,IAAI,OAAO,QAAQ,QAAA,EAAU;AAC3B,IAAA,MAAA,CAAO,MAAM,uDAAA,EAAyD;AAAA,MACpE;AAAA,KACD,CAAA;AACD,IAAA,OAAO,EAAE,IAAA,EAAM,sBAAA,EAAwB,GAAA,EAAI;AAAA,EAC7C;AACA,EAAA,OAAO,IAAA;AACT,CAAA;AAEA,IAAM,oBAAA,GAAuB,CAC3B,WAAA,EACA,MAAA,KACoB;AACpB,EAAA,MAAM,KAAA,GAAQ,YAAY,gCAAgC,CAAA;AAG1D,EAAA,IAAI,OAAO,UAAU,QAAA,EAAU;AAC7B,IAAA,MAAA,CAAO,MAAM,kDAAA,EAAoD;AAAA,MAC/D;AAAA,KACD,CAAA;AACD,IAAA,OAAO,EAAE,IAAA,EAAM,iBAAA,EAAmB,KAAA,EAAM;AAAA,EAC1C;AACA,EAAA,OAAO,IAAA;AACT,CAAA;AAEA,IAAM,uBAAA,GAA0B,CAC9B,WAAA,EACA,MAAA,KACoB;AACpB,EAAA,MAAM,KAAA,GAAQ,YAAY,gCAAgC,CAAA;AAG1D,EAAA,IAAI,OAAO,UAAU,QAAA,EAAU;AAC7B,IAAA,MAAA,CAAO,MAAM,qDAAA,EAAuD;AAAA,MAClE;AAAA,KACD,CAAA;AACD,IAAA,OAAO,EAAE,IAAA,EAAM,oBAAA,EAAsB,KAAA,EAAM;AAAA,EAC7C;AACA,EAAA,OAAO,IAAA;AACT,CAAA;AAEA,IAAM,eAAA,GAAkB,CACtB,WAAA,EACA,MAAA,KACoB;AACpB,EAAA,MAAM,QAAA,GAAW,YAAY,mCAAmC,CAAA;AAGhE,EAAA,IAAI,OAAO,aAAa,QAAA,EAAU;AAChC,IAAA,MAAA,CAAO,MAAM,2CAAA,EAA6C;AAAA,MACxD;AAAA,KACD,CAAA;AACD,IAAA,OAAO,EAAE,IAAA,EAAM,UAAA,EAAY,QAAA,EAAS;AAAA,EACtC;AACA,EAAA,OAAO,IAAA;AACT,CAAA;AAEO,IAAM,qBAAA,GAAwB,CACnC,WAAA,EACA,MAAA,KACoB;AACpB,EAAA,MAAM,oBAAA,GAAuB,YAAY,+BAA+B,CAAA;AAIxE,EAAA,IAAI,yBAAyB,sBAAA,EAAwB;AACnD,IAAA,OAAO,wBAAA,CAAyB,aAAa,MAAM,CAAA;AAAA,EACrD;AAEA,EAAA,IAAI,yBAAyB,iBAAA,EAAmB;AAC9C,IAAA,OAAO,oBAAA,CAAqB,aAAa,MAAM,CAAA;AAAA,EACjD;AAEA,EAAA,IAAI,yBAAyB,oBAAA,EAAsB;AACjD,IAAA,OAAO,uBAAA,CAAwB,aAAa,MAAM,CAAA;AAAA,EACpD;AAEA,EAAA,IAAI,yBAAyB,UAAA,EAAY;AACvC,IAAA,OAAO,eAAA,CAAgB,aAAa,MAAM,CAAA;AAAA,EAC5C;AAEA,EAAA,OAAO,IAAA;AACT,CAAA;;;ACxFO,IAAM,0BAAA,GAA6B,CACxC,WAAA,KACoB;AACpB,EAAA,MAAM,YAAA,GAAe,YAAY,YAAY,CAAA;AAE7C,EAAA,IAAI,iBAAiB,QAAA,EAAU;AAC7B,IAAA,MAAM,UAAU,WAAA,CAAY,OAAA;AAC5B,IAAA,IAAI,OAAO,OAAA,KAAY,QAAA,IAAY,OAAA,GAAU,CAAA,EAAG;AAC9C,MAAA,OAAO,EAAE,IAAA,EAAM,MAAA,EAAQ,OAAA,EAAQ;AAAA,IACjC;AAAA,EACF;AAEA,EAAA,IAAI,iBAAiB,YAAA,EAAc;AACjC,IAAA,MAAM,SAAS,WAAA,CAAY,MAAA;AAC3B,IAAA,IAAI,OAAO,MAAA,KAAW,QAAA,IAAY,MAAA,GAAS,CAAA,EAAG;AAC5C,MAAA,OAAO,EAAE,IAAA,EAAM,UAAA,EAAY,MAAA,EAAO;AAAA,IACpC;AAAA,EACF;AAEA,EAAA,IAAI,iBAAiB,aAAA,EAAe;AAClC,IAAA,OAAO,EAAE,MAAM,MAAA,EAAO;AAAA,EACxB;AAEA,EAAA,OAAO,IAAA;AACT,CAAA;;;AC2BO,IAAM,kBAAA,GAAqB,CAChC,WAAA,EACA,MAAA,KACoB;AACpB,EAAA,IAAI,CAAC,WAAA,EAAa;AAChB,IAAA,OAAO,IAAA;AAAA,EACT;AAGA,EAAA,MAAM,cAAA,GAAiB,qBAAA,CAAsB,WAAA,EAAa,MAAM,CAAA;AAChE,EAAA,IAAI,cAAA,EAAgB;AAClB,IAAA,OAAO,cAAA;AAAA,EACT;AAGA,EAAA,MAAM,gBAAA,GAAmB,2BAA2B,WAAW,CAAA;AAC/D,EAAA,IAAI,gBAAA,EAAkB;AACpB,IAAA,OAAO,gBAAA;AAAA,EACT;AAEA,EAAA,MAAM,YAAA,GAAe,YAAY,YAAY,CAAA;AAC7C,EAAA,MAAA,CAAO,IAAA,CAAK,2BAAA,EAA6B,EAAE,YAAA,EAAc,CAAA;AACzD,EAAA,OAAO,IAAA;AACT,CAAA;ACzDA,IAAM,sBAAA,GAAyB,CAC7B,aAAA,KACkB;AAClB,EAAA,IAAI,CAAC,eAAe,OAAO,IAAA;AAE3B,EAAA,MAAM,QAAA,GAAW,cAAc,YAAY,CAAA;AAE3C,EAAA,IAAI,aAAa,2BAAA,EAA6B;AAC5C,IAAA,MAAM,aAAa,aAAA,CAAc,MAAA;AACjC,IAAA,IAAI,OAAO,eAAe,QAAA,EAAU;AAClC,MAAA,OAAO;AAAA,QACL,IAAA,EAAM,YAAA;AAAA,QACN,KAAA,EAAO,EAAE,IAAA,EAAM,MAAA,EAAQ,OAAO,UAAA;AAAW,OAC3C;AAAA,IACF;AAAA,EACF;AAEA,EAAA,IAAI,aAAa,uBAAA,EAAyB;AACxC,IAAA,MAAM,MAAM,aAAA,CAAc,GAAA;AAC1B,IAAA,MAAM,OAAO,aAAA,CAAc,IAAA;AAC3B,IAAA,IAAI,OAAO,GAAA,KAAQ,QAAA,IAAY,OAAO,SAAS,QAAA,EAAU;AACvD,MAAA,OAAO;AAAA,QACL,IAAA,EAAM,YAAA;AAAA,QACN,OAAO,EAAE,IAAA,EAAM,SAAS,GAAA,EAAK,GAAA,EAAK,KAAK,IAAA;AAAK,OAC9C;AAAA,IACF;AAAA,EACF;AAEA,EAAA,OAAO,IAAA;AACT,CAAA;AAEO,IAAM,gBAAA,GAAmB,CAC9B,SAAA,EACA,MAAA,KACkB;AAClB,EAAA,IAAI,CAAC,SAAA,EAAW;AACd,IAAA,OAAO,EAAE,MAAM,MAAA,EAAO;AAAA,EACxB;AAEA,EAAA,MAAM,UAAA,GAAa,UAAU,YAAY,CAAA;AAEzC,EAAA,IAAI,eAAe,QAAA,EAAU;AAC3B,IAAA,OAAO,EAAE,MAAM,MAAA,EAAO;AAAA,EACxB;AAEA,EAAA,IAAI,eAAe,aAAA,EAAe;AAChC,IAAA,MAAM,gBAAgB,SAAA,CAAU,aAAA;AAGhC,IAAA,MAAM,MAAA,GAAS,uBAAuB,aAAa,CAAA;AACnD,IAAA,IAAI,QAAQ,OAAO,MAAA;AAAA,EACrB;AAEA,EAAA,MAAA,CAAO,IAAA,CAAK,yBAAA,EAA2B,EAAE,UAAA,EAAY,CAAA;AACrD,EAAA,OAAO,EAAE,MAAM,MAAA,EAAO;AACxB,CAAA;;;ACxEO,IAAM,gBAAA,GAAmB,CAC9B,OAAA,KAC0D;AAC1D,EAAA,MAAM,MAAM,OAAA,CAAQ,SAAA;AACpB,EAAA,MAAM,KAAA,GAAQ,KAAK,WAAA,EAAY;AAE/B,EAAA,QAAQ,KAAA;AAAO,IACb,KAAK,QAAA;AAAA,IACL,KAAK,QAAA;AAAA,IACL,KAAK,UAAA;AAAA,IACL,KAAK,MAAA;AACH,MAAA,OAAO,KAAA;AAAA,IACT,KAAK,SAAA;AACH,MAAA,OAAO,MAAA;AAAA,IACT;AACE,MAAA,OAAO,MAAA;AAAA;AAEb,CAAA;AAEO,IAAM,0BAAA,GAA6B,CACxC,UAAA,EACA,MAAA,KACuB;AACvB,EAAA,IAAI,WAAW,GAAA,EAAK;AAClB,IAAA,MAAM,MAAM,UAAA,CAAW,GAAA;AACvB,IAAA,IAAI,OAAO,GAAA,CAAI,KAAA,KAAU,QAAA,EAAU;AACjC,MAAA,MAAA,CAAO,MAAM,oCAAA,EAAsC;AAAA,QACjD,OAAO,GAAA,CAAI;AAAA,OACZ,CAAA;AACD,MAAA,OAAO,GAAA,CAAI,KAAA;AAAA,IACb;AAAA,EACF;AAEA,EAAA,IAAI,UAAA,CAAW,KAAA,IAAS,OAAO,UAAA,CAAW,UAAU,QAAA,EAAU;AAC5D,IAAA,MAAA,CAAO,MAAM,oCAAA,EAAsC;AAAA,MACjD,OAAO,UAAA,CAAW;AAAA,KACnB,CAAA;AACD,IAAA,OAAO,UAAA,CAAW,KAAA;AAAA,EACpB;AAEA,EAAA,OAAO,MAAA;AACT,CAAA;AAEO,IAAM,iBAAA,GAAoB,CAC/B,OAAA,EACA,MAAA,KACwC;AACxC,EAAA,MAAM,aAAa,OAAA,CAAQ,UAAA;AAC3B,EAAA,IAAI,CAAC,UAAA,EAAY;AACf,IAAA,OAAO,MAAA;AAAA,EACT;AAEA,EAAA,MAAA,CAAO,MAAM,qCAAqC,CAAA;AAClD,EAAA,OAAO,EAAE,GAAG,UAAA,EAAW;AACzB,CAAA;;;AC5CA,IAAM,2BAAA,GAA8B,CAClC,OAAA,EACA,UAAA,EACA,MAAA,KACkB;AAClB,EAAA,MAAM,MAAA,GAAS,gBAAA;AAAA,IACb,OAAA,CAAQ,MAAA;AAAA,IACR;AAAA,GACF;AACA,EAAA,IAAI,CAAC,QAAQ,OAAO,IAAA;AAEpB,EAAA,IAAI,MAAA,CAAO,IAAA,KAAS,MAAA,IAAU,UAAA,EAAY;AACxC,IAAA,MAAM,UAAA,GAAa,0BAAA,CAA2B,UAAA,EAAY,MAAM,CAAA;AAChE,IAAA,IAAI,eAAe,MAAA,EAAW;AAC5B,MAAA,MAAA,CAAO,MAAM,wDAAA,EAA0D;AAAA,QACrE,KAAA,EAAO;AAAA,OACR,CAAA;AACD,MAAA,OAAO;AAAA,QACL,IAAA,EAAM,OAAA;AAAA,QACN,KAAA,EAAO;AAAA,UACL,IAAA,EAAM,OAAA;AAAA,UACN,KAAA,EAAO;AAAA;AACT,OACF;AAAA,IACF;AAAA,EACF;AAEA,EAAA,OAAO,MAAA;AACT,CAAA;AAEA,IAAM,mBAAmB,CACvB,SAAA,EACA,MACA,QAAA,EACA,MAAA,EACA,SACA,UAAA,KACgB;AAChB,EAAA,MAAM,IAAA,GAAoB;AAAA,IACxB,SAAA;AAAA,IACA,IAAA;AAAA,IACA,cAAc,QAAA,CAAS,IAAA;AAAA,IACvB,QAAA;AAAA,IACA,YAAY,MAAA,CAAO,IAAA;AAAA,IACnB,MAAA;AAAA,IACA,SAAA,EAAW,iBAAiB,OAAO;AAAA,GACrC;AAEA,EAAA,OAAO,UAAA,GAAa,EAAE,GAAG,IAAA,EAAM,YAAY,EAAE,GAAA,EAAK,UAAA,EAAW,EAAE,GAAI,IAAA;AACrE,CAAA;AAEO,IAAM,cAAA,GAAiB,CAC5B,OAAA,EACA,SAAA,EACA,MAAA,KACuB;AACvB,EAAA,MAAA,CAAO,KAAA,CAAM,qBAAA,EAAuB,EAAE,SAAA,EAAW,CAAA;AAEjD,EAAA,MAAM,QAAA,GAAW,QAAQ,YAAY,CAAA;AACrC,EAAA,IAAI,aAAa,UAAA,EAAY;AAC3B,IAAA,MAAA,CAAO,IAAA,CAAK,qCAAA,EAAuC,EAAE,SAAA,EAAW,CAAA;AAChE,IAAA,OAAO,IAAA;AAAA,EACT;AAEA,EAAA,MAAM,QAAA,GAAW,kBAAA;AAAA,IACf,OAAA,CAAQ,QAAA;AAAA,IACR;AAAA,GACF;AACA,EAAA,IAAI,CAAC,QAAA,EAAU;AACb,IAAA,MAAA,CAAO,IAAA,CAAK,sCAAA,EAAwC,EAAE,SAAA,EAAW,CAAA;AACjE,IAAA,OAAO,IAAA;AAAA,EACT;AAEA,EAAA,MAAM,UAAA,GAAa,iBAAA,CAAkB,OAAA,EAAS,MAAM,CAAA;AACpD,EAAA,MAAM,MAAA,GAAS,2BAAA,CAA4B,OAAA,EAAS,UAAA,EAAY,MAAM,CAAA;AACtE,EAAA,IAAI,CAAC,MAAA,EAAQ;AACX,IAAA,MAAA,CAAO,IAAA,CAAK,oCAAA,EAAsC,EAAE,SAAA,EAAW,CAAA;AAC/D,IAAA,OAAO,IAAA;AAAA,EACT;AAEA,EAAA,OAAO,gBAAA;AAAA,IACL,SAAA;AAAA,IACA,OAAA,CAAQ,IAAA;AAAA,IACR,QAAA;AAAA,IACA,MAAA;AAAA,IACA,OAAA;AAAA,IACA;AAAA,GACF;AACF,CAAA;;;AC/FA,IAAM,wBAAA,GAA2B,CAC/B,UAAA,EACA,MAAA,KACwC;AACxC,EAAA,MAAM,aAAa,UAAA,CAAW,UAAA;AAG9B,EAAA,IAAI,CAAC,UAAA,EAAY;AACf,IAAA,OAAO,MAAA;AAAA,EACT;AAEA,EAAA,MAAA,CAAO,MAAM,wCAAwC,CAAA;AAGrD,EAAA,OAAO,EAAE,GAAG,UAAA,EAAW;AACzB,CAAA;AAEA,IAAM,YAAA,GAAe,CACnB,QAAA,EACA,MAAA,KACuB;AACvB,EAAA,MAAM,QAA4B,EAAC;AAEnC,EAAA,IAAI,CAAC,UAAU,OAAO,KAAA;AAEtB,EAAA,MAAM,YAAY,KAAA,CAAM,OAAA,CAAQ,QAAQ,CAAA,GAAI,QAAA,GAAW,CAAC,QAAQ,CAAA;AAChE,EAAA,IAAI,SAAA,GAAY,CAAA;AAEhB,EAAA,KAAA,MAAW,WAAW,SAAA,EAAW;AAC/B,IAAA,MAAM,IAAA,GAAO,cAAA;AAAA,MACX,OAAA;AAAA,MACA,SAAA;AAAA,MACA;AAAA,KACF;AACA,IAAA,IAAI,IAAA,EAAM;AACR,MAAA,KAAA,CAAM,KAAK,IAAI,CAAA;AACf,MAAA,SAAA,EAAA;AAAA,IACF;AAAA,EACF;AAEA,EAAA,OAAO,KAAA;AACT,CAAA;AAEO,IAAM,iBAAA,GAAoB,CAC/B,UAAA,EACA,MAAA,KACY;AACZ,EAAA,MAAA,CAAO,MAAM,wBAAwB,CAAA;AAErC,EAAA,MAAM,SAAA,GAAY,WAAW,SAAS,CAAA;AACtC,EAAA,MAAM,WAAA,GAAc,cAAA,CAAe,SAAA,CAAU,SAAS,CAAA;AACtD,EAAA,MAAM,QAAQ,WAAA,CAAY,OAAA,GACtB,gBAAA,CAAiB,WAAA,CAAY,IAAI,CAAA,GACjC,SAAA;AAEJ,EAAA,MAAM,OAAO,UAAA,CAAW,IAAA;AACxB,EAAA,MAAM,KAAA,GAAQ,YAAA,CAAa,UAAA,CAAW,IAAA,EAAM,MAAM,CAAA;AAClD,EAAA,MAAM,UAAA,GAAa,wBAAA,CAAyB,UAAA,EAAY,MAAM,CAAA;AAE9D,EAAA,MAAM,OAAA,GAAmB;AAAA,IACvB,IAAA;AAAA,IACA,KAAA;AAAA,IACA;AAAA,GACF;AAEA,EAAA,IAAI,UAAA,EAAY;AACd,IAAA,OAAO;AAAA,MACL,GAAG,OAAA;AAAA,MACH,UAAA,EAAY;AAAA,QACV,GAAA,EAAK;AAAA;AACP,KACF;AAAA,EACF;AAEA,EAAA,OAAO,OAAA;AACT,CAAA;;;AC1EA,IAAM,oBAAA,GAAuB,CAC3B,sBAAA,EACA,MAAA,KACwC;AACxC,EAAA,MAAM,aAAa,sBAAA,CAAuB,UAAA;AAG1C,EAAA,IAAI,CAAC,UAAA,EAAY;AACf,IAAA,OAAO,MAAA;AAAA,EACT;AAEA,EAAA,MAAA,CAAO,MAAM,uDAAuD,CAAA;AAGpE,EAAA,OAAO,EAAE,GAAG,UAAA,EAAW;AACzB,CAAA;AAEA,IAAM,kBAAA,GAAqB,CACzB,sBAAA,KAC4B;AAC5B,EAAA,MAAM,WAAW,sBAAA,CAAuB,QAAA;AAIxC,EAAA,IAAI,CAAC,QAAA,EAAU;AACb,IAAA,MAAM,sBAAsB,+BAA+B,CAAA;AAAA,EAC7D;AAEA,EAAA,MAAM,YAAA,GAAe,KAAA,CAAM,OAAA,CAAQ,QAAA,CAAS,OAAO,IAC/C,QAAA,CAAS,OAAA,GACT,CAAC,QAAA,CAAS,OAAO,CAAA;AAErB,EAAA,IAAI,YAAA,CAAa,WAAW,CAAA,EAAG;AAC7B,IAAA,MAAM,sBAAsB,mCAAmC,CAAA;AAAA,EACjE;AAEA,EAAA,OAAO,aAAa,CAAC,CAAA;AACvB,CAAA;AAEO,IAAM,eAAA,GAAkB,CAC7B,OAAA,EACA,MAAA,KACQ;AACR,EAAA,MAAA,CAAO,MAAM,uBAAuB,CAAA;AAEpC,EAAA,MAAM,yBAAyB,OAAA,CAAQ,sBAAA;AAKvC,EAAA,MAAM,UAAA,GAAa,mBAAmB,sBAAsB,CAAA;AAC5D,EAAA,MAAM,OAAA,GAAU,iBAAA,CAAkB,UAAA,EAAY,MAAM,CAAA;AACpD,EAAA,MAAM,aAAA,GAAgB,oBAAA,CAAqB,sBAAA,EAAwB,MAAM,CAAA;AACzE,EAAA,MAAM,QAAA,GAAW,qBAAA,CAAsB,sBAAA,EAAwB,OAAO,CAAA;AAEtE,EAAA,MAAM,GAAA,GAAW;AAAA,IACf,OAAA,EAAS,KAAA;AAAA,IACT,IAAA,EAAM,oBAAA;AAAA,IACN,QAAA;AAAA,IACA,UAAA,EAAY;AAAA,MACV,kBAAA,EAAoB,OAAA;AAAA,MACpB,GAAI,aAAA,IAAiB,EAAE,GAAA,EAAK,aAAA;AAAc;AAC5C,GACF;AAEA,EAAA,MAAA,CAAO,MAAM,gCAAgC,CAAA;AAC7C,EAAA,OAAO,GAAA;AACT,CAAA;;;ACvEO,IAAM,iBAAA,GAAoB,CAC/B,sBAAA,EACA,GAAA,KACS;AACT,EAAA,IAAI,GAAA,CAAI,SAAS,OAAA,EAAS;AACxB,IAAA,sBAAA,CAAuB,sBAAsB,CAAA,GAAI,GAAA,CAAI,QAAA,CAAS,OAAA;AAAA,EAChE;AACA,EAAA,IAAI,GAAA,CAAI,SAAS,YAAA,EAAc;AAC7B,IAAA,sBAAA,CAAuB,uBAAuB,CAAA,GAAI,GAAA,CAAI,QAAA,CAAS,YAAA;AAAA,EACjE;AACA,EAAA,IAAI,GAAA,CAAI,SAAS,OAAA,EAAS;AACxB,IAAA,sBAAA,CAAuB,kBAAkB,CAAA,GAAI,GAAA,CAAI,QAAA,CAAS,OAAA;AAAA,EAC5D;AACA,EAAA,IAAI,GAAA,CAAI,SAAS,YAAA,EAAc;AAC7B,IAAA,sBAAA,CAAuB,uBAAuB,CAAA,GAAI,GAAA,CAAI,QAAA,CAAS,YAAA;AAAA,EACjE;AACF,CAAA;;;AChBA,IAAM,mBAAA,GAAsB,CAC1B,WAAA,EACA,QAAA,KACS;AACT,EAAA,IAAI,QAAA,CAAS,IAAA,KAAS,sBAAA,IAA0B,QAAA,CAAS,QAAQ,MAAA,EAAW;AAC1E,IAAA,WAAA,CAAY,+BAA+B,CAAA,GAAI,sBAAA;AAC/C,IAAA,WAAA,CAAY,8BAA8B,IAAI,QAAA,CAAS,GAAA;AAAA,EACzD,WACE,QAAA,CAAS,IAAA,KAAS,iBAAA,IAClB,QAAA,CAAS,UAAU,MAAA,EACnB;AACA,IAAA,WAAA,CAAY,+BAA+B,CAAA,GAAI,iBAAA;AAC/C,IAAA,WAAA,CAAY,gCAAgC,IAAI,QAAA,CAAS,KAAA;AAAA,EAC3D,WACE,QAAA,CAAS,IAAA,KAAS,oBAAA,IAClB,QAAA,CAAS,UAAU,MAAA,EACnB;AACA,IAAA,WAAA,CAAY,+BAA+B,CAAA,GAAI,oBAAA;AAC/C,IAAA,WAAA,CAAY,gCAAgC,IAAI,QAAA,CAAS,KAAA;AAAA,EAC3D,WAAW,QAAA,CAAS,IAAA,KAAS,UAAA,IAAc,QAAA,CAAS,aAAa,MAAA,EAAW;AAC1E,IAAA,WAAA,CAAY,+BAA+B,CAAA,GAAI,UAAA;AAC/C,IAAA,WAAA,CAAY,mCAAmC,IAAI,QAAA,CAAS,QAAA;AAAA,EAC9D;AACF,CAAA;AAEO,IAAM,oBAAA,GAAuB,CAAC,IAAA,KAEN;AAC7B,EAAA,MAAM,WAAW,IAAA,CAAK,QAAA;AAGtB,EAAA,IAAI,QAAA,CAAS,SAAS,MAAA,EAAQ;AAC5B,IAAA,OAAO;AAAA,MACL,YAAA,EAAc,QAAA;AAAA,MACd,SAAS,QAAA,CAAS;AAAA,KACpB;AAAA,EACF;AAEA,EAAA,IAAI,QAAA,CAAS,SAAS,UAAA,EAAY;AAChC,IAAA,OAAO;AAAA,MACL,YAAA,EAAc,YAAA;AAAA,MACd,QAAQ,QAAA,CAAS;AAAA,KACnB;AAAA,EACF;AAIA,EAAA,MAAM,WAAA,GAAuC;AAAA,IAC3C,YAAA,EAAc;AAAA,GAChB;AAEA,EAAA,mBAAA,CAAoB,aAAa,QAAQ,CAAA;AAEzC,EAAA,OAAO,WAAA;AACT,CAAA;;;ACtDO,IAAM,qBAAA,GAAwB,CAAC,KAAA,KAKP;AAC7B,EAAA,IAAI,KAAA,CAAM,SAAS,MAAA,EAAQ;AACzB,IAAA,OAAO;AAAA,MACL,YAAA,EAAc,aAAA;AAAA,MACd,aAAA,EAAe;AAAA,QACb,YAAA,EAAc,2BAAA;AAAA,QACd,QAAQ,KAAA,CAAM;AAAA;AAChB,KACF;AAAA,EACF;AAEA,EAAA,IAAI,KAAA,CAAM,IAAA,KAAS,KAAA,IAAS,KAAA,CAAM,SAAS,OAAA,EAAS;AAClD,IAAA,MAAM,GAAA,GAAM,KAAA,IAAS,KAAA,GAAQ,KAAA,CAAM,MAAM,KAAA,CAAM,KAAA;AAC/C,IAAA,MAAM,GAAA,GAAM,KAAA,IAAS,KAAA,GAAQ,KAAA,CAAM,MAAM,KAAA,CAAM,KAAA;AAC/C,IAAA,OAAO;AAAA,MACL,YAAA,EAAc,aAAA;AAAA,MACd,aAAA,EAAe;AAAA,QACb,YAAA,EAAc,uBAAA;AAAA,QACd,GAAA,EAAK,GAAA;AAAA,QACL,IAAA,EAAM;AAAA;AACR,KACF;AAAA,EACF;AAEA,EAAA,OAAO,EAAE,cAAc,QAAA,EAAS;AAClC,CAAA;AAEO,IAAM,gBAAA,GAAmB,CAAC,KAAA,KAKF;AAC7B,EAAA,IAAI,KAAA,CAAM,IAAA,KAAS,mBAAA,IAAuB,KAAA,CAAM,SAAS,OAAA,EAAS;AAChE,IAAA,MAAM,GAAA,GAAM,KAAA,IAAS,KAAA,GAAQ,KAAA,CAAM,MAAM,KAAA,CAAM,KAAA;AAC/C,IAAA,MAAM,GAAA,GAAM,KAAA,IAAS,KAAA,GAAQ,KAAA,CAAM,MAAM,KAAA,CAAM,KAAA;AAC/C,IAAA,OAAO;AAAA,MACL,YAAA,EAAc,SAAA;AAAA,MACd,SAAA,EAAW;AAAA,QACT,YAAA,EAAc,mBAAA;AAAA,QACd,oBAAA,EAAsB,GAAA;AAAA,QACtB,qBAAA,EAAuB;AAAA;AACzB,KACF;AAAA,EACF;AAEA,EAAA,OAAO,EAAE,cAAc,QAAA,EAAS;AAClC,CAAA;AAEO,IAAM,mBAAA,GAAsB,CAAC,KAAA,KAKL;AAC7B,EAAA,IAAI,KAAA,CAAM,IAAA,KAAS,KAAA,IAAS,KAAA,CAAM,SAAS,OAAA,EAAS;AAClD,IAAA,MAAM,GAAA,GAAM,KAAA,IAAS,KAAA,GAAQ,KAAA,CAAM,MAAM,KAAA,CAAM,KAAA;AAC/C,IAAA,MAAM,GAAA,GAAM,KAAA,IAAS,KAAA,GAAQ,KAAA,CAAM,MAAM,KAAA,CAAM,KAAA;AAC/C,IAAA,OAAO;AAAA,MACL,YAAA,EAAc,WAAA;AAAA,MACd,WAAA,EAAa;AAAA,QACX,YAAA,EAAc,qBAAA;AAAA,QACd,GAAA,EAAK,GAAA;AAAA,QACL,IAAA,EAAM;AAAA;AACR,KACF;AAAA,EACF;AAEA,EAAA,OAAO,EAAE,cAAc,QAAA,EAAS;AAClC,CAAA;AAEO,IAAM,kBAAA,GAAqB,CAChC,IAAA,KAC4B;AAC5B,EAAA,IAAI,IAAA,CAAK,MAAA,CAAO,IAAA,KAAS,MAAA,EAAQ;AAC/B,IAAA,OAAO,EAAE,cAAc,QAAA,EAAS;AAAA,EAClC;AAEA,EAAA,IAAI,IAAA,CAAK,MAAA,CAAO,IAAA,KAAS,YAAA,EAAc;AACrC,IAAA,MAAM,KAAA,GAAQ,KAAK,MAAA,CAAO,KAAA;AAC1B,IAAA,IAAI,KAAA,IAAS,UAAU,KAAA,EAAO;AAC5B,MAAA,OAAO,sBAAsB,KAAK,CAAA;AAAA,IACpC;AAAA,EACF;AAEA,EAAA,IAAI,IAAA,CAAK,MAAA,CAAO,IAAA,KAAS,MAAA,EAAQ;AAC/B,IAAA,MAAM,KAAA,GAAQ,KAAK,MAAA,CAAO,KAAA;AAC1B,IAAA,IAAI,KAAA,IAAS,UAAU,KAAA,EAAO;AAC5B,MAAA,OAAO,iBAAiB,KAAK,CAAA;AAAA,IAC/B;AAAA,EACF;AAEA,EAAA,IAAI,IAAA,CAAK,MAAA,CAAO,IAAA,KAAS,SAAA,EAAW;AAClC,IAAA,MAAM,KAAA,GAAQ,KAAK,MAAA,CAAO,KAAA;AAC1B,IAAA,IAAI,KAAA,IAAS,UAAU,KAAA,EAAO;AAC5B,MAAA,OAAO,oBAAoB,KAAK,CAAA;AAAA,IAClC;AAAA,EACF;AAEA,EAAA,OAAO,EAAE,cAAc,QAAA,EAAS;AAClC,CAAA;;;ACtGA,IAAM,eAAA,GAAkB,CAAC,GAAA,KAAwB;AAC/C,EAAA,OAAO,GAAA,CAAI,OAAO,CAAC,CAAA,CAAE,aAAY,GAAI,GAAA,CAAI,MAAM,CAAC,CAAA;AAClD,CAAA;AAEA,IAAM,kBAAA,GAAqB,CACzB,IAAA,EACA,OAAA,EACA,MAAA,KACS;AACT,EAAA,IAAI,IAAA,CAAK,MAAA,CAAO,IAAA,KAAS,OAAA,EAAS;AAChC,IAAA,MAAA,CAAO,MAAM,yCAAA,EAA2C;AAAA,MACtD,WAAW,IAAA,CAAK;AAAA,KACjB,CAAA;AACD,IAAA,MAAM,UAAA,GAAa,KAAK,MAAA,CAAO,KAAA;AAC/B,IAAA,IAAI,UAAA,IAAc,MAAA,IAAU,UAAA,IAAc,UAAA,CAAW,SAAS,OAAA,EAAS;AACrE,MAAA,OAAA,CAAQ,UAAA,GAAa;AAAA,QACnB,GAAA,EAAK;AAAA,UACH,SAAA,EAAW,uDAAA;AAAA,UACX,OAAO,UAAA,CAAW;AAAA;AACpB,OACF;AAAA,IACF;AAAA,EACF;AACF,CAAA;AAEO,IAAM,gBAAA,GAAmB,CAC9B,IAAA,EACA,KAAA,EACA,MAAA,KAC4B;AAC5B,EAAA,MAAA,CAAO,MAAM,wBAAA,EAA0B,EAAE,SAAA,EAAW,IAAA,CAAK,WAAW,CAAA;AAEpE,EAAA,MAAM,OAAA,GAAmC;AAAA,IACvC,YAAA,EAAc,QAAA;AAAA,IACd,QAAQ,KAAA,GAAQ;AAAA,GAClB;AAEA,EAAA,IAAI,KAAK,IAAA,EAAM;AACb,IAAA,OAAA,CAAQ,OAAO,IAAA,CAAK,IAAA;AAAA,EACtB;AAEA,EAAA,OAAA,CAAQ,QAAA,GAAW,qBAAqB,IAAI,CAAA;AAE5C,EAAA,IAAI,KAAK,SAAA,EAAW;AAClB,IAAA,OAAA,CAAQ,SAAA,GAAY,eAAA,CAAgB,IAAA,CAAK,SAAS,CAAA;AAAA,EACpD;AAEA,EAAA,OAAA,CAAQ,MAAA,GAAS,mBAAmB,IAAI,CAAA;AAExC,EAAA,IAAI,IAAA,CAAK,YAAY,GAAA,EAAK;AACxB,IAAA,MAAA,CAAO,MAAM,qCAAA,EAAuC;AAAA,MAClD,WAAW,IAAA,CAAK;AAAA,KACjB,CAAA;AACD,IAAA,OAAA,CAAQ,UAAA,GAAa,KAAK,UAAA,CAAW,GAAA;AAAA,EACvC,CAAA,MAAO;AACL,IAAA,kBAAA,CAAmB,IAAA,EAAM,SAAS,MAAM,CAAA;AAAA,EAC1C;AAEA,EAAA,OAAO,OAAA;AACT,CAAA;;;ACxDA,IAAM,eAAA,GAAkB,CACtB,OAAA,EACA,MAAA,KAC4B;AAC5B,EAAA,MAAM,QAAA,GACJ,gBAAA,CAAiB,OAAA,CAAQ,KAAsC,CAAA,IAAK,OAAA;AAEtE,EAAA,MAAM,QAAA,GAAW,QAAQ,KAAA,CAAM,GAAA;AAAA,IAAI,CAAC,IAAA,EAAM,KAAA,KACxC,gBAAA,CAAiB,IAAA,EAAqB,OAAO,MAAM;AAAA,GACrD;AAEA,EAAA,MAAM,UAAA,GAAsC;AAAA,IAC1C,SAAA,EAAW,QAAA;AAAA,IACX,MAAM,OAAA,CAAQ,IAAA;AAAA,IACd,IAAA,EAAM;AAAA,GACR;AAEA,EAAA,IAAI,OAAA,CAAQ,YAAY,GAAA,EAAK;AAC3B,IAAA,MAAA,CAAO,MAAM,wCAAwC,CAAA;AACrD,IAAA,UAAA,CAAW,UAAA,GAAa,QAAQ,UAAA,CAAW,GAAA;AAAA,EAC7C;AAEA,EAAA,OAAO,UAAA;AACT,CAAA;AAEA,IAAM,2BAAA,GAA8B,CAClC,UAAA,EACA,GAAA,EACA,MAAA,KAC4B;AAC5B,EAAA,MAAM,sBAAA,GAAkD;AAAA,IACtD,SAAA,EAAW,4DAAA;AAAA,IACX,aAAA,EAAe,2CAAA;AAAA,IACf,gBAAA,EAAkB,sCAAA;AAAA,IAClB,QAAA,EAAU;AAAA,MACR,OAAA,EAAS;AAAA;AACX,GACF;AAEA,EAAA,iBAAA,CAAkB,wBAAwB,GAAG,CAAA;AAE7C,EAAA,IAAI,GAAA,CAAI,YAAY,GAAA,EAAK;AACvB,IAAA,MAAA,CAAO,MAAM,uDAAuD,CAAA;AACpE,IAAA,sBAAA,CAAuB,UAAA,GAAa,IAAI,UAAA,CAAW,GAAA;AAAA,EACrD;AAEA,EAAA,OAAO,sBAAA;AACT,CAAA;AAEO,IAAM,eAAA,GAAkB,CAAC,GAAA,EAAU,MAAA,KAA4B;AACpE,EAAA,MAAA,CAAO,MAAM,iCAAiC,CAAA;AAE9C,EAAA,IAAI,CAAC,GAAA,CAAI,UAAA,EAAY,kBAAA,EAAoB;AACvC,IAAA,MAAMA,qBAAAA;AAAA,MACJ;AAAA,KACF;AAAA,EACF;AAEA,EAAA,MAAM,OAAA,GAAU,IAAI,UAAA,CAAW,kBAAA;AAC/B,EAAA,MAAM,UAAA,GAAa,eAAA,CAAgB,OAAA,EAAS,MAAM,CAAA;AAClD,EAAA,MAAM,sBAAA,GAAyB,2BAAA;AAAA,IAC7B,UAAA;AAAA,IACA,GAAA;AAAA,IACA;AAAA,GACF;AAEA,EAAA,OAAO;AAAA,IACL,sBAAA,EAAwB;AAAA,GAC1B;AACF,CAAA;;;ACnEO,IAAM,sBAAA,GACX,CAAC,MAAA,KACD,OAAO,SAAA,KAAoC;AACzC,EAAA,MAAA,CAAO,MAAM,kBAAA,EAAoB,EAAE,SAAA,EAAW,SAAA,CAAU,QAAQ,CAAA;AAEhE,EAAA,IAAI,OAAA;AACJ,EAAA,IAAI;AACF,IAAA,MAAM,MAAA,GAAS,IAAI,SAAA,CAAU;AAAA,MAC3B,gBAAA,EAAkB,KAAA;AAAA,MAClB,mBAAA,EAAqB,IAAA;AAAA,MACrB,mBAAA,EAAqB;AAAA,KACtB,CAAA;AAED,IAAA,OAAA,GAAU,MAAA,CAAO,MAAM,SAAS,CAAA;AAAA,EAClC,SAAS,KAAA,EAAO;AACd,IAAA,MAAA,CAAO,KAAA,CAAM,yBAAA,EAA2B,EAAE,KAAA,EAAO,CAAA;AACjD,IAAA,MAAMA,qBAAAA,CAAsB,4BAA4B,KAAK,CAAA;AAAA,EAC/D;AAEA,EAAA,IACE,CAAC,OAAA,IACD,OAAO,YAAY,QAAA,IACnB,EAAE,4BAA4B,OAAA,CAAA,EAC9B;AACA,IAAA,MAAM,KAAA,GAAQA,qBAAAA;AAAA,MACZ;AAAA,KACF;AACA,IAAA,MAAA,CAAO,KAAA,CAAM,uBAAA,EAAyB,EAAE,KAAA,EAAO,CAAA;AAC/C,IAAA,MAAM,KAAA;AAAA,EACR;AAEA,EAAA,MAAA,CAAO,KAAK,8BAA8B,CAAA;AAE1C,EAAA,OAAO,eAAA,CAAgB,SAAS,MAAM,CAAA;AACxC;AAEK,IAAM,sBAAA,GACX,CAAC,MAAA,EAAgB,SAAA,KACjB,OAAO,GAAA,KAA8B;AACnC,EAAA,MAAA,CAAO,MAAM,qBAAqB,CAAA;AAElC,EAAA,IAAI,OAAA;AACJ,EAAA,IAAI;AACF,IAAA,OAAA,GAAUC,gBAAAA,CAAgB,KAAK,MAAM,CAAA;AAAA,EACvC,SAAS,KAAA,EAAO;AACd,IAAA,MAAA,CAAO,KAAA,CAAM,wCAAA,EAA0C,EAAE,KAAA,EAAO,CAAA;AAChE,IAAA,MAAMD,qBAAAA,CAAsB,gCAAgC,KAAK,CAAA;AAAA,EACnE;AAEA,EAAA,IAAI,SAAA;AACJ,EAAA,IAAI;AACF,IAAA,MAAM,OAAA,GAAU,IAAI,UAAA,CAAW;AAAA,MAC7B,gBAAA,EAAkB,KAAA;AAAA,MAClB,mBAAA,EAAqB,IAAA;AAAA,MACrB,MAAA,EAAQ,IAAA;AAAA,MACR,QAAA,EAAU;AAAA,KACX,CAAA;AAED,IAAA,SAAA,GAAY,OAAA,CAAQ,MAAM,OAAO,CAAA;AAAA,EACnC,SAAS,KAAA,EAAO;AACd,IAAA,MAAA,CAAO,KAAA,CAAM,yBAAA,EAA2B,EAAE,KAAA,EAAO,CAAA;AACjD,IAAA,MAAMA,qBAAAA,CAAsB,2BAA2B,KAAK,CAAA;AAAA,EAC9D;AAEA,EAAA,MAAA,CAAO,MAAM,sCAAsC,CAAA;AAEnD,EAAA,MAAM,gBAAA,GAAmB,MAAM,SAAA,CAAU,SAAS,CAAA;AAClD,EAAA,IAAI,CAAC,iBAAiB,KAAA,EAAO;AAC3B,IAAA,MAAA,CAAO,MAAM,8CAAA,EAAgD;AAAA,MAC3D,QAAQ,gBAAA,CAAiB;AAAA,KAC1B,CAAA;AACD,IAAA,MAAM,wBAAA;AAAA,MACJ,mDAAA;AAAA,MACA,gBAAA,CAAiB,MAAA,CAAO,GAAA,CAAI,CAAC,GAAA,MAAS;AAAA,QACpC,OAAO,GAAA,CAAI,IAAA;AAAA,QACX,SAAS,GAAA,CAAI;AAAA,OACf,CAAE;AAAA,KACJ;AAAA,EACF;AAEA,EAAA,MAAA,CAAO,KAAK,iCAAiC,CAAA;AAC7C,EAAA,OAAO,SAAA;AACT;AAEF,IAAMC,gBAAAA,GAAkB,CAAC,GAAA,EAAU,MAAA,KAA4B;AAC7D,EAAA,OAAO,eAAA,CAAyB,KAAK,MAAM,CAAA;AAC7C,CAAA;AC5FO,IAAM,qBAAA,GACX,CAAC,MAAA,KACD,OAAO,SAAA,KAAoD;AACzD,EAAA,IAAI;AACF,IAAA,MAAA,CAAO,MAAM,8BAA8B,CAAA;AAG3C,IAAA,MAAM,aAAA,GAAgB,YAAA,CAAa,QAAA,CAAS,SAAA,EAAW;AAAA,MACrD,sBAAA,EAAwB;AAAA,KACzB,CAAA;AAED,IAAA,IAAI,kBAAkB,IAAA,EAAM;AAC1B,MAAA,MAAA,CAAO,KAAK,2BAAA,EAA6B;AAAA,QACvC,OAAO,aAAA,CAAc;AAAA,OACtB,CAAA;AAED,MAAA,OAAO;AAAA,QACL,KAAA,EAAO,KAAA;AAAA,QACP,MAAA,EAAQ;AAAA,UACN;AAAA,YACE,IAAA,EAAM,CAAA,KAAA,EAAQ,aAAA,CAAc,GAAA,CAAI,IAAI,CAAA,CAAA;AAAA,YACpC,OAAA,EAAS,CAAA,uBAAA,EAA0B,aAAA,CAAc,GAAA,CAAI,GAAG,CAAA;AAAA;AAC1D;AACF,OACF;AAAA,IACF;AAEA,IAAA,MAAA,CAAO,KAAK,4BAA4B,CAAA;AACxC,IAAA,OAAO,EAAE,KAAA,EAAO,IAAA,EAAM,MAAA,EAAQ,EAAC,EAAE;AAAA,EACnC,SAAS,KAAA,EAAO;AACd,IAAA,MAAA,CAAO,KAAA,CAAM,uBAAA,EAAyB,EAAE,KAAA,EAAO,CAAA;AAE/C,IAAA,OAAO;AAAA,MACL,KAAA,EAAO,KAAA;AAAA,MACP,MAAA,EAAQ;AAAA,QACN;AAAA,UACE,IAAA,EAAM,MAAA;AAAA,UACN,OAAA,EAAS,KAAA,YAAiB,KAAA,GAAQ,KAAA,CAAM,OAAA,GAAU;AAAA;AACpD;AACF,KACF;AAAA,EACF;AACF;;;AChCK,IAAM,kBAAA,GAAqB,CAAC,MAAA,KAAkC;AACnE,EAAA,MAAM,GAAA,GAAM,UAAU,mBAAA,EAAoB;AAC1C,EAAA,MAAM,YAAA,GAAe,sBAAsB,GAAG,CAAA;AAE9C,EAAA,OAAO;AAAA,IACL,SAAA,EAAW,uBAAuB,GAAG,CAAA;AAAA,IACrC,SAAA,EAAW,sBAAA,CAAuB,GAAA,EAAK,YAAY,CAAA;AAAA,IACnD;AAAA,GACF;AACF","file":"index.js","sourcesContent":["import type { KRD } from \"@kaiord/core\";\nimport type { Workout } from \"@kaiord/core\";\n\nexport const extractKaiordMetadata = (\n trainingCenterDatabase: Record<string, unknown>,\n workout: Workout\n): KRD[\"metadata\"] => {\n const metadata: KRD[\"metadata\"] = {\n created:\n (trainingCenterDatabase[\"@_kaiord:timeCreated\"] as string | undefined) ||\n new Date().toISOString(),\n sport: workout.sport,\n subSport: workout.subSport,\n };\n\n if (trainingCenterDatabase[\"@_kaiord:manufacturer\"]) {\n metadata.manufacturer = trainingCenterDatabase[\n \"@_kaiord:manufacturer\"\n ] as string;\n }\n if (trainingCenterDatabase[\"@_kaiord:product\"]) {\n metadata.product = trainingCenterDatabase[\"@_kaiord:product\"] as string;\n }\n if (trainingCenterDatabase[\"@_kaiord:serialNumber\"]) {\n const serialNumber = trainingCenterDatabase[\"@_kaiord:serialNumber\"];\n // Convert to string if it's a number (XML parser may parse as number)\n metadata.serialNumber =\n typeof serialNumber === \"number\"\n ? String(serialNumber)\n : (serialNumber as string);\n }\n\n return metadata;\n};\n","import { z } from \"zod\";\nimport type { Sport } from \"@kaiord/core\";\n\nexport const tcxSportSchema = z.enum([\"Running\", \"Biking\", \"Other\"]);\n\nexport type TcxSport = z.infer<typeof tcxSportSchema>;\n\nexport const TCX_TO_KRD_SPORT: Record<TcxSport, Sport> = {\n Running: \"running\",\n Biking: \"cycling\",\n Other: \"generic\",\n};\n\nexport const KRD_TO_TCX_SPORT: Record<Sport, TcxSport> = {\n running: \"Running\",\n cycling: \"Biking\",\n swimming: \"Other\",\n generic: \"Other\",\n};\n","import type { Duration } from \"@kaiord/core\";\nimport type { Logger } from \"@kaiord/core\";\n\nconst restoreHeartRateLessThan = (\n tcxDuration: Record<string, unknown>,\n logger: Logger\n): Duration | null => {\n const bpm = tcxDuration[\"@_kaiord:originalDurationBpm\"] as number | undefined;\n if (typeof bpm === \"number\") {\n logger.debug(\"Restoring heart_rate_less_than from kaiord attributes\", {\n bpm,\n });\n return { type: \"heart_rate_less_than\", bpm };\n }\n return null;\n};\n\nconst restorePowerLessThan = (\n tcxDuration: Record<string, unknown>,\n logger: Logger\n): Duration | null => {\n const watts = tcxDuration[\"@_kaiord:originalDurationWatts\"] as\n | number\n | undefined;\n if (typeof watts === \"number\") {\n logger.debug(\"Restoring power_less_than from kaiord attributes\", {\n watts,\n });\n return { type: \"power_less_than\", watts };\n }\n return null;\n};\n\nconst restorePowerGreaterThan = (\n tcxDuration: Record<string, unknown>,\n logger: Logger\n): Duration | null => {\n const watts = tcxDuration[\"@_kaiord:originalDurationWatts\"] as\n | number\n | undefined;\n if (typeof watts === \"number\") {\n logger.debug(\"Restoring power_greater_than from kaiord attributes\", {\n watts,\n });\n return { type: \"power_greater_than\", watts };\n }\n return null;\n};\n\nconst restoreCalories = (\n tcxDuration: Record<string, unknown>,\n logger: Logger\n): Duration | null => {\n const calories = tcxDuration[\"@_kaiord:originalDurationCalories\"] as\n | number\n | undefined;\n if (typeof calories === \"number\") {\n logger.debug(\"Restoring calories from kaiord attributes\", {\n calories,\n });\n return { type: \"calories\", calories };\n }\n return null;\n};\n\nexport const restoreKaiordDuration = (\n tcxDuration: Record<string, unknown>,\n logger: Logger\n): Duration | null => {\n const originalDurationType = tcxDuration[\"@_kaiord:originalDurationType\"] as\n | string\n | undefined;\n\n if (originalDurationType === \"heart_rate_less_than\") {\n return restoreHeartRateLessThan(tcxDuration, logger);\n }\n\n if (originalDurationType === \"power_less_than\") {\n return restorePowerLessThan(tcxDuration, logger);\n }\n\n if (originalDurationType === \"power_greater_than\") {\n return restorePowerGreaterThan(tcxDuration, logger);\n }\n\n if (originalDurationType === \"calories\") {\n return restoreCalories(tcxDuration, logger);\n }\n\n return null;\n};\n","import type { Duration } from \"@kaiord/core\";\n\nexport const convertStandardTcxDuration = (\n tcxDuration: Record<string, unknown>\n): Duration | null => {\n const durationType = tcxDuration[\"@_xsi:type\"] as string | undefined;\n\n if (durationType === \"Time_t\") {\n const seconds = tcxDuration.Seconds as number | undefined;\n if (typeof seconds === \"number\" && seconds > 0) {\n return { type: \"time\", seconds };\n }\n }\n\n if (durationType === \"Distance_t\") {\n const meters = tcxDuration.Meters as number | undefined;\n if (typeof meters === \"number\" && meters > 0) {\n return { type: \"distance\", meters };\n }\n }\n\n if (durationType === \"LapButton_t\") {\n return { type: \"open\" };\n }\n\n return null;\n};\n","import { durationTypeSchema, type Duration } from \"@kaiord/core\";\nimport type { Logger } from \"@kaiord/core\";\nimport { tcxDurationTypeSchema } from \"../schemas/tcx-duration\";\nimport { restoreKaiordDuration } from \"./duration-kaiord-restorer\";\nimport { convertStandardTcxDuration } from \"./duration-standard-converter\";\n\nexport type TcxDurationData = {\n durationType?: string;\n seconds?: number;\n meters?: number;\n};\n\nexport const mapTcxDuration = (data: TcxDurationData): Duration => {\n const result = tcxDurationTypeSchema.safeParse(data.durationType);\n\n if (!result.success) {\n return { type: durationTypeSchema.enum.open };\n }\n\n // Time duration\n if (\n result.data === tcxDurationTypeSchema.enum.Time &&\n data.seconds !== undefined\n ) {\n return {\n type: durationTypeSchema.enum.time,\n seconds: data.seconds,\n };\n }\n\n // Distance duration\n if (\n result.data === tcxDurationTypeSchema.enum.Distance &&\n data.meters !== undefined\n ) {\n return {\n type: durationTypeSchema.enum.distance,\n meters: data.meters,\n };\n }\n\n // LapButton duration\n if (result.data === tcxDurationTypeSchema.enum.LapButton) {\n return {\n type: durationTypeSchema.enum.open,\n };\n }\n\n // Default to open for unsupported types (HeartRateAbove, HeartRateBelow, CaloriesBurned)\n // These will be handled by the converter and stored in extensions\n return { type: durationTypeSchema.enum.open };\n};\n\nexport const convertTcxDuration = (\n tcxDuration: Record<string, unknown> | undefined,\n logger: Logger\n): Duration | null => {\n if (!tcxDuration) {\n return null;\n }\n\n // First check for kaiord attributes to restore advanced duration types\n const kaiordDuration = restoreKaiordDuration(tcxDuration, logger);\n if (kaiordDuration) {\n return kaiordDuration;\n }\n\n // Then check for standard TCX duration types\n const standardDuration = convertStandardTcxDuration(tcxDuration);\n if (standardDuration) {\n return standardDuration;\n }\n\n const durationType = tcxDuration[\"@_xsi:type\"] as string | undefined;\n logger.warn(\"Unsupported duration type\", { durationType });\n return null;\n};\n\n// KRD → TCX duration mappers\n\nexport type TcxDurationElement = Record<string, unknown>;\n\nexport const mapTimeDurationToTcx = (seconds: number): TcxDurationElement => {\n return {\n \"@_xsi:type\": \"Time_t\",\n Seconds: seconds,\n };\n};\n\nexport const mapDistanceDurationToTcx = (\n meters: number\n): TcxDurationElement => {\n return {\n \"@_xsi:type\": \"Distance_t\",\n Meters: meters,\n };\n};\n\nexport const mapOpenDurationToTcx = (): TcxDurationElement => {\n return {\n \"@_xsi:type\": \"LapButton_t\",\n };\n};\n","import type { Target } from \"@kaiord/core\";\nimport { targetTypeSchema, type TargetType } from \"@kaiord/core\";\nimport type { Logger } from \"@kaiord/core\";\nimport { tcxTargetTypeSchema } from \"../schemas/tcx-target\";\n\nexport const mapTargetType = (\n tcxTargetType: string | undefined\n): TargetType => {\n if (tcxTargetType === tcxTargetTypeSchema.enum.HeartRate)\n return targetTypeSchema.enum.heart_rate;\n if (tcxTargetType === tcxTargetTypeSchema.enum.Speed)\n return targetTypeSchema.enum.pace;\n if (tcxTargetType === tcxTargetTypeSchema.enum.Cadence)\n return targetTypeSchema.enum.cadence;\n if (tcxTargetType === tcxTargetTypeSchema.enum.None)\n return targetTypeSchema.enum.open;\n return targetTypeSchema.enum.open;\n};\n\nconst convertHeartRateTarget = (\n heartRateZone: Record<string, unknown> | undefined\n): Target | null => {\n if (!heartRateZone) return null;\n\n const zoneType = heartRateZone[\"@_xsi:type\"] as string | undefined;\n\n if (zoneType === \"PredefinedHeartRateZone_t\") {\n const zoneNumber = heartRateZone.Number as number | undefined;\n if (typeof zoneNumber === \"number\") {\n return {\n type: \"heart_rate\",\n value: { unit: \"zone\", value: zoneNumber },\n };\n }\n }\n\n if (zoneType === \"CustomHeartRateZone_t\") {\n const low = heartRateZone.Low as number | undefined;\n const high = heartRateZone.High as number | undefined;\n if (typeof low === \"number\" && typeof high === \"number\") {\n return {\n type: \"heart_rate\",\n value: { unit: \"range\", min: low, max: high },\n };\n }\n }\n\n return null;\n};\n\nexport const convertTcxTarget = (\n tcxTarget: Record<string, unknown> | undefined,\n logger: Logger\n): Target | null => {\n if (!tcxTarget) {\n return { type: \"open\" };\n }\n\n const targetType = tcxTarget[\"@_xsi:type\"] as string | undefined;\n\n if (targetType === \"None_t\") {\n return { type: \"open\" };\n }\n\n if (targetType === \"HeartRate_t\") {\n const heartRateZone = tcxTarget.HeartRateZone as\n | Record<string, unknown>\n | undefined;\n const result = convertHeartRateTarget(heartRateZone);\n if (result) return result;\n }\n\n logger.warn(\"Unsupported target type\", { targetType });\n return { type: \"open\" };\n};\n","import type { Logger } from \"@kaiord/core\";\n\nexport const extractIntensity = (\n tcxStep: Record<string, unknown>\n): \"warmup\" | \"active\" | \"cooldown\" | \"rest\" | undefined => {\n const raw = tcxStep.Intensity as string | undefined;\n const value = raw?.toLowerCase();\n\n switch (value) {\n case \"warmup\":\n case \"active\":\n case \"cooldown\":\n case \"rest\":\n return value;\n case \"resting\":\n return \"rest\";\n default:\n return undefined;\n }\n};\n\nexport const extractPowerFromExtensions = (\n extensions: Record<string, unknown>,\n logger: Logger\n): number | undefined => {\n if (extensions.TPX) {\n const tpx = extensions.TPX as Record<string, unknown>;\n if (typeof tpx.Watts === \"number\") {\n logger.debug(\"Found power data in TCX extensions\", {\n watts: tpx.Watts,\n });\n return tpx.Watts;\n }\n }\n\n if (extensions.Power && typeof extensions.Power === \"number\") {\n logger.debug(\"Found power data in TCX extensions\", {\n watts: extensions.Power,\n });\n return extensions.Power;\n }\n\n return undefined;\n};\n\nexport const extractExtensions = (\n tcxStep: Record<string, unknown>,\n logger: Logger\n): Record<string, unknown> | undefined => {\n const extensions = tcxStep.Extensions as Record<string, unknown> | undefined;\n if (!extensions) {\n return undefined;\n }\n\n logger.debug(\"Extracting TCX extensions from step\");\n return { ...extensions };\n};\n","import type { Duration } from \"@kaiord/core\";\nimport type { Target } from \"@kaiord/core\";\nimport type { WorkoutStep } from \"@kaiord/core\";\nimport type { Logger } from \"@kaiord/core\";\nimport { convertTcxDuration } from \"../duration/duration.mapper\";\nimport { convertTcxTarget } from \"../target/target.mapper\";\nimport {\n extractExtensions,\n extractIntensity,\n extractPowerFromExtensions,\n} from \"./step-helpers\";\n\nconst convertTargetWithExtensions = (\n tcxStep: Record<string, unknown>,\n extensions: Record<string, unknown> | undefined,\n logger: Logger\n): Target | null => {\n const target = convertTcxTarget(\n tcxStep.Target as Record<string, unknown> | undefined,\n logger\n );\n if (!target) return null;\n\n if (target.type === \"open\" && extensions) {\n const powerWatts = extractPowerFromExtensions(extensions, logger);\n if (powerWatts !== undefined) {\n logger.debug(\"Converting open target to power target from extensions\", {\n watts: powerWatts,\n });\n return {\n type: \"power\",\n value: {\n unit: \"watts\",\n value: powerWatts,\n },\n };\n }\n }\n\n return target;\n};\n\nconst buildWorkoutStep = (\n stepIndex: number,\n name: string | undefined,\n duration: Duration,\n target: Target,\n tcxStep: Record<string, unknown>,\n extensions: Record<string, unknown> | undefined\n): WorkoutStep => {\n const step: WorkoutStep = {\n stepIndex,\n name,\n durationType: duration.type,\n duration,\n targetType: target.type,\n target,\n intensity: extractIntensity(tcxStep),\n };\n\n return extensions ? { ...step, extensions: { tcx: extensions } } : step;\n};\n\nexport const convertTcxStep = (\n tcxStep: Record<string, unknown>,\n stepIndex: number,\n logger: Logger\n): WorkoutStep | null => {\n logger.debug(\"Converting TCX step\", { stepIndex });\n\n const stepType = tcxStep[\"@_xsi:type\"] as string | undefined;\n if (stepType === \"Repeat_t\") {\n logger.warn(\"Repetition blocks not yet supported\", { stepIndex });\n return null;\n }\n\n const duration = convertTcxDuration(\n tcxStep.Duration as Record<string, unknown> | undefined,\n logger\n );\n if (!duration) {\n logger.warn(\"Step has no valid duration, skipping\", { stepIndex });\n return null;\n }\n\n const extensions = extractExtensions(tcxStep, logger);\n const target = convertTargetWithExtensions(tcxStep, extensions, logger);\n if (!target) {\n logger.warn(\"Step has no valid target, skipping\", { stepIndex });\n return null;\n }\n\n return buildWorkoutStep(\n stepIndex,\n tcxStep.Name as string | undefined,\n duration,\n target,\n tcxStep,\n extensions\n );\n};\n","import type { Workout, WorkoutStep } from \"@kaiord/core\";\nimport type { Logger } from \"@kaiord/core\";\nimport { TCX_TO_KRD_SPORT, tcxSportSchema } from \"../schemas/tcx-sport\";\nimport { convertTcxStep } from \"./step.converter\";\n\nconst extractWorkoutExtensions = (\n tcxWorkout: Record<string, unknown>,\n logger: Logger\n): Record<string, unknown> | undefined => {\n const extensions = tcxWorkout.Extensions as\n | Record<string, unknown>\n | undefined;\n if (!extensions) {\n return undefined;\n }\n\n logger.debug(\"Extracting TCX extensions from workout\");\n\n // Store the raw TCX extensions for round-trip preservation\n return { ...extensions };\n};\n\nconst convertSteps = (\n tcxSteps: unknown,\n logger: Logger\n): Array<WorkoutStep> => {\n const steps: Array<WorkoutStep> = [];\n\n if (!tcxSteps) return steps;\n\n const stepArray = Array.isArray(tcxSteps) ? tcxSteps : [tcxSteps];\n let stepIndex = 0;\n\n for (const tcxStep of stepArray) {\n const step = convertTcxStep(\n tcxStep as Record<string, unknown>,\n stepIndex,\n logger\n );\n if (step) {\n steps.push(step);\n stepIndex++;\n }\n }\n\n return steps;\n};\n\nexport const convertTcxWorkout = (\n tcxWorkout: Record<string, unknown>,\n logger: Logger\n): Workout => {\n logger.debug(\"Converting TCX workout\");\n\n const sportAttr = tcxWorkout[\"@_Sport\"] as string | undefined;\n const sportResult = tcxSportSchema.safeParse(sportAttr);\n const sport = sportResult.success\n ? TCX_TO_KRD_SPORT[sportResult.data]\n : \"generic\";\n\n const name = tcxWorkout.Name as string | undefined;\n const steps = convertSteps(tcxWorkout.Step, logger);\n const extensions = extractWorkoutExtensions(tcxWorkout, logger);\n\n const workout: Workout = {\n name,\n sport,\n steps,\n };\n\n if (extensions) {\n return {\n ...workout,\n extensions: {\n tcx: extensions,\n },\n };\n }\n\n return workout;\n};\n","import type { KRD } from \"@kaiord/core\";\nimport { createTcxParsingError } from \"@kaiord/core\";\nimport type { Logger } from \"@kaiord/core\";\nimport { extractKaiordMetadata } from \"./metadata-extractor\";\nimport { convertTcxWorkout } from \"./workout.converter\";\n\nconst extractTcxExtensions = (\n trainingCenterDatabase: Record<string, unknown>,\n logger: Logger\n): Record<string, unknown> | undefined => {\n const extensions = trainingCenterDatabase.Extensions as\n | Record<string, unknown>\n | undefined;\n if (!extensions) {\n return undefined;\n }\n\n logger.debug(\"Extracting TCX extensions from TrainingCenterDatabase\");\n\n // Store the raw TCX extensions for round-trip preservation\n return { ...extensions };\n};\n\nconst extractWorkoutData = (\n trainingCenterDatabase: Record<string, unknown>\n): Record<string, unknown> => {\n const workouts = trainingCenterDatabase.Workouts as\n | Record<string, unknown>\n | undefined;\n\n if (!workouts) {\n throw createTcxParsingError(\"No workouts found in TCX file\");\n }\n\n const workoutArray = Array.isArray(workouts.Workout)\n ? workouts.Workout\n : [workouts.Workout];\n\n if (workoutArray.length === 0) {\n throw createTcxParsingError(\"No workout data found in TCX file\");\n }\n\n return workoutArray[0] as Record<string, unknown>;\n};\n\nexport const convertTcxToKRD = (\n tcxData: Record<string, unknown>,\n logger: Logger\n): KRD => {\n logger.debug(\"Converting TCX to KRD\");\n\n const trainingCenterDatabase = tcxData.TrainingCenterDatabase as Record<\n string,\n unknown\n >;\n\n const tcxWorkout = extractWorkoutData(trainingCenterDatabase);\n const workout = convertTcxWorkout(tcxWorkout, logger);\n const tcxExtensions = extractTcxExtensions(trainingCenterDatabase, logger);\n const metadata = extractKaiordMetadata(trainingCenterDatabase, workout);\n\n const krd: KRD = {\n version: \"1.0\",\n type: \"structured_workout\",\n metadata,\n extensions: {\n structured_workout: workout,\n ...(tcxExtensions && { tcx: tcxExtensions }),\n },\n };\n\n logger.debug(\"TCX to KRD conversion complete\");\n return krd;\n};\n","import type { KRD } from \"@kaiord/core\";\n\nexport const addKaiordMetadata = (\n trainingCenterDatabase: Record<string, unknown>,\n krd: KRD\n): void => {\n if (krd.metadata.created) {\n trainingCenterDatabase[\"@_kaiord:timeCreated\"] = krd.metadata.created;\n }\n if (krd.metadata.manufacturer) {\n trainingCenterDatabase[\"@_kaiord:manufacturer\"] = krd.metadata.manufacturer;\n }\n if (krd.metadata.product) {\n trainingCenterDatabase[\"@_kaiord:product\"] = krd.metadata.product;\n }\n if (krd.metadata.serialNumber) {\n trainingCenterDatabase[\"@_kaiord:serialNumber\"] = krd.metadata.serialNumber;\n }\n};\n","import type { Duration } from \"@kaiord/core\";\n\nconst addKaiordAttributes = (\n tcxDuration: Record<string, unknown>,\n duration: Duration\n): void => {\n if (duration.type === \"heart_rate_less_than\" && duration.bpm !== undefined) {\n tcxDuration[\"@_kaiord:originalDurationType\"] = \"heart_rate_less_than\";\n tcxDuration[\"@_kaiord:originalDurationBpm\"] = duration.bpm;\n } else if (\n duration.type === \"power_less_than\" &&\n duration.watts !== undefined\n ) {\n tcxDuration[\"@_kaiord:originalDurationType\"] = \"power_less_than\";\n tcxDuration[\"@_kaiord:originalDurationWatts\"] = duration.watts;\n } else if (\n duration.type === \"power_greater_than\" &&\n duration.watts !== undefined\n ) {\n tcxDuration[\"@_kaiord:originalDurationType\"] = \"power_greater_than\";\n tcxDuration[\"@_kaiord:originalDurationWatts\"] = duration.watts;\n } else if (duration.type === \"calories\" && duration.calories !== undefined) {\n tcxDuration[\"@_kaiord:originalDurationType\"] = \"calories\";\n tcxDuration[\"@_kaiord:originalDurationCalories\"] = duration.calories;\n }\n};\n\nexport const convertDurationToTcx = (step: {\n duration: Duration;\n}): Record<string, unknown> => {\n const duration = step.duration;\n\n // Standard TCX duration types\n if (duration.type === \"time\") {\n return {\n \"@_xsi:type\": \"Time_t\",\n Seconds: duration.seconds,\n };\n }\n\n if (duration.type === \"distance\") {\n return {\n \"@_xsi:type\": \"Distance_t\",\n Meters: duration.meters,\n };\n }\n\n // For advanced duration types not natively supported by TCX,\n // use LapButton_t and preserve original type in kaiord attributes\n const tcxDuration: Record<string, unknown> = {\n \"@_xsi:type\": \"LapButton_t\",\n };\n\n addKaiordAttributes(tcxDuration, duration);\n\n return tcxDuration;\n};\n","import type { WorkoutStep } from \"@kaiord/core\";\n\nexport const convertHeartRateToTcx = (value: {\n unit: string;\n value?: number;\n min?: number;\n max?: number;\n}): Record<string, unknown> => {\n if (value.unit === \"zone\") {\n return {\n \"@_xsi:type\": \"HeartRate_t\",\n HeartRateZone: {\n \"@_xsi:type\": \"PredefinedHeartRateZone_t\",\n Number: value.value,\n },\n };\n }\n\n if (value.unit === \"bpm\" || value.unit === \"range\") {\n const min = \"min\" in value ? value.min : value.value;\n const max = \"max\" in value ? value.max : value.value;\n return {\n \"@_xsi:type\": \"HeartRate_t\",\n HeartRateZone: {\n \"@_xsi:type\": \"CustomHeartRateZone_t\",\n Low: min,\n High: max,\n },\n };\n }\n\n return { \"@_xsi:type\": \"None_t\" };\n};\n\nexport const convertPaceToTcx = (value: {\n unit: string;\n value?: number;\n min?: number;\n max?: number;\n}): Record<string, unknown> => {\n if (value.unit === \"meters_per_second\" || value.unit === \"range\") {\n const min = \"min\" in value ? value.min : value.value;\n const max = \"max\" in value ? value.max : value.value;\n return {\n \"@_xsi:type\": \"Speed_t\",\n SpeedZone: {\n \"@_xsi:type\": \"CustomSpeedZone_t\",\n LowInMetersPerSecond: min,\n HighInMetersPerSecond: max,\n },\n };\n }\n\n return { \"@_xsi:type\": \"None_t\" };\n};\n\nexport const convertCadenceToTcx = (value: {\n unit: string;\n value?: number;\n min?: number;\n max?: number;\n}): Record<string, unknown> => {\n if (value.unit === \"rpm\" || value.unit === \"range\") {\n const min = \"min\" in value ? value.min : value.value;\n const max = \"max\" in value ? value.max : value.value;\n return {\n \"@_xsi:type\": \"Cadence_t\",\n CadenceZone: {\n \"@_xsi:type\": \"CustomCadenceZone_t\",\n Low: min,\n High: max,\n },\n };\n }\n\n return { \"@_xsi:type\": \"None_t\" };\n};\n\nexport const convertTargetToTcx = (\n step: WorkoutStep\n): Record<string, unknown> => {\n if (step.target.type === \"open\") {\n return { \"@_xsi:type\": \"None_t\" };\n }\n\n if (step.target.type === \"heart_rate\") {\n const value = step.target.value;\n if (value && \"unit\" in value) {\n return convertHeartRateToTcx(value);\n }\n }\n\n if (step.target.type === \"pace\") {\n const value = step.target.value;\n if (value && \"unit\" in value) {\n return convertPaceToTcx(value);\n }\n }\n\n if (step.target.type === \"cadence\") {\n const value = step.target.value;\n if (value && \"unit\" in value) {\n return convertCadenceToTcx(value);\n }\n }\n\n return { \"@_xsi:type\": \"None_t\" };\n};\n","import type { WorkoutStep } from \"@kaiord/core\";\nimport type { Logger } from \"@kaiord/core\";\nimport { convertDurationToTcx } from \"./duration-to-tcx-encoder\";\nimport { convertTargetToTcx } from \"./target-to-tcx.converter\";\n\nconst capitalizeFirst = (str: string): string => {\n return str.charAt(0).toUpperCase() + str.slice(1);\n};\n\nconst addPowerExtensions = (\n step: WorkoutStep,\n tcxStep: Record<string, unknown>,\n logger: Logger\n): void => {\n if (step.target.type === \"power\") {\n logger.debug(\"Encoding power target to TCX extensions\", {\n stepIndex: step.stepIndex,\n });\n const powerValue = step.target.value;\n if (powerValue && \"unit\" in powerValue && powerValue.unit === \"watts\") {\n tcxStep.Extensions = {\n TPX: {\n \"@_xmlns\": \"http://www.garmin.com/xmlschemas/ActivityExtension/v2\",\n Watts: powerValue.value,\n },\n };\n }\n }\n};\n\nexport const convertStepToTcx = (\n step: WorkoutStep,\n index: number,\n logger: Logger\n): Record<string, unknown> => {\n logger.debug(\"Converting step to TCX\", { stepIndex: step.stepIndex });\n\n const tcxStep: Record<string, unknown> = {\n \"@_xsi:type\": \"Step_t\",\n StepId: index + 1,\n };\n\n if (step.name) {\n tcxStep.Name = step.name;\n }\n\n tcxStep.Duration = convertDurationToTcx(step);\n\n if (step.intensity) {\n tcxStep.Intensity = capitalizeFirst(step.intensity);\n }\n\n tcxStep.Target = convertTargetToTcx(step);\n\n if (step.extensions?.tcx) {\n logger.debug(\"Restoring step-level TCX extensions\", {\n stepIndex: step.stepIndex,\n });\n tcxStep.Extensions = step.extensions.tcx;\n } else {\n addPowerExtensions(step, tcxStep, logger);\n }\n\n return tcxStep;\n};\n","import type { KRD } from \"@kaiord/core\";\nimport type { Workout, WorkoutStep } from \"@kaiord/core\";\nimport { createTcxParsingError } from \"@kaiord/core\";\nimport type { Logger } from \"@kaiord/core\";\nimport { KRD_TO_TCX_SPORT } from \"../schemas/tcx-sport\";\nimport { addKaiordMetadata } from \"./metadata-builder\";\nimport { convertStepToTcx } from \"./step-to-tcx.converter\";\n\nconst buildTcxWorkout = (\n workout: Workout,\n logger: Logger\n): Record<string, unknown> => {\n const tcxSport =\n KRD_TO_TCX_SPORT[workout.sport as keyof typeof KRD_TO_TCX_SPORT] || \"Other\";\n\n const tcxSteps = workout.steps.map((step, index) =>\n convertStepToTcx(step as WorkoutStep, index, logger)\n );\n\n const tcxWorkout: Record<string, unknown> = {\n \"@_Sport\": tcxSport,\n Name: workout.name,\n Step: tcxSteps,\n };\n\n if (workout.extensions?.tcx) {\n logger.debug(\"Restoring workout-level TCX extensions\");\n tcxWorkout.Extensions = workout.extensions.tcx;\n }\n\n return tcxWorkout;\n};\n\nconst buildTrainingCenterDatabase = (\n tcxWorkout: Record<string, unknown>,\n krd: KRD,\n logger: Logger\n): Record<string, unknown> => {\n const trainingCenterDatabase: Record<string, unknown> = {\n \"@_xmlns\": \"http://www.garmin.com/xmlschemas/TrainingCenterDatabase/v2\",\n \"@_xmlns:xsi\": \"http://www.w3.org/2001/XMLSchema-instance\",\n \"@_xmlns:kaiord\": \"http://kaiord.dev/tcx-extensions/1.0\",\n Workouts: {\n Workout: tcxWorkout,\n },\n };\n\n addKaiordMetadata(trainingCenterDatabase, krd);\n\n if (krd.extensions?.tcx) {\n logger.debug(\"Restoring TrainingCenterDatabase-level TCX extensions\");\n trainingCenterDatabase.Extensions = krd.extensions.tcx;\n }\n\n return trainingCenterDatabase;\n};\n\nexport const convertKRDToTcx = (krd: KRD, logger: Logger): unknown => {\n logger.debug(\"Converting KRD to TCX structure\");\n\n if (!krd.extensions?.structured_workout) {\n throw createTcxParsingError(\n \"KRD does not contain workout data in extensions\"\n );\n }\n\n const workout = krd.extensions.structured_workout as Workout;\n const tcxWorkout = buildTcxWorkout(workout, logger);\n const trainingCenterDatabase = buildTrainingCenterDatabase(\n tcxWorkout,\n krd,\n logger\n );\n\n return {\n TrainingCenterDatabase: trainingCenterDatabase,\n };\n};\n","import { XMLBuilder, XMLParser } from \"fast-xml-parser\";\nimport type { KRD } from \"@kaiord/core\";\nimport { createTcxParsingError, createTcxValidationError } from \"@kaiord/core\";\nimport type { Logger } from \"@kaiord/core\";\nimport type { TcxReader } from \"@kaiord/core\";\nimport type { TcxValidator } from \"@kaiord/core\";\nimport type { TcxWriter } from \"@kaiord/core\";\nimport { convertTcxToKRD } from \"./workout/krd.converter\";\nimport { convertKRDToTcx as convertKRDToTcxStructure } from \"./workout/tcx.converter\";\n\nexport const createFastXmlTcxReader =\n (logger: Logger): TcxReader =>\n async (xmlString: string): Promise<KRD> => {\n logger.debug(\"Parsing TCX file\", { xmlLength: xmlString.length });\n\n let tcxData: unknown;\n try {\n const parser = new XMLParser({\n ignoreAttributes: false,\n attributeNamePrefix: \"@_\",\n parseAttributeValue: true,\n });\n\n tcxData = parser.parse(xmlString);\n } catch (error) {\n logger.error(\"Failed to parse TCX XML\", { error });\n throw createTcxParsingError(\"Failed to parse TCX file\", error);\n }\n\n if (\n !tcxData ||\n typeof tcxData !== \"object\" ||\n !(\"TrainingCenterDatabase\" in tcxData)\n ) {\n const error = createTcxParsingError(\n \"Invalid TCX format: missing TrainingCenterDatabase element\"\n );\n logger.error(\"Invalid TCX structure\", { error });\n throw error;\n }\n\n logger.info(\"TCX file parsed successfully\");\n\n return convertTcxToKRD(tcxData, logger);\n };\n\nexport const createFastXmlTcxWriter =\n (logger: Logger, validator: TcxValidator): TcxWriter =>\n async (krd: KRD): Promise<string> => {\n logger.debug(\"Encoding KRD to TCX\");\n\n let tcxData: unknown;\n try {\n tcxData = convertKRDToTcx(krd, logger);\n } catch (error) {\n logger.error(\"Failed to convert KRD to TCX structure\", { error });\n throw createTcxParsingError(\"Failed to convert KRD to TCX\", error);\n }\n\n let xmlString: string;\n try {\n const builder = new XMLBuilder({\n ignoreAttributes: false,\n attributeNamePrefix: \"@_\",\n format: true,\n indentBy: \" \",\n });\n\n xmlString = builder.build(tcxData);\n } catch (error) {\n logger.error(\"Failed to build TCX XML\", { error });\n throw createTcxParsingError(\"Failed to build TCX XML\", error);\n }\n\n logger.debug(\"Validating generated TCX against XSD\");\n\n const validationResult = await validator(xmlString);\n if (!validationResult.valid) {\n logger.error(\"Generated TCX does not conform to XSD schema\", {\n errors: validationResult.errors,\n });\n throw createTcxValidationError(\n \"Generated TCX file does not conform to XSD schema\",\n validationResult.errors.map((err) => ({\n field: err.path,\n message: err.message,\n }))\n );\n }\n\n logger.info(\"KRD encoded to TCX successfully\");\n return xmlString;\n };\n\nconst convertKRDToTcx = (krd: KRD, logger: Logger): unknown => {\n return convertKRDToTcxStructure(krd, logger);\n};\n","import { XMLValidator } from \"fast-xml-parser\";\nimport type { Logger } from \"@kaiord/core\";\nimport type { TcxValidationResult, TcxValidator } from \"@kaiord/core\";\n\nexport const createXsdTcxValidator =\n (logger: Logger): TcxValidator =>\n async (xmlString: string): Promise<TcxValidationResult> => {\n try {\n logger.debug(\"Validating TCX XML structure\");\n\n // Validate XML structure using fast-xml-parser\n const xmlValidation = XMLValidator.validate(xmlString, {\n allowBooleanAttributes: true,\n });\n\n if (xmlValidation !== true) {\n logger.warn(\"TCX XML validation failed\", {\n error: xmlValidation.err,\n });\n\n return {\n valid: false,\n errors: [\n {\n path: `line ${xmlValidation.err.line}`,\n message: `XML validation failed: ${xmlValidation.err.msg}`,\n },\n ],\n };\n }\n\n logger.info(\"TCX XML structure is valid\");\n return { valid: true, errors: [] };\n } catch (error) {\n logger.error(\"TCX validation failed\", { error });\n\n return {\n valid: false,\n errors: [\n {\n path: \"root\",\n message: error instanceof Error ? error.message : \"Unknown error\",\n },\n ],\n };\n }\n };\n","import type { Logger, TcxReader, TcxValidator, TcxWriter } from \"@kaiord/core\";\nimport { createConsoleLogger } from \"@kaiord/core\";\nimport {\n createFastXmlTcxReader,\n createFastXmlTcxWriter,\n} from \"./adapters/fast-xml-parser\";\nimport { createXsdTcxValidator } from \"./adapters/xsd-validator\";\n\nexport type TcxProviders = {\n tcxReader: TcxReader;\n tcxWriter: TcxWriter;\n tcxValidator: TcxValidator;\n};\n\nexport const createTcxProviders = (logger?: Logger): TcxProviders => {\n const log = logger || createConsoleLogger();\n const tcxValidator = createXsdTcxValidator(log);\n\n return {\n tcxReader: createFastXmlTcxReader(log),\n tcxWriter: createFastXmlTcxWriter(log, tcxValidator),\n tcxValidator,\n };\n};\n"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@kaiord/tcx",
|
|
3
|
+
"version": "4.0.0",
|
|
4
|
+
"description": "TCX format adapter for Kaiord workout data conversion",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"sideEffects": false,
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"import": "./dist/index.js",
|
|
12
|
+
"types": "./dist/index.d.ts"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist"
|
|
17
|
+
],
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"fast-xml-parser": "^5.3.4",
|
|
20
|
+
"zod": "^3.22.4",
|
|
21
|
+
"@kaiord/core": "^4.1.0"
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"@types/node": "^24.0.0",
|
|
25
|
+
"@vitest/coverage-v8": "^4.0.18",
|
|
26
|
+
"tsup": "^8.5.1",
|
|
27
|
+
"tsx": "^4.7.0",
|
|
28
|
+
"typescript": "^5.3.3",
|
|
29
|
+
"vitest": "^4.0.18"
|
|
30
|
+
},
|
|
31
|
+
"repository": {
|
|
32
|
+
"type": "git",
|
|
33
|
+
"url": "https://github.com/pablo-albaladejo/kaiord.git",
|
|
34
|
+
"directory": "packages/tcx"
|
|
35
|
+
},
|
|
36
|
+
"homepage": "https://www.npmjs.com/package/@kaiord/tcx",
|
|
37
|
+
"bugs": {
|
|
38
|
+
"url": "https://github.com/pablo-albaladejo/kaiord/issues"
|
|
39
|
+
},
|
|
40
|
+
"publishConfig": {
|
|
41
|
+
"access": "public"
|
|
42
|
+
},
|
|
43
|
+
"keywords": [
|
|
44
|
+
"kaiord",
|
|
45
|
+
"tcx",
|
|
46
|
+
"workout",
|
|
47
|
+
"converter",
|
|
48
|
+
"fitness"
|
|
49
|
+
],
|
|
50
|
+
"author": "Kaiord Contributors",
|
|
51
|
+
"license": "MIT",
|
|
52
|
+
"scripts": {
|
|
53
|
+
"build": "tsup",
|
|
54
|
+
"test": "vitest --run",
|
|
55
|
+
"test:watch": "vitest",
|
|
56
|
+
"lint": "tsc --noEmit && eslint . && prettier --check src",
|
|
57
|
+
"lint:fix": "eslint . --fix && prettier --write src",
|
|
58
|
+
"clean": "rm -rf dist"
|
|
59
|
+
}
|
|
60
|
+
}
|