trackswap 0.0.1 → 0.0.2
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/dist/FIT/base.d.ts +91 -0
- package/dist/FIT/base.js +101 -0
- package/dist/FIT/crc.d.ts +1 -0
- package/dist/FIT/crc.js +16 -0
- package/dist/FIT/decoder.d.ts +83 -0
- package/dist/FIT/decoder.js +323 -0
- package/dist/FIT/encoder.d.ts +142 -0
- package/dist/FIT/encoder.js +445 -0
- package/dist/FIT/index.d.ts +12 -0
- package/dist/FIT/index.js +13 -0
- package/dist/FIT/mesg.d.ts +40 -0
- package/dist/FIT/mesg.js +208 -0
- package/dist/FIT/middleware.d.ts +48 -0
- package/dist/FIT/middleware.js +206 -0
- package/dist/FIT/plugins.d.ts +26 -0
- package/dist/FIT/plugins.js +104 -0
- package/dist/FIT/processor.d.ts +58 -0
- package/dist/FIT/processor.js +170 -0
- package/dist/FIT/types/course_point.d.ts +56 -0
- package/dist/FIT/types/course_point.js +58 -0
- package/dist/FIT/types/event.d.ts +49 -0
- package/dist/FIT/types/event.js +50 -0
- package/dist/FIT/types/event_type.d.ts +18 -0
- package/dist/FIT/types/event_type.js +19 -0
- package/dist/FIT/types/file.d.ts +23 -0
- package/dist/FIT/types/file.js +24 -0
- package/dist/FIT/types/index.d.ts +119 -0
- package/dist/FIT/types/index.js +297 -0
- package/dist/FIT/types/mesg_num.d.ts +105 -0
- package/dist/FIT/types/mesg_num.js +106 -0
- package/dist/FIT/types/sport.d.ts +47 -0
- package/dist/FIT/types/sport.js +48 -0
- package/dist/FIT/types.d.ts +231 -0
- package/dist/FIT/types.js +1 -0
- package/dist/FIT/util.d.ts +1 -0
- package/dist/FIT/util.js +7 -0
- package/dist/GPX/base.d.ts +43 -67
- package/dist/GPX/base.js +40 -67
- package/dist/GPX/converters.d.ts +12 -36
- package/dist/GPX/converters.js +11 -103
- package/dist/GPX/decoder.d.ts +5 -5
- package/dist/GPX/decoder.js +6 -0
- package/dist/GPX/encoder.d.ts +4 -0
- package/dist/GPX/encoder.js +3 -0
- package/dist/GPX/index.d.ts +1 -1
- package/dist/GPX/index.js +1 -1
- package/dist/GPX/middleware.d.ts +27 -0
- package/dist/GPX/middleware.js +93 -0
- package/dist/GPX/processor.d.ts +1 -1
- package/dist/GPX/processor.js +12 -11
- package/dist/GPX/types.d.ts +2 -3
- package/dist/TCX/base.d.ts +52 -0
- package/dist/TCX/base.js +62 -0
- package/dist/TCX/converters.d.ts +75 -0
- package/dist/TCX/converters.js +333 -0
- package/dist/TCX/decoder.d.ts +87 -0
- package/dist/TCX/decoder.js +350 -0
- package/dist/TCX/encoder.d.ts +150 -0
- package/dist/TCX/encoder.js +513 -0
- package/dist/TCX/index.d.ts +10 -0
- package/dist/TCX/index.js +9 -0
- package/dist/TCX/middleware.d.ts +27 -0
- package/dist/TCX/middleware.js +93 -0
- package/dist/TCX/processor.d.ts +48 -0
- package/dist/TCX/processor.js +197 -0
- package/dist/TCX/types.d.ts +203 -0
- package/dist/TCX/types.js +1 -0
- package/dist/core/base.d.ts +119 -0
- package/dist/core/base.js +138 -0
- package/dist/core/decoder.d.ts +66 -0
- package/dist/core/decoder.js +90 -0
- package/dist/core/index.d.ts +2 -0
- package/dist/core/index.js +4 -0
- package/dist/core/processor.d.ts +69 -0
- package/dist/core/processor.js +265 -0
- package/dist/core/xml-utils.d.ts +41 -0
- package/dist/core/xml-utils.js +140 -0
- package/package.json +8 -5
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { TCXContext } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* 流水线阶段
|
|
4
|
+
*/
|
|
5
|
+
export declare enum PipelineStage {
|
|
6
|
+
TOKENIZE = "tokenize",
|
|
7
|
+
AST_GENERATE = "ast_generate",
|
|
8
|
+
CONVERT = "convert",
|
|
9
|
+
COMPLETE = "complete"
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* 流水线处理器接口
|
|
13
|
+
*/
|
|
14
|
+
export interface IPipelineProcessor {
|
|
15
|
+
stage: PipelineStage;
|
|
16
|
+
process(context: TCXContext): Promise<TCXContext>;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Tokenize处理器,将XML转为Token
|
|
20
|
+
*/
|
|
21
|
+
export declare class TokenizeProcessor implements IPipelineProcessor {
|
|
22
|
+
stage: PipelineStage;
|
|
23
|
+
process(context: TCXContext): Promise<TCXContext>;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* AST生成处理器
|
|
27
|
+
*/
|
|
28
|
+
export declare class AstGenerateProcessor implements IPipelineProcessor {
|
|
29
|
+
stage: PipelineStage;
|
|
30
|
+
process(context: TCXContext): Promise<TCXContext>;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* 转换处理器
|
|
34
|
+
*/
|
|
35
|
+
export declare class ConvertProcessor implements IPipelineProcessor {
|
|
36
|
+
private getConverter;
|
|
37
|
+
stage: PipelineStage;
|
|
38
|
+
constructor(getConverter: (tag: string) => any);
|
|
39
|
+
process(context: TCXContext): Promise<TCXContext>;
|
|
40
|
+
private assignResult;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* 完成处理器
|
|
44
|
+
*/
|
|
45
|
+
export declare class CompleteProcessor implements IPipelineProcessor {
|
|
46
|
+
stage: PipelineStage;
|
|
47
|
+
process(context: TCXContext): Promise<TCXContext>;
|
|
48
|
+
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { Parser } from "htmlparser2";
|
|
2
|
+
/**
|
|
3
|
+
* 流水线阶段
|
|
4
|
+
*/
|
|
5
|
+
export var PipelineStage;
|
|
6
|
+
(function (PipelineStage) {
|
|
7
|
+
PipelineStage["TOKENIZE"] = "tokenize";
|
|
8
|
+
PipelineStage["AST_GENERATE"] = "ast_generate";
|
|
9
|
+
PipelineStage["CONVERT"] = "convert";
|
|
10
|
+
PipelineStage["COMPLETE"] = "complete";
|
|
11
|
+
})(PipelineStage || (PipelineStage = {}));
|
|
12
|
+
/**
|
|
13
|
+
* Tokenize处理器,将XML转为Token
|
|
14
|
+
*/
|
|
15
|
+
export class TokenizeProcessor {
|
|
16
|
+
constructor() {
|
|
17
|
+
this.stage = PipelineStage.TOKENIZE;
|
|
18
|
+
}
|
|
19
|
+
async process(context) {
|
|
20
|
+
const startTime = Date.now();
|
|
21
|
+
if (!context.xmlContent) {
|
|
22
|
+
throw new Error("XML内容不能为空");
|
|
23
|
+
}
|
|
24
|
+
const tokens = [];
|
|
25
|
+
const parser = new Parser({
|
|
26
|
+
onopentag(name, attribs) {
|
|
27
|
+
tokens.push({
|
|
28
|
+
type: "open",
|
|
29
|
+
tag: name,
|
|
30
|
+
attributes: attribs,
|
|
31
|
+
});
|
|
32
|
+
},
|
|
33
|
+
ontext(text) {
|
|
34
|
+
const filteredText = text.replace(/\n/g, "").trim();
|
|
35
|
+
if (filteredText) {
|
|
36
|
+
tokens.push({ type: "text", tag: "text", value: filteredText });
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
onclosetag(name) {
|
|
40
|
+
tokens.push({ type: "close", tag: name });
|
|
41
|
+
},
|
|
42
|
+
}, { xmlMode: true });
|
|
43
|
+
parser.write(context.xmlContent);
|
|
44
|
+
parser.end();
|
|
45
|
+
context.tokens = tokens;
|
|
46
|
+
context.performance.tokenizeTime = Date.now() - startTime;
|
|
47
|
+
return context;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* AST生成处理器
|
|
52
|
+
*/
|
|
53
|
+
export class AstGenerateProcessor {
|
|
54
|
+
constructor() {
|
|
55
|
+
this.stage = PipelineStage.AST_GENERATE;
|
|
56
|
+
}
|
|
57
|
+
async process(context) {
|
|
58
|
+
const startTime = Date.now();
|
|
59
|
+
if (!context.tokens) {
|
|
60
|
+
throw new Error("Tokens不能为空");
|
|
61
|
+
}
|
|
62
|
+
const stack = [];
|
|
63
|
+
let root = undefined;
|
|
64
|
+
let current = null;
|
|
65
|
+
for (const token of context.tokens) {
|
|
66
|
+
if (token.type === "open") {
|
|
67
|
+
const newElement = {
|
|
68
|
+
tag: token.tag,
|
|
69
|
+
attributes: token.attributes || {},
|
|
70
|
+
children: [],
|
|
71
|
+
};
|
|
72
|
+
if (!root) {
|
|
73
|
+
root = newElement;
|
|
74
|
+
}
|
|
75
|
+
else if (current) {
|
|
76
|
+
current.children?.push(newElement);
|
|
77
|
+
}
|
|
78
|
+
stack.push(newElement);
|
|
79
|
+
current = newElement;
|
|
80
|
+
}
|
|
81
|
+
else if (token.type === "text" && current) {
|
|
82
|
+
current.value = token.value;
|
|
83
|
+
}
|
|
84
|
+
else if (token.type === "close") {
|
|
85
|
+
stack.pop();
|
|
86
|
+
current = stack[stack.length - 1] || null;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
context.ast = root;
|
|
90
|
+
context.performance.astTime = Date.now() - startTime;
|
|
91
|
+
return context;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* 转换处理器
|
|
96
|
+
*/
|
|
97
|
+
export class ConvertProcessor {
|
|
98
|
+
constructor(getConverter) {
|
|
99
|
+
this.getConverter = getConverter;
|
|
100
|
+
this.stage = PipelineStage.CONVERT;
|
|
101
|
+
}
|
|
102
|
+
async process(context) {
|
|
103
|
+
const startTime = Date.now();
|
|
104
|
+
if (!context.ast) {
|
|
105
|
+
throw new Error("AST不能为空");
|
|
106
|
+
}
|
|
107
|
+
const tcx = {};
|
|
108
|
+
// 处理根节点属性
|
|
109
|
+
if (context.ast.attributes) {
|
|
110
|
+
Object.entries(context.ast.attributes).forEach(([key, value]) => {
|
|
111
|
+
if (key.startsWith("xmlns:")) {
|
|
112
|
+
tcx[key] = value.replace(/'/g, "'");
|
|
113
|
+
}
|
|
114
|
+
else if (key === "xsi:schemaLocation") {
|
|
115
|
+
tcx[key] = value.replace(/'/g, "'");
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
tcx[key] = value;
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
// 处理子节点
|
|
123
|
+
context.ast.children?.forEach((child) => {
|
|
124
|
+
const converter = this.getConverter(child.tag);
|
|
125
|
+
if (!converter) {
|
|
126
|
+
console.error(`标签 ${child.tag} 未找到对应的 converter`);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
try {
|
|
130
|
+
const result = converter.convert(child, context);
|
|
131
|
+
if (result) {
|
|
132
|
+
this.assignResult(tcx, child.tag, result);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
catch (error) {
|
|
136
|
+
context.errors.push(error);
|
|
137
|
+
console.error(`转换器 ${converter.name} 处理标签 ${child.tag} 时出错:`, error);
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
context.result = tcx;
|
|
141
|
+
context.performance.convertTime = Date.now() - startTime;
|
|
142
|
+
return context;
|
|
143
|
+
}
|
|
144
|
+
assignResult(tcx, tag, result) {
|
|
145
|
+
switch (tag) {
|
|
146
|
+
case "Folders":
|
|
147
|
+
tcx.Folders = result;
|
|
148
|
+
break;
|
|
149
|
+
case "Activities":
|
|
150
|
+
tcx.Activities = result;
|
|
151
|
+
break;
|
|
152
|
+
case "Workouts":
|
|
153
|
+
tcx.Workouts = result;
|
|
154
|
+
break;
|
|
155
|
+
case "Courses":
|
|
156
|
+
tcx.Courses = result;
|
|
157
|
+
break;
|
|
158
|
+
case "Author":
|
|
159
|
+
tcx.Author = result;
|
|
160
|
+
break;
|
|
161
|
+
case "Extensions":
|
|
162
|
+
tcx.Extensions = result;
|
|
163
|
+
break;
|
|
164
|
+
default:
|
|
165
|
+
// 对于其他标签,直接赋值
|
|
166
|
+
tcx[tag] = result;
|
|
167
|
+
break;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* 完成处理器
|
|
173
|
+
*/
|
|
174
|
+
export class CompleteProcessor {
|
|
175
|
+
constructor() {
|
|
176
|
+
this.stage = PipelineStage.COMPLETE;
|
|
177
|
+
}
|
|
178
|
+
async process(context) {
|
|
179
|
+
context.performance.endTime = Date.now();
|
|
180
|
+
// 记录性能指标
|
|
181
|
+
const totalTime = context.performance.endTime - context.performance.startTime;
|
|
182
|
+
context.metadata.set("performance", {
|
|
183
|
+
totalTime,
|
|
184
|
+
tokenizeTime: context.performance.tokenizeTime,
|
|
185
|
+
astTime: context.performance.astTime,
|
|
186
|
+
convertTime: context.performance.convertTime,
|
|
187
|
+
});
|
|
188
|
+
// 更新统计信息
|
|
189
|
+
if (context.stats) {
|
|
190
|
+
context.stats.endTime = Date.now();
|
|
191
|
+
if (context.tokens) {
|
|
192
|
+
context.stats.processedTokens = context.tokens.length;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return context;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { ExtensionsType, Token, TokenAST } from "../types.js";
|
|
2
|
+
import { IProcessingContext } from "../core/base.js";
|
|
3
|
+
/**
|
|
4
|
+
* TCX 文件类型
|
|
5
|
+
*/
|
|
6
|
+
export interface TCXFileType {
|
|
7
|
+
version?: string;
|
|
8
|
+
creator?: string;
|
|
9
|
+
xmlns?: string;
|
|
10
|
+
"xmlns:gte"?: string;
|
|
11
|
+
"xmlns:xsi"?: string;
|
|
12
|
+
"xsi:schemaLocation"?: string;
|
|
13
|
+
Folders?: FolderType;
|
|
14
|
+
Activities?: ActivityListType;
|
|
15
|
+
Workouts?: WorkoutListType;
|
|
16
|
+
Courses?: CourseListType;
|
|
17
|
+
Author?: AbstractSourceType;
|
|
18
|
+
Extensions?: any;
|
|
19
|
+
[key: string]: any | undefined;
|
|
20
|
+
}
|
|
21
|
+
export interface FolderType {
|
|
22
|
+
History?: HistoryType;
|
|
23
|
+
Workouts?: WorkoutListType;
|
|
24
|
+
Courses?: any;
|
|
25
|
+
}
|
|
26
|
+
export interface WorkoutListType {
|
|
27
|
+
Workout?: WorkoutType[];
|
|
28
|
+
}
|
|
29
|
+
export interface WorkoutType {
|
|
30
|
+
Name?: string;
|
|
31
|
+
Step?: AbstractStepType;
|
|
32
|
+
ScheduledOn?: string;
|
|
33
|
+
Notes?: string;
|
|
34
|
+
Creator?: AbstractSourceType;
|
|
35
|
+
Extensions?: ExtensionsType;
|
|
36
|
+
}
|
|
37
|
+
export interface AbstractSourceType {
|
|
38
|
+
Name?: string;
|
|
39
|
+
}
|
|
40
|
+
export interface AbstractStepType {
|
|
41
|
+
StepId?: string;
|
|
42
|
+
}
|
|
43
|
+
export interface CourseListType {
|
|
44
|
+
Course?: CourseType[];
|
|
45
|
+
}
|
|
46
|
+
export interface CourseType {
|
|
47
|
+
Name?: string;
|
|
48
|
+
Lap?: CourseLapType[];
|
|
49
|
+
Track?: TrackType[];
|
|
50
|
+
Notes?: string;
|
|
51
|
+
CoursePoint?: CoursePointType[];
|
|
52
|
+
Creator?: AbstractSourceType;
|
|
53
|
+
Extensions?: ExtensionsType;
|
|
54
|
+
}
|
|
55
|
+
export interface CoursePointType {
|
|
56
|
+
Name?: string;
|
|
57
|
+
Time?: number;
|
|
58
|
+
Position?: PositionType;
|
|
59
|
+
AltitudeMeters?: number;
|
|
60
|
+
PointType?: string;
|
|
61
|
+
Notes?: string;
|
|
62
|
+
Extensions?: ExtensionsType;
|
|
63
|
+
}
|
|
64
|
+
export interface CourseLapType {
|
|
65
|
+
TotalTimeSeconds?: number;
|
|
66
|
+
DistanceMeters?: number;
|
|
67
|
+
BeginPosition?: PositionType;
|
|
68
|
+
BeginAltitudeMeters?: number;
|
|
69
|
+
EndPosition?: PositionType;
|
|
70
|
+
EndAltitudeMeters?: number;
|
|
71
|
+
AverageHeartRateBpm?: number;
|
|
72
|
+
MaximumHeartRateBpm?: number;
|
|
73
|
+
Intensity?: IntensityType;
|
|
74
|
+
Cadence?: CadenceType;
|
|
75
|
+
Extensions?: ExtensionsType;
|
|
76
|
+
}
|
|
77
|
+
export interface HistoryType {
|
|
78
|
+
Running?: HistoryFolderType;
|
|
79
|
+
Biking?: HistoryFolderType;
|
|
80
|
+
Other?: HistoryFolderType;
|
|
81
|
+
MultiSport?: HistoryFolderType;
|
|
82
|
+
Extensions?: ExtensionsType;
|
|
83
|
+
}
|
|
84
|
+
export interface HistoryFolderType {
|
|
85
|
+
Folder?: HistoryFolderType;
|
|
86
|
+
ActivityRef?: ActivityRefType;
|
|
87
|
+
Week?: WeekType;
|
|
88
|
+
Notes?: string;
|
|
89
|
+
Extensions?: any;
|
|
90
|
+
}
|
|
91
|
+
export interface ActivityRefType {
|
|
92
|
+
Id: string;
|
|
93
|
+
}
|
|
94
|
+
export interface WeekType {
|
|
95
|
+
Notes?: string;
|
|
96
|
+
StartDay?: string;
|
|
97
|
+
}
|
|
98
|
+
export interface ActivityListType {
|
|
99
|
+
Activity?: ActivityType[];
|
|
100
|
+
MultiSportSession?: MultiSportSessionType[];
|
|
101
|
+
}
|
|
102
|
+
export interface ActivityType {
|
|
103
|
+
Sport?: "Other" | "Running" | "Biking";
|
|
104
|
+
Id: string;
|
|
105
|
+
Lap: ActivityLapType[];
|
|
106
|
+
Notes?: string;
|
|
107
|
+
Training?: any;
|
|
108
|
+
Creator?: any;
|
|
109
|
+
Extensions?: any;
|
|
110
|
+
}
|
|
111
|
+
export interface TrainingType {
|
|
112
|
+
QuickWorkoutResults?: QuickWorkoutType;
|
|
113
|
+
Plan?: PlanType;
|
|
114
|
+
VirtualPartner: boolean;
|
|
115
|
+
}
|
|
116
|
+
export interface QuickWorkoutType {
|
|
117
|
+
TotalTimeSeconds: number;
|
|
118
|
+
DistanceMeters: number;
|
|
119
|
+
}
|
|
120
|
+
export interface PlanType {
|
|
121
|
+
Name?: string;
|
|
122
|
+
Extensions?: any;
|
|
123
|
+
Type: "Workout" | "Course";
|
|
124
|
+
IntervalWorkout: boolean;
|
|
125
|
+
}
|
|
126
|
+
export interface ActivityLapType {
|
|
127
|
+
TotalTimeSeconds?: number;
|
|
128
|
+
DistanceMeters?: number;
|
|
129
|
+
MaximumSpeed?: number;
|
|
130
|
+
Calories?: number;
|
|
131
|
+
Heartratebpm?: number;
|
|
132
|
+
AverageHeartRateBpm?: number;
|
|
133
|
+
MaximumHeartRateBpm?: number;
|
|
134
|
+
Intensity?: IntensityType;
|
|
135
|
+
Cadence?: CadenceType;
|
|
136
|
+
TriggerMethod?: TriggerMethodType;
|
|
137
|
+
Track?: TrackType[];
|
|
138
|
+
Notes?: string;
|
|
139
|
+
Extensions?: any;
|
|
140
|
+
}
|
|
141
|
+
export type TriggerMethodType = "Manual" | "Distance" | "Location" | "Time" | "HeartRate";
|
|
142
|
+
/**
|
|
143
|
+
* Active(高强度)
|
|
144
|
+
* Resting(低强度)
|
|
145
|
+
*/
|
|
146
|
+
export type IntensityType = "Active" | "Resting";
|
|
147
|
+
export interface CadenceType {
|
|
148
|
+
Low: number;
|
|
149
|
+
High: number;
|
|
150
|
+
}
|
|
151
|
+
export interface HeartRateInBeatsPerMinuteType {
|
|
152
|
+
Value: number;
|
|
153
|
+
}
|
|
154
|
+
export interface MultiSportSessionType {
|
|
155
|
+
Id?: string;
|
|
156
|
+
FirstSport?: FirstSportType;
|
|
157
|
+
NextSport?: NextSportType[];
|
|
158
|
+
Notes?: string;
|
|
159
|
+
}
|
|
160
|
+
export interface FirstSportType {
|
|
161
|
+
Activity?: ActivityType;
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Each sport contains an optional transition and a run.
|
|
165
|
+
*/
|
|
166
|
+
export interface NextSportType {
|
|
167
|
+
Transition?: ActivityLapType;
|
|
168
|
+
Activity?: ActivityType;
|
|
169
|
+
}
|
|
170
|
+
export interface TrackType {
|
|
171
|
+
Trackpoint?: TrackpointType[];
|
|
172
|
+
}
|
|
173
|
+
export interface TrackpointType {
|
|
174
|
+
Time?: string;
|
|
175
|
+
Position?: PositionType;
|
|
176
|
+
AltitudeMeters?: number;
|
|
177
|
+
DistanceMeters?: number;
|
|
178
|
+
HeartRateBpm?: number;
|
|
179
|
+
Cadence?: CadenceType;
|
|
180
|
+
SensorState?: "Present" | "Absent";
|
|
181
|
+
Extensions?: any;
|
|
182
|
+
}
|
|
183
|
+
export interface PositionType {
|
|
184
|
+
LatitudeDegrees?: string;
|
|
185
|
+
LongitudeDegrees?: string;
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* TCX 解码上下文
|
|
189
|
+
*/
|
|
190
|
+
export interface TCXContext extends IProcessingContext {
|
|
191
|
+
rawData?: Buffer;
|
|
192
|
+
xmlContent?: string;
|
|
193
|
+
tokens?: Token[];
|
|
194
|
+
ast?: TokenAST;
|
|
195
|
+
result?: TCXFileType;
|
|
196
|
+
performance: {
|
|
197
|
+
startTime: number;
|
|
198
|
+
tokenizeTime?: number;
|
|
199
|
+
astTime?: number;
|
|
200
|
+
convertTime?: number;
|
|
201
|
+
endTime?: number;
|
|
202
|
+
};
|
|
203
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { ExtensionsType, Token, TokenAST } from "../types.js";
|
|
2
|
+
/**
|
|
3
|
+
* 通用插件接口
|
|
4
|
+
*/
|
|
5
|
+
export interface IPlugin<TContext = any> {
|
|
6
|
+
/** 插件名称 */
|
|
7
|
+
name: string;
|
|
8
|
+
/** 插件版本 */
|
|
9
|
+
version?: string;
|
|
10
|
+
/** 插件描述 */
|
|
11
|
+
description?: string;
|
|
12
|
+
/** 依赖的其他插件 */
|
|
13
|
+
dependencies?: string[];
|
|
14
|
+
/** 支持的标签 */
|
|
15
|
+
supportedTags?: string[];
|
|
16
|
+
/** 插件初始化 */
|
|
17
|
+
initialize?(context: TContext): Promise<void> | void;
|
|
18
|
+
/** 插件销毁 */
|
|
19
|
+
destroy?(context: TContext): Promise<void> | void;
|
|
20
|
+
/** 插件验证 */
|
|
21
|
+
validate?(context: TContext): Promise<boolean> | boolean;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* 通用转换器插件接口
|
|
25
|
+
*/
|
|
26
|
+
export interface IConverterPlugin<TContext = any> extends IPlugin<TContext> {
|
|
27
|
+
/** 转换方法 */
|
|
28
|
+
convert(ast: TokenAST, context: TContext): any | undefined;
|
|
29
|
+
/** 是否支持该标签 */
|
|
30
|
+
supports(tag: string): boolean;
|
|
31
|
+
/** 优先级(数字越小优先级越高) */
|
|
32
|
+
priority?: number;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* 通用中间件插件接口
|
|
36
|
+
*/
|
|
37
|
+
export interface IMiddlewarePlugin<TContext = any, TResult = any> extends IPlugin<TContext> {
|
|
38
|
+
/** 优先级(数字越小优先级越高) */
|
|
39
|
+
priority?: number;
|
|
40
|
+
/** 在tokenize阶段执行 */
|
|
41
|
+
onTokenize?(tokens: Token[], context: TContext): Promise<Token[]> | Token[];
|
|
42
|
+
/** 在AST生成阶段执行 */
|
|
43
|
+
onAstGenerate?(ast: TokenAST, context: TContext): Promise<TokenAST> | TokenAST;
|
|
44
|
+
/** 在转换阶段执行 */
|
|
45
|
+
onConvert?(result: any, context: TContext): Promise<any> | any;
|
|
46
|
+
/** 在完成阶段执行 */
|
|
47
|
+
onComplete?(result: TResult, context: TContext): Promise<TResult> | TResult;
|
|
48
|
+
/** 错误处理 */
|
|
49
|
+
onError?(error: Error, context: TContext): Promise<void> | void;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* 通用处理上下文
|
|
53
|
+
*/
|
|
54
|
+
export interface IProcessingContext {
|
|
55
|
+
/** 元数据存储 */
|
|
56
|
+
metadata: Map<string, any>;
|
|
57
|
+
/** 错误列表 */
|
|
58
|
+
errors: Error[];
|
|
59
|
+
/** 警告列表 */
|
|
60
|
+
warnings: string[];
|
|
61
|
+
/** 处理统计信息 */
|
|
62
|
+
stats: {
|
|
63
|
+
startTime: number;
|
|
64
|
+
endTime?: number;
|
|
65
|
+
processedTokens: number;
|
|
66
|
+
convertedElements: number;
|
|
67
|
+
};
|
|
68
|
+
/** 用户自定义数据 */
|
|
69
|
+
userData?: Record<string, any>;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* 通用基础转换器抽象类
|
|
73
|
+
*/
|
|
74
|
+
export declare abstract class BaseConverter<TContext extends IProcessingContext = IProcessingContext> implements IConverterPlugin<TContext> {
|
|
75
|
+
abstract name: string;
|
|
76
|
+
abstract supportedTags: string[];
|
|
77
|
+
version: string;
|
|
78
|
+
priority: number;
|
|
79
|
+
abstract convert(ast: TokenAST, context: TContext): any | undefined;
|
|
80
|
+
supports(tag: string): boolean;
|
|
81
|
+
initialize(context: TContext): Promise<void>;
|
|
82
|
+
destroy(context: TContext): Promise<void>;
|
|
83
|
+
validate(context: TContext): Promise<boolean>;
|
|
84
|
+
protected parseFloat(value: string | number | undefined): number;
|
|
85
|
+
protected parseInt(value: string | number | undefined): number;
|
|
86
|
+
protected parseString(value: string | number | undefined): string;
|
|
87
|
+
protected parseBoolean(value: string | boolean | undefined): boolean;
|
|
88
|
+
protected parseDate(value: string | undefined): Date | undefined;
|
|
89
|
+
protected hasRequiredAttributes(ast: TokenAST, required: string[]): boolean;
|
|
90
|
+
protected extractAttributes<U extends Record<string, any>>(ast: TokenAST, target: U, mapping: Record<string, keyof U>): void;
|
|
91
|
+
protected processChildren<U extends Record<string, any>>(ast: TokenAST, target: U, processors: Record<string, (child: TokenAST, target: U) => void>): void;
|
|
92
|
+
protected getChildValue(ast: TokenAST, tag: string): string | undefined;
|
|
93
|
+
protected getChildByTag(ast: TokenAST, tag: string): TokenAST | undefined;
|
|
94
|
+
protected getChildrenByTag(ast: TokenAST, tag: string): TokenAST[];
|
|
95
|
+
/**
|
|
96
|
+
* 通用的 Extensions 处理方法
|
|
97
|
+
*/
|
|
98
|
+
protected convertExtensions(extensionsAST: TokenAST): ExtensionsType;
|
|
99
|
+
/**
|
|
100
|
+
* 递归转换 Extensions 内容
|
|
101
|
+
*/
|
|
102
|
+
protected convertExtensionContent(extension: TokenAST): any;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* 通用基础中间件抽象类
|
|
106
|
+
*/
|
|
107
|
+
export declare abstract class BaseMiddleware<TContext extends IProcessingContext = IProcessingContext, TResult = any> implements IMiddlewarePlugin<TContext, TResult> {
|
|
108
|
+
abstract name: string;
|
|
109
|
+
version: string;
|
|
110
|
+
description: string;
|
|
111
|
+
initialize(context: TContext): Promise<void>;
|
|
112
|
+
destroy(context: TContext): Promise<void>;
|
|
113
|
+
validate(context: TContext): Promise<boolean>;
|
|
114
|
+
onTokenize(tokens: Token[], context: TContext): Promise<Token[]>;
|
|
115
|
+
onAstGenerate(ast: TokenAST, context: TContext): Promise<TokenAST>;
|
|
116
|
+
onConvert(result: any, context: TContext): Promise<any>;
|
|
117
|
+
onComplete(result: TResult, context: TContext): Promise<TResult>;
|
|
118
|
+
onError(error: Error, context: TContext): Promise<void>;
|
|
119
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
// ============ 基础工具类 ============
|
|
2
|
+
/**
|
|
3
|
+
* 通用基础转换器抽象类
|
|
4
|
+
*/
|
|
5
|
+
export class BaseConverter {
|
|
6
|
+
constructor() {
|
|
7
|
+
this.version = "1.0.0";
|
|
8
|
+
this.priority = 100;
|
|
9
|
+
}
|
|
10
|
+
supports(tag) {
|
|
11
|
+
return this.supportedTags.includes(tag);
|
|
12
|
+
}
|
|
13
|
+
async initialize(context) {
|
|
14
|
+
context.metadata.set(`plugin:${this.name}:initialized`, true);
|
|
15
|
+
}
|
|
16
|
+
async destroy(context) {
|
|
17
|
+
context.metadata.delete(`plugin:${this.name}:initialized`);
|
|
18
|
+
}
|
|
19
|
+
async validate(context) {
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
// ============ 通用工具方法 ============
|
|
23
|
+
parseFloat(value) {
|
|
24
|
+
if (typeof value === "number")
|
|
25
|
+
return value;
|
|
26
|
+
if (typeof value === "string")
|
|
27
|
+
return parseFloat(value) || 0;
|
|
28
|
+
return 0;
|
|
29
|
+
}
|
|
30
|
+
parseInt(value) {
|
|
31
|
+
if (typeof value === "number")
|
|
32
|
+
return Math.floor(value);
|
|
33
|
+
if (typeof value === "string")
|
|
34
|
+
return parseInt(value, 10) || 0;
|
|
35
|
+
return 0;
|
|
36
|
+
}
|
|
37
|
+
parseString(value) {
|
|
38
|
+
return String(value || "");
|
|
39
|
+
}
|
|
40
|
+
parseBoolean(value) {
|
|
41
|
+
if (typeof value === "boolean")
|
|
42
|
+
return value;
|
|
43
|
+
if (typeof value === "string")
|
|
44
|
+
return value.toLowerCase() === "true";
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
parseDate(value) {
|
|
48
|
+
if (!value)
|
|
49
|
+
return undefined;
|
|
50
|
+
const date = new Date(value);
|
|
51
|
+
return isNaN(date.getTime()) ? undefined : date;
|
|
52
|
+
}
|
|
53
|
+
hasRequiredAttributes(ast, required) {
|
|
54
|
+
return required.every((attr) => ast.attributes?.[attr]);
|
|
55
|
+
}
|
|
56
|
+
extractAttributes(ast, target, mapping) {
|
|
57
|
+
if (!ast.attributes)
|
|
58
|
+
return;
|
|
59
|
+
Object.entries(ast.attributes).forEach(([key, value]) => {
|
|
60
|
+
const targetKey = mapping[key];
|
|
61
|
+
if (targetKey && targetKey in target) {
|
|
62
|
+
target[targetKey] = String(value || "");
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
processChildren(ast, target, processors) {
|
|
67
|
+
ast.children?.forEach((child) => {
|
|
68
|
+
const fn = processors[child.tag];
|
|
69
|
+
if (fn)
|
|
70
|
+
fn(child, target);
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
getChildValue(ast, tag) {
|
|
74
|
+
const child = ast.children?.find((c) => c.tag === tag);
|
|
75
|
+
return child?.value ? String(child.value) : undefined;
|
|
76
|
+
}
|
|
77
|
+
getChildByTag(ast, tag) {
|
|
78
|
+
return ast.children?.find((c) => c.tag === tag);
|
|
79
|
+
}
|
|
80
|
+
getChildrenByTag(ast, tag) {
|
|
81
|
+
return ast.children?.filter((c) => c.tag === tag) || [];
|
|
82
|
+
}
|
|
83
|
+
// ============ 扩展处理工具 ============
|
|
84
|
+
/**
|
|
85
|
+
* 通用的 Extensions 处理方法
|
|
86
|
+
*/
|
|
87
|
+
convertExtensions(extensionsAST) {
|
|
88
|
+
const extensions = {};
|
|
89
|
+
extensionsAST.children?.forEach((child) => {
|
|
90
|
+
extensions[child.tag] = this.convertExtensionContent(child);
|
|
91
|
+
});
|
|
92
|
+
return extensions;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* 递归转换 Extensions 内容
|
|
96
|
+
*/
|
|
97
|
+
convertExtensionContent(extension) {
|
|
98
|
+
if (extension.children?.length) {
|
|
99
|
+
const result = {};
|
|
100
|
+
extension.children.forEach((child) => {
|
|
101
|
+
result[child.tag] = this.convertExtensionContent(child);
|
|
102
|
+
});
|
|
103
|
+
return result;
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
return extension.value;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* 通用基础中间件抽象类
|
|
112
|
+
*/
|
|
113
|
+
export class BaseMiddleware {
|
|
114
|
+
constructor() {
|
|
115
|
+
this.version = "1.0.0";
|
|
116
|
+
this.description = "";
|
|
117
|
+
}
|
|
118
|
+
async initialize(context) { }
|
|
119
|
+
async destroy(context) { }
|
|
120
|
+
async validate(context) {
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
async onTokenize(tokens, context) {
|
|
124
|
+
return tokens;
|
|
125
|
+
}
|
|
126
|
+
async onAstGenerate(ast, context) {
|
|
127
|
+
return ast;
|
|
128
|
+
}
|
|
129
|
+
async onConvert(result, context) {
|
|
130
|
+
return result;
|
|
131
|
+
}
|
|
132
|
+
async onComplete(result, context) {
|
|
133
|
+
return result;
|
|
134
|
+
}
|
|
135
|
+
async onError(error, context) {
|
|
136
|
+
console.error(`中间件 ${this.name} 处理错误:`, error);
|
|
137
|
+
}
|
|
138
|
+
}
|