@takuhon/core 0.1.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 +202 -0
- package/NOTICE +10 -0
- package/README.md +112 -0
- package/dist/index.d.ts +1843 -0
- package/dist/index.js +919 -0
- package/dist/index.js.map +1 -0
- package/package.json +56 -0
- package/takuhon.schema.json +355 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,919 @@
|
|
|
1
|
+
// takuhon.schema.json
|
|
2
|
+
var takuhon_schema_default = {
|
|
3
|
+
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
4
|
+
$id: "https://takuhon.example/schemas/0.1.0/takuhon.schema.json",
|
|
5
|
+
title: "Takuhon Profile",
|
|
6
|
+
description: "Portable profile data format consumed by @takuhon/core. The canonical contract for profile content authored as takuhon.json.",
|
|
7
|
+
type: "object",
|
|
8
|
+
additionalProperties: false,
|
|
9
|
+
required: [
|
|
10
|
+
"schemaVersion",
|
|
11
|
+
"profile",
|
|
12
|
+
"links",
|
|
13
|
+
"careers",
|
|
14
|
+
"projects",
|
|
15
|
+
"skills",
|
|
16
|
+
"contact",
|
|
17
|
+
"settings",
|
|
18
|
+
"meta"
|
|
19
|
+
],
|
|
20
|
+
properties: {
|
|
21
|
+
schemaVersion: {
|
|
22
|
+
type: "string",
|
|
23
|
+
pattern: "^[0-9]+\\.[0-9]+\\.[0-9]+(-[a-zA-Z0-9.-]+)?$",
|
|
24
|
+
description: "Semantic version of the takuhon schema this document conforms to."
|
|
25
|
+
},
|
|
26
|
+
profile: { $ref: "#/$defs/Profile" },
|
|
27
|
+
links: {
|
|
28
|
+
type: "array",
|
|
29
|
+
maxItems: 100,
|
|
30
|
+
items: { $ref: "#/$defs/Link" }
|
|
31
|
+
},
|
|
32
|
+
careers: {
|
|
33
|
+
type: "array",
|
|
34
|
+
maxItems: 50,
|
|
35
|
+
items: { $ref: "#/$defs/Career" }
|
|
36
|
+
},
|
|
37
|
+
projects: {
|
|
38
|
+
type: "array",
|
|
39
|
+
maxItems: 100,
|
|
40
|
+
items: { $ref: "#/$defs/Project" }
|
|
41
|
+
},
|
|
42
|
+
skills: {
|
|
43
|
+
type: "array",
|
|
44
|
+
maxItems: 200,
|
|
45
|
+
items: { $ref: "#/$defs/Skill" }
|
|
46
|
+
},
|
|
47
|
+
contact: { $ref: "#/$defs/Contact" },
|
|
48
|
+
settings: { $ref: "#/$defs/Settings" },
|
|
49
|
+
meta: { $ref: "#/$defs/Meta" }
|
|
50
|
+
},
|
|
51
|
+
$defs: {
|
|
52
|
+
LocaleTag: {
|
|
53
|
+
type: "string",
|
|
54
|
+
minLength: 2,
|
|
55
|
+
maxLength: 35,
|
|
56
|
+
pattern: "^[a-zA-Z]{2,3}(-[a-zA-Z0-9]{2,8})*$",
|
|
57
|
+
description: "BCP-47 language tag (e.g., 'en', 'ja', 'zh-Hant', 'pt-BR')."
|
|
58
|
+
},
|
|
59
|
+
Iso3166Alpha2: {
|
|
60
|
+
type: "string",
|
|
61
|
+
pattern: "^[A-Z]{2}$",
|
|
62
|
+
description: "ISO 3166-1 alpha-2 country code (uppercase, two letters)."
|
|
63
|
+
},
|
|
64
|
+
YearMonth: {
|
|
65
|
+
type: "string",
|
|
66
|
+
pattern: "^[0-9]{4}-(0[1-9]|1[0-2])$",
|
|
67
|
+
description: "Year-month in 'YYYY-MM' format (Gregorian calendar)."
|
|
68
|
+
},
|
|
69
|
+
IsoDateTime: {
|
|
70
|
+
type: "string",
|
|
71
|
+
format: "date-time",
|
|
72
|
+
description: "ISO 8601 date-time (e.g., 2026-05-11T12:34:56Z)."
|
|
73
|
+
},
|
|
74
|
+
Url: {
|
|
75
|
+
type: "string",
|
|
76
|
+
format: "uri",
|
|
77
|
+
maxLength: 2048
|
|
78
|
+
},
|
|
79
|
+
Email: {
|
|
80
|
+
type: "string",
|
|
81
|
+
format: "email",
|
|
82
|
+
maxLength: 254
|
|
83
|
+
},
|
|
84
|
+
Slug: {
|
|
85
|
+
type: "string",
|
|
86
|
+
minLength: 1,
|
|
87
|
+
maxLength: 64,
|
|
88
|
+
pattern: "^[a-z0-9][a-z0-9-]*$",
|
|
89
|
+
description: "URL-safe identifier (lowercase alphanumerics and hyphens, must start with alphanumeric)."
|
|
90
|
+
},
|
|
91
|
+
LocalizedTitle: {
|
|
92
|
+
type: "object",
|
|
93
|
+
minProperties: 1,
|
|
94
|
+
propertyNames: {
|
|
95
|
+
type: "string",
|
|
96
|
+
pattern: "^[a-zA-Z]{2,3}(-[a-zA-Z0-9]{2,8})*$"
|
|
97
|
+
},
|
|
98
|
+
additionalProperties: {
|
|
99
|
+
type: "string",
|
|
100
|
+
minLength: 1,
|
|
101
|
+
maxLength: 200
|
|
102
|
+
},
|
|
103
|
+
description: "Map of BCP-47 locale tag to short title-like string (max 200 chars per value)."
|
|
104
|
+
},
|
|
105
|
+
LocalizedBody: {
|
|
106
|
+
type: "object",
|
|
107
|
+
minProperties: 1,
|
|
108
|
+
propertyNames: {
|
|
109
|
+
type: "string",
|
|
110
|
+
pattern: "^[a-zA-Z]{2,3}(-[a-zA-Z0-9]{2,8})*$"
|
|
111
|
+
},
|
|
112
|
+
additionalProperties: {
|
|
113
|
+
type: "string",
|
|
114
|
+
minLength: 1,
|
|
115
|
+
maxLength: 5e3
|
|
116
|
+
},
|
|
117
|
+
description: "Map of BCP-47 locale tag to body-length string (max 5000 chars per value)."
|
|
118
|
+
},
|
|
119
|
+
LinkType: {
|
|
120
|
+
type: "string",
|
|
121
|
+
enum: [
|
|
122
|
+
"website",
|
|
123
|
+
"blog",
|
|
124
|
+
"github",
|
|
125
|
+
"gitlab",
|
|
126
|
+
"linkedin",
|
|
127
|
+
"x",
|
|
128
|
+
"mastodon",
|
|
129
|
+
"bluesky",
|
|
130
|
+
"instagram",
|
|
131
|
+
"youtube",
|
|
132
|
+
"threads",
|
|
133
|
+
"facebook",
|
|
134
|
+
"email",
|
|
135
|
+
"rss",
|
|
136
|
+
"custom"
|
|
137
|
+
],
|
|
138
|
+
description: "Identifies the kind of link. 'custom' requires iconUrl."
|
|
139
|
+
},
|
|
140
|
+
Profile: {
|
|
141
|
+
type: "object",
|
|
142
|
+
additionalProperties: true,
|
|
143
|
+
required: ["displayName"],
|
|
144
|
+
properties: {
|
|
145
|
+
displayName: { $ref: "#/$defs/LocalizedTitle" },
|
|
146
|
+
tagline: { $ref: "#/$defs/LocalizedTitle" },
|
|
147
|
+
bio: { $ref: "#/$defs/LocalizedBody" },
|
|
148
|
+
avatar: { $ref: "#/$defs/Avatar" },
|
|
149
|
+
location: { $ref: "#/$defs/Address" }
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
Avatar: {
|
|
153
|
+
type: "object",
|
|
154
|
+
additionalProperties: true,
|
|
155
|
+
required: ["url"],
|
|
156
|
+
properties: {
|
|
157
|
+
url: {
|
|
158
|
+
type: "string",
|
|
159
|
+
format: "uri-reference",
|
|
160
|
+
maxLength: 2048,
|
|
161
|
+
description: "URL or path to the avatar image."
|
|
162
|
+
},
|
|
163
|
+
alt: { $ref: "#/$defs/LocalizedTitle" }
|
|
164
|
+
}
|
|
165
|
+
},
|
|
166
|
+
Address: {
|
|
167
|
+
type: "object",
|
|
168
|
+
additionalProperties: true,
|
|
169
|
+
properties: {
|
|
170
|
+
country: { $ref: "#/$defs/Iso3166Alpha2" },
|
|
171
|
+
region: { type: "string", maxLength: 100 },
|
|
172
|
+
locality: { $ref: "#/$defs/LocalizedTitle" },
|
|
173
|
+
display: { $ref: "#/$defs/LocalizedTitle" }
|
|
174
|
+
}
|
|
175
|
+
},
|
|
176
|
+
Link: {
|
|
177
|
+
type: "object",
|
|
178
|
+
additionalProperties: false,
|
|
179
|
+
required: ["id", "type", "url"],
|
|
180
|
+
properties: {
|
|
181
|
+
id: { $ref: "#/$defs/Slug" },
|
|
182
|
+
type: { $ref: "#/$defs/LinkType" },
|
|
183
|
+
label: { $ref: "#/$defs/LocalizedTitle" },
|
|
184
|
+
url: { $ref: "#/$defs/Url" },
|
|
185
|
+
featured: { type: "boolean" },
|
|
186
|
+
order: { type: "integer", minimum: 0 },
|
|
187
|
+
iconUrl: { $ref: "#/$defs/Url" }
|
|
188
|
+
},
|
|
189
|
+
allOf: [
|
|
190
|
+
{
|
|
191
|
+
if: {
|
|
192
|
+
properties: { type: { const: "custom" } },
|
|
193
|
+
required: ["type"]
|
|
194
|
+
},
|
|
195
|
+
then: {
|
|
196
|
+
properties: { iconUrl: { $ref: "#/$defs/Url" } },
|
|
197
|
+
required: ["iconUrl"]
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
]
|
|
201
|
+
},
|
|
202
|
+
Career: {
|
|
203
|
+
type: "object",
|
|
204
|
+
additionalProperties: true,
|
|
205
|
+
required: ["id", "organization", "role", "startDate"],
|
|
206
|
+
properties: {
|
|
207
|
+
id: { $ref: "#/$defs/Slug" },
|
|
208
|
+
organization: { $ref: "#/$defs/LocalizedTitle" },
|
|
209
|
+
role: { $ref: "#/$defs/LocalizedTitle" },
|
|
210
|
+
description: { $ref: "#/$defs/LocalizedBody" },
|
|
211
|
+
startDate: { $ref: "#/$defs/YearMonth" },
|
|
212
|
+
endDate: {
|
|
213
|
+
anyOf: [{ $ref: "#/$defs/YearMonth" }, { type: "null" }]
|
|
214
|
+
},
|
|
215
|
+
isCurrent: { type: "boolean" },
|
|
216
|
+
url: { $ref: "#/$defs/Url" },
|
|
217
|
+
location: { $ref: "#/$defs/Address" },
|
|
218
|
+
order: { type: "integer", minimum: 0 }
|
|
219
|
+
}
|
|
220
|
+
},
|
|
221
|
+
Project: {
|
|
222
|
+
type: "object",
|
|
223
|
+
additionalProperties: true,
|
|
224
|
+
required: ["id", "title"],
|
|
225
|
+
properties: {
|
|
226
|
+
id: { $ref: "#/$defs/Slug" },
|
|
227
|
+
title: { $ref: "#/$defs/LocalizedTitle" },
|
|
228
|
+
description: { $ref: "#/$defs/LocalizedBody" },
|
|
229
|
+
url: { $ref: "#/$defs/Url" },
|
|
230
|
+
tags: {
|
|
231
|
+
type: "array",
|
|
232
|
+
maxItems: 30,
|
|
233
|
+
items: { type: "string", minLength: 1, maxLength: 50 }
|
|
234
|
+
},
|
|
235
|
+
relatedCareerId: { $ref: "#/$defs/Slug" },
|
|
236
|
+
startDate: { $ref: "#/$defs/YearMonth" },
|
|
237
|
+
endDate: {
|
|
238
|
+
anyOf: [{ $ref: "#/$defs/YearMonth" }, { type: "null" }]
|
|
239
|
+
},
|
|
240
|
+
highlighted: { type: "boolean" },
|
|
241
|
+
order: { type: "integer", minimum: 0 }
|
|
242
|
+
}
|
|
243
|
+
},
|
|
244
|
+
Skill: {
|
|
245
|
+
type: "object",
|
|
246
|
+
additionalProperties: true,
|
|
247
|
+
required: ["id", "label"],
|
|
248
|
+
properties: {
|
|
249
|
+
id: { $ref: "#/$defs/Slug" },
|
|
250
|
+
label: { type: "string", minLength: 1, maxLength: 100 },
|
|
251
|
+
category: {
|
|
252
|
+
type: "string",
|
|
253
|
+
minLength: 1,
|
|
254
|
+
maxLength: 64,
|
|
255
|
+
description: "Recommended values (extensible): programming, design, business, communication, language, music, art, sports, other."
|
|
256
|
+
},
|
|
257
|
+
order: { type: "integer", minimum: 0 }
|
|
258
|
+
}
|
|
259
|
+
},
|
|
260
|
+
Contact: {
|
|
261
|
+
type: "object",
|
|
262
|
+
additionalProperties: true,
|
|
263
|
+
properties: {
|
|
264
|
+
email: { $ref: "#/$defs/Email" },
|
|
265
|
+
showEmail: { type: "boolean", default: false },
|
|
266
|
+
formUrl: { $ref: "#/$defs/Url" }
|
|
267
|
+
}
|
|
268
|
+
},
|
|
269
|
+
Settings: {
|
|
270
|
+
type: "object",
|
|
271
|
+
additionalProperties: true,
|
|
272
|
+
required: ["defaultLocale", "availableLocales"],
|
|
273
|
+
properties: {
|
|
274
|
+
defaultLocale: { $ref: "#/$defs/LocaleTag" },
|
|
275
|
+
fallbackLocale: { $ref: "#/$defs/LocaleTag" },
|
|
276
|
+
availableLocales: {
|
|
277
|
+
type: "array",
|
|
278
|
+
minItems: 1,
|
|
279
|
+
maxItems: 50,
|
|
280
|
+
uniqueItems: true,
|
|
281
|
+
items: { $ref: "#/$defs/LocaleTag" }
|
|
282
|
+
},
|
|
283
|
+
theme: {
|
|
284
|
+
type: "string",
|
|
285
|
+
minLength: 1,
|
|
286
|
+
maxLength: 64,
|
|
287
|
+
description: "UI theme identifier. 'default' is the built-in theme; adapters may add more."
|
|
288
|
+
},
|
|
289
|
+
showPoweredBy: {
|
|
290
|
+
type: "boolean",
|
|
291
|
+
default: true,
|
|
292
|
+
description: "Display the 'Powered by takuhon' attribution in the rendered profile."
|
|
293
|
+
},
|
|
294
|
+
enableJsonLd: {
|
|
295
|
+
type: "boolean",
|
|
296
|
+
default: true,
|
|
297
|
+
description: "Emit Schema.org JSON-LD on the rendered profile page."
|
|
298
|
+
},
|
|
299
|
+
enableApi: {
|
|
300
|
+
type: "boolean",
|
|
301
|
+
default: true,
|
|
302
|
+
description: "Expose the public read API endpoints (GET /api/profile, /api/jsonld, /api/schema, /takuhon.json)."
|
|
303
|
+
},
|
|
304
|
+
enableAnalytics: {
|
|
305
|
+
type: "boolean",
|
|
306
|
+
default: false,
|
|
307
|
+
description: "Opt-in flag for first-party analytics. Default is false to keep takuhon privacy-respecting by default."
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
},
|
|
311
|
+
Meta: {
|
|
312
|
+
type: "object",
|
|
313
|
+
additionalProperties: true,
|
|
314
|
+
required: ["contentLicense"],
|
|
315
|
+
properties: {
|
|
316
|
+
createdAt: { $ref: "#/$defs/IsoDateTime" },
|
|
317
|
+
updatedAt: { $ref: "#/$defs/IsoDateTime" },
|
|
318
|
+
generator: {
|
|
319
|
+
type: "string",
|
|
320
|
+
minLength: 1,
|
|
321
|
+
maxLength: 100,
|
|
322
|
+
description: "Tool that produced this document (e.g. 'Takuhon', 'create-takuhon@0.1.0')."
|
|
323
|
+
},
|
|
324
|
+
contentLicense: { $ref: "#/$defs/ContentLicense" }
|
|
325
|
+
}
|
|
326
|
+
},
|
|
327
|
+
ContentLicense: {
|
|
328
|
+
type: "object",
|
|
329
|
+
additionalProperties: false,
|
|
330
|
+
required: ["spdxId"],
|
|
331
|
+
properties: {
|
|
332
|
+
spdxId: {
|
|
333
|
+
type: "string",
|
|
334
|
+
minLength: 1,
|
|
335
|
+
maxLength: 64,
|
|
336
|
+
description: "SPDX identifier (e.g., 'CC-BY-4.0', 'CC0-1.0') or 'Proprietary'. No default; the profile owner must choose explicitly."
|
|
337
|
+
},
|
|
338
|
+
url: { $ref: "#/$defs/Url" },
|
|
339
|
+
attribution: {
|
|
340
|
+
type: "object",
|
|
341
|
+
additionalProperties: false,
|
|
342
|
+
properties: {
|
|
343
|
+
name: { type: "string", minLength: 1, maxLength: 200 },
|
|
344
|
+
url: { $ref: "#/$defs/Url" }
|
|
345
|
+
}
|
|
346
|
+
},
|
|
347
|
+
rights: {
|
|
348
|
+
type: "string",
|
|
349
|
+
minLength: 1,
|
|
350
|
+
maxLength: 1e3,
|
|
351
|
+
description: "Free-form rights statement (used when spdxId='Proprietary' or for additional notices)."
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
// src/schema.ts
|
|
359
|
+
var schema = takuhon_schema_default;
|
|
360
|
+
|
|
361
|
+
// src/validate.ts
|
|
362
|
+
import Ajv2020 from "ajv/dist/2020.js";
|
|
363
|
+
import addFormats from "ajv-formats";
|
|
364
|
+
var SUPPORTED_SCHEMA_VERSIONS = ["0.1.0"];
|
|
365
|
+
var ajv = new Ajv2020({
|
|
366
|
+
allErrors: true,
|
|
367
|
+
strict: true
|
|
368
|
+
});
|
|
369
|
+
addFormats(ajv);
|
|
370
|
+
var compiled = ajv.compile(schema);
|
|
371
|
+
function validate(data) {
|
|
372
|
+
if (typeof data !== "object" || data === null || Array.isArray(data)) {
|
|
373
|
+
return {
|
|
374
|
+
ok: false,
|
|
375
|
+
errors: [
|
|
376
|
+
{
|
|
377
|
+
pointer: "",
|
|
378
|
+
message: "takuhon.json must be a JSON object.",
|
|
379
|
+
keyword: "type"
|
|
380
|
+
}
|
|
381
|
+
]
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
const candidate = data;
|
|
385
|
+
if (typeof candidate.schemaVersion === "string" && !isSupportedVersion(candidate.schemaVersion)) {
|
|
386
|
+
return {
|
|
387
|
+
ok: false,
|
|
388
|
+
errors: [
|
|
389
|
+
{
|
|
390
|
+
pointer: "/schemaVersion",
|
|
391
|
+
message: `schemaVersion "${candidate.schemaVersion}" is not in the supported window (${SUPPORTED_SCHEMA_VERSIONS.join(
|
|
392
|
+
", "
|
|
393
|
+
)}).`,
|
|
394
|
+
keyword: "schemaVersion"
|
|
395
|
+
}
|
|
396
|
+
]
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
if (compiled(data)) {
|
|
400
|
+
return { ok: true, data };
|
|
401
|
+
}
|
|
402
|
+
return {
|
|
403
|
+
ok: false,
|
|
404
|
+
errors: (compiled.errors ?? []).map(toValidationError)
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
function isSupportedVersion(value) {
|
|
408
|
+
return SUPPORTED_SCHEMA_VERSIONS.includes(value);
|
|
409
|
+
}
|
|
410
|
+
function toValidationError(err) {
|
|
411
|
+
let pointer = err.instancePath;
|
|
412
|
+
if (err.keyword === "required") {
|
|
413
|
+
const missing = err.params.missingProperty;
|
|
414
|
+
if (typeof missing === "string") {
|
|
415
|
+
pointer = `${err.instancePath}/${escapePointerSegment(missing)}`;
|
|
416
|
+
}
|
|
417
|
+
} else if (err.keyword === "additionalProperties") {
|
|
418
|
+
const extra = err.params.additionalProperty;
|
|
419
|
+
if (typeof extra === "string") {
|
|
420
|
+
pointer = `${err.instancePath}/${escapePointerSegment(extra)}`;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
return {
|
|
424
|
+
pointer,
|
|
425
|
+
message: err.message ?? "Validation failed.",
|
|
426
|
+
keyword: err.keyword,
|
|
427
|
+
schemaPointer: err.schemaPath ? err.schemaPath : void 0
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
function escapePointerSegment(segment) {
|
|
431
|
+
return segment.replace(/~/g, "~0").replace(/\//g, "~1");
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// src/normalize.ts
|
|
435
|
+
function normalize(data) {
|
|
436
|
+
const out = JSON.parse(JSON.stringify(data));
|
|
437
|
+
normalizeProfile(out.profile);
|
|
438
|
+
for (const link of out.links) {
|
|
439
|
+
cleanOptionalLocalized(link, "label");
|
|
440
|
+
}
|
|
441
|
+
out.links = stableSortByOrder(out.links);
|
|
442
|
+
for (const career of out.careers) {
|
|
443
|
+
cleanRequiredLocalized(career.organization);
|
|
444
|
+
cleanRequiredLocalized(career.role);
|
|
445
|
+
cleanOptionalLocalized(career, "description");
|
|
446
|
+
}
|
|
447
|
+
out.careers = stableSortByOrder(out.careers);
|
|
448
|
+
for (const project of out.projects) {
|
|
449
|
+
cleanRequiredLocalized(project.title);
|
|
450
|
+
cleanOptionalLocalized(project, "description");
|
|
451
|
+
}
|
|
452
|
+
out.projects = stableSortByOrder(out.projects);
|
|
453
|
+
out.skills = stableSortByOrder(out.skills);
|
|
454
|
+
return out;
|
|
455
|
+
}
|
|
456
|
+
function normalizeProfile(profile) {
|
|
457
|
+
cleanRequiredLocalized(profile.displayName);
|
|
458
|
+
cleanOptionalLocalized(profile, "tagline");
|
|
459
|
+
cleanOptionalLocalized(profile, "bio");
|
|
460
|
+
if (profile.avatar) {
|
|
461
|
+
cleanOptionalLocalized(profile.avatar, "alt");
|
|
462
|
+
}
|
|
463
|
+
if (profile.location) {
|
|
464
|
+
cleanOptionalLocalized(profile.location, "locality");
|
|
465
|
+
cleanOptionalLocalized(profile.location, "display");
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
function cleanRequiredLocalized(map) {
|
|
469
|
+
for (const key of Object.keys(map)) {
|
|
470
|
+
const value = map[key];
|
|
471
|
+
if (value === void 0 || value.trim() === "") {
|
|
472
|
+
delete map[key];
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
function cleanOptionalLocalized(parent, key) {
|
|
477
|
+
const map = parent[key];
|
|
478
|
+
if (!map) return;
|
|
479
|
+
for (const k of Object.keys(map)) {
|
|
480
|
+
const value = map[k];
|
|
481
|
+
if (value === void 0 || value.trim() === "") {
|
|
482
|
+
delete map[k];
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
if (Object.keys(map).length === 0) {
|
|
486
|
+
delete parent[key];
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
function stableSortByOrder(items) {
|
|
490
|
+
return items.slice().sort((a, b) => {
|
|
491
|
+
const ao = a.order ?? Number.POSITIVE_INFINITY;
|
|
492
|
+
const bo = b.order ?? Number.POSITIVE_INFINITY;
|
|
493
|
+
return ao - bo;
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// src/locale-tag.ts
|
|
498
|
+
var BCP47_PATTERN = /^[a-zA-Z]{2,3}(-[a-zA-Z0-9]+)*$/;
|
|
499
|
+
function isValidBcp47(tag) {
|
|
500
|
+
return BCP47_PATTERN.test(tag);
|
|
501
|
+
}
|
|
502
|
+
function expandRegional(tag) {
|
|
503
|
+
if (!isValidBcp47(tag)) return [];
|
|
504
|
+
const parts = tag.split("-");
|
|
505
|
+
const out = [];
|
|
506
|
+
for (let i = parts.length; i >= 1; i--) {
|
|
507
|
+
out.push(parts.slice(0, i).join("-"));
|
|
508
|
+
}
|
|
509
|
+
return out;
|
|
510
|
+
}
|
|
511
|
+
function lookupCaseInsensitive(map, key) {
|
|
512
|
+
if (!map) return void 0;
|
|
513
|
+
const direct = map[key];
|
|
514
|
+
if (direct !== void 0) return direct;
|
|
515
|
+
const lower = key.toLowerCase();
|
|
516
|
+
for (const k of Object.keys(map)) {
|
|
517
|
+
if (k.toLowerCase() === lower) return map[k];
|
|
518
|
+
}
|
|
519
|
+
return void 0;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// src/resolve-locale.ts
|
|
523
|
+
function resolveLocale(data, locale, fallbackLocale) {
|
|
524
|
+
const candidates = buildCandidates(data, locale, fallbackLocale);
|
|
525
|
+
const displayPick = pickLocalizedWithTag(data.profile.displayName, candidates);
|
|
526
|
+
return {
|
|
527
|
+
schemaVersion: data.schemaVersion,
|
|
528
|
+
profile: resolveProfile(data.profile, candidates, displayPick?.value ?? ""),
|
|
529
|
+
links: data.links.map((l) => resolveLink(l, candidates)),
|
|
530
|
+
careers: data.careers.map((c) => resolveCareer(c, candidates)),
|
|
531
|
+
projects: data.projects.map((p) => resolveProject(p, candidates)),
|
|
532
|
+
skills: data.skills,
|
|
533
|
+
contact: data.contact,
|
|
534
|
+
settings: data.settings,
|
|
535
|
+
meta: data.meta,
|
|
536
|
+
resolvedLocale: displayPick?.tag ?? candidates[0] ?? ""
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
function buildCandidates(data, locale, fallbackLocale) {
|
|
540
|
+
const raw = [];
|
|
541
|
+
if (locale !== void 0) raw.push(...expandRegional(locale));
|
|
542
|
+
if (fallbackLocale !== void 0) raw.push(...expandRegional(fallbackLocale));
|
|
543
|
+
raw.push(...expandRegional(data.settings.defaultLocale));
|
|
544
|
+
if (data.settings.fallbackLocale !== void 0) {
|
|
545
|
+
raw.push(...expandRegional(data.settings.fallbackLocale));
|
|
546
|
+
}
|
|
547
|
+
for (const tag of data.settings.availableLocales) {
|
|
548
|
+
raw.push(...expandRegional(tag));
|
|
549
|
+
}
|
|
550
|
+
return dedupCaseInsensitive(raw);
|
|
551
|
+
}
|
|
552
|
+
function dedupCaseInsensitive(tags) {
|
|
553
|
+
const seen = /* @__PURE__ */ new Set();
|
|
554
|
+
const out = [];
|
|
555
|
+
for (const tag of tags) {
|
|
556
|
+
if (!isValidBcp47(tag)) continue;
|
|
557
|
+
const lower = tag.toLowerCase();
|
|
558
|
+
if (seen.has(lower)) continue;
|
|
559
|
+
seen.add(lower);
|
|
560
|
+
out.push(tag);
|
|
561
|
+
}
|
|
562
|
+
return out;
|
|
563
|
+
}
|
|
564
|
+
function pickLocalizedWithTag(field, candidates) {
|
|
565
|
+
if (!field) return void 0;
|
|
566
|
+
for (const tag of candidates) {
|
|
567
|
+
const hit = lookupCaseInsensitive(field, tag);
|
|
568
|
+
if (hit !== void 0 && hit.trim() !== "") {
|
|
569
|
+
return { value: hit, tag };
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
return void 0;
|
|
573
|
+
}
|
|
574
|
+
function pickLocalized(field, candidates) {
|
|
575
|
+
return pickLocalizedWithTag(field, candidates)?.value;
|
|
576
|
+
}
|
|
577
|
+
function resolveProfile(profile, candidates, displayName) {
|
|
578
|
+
const out = { displayName };
|
|
579
|
+
const tagline = pickLocalized(profile.tagline, candidates);
|
|
580
|
+
if (tagline !== void 0) out.tagline = tagline;
|
|
581
|
+
const bio = pickLocalized(profile.bio, candidates);
|
|
582
|
+
if (bio !== void 0) out.bio = bio;
|
|
583
|
+
if (profile.avatar) out.avatar = resolveAvatar(profile.avatar, candidates);
|
|
584
|
+
if (profile.location) out.location = resolveAddress(profile.location, candidates);
|
|
585
|
+
return out;
|
|
586
|
+
}
|
|
587
|
+
function resolveAvatar(avatar, candidates) {
|
|
588
|
+
const out = { url: avatar.url };
|
|
589
|
+
const alt = pickLocalized(avatar.alt, candidates);
|
|
590
|
+
if (alt !== void 0) out.alt = alt;
|
|
591
|
+
return out;
|
|
592
|
+
}
|
|
593
|
+
function resolveAddress(address, candidates) {
|
|
594
|
+
const out = {};
|
|
595
|
+
if (address.country !== void 0) out.country = address.country;
|
|
596
|
+
if (address.region !== void 0) out.region = address.region;
|
|
597
|
+
const locality = pickLocalized(address.locality, candidates);
|
|
598
|
+
if (locality !== void 0) out.locality = locality;
|
|
599
|
+
const display = pickLocalized(address.display, candidates);
|
|
600
|
+
if (display !== void 0) out.display = display;
|
|
601
|
+
return out;
|
|
602
|
+
}
|
|
603
|
+
function resolveLink(link, candidates) {
|
|
604
|
+
const label = pickLocalized(link.label, candidates);
|
|
605
|
+
if (link.type === "custom") {
|
|
606
|
+
const out2 = {
|
|
607
|
+
id: link.id,
|
|
608
|
+
type: "custom",
|
|
609
|
+
url: link.url,
|
|
610
|
+
iconUrl: link.iconUrl
|
|
611
|
+
};
|
|
612
|
+
if (label !== void 0) out2.label = label;
|
|
613
|
+
if (link.featured !== void 0) out2.featured = link.featured;
|
|
614
|
+
if (link.order !== void 0) out2.order = link.order;
|
|
615
|
+
return out2;
|
|
616
|
+
}
|
|
617
|
+
const out = {
|
|
618
|
+
id: link.id,
|
|
619
|
+
type: link.type,
|
|
620
|
+
url: link.url
|
|
621
|
+
};
|
|
622
|
+
if (label !== void 0) out.label = label;
|
|
623
|
+
if (link.featured !== void 0) out.featured = link.featured;
|
|
624
|
+
if (link.order !== void 0) out.order = link.order;
|
|
625
|
+
if (link.iconUrl !== void 0) out.iconUrl = link.iconUrl;
|
|
626
|
+
return out;
|
|
627
|
+
}
|
|
628
|
+
function resolveCareer(career, candidates) {
|
|
629
|
+
const out = {
|
|
630
|
+
id: career.id,
|
|
631
|
+
organization: pickLocalized(career.organization, candidates) ?? "",
|
|
632
|
+
role: pickLocalized(career.role, candidates) ?? "",
|
|
633
|
+
startDate: career.startDate
|
|
634
|
+
};
|
|
635
|
+
const description = pickLocalized(career.description, candidates);
|
|
636
|
+
if (description !== void 0) out.description = description;
|
|
637
|
+
if (career.endDate !== void 0) out.endDate = career.endDate;
|
|
638
|
+
if (career.isCurrent !== void 0) out.isCurrent = career.isCurrent;
|
|
639
|
+
if (career.url !== void 0) out.url = career.url;
|
|
640
|
+
if (career.order !== void 0) out.order = career.order;
|
|
641
|
+
if (career.location) out.location = resolveAddress(career.location, candidates);
|
|
642
|
+
return out;
|
|
643
|
+
}
|
|
644
|
+
function resolveProject(project, candidates) {
|
|
645
|
+
const out = {
|
|
646
|
+
id: project.id,
|
|
647
|
+
title: pickLocalized(project.title, candidates) ?? ""
|
|
648
|
+
};
|
|
649
|
+
const description = pickLocalized(project.description, candidates);
|
|
650
|
+
if (description !== void 0) out.description = description;
|
|
651
|
+
if (project.url !== void 0) out.url = project.url;
|
|
652
|
+
if (project.tags !== void 0) out.tags = project.tags;
|
|
653
|
+
if (project.relatedCareerId !== void 0) out.relatedCareerId = project.relatedCareerId;
|
|
654
|
+
if (project.startDate !== void 0) out.startDate = project.startDate;
|
|
655
|
+
if (project.endDate !== void 0) out.endDate = project.endDate;
|
|
656
|
+
if (project.highlighted !== void 0) out.highlighted = project.highlighted;
|
|
657
|
+
if (project.order !== void 0) out.order = project.order;
|
|
658
|
+
return out;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
// src/jsonld.ts
|
|
662
|
+
var SAMEAS_IDENTITY_TYPES = /* @__PURE__ */ new Set([
|
|
663
|
+
"github",
|
|
664
|
+
"gitlab",
|
|
665
|
+
"linkedin",
|
|
666
|
+
"x",
|
|
667
|
+
"mastodon",
|
|
668
|
+
"bluesky",
|
|
669
|
+
"instagram",
|
|
670
|
+
"youtube",
|
|
671
|
+
"threads",
|
|
672
|
+
"facebook",
|
|
673
|
+
"website",
|
|
674
|
+
"blog"
|
|
675
|
+
]);
|
|
676
|
+
function generatePersonJsonLd(data) {
|
|
677
|
+
const person = buildPerson(data, deriveCanonicalUrl(data));
|
|
678
|
+
return { "@context": "https://schema.org", ...person };
|
|
679
|
+
}
|
|
680
|
+
function generateProfilePageJsonLd(data) {
|
|
681
|
+
const canonicalUrl = deriveCanonicalUrl(data);
|
|
682
|
+
const person = buildPerson(data, canonicalUrl);
|
|
683
|
+
const out = {};
|
|
684
|
+
out["@context"] = "https://schema.org";
|
|
685
|
+
out["@type"] = "ProfilePage";
|
|
686
|
+
if (canonicalUrl !== void 0) out.url = canonicalUrl;
|
|
687
|
+
out.inLanguage = data.resolvedLocale;
|
|
688
|
+
if (data.meta.createdAt !== void 0) out.dateCreated = data.meta.createdAt;
|
|
689
|
+
if (data.meta.updatedAt !== void 0) out.dateModified = data.meta.updatedAt;
|
|
690
|
+
if (data.profile.avatar !== void 0) out.primaryImageOfPage = data.profile.avatar.url;
|
|
691
|
+
out.mainEntity = person;
|
|
692
|
+
return out;
|
|
693
|
+
}
|
|
694
|
+
function generateJsonLd(data) {
|
|
695
|
+
return [generateProfilePageJsonLd(data)];
|
|
696
|
+
}
|
|
697
|
+
function deriveCanonicalUrl(data) {
|
|
698
|
+
const featured = data.links.find((l) => l.type === "website" && l.featured === true);
|
|
699
|
+
return featured?.url;
|
|
700
|
+
}
|
|
701
|
+
function buildPerson(data, canonicalUrl) {
|
|
702
|
+
const { profile, careers, projects, links, skills, contact } = data;
|
|
703
|
+
const out = {};
|
|
704
|
+
out["@type"] = "Person";
|
|
705
|
+
if (canonicalUrl !== void 0) out["@id"] = `${canonicalUrl}#person`;
|
|
706
|
+
out.name = profile.displayName;
|
|
707
|
+
if (profile.bio !== void 0) out.description = profile.bio;
|
|
708
|
+
const image = buildImage(profile.avatar);
|
|
709
|
+
if (image !== void 0) out.image = image;
|
|
710
|
+
if (canonicalUrl !== void 0) out.url = canonicalUrl;
|
|
711
|
+
const { current, past } = partitionCareers(careers);
|
|
712
|
+
const roleFields = buildCurrentRoleFields(current);
|
|
713
|
+
if (roleFields.jobTitle !== void 0) out.jobTitle = roleFields.jobTitle;
|
|
714
|
+
if (roleFields.worksFor !== void 0) out.worksFor = roleFields.worksFor;
|
|
715
|
+
const address = buildAddress(profile.location);
|
|
716
|
+
if (address !== void 0) out.address = address;
|
|
717
|
+
const email = buildEmail(contact);
|
|
718
|
+
if (email !== void 0) out.email = email;
|
|
719
|
+
const knowsAbout = buildKnowsAbout(skills);
|
|
720
|
+
if (knowsAbout !== void 0) out.knowsAbout = knowsAbout;
|
|
721
|
+
const sameAs = buildSameAs(links);
|
|
722
|
+
if (sameAs !== void 0) out.sameAs = sameAs;
|
|
723
|
+
const subjectOf = [...buildPastRoles(past), ...buildProjects(projects)];
|
|
724
|
+
if (subjectOf.length > 0) out.subjectOf = subjectOf;
|
|
725
|
+
return out;
|
|
726
|
+
}
|
|
727
|
+
function buildImage(avatar) {
|
|
728
|
+
if (!avatar) return void 0;
|
|
729
|
+
const out = {};
|
|
730
|
+
out["@type"] = "ImageObject";
|
|
731
|
+
out.url = avatar.url;
|
|
732
|
+
if (avatar.alt !== void 0) out.caption = avatar.alt;
|
|
733
|
+
return out;
|
|
734
|
+
}
|
|
735
|
+
function buildAddress(location) {
|
|
736
|
+
if (!location) return void 0;
|
|
737
|
+
const hasAny = location.country !== void 0 || location.region !== void 0 || location.locality !== void 0;
|
|
738
|
+
if (!hasAny) return void 0;
|
|
739
|
+
const out = {};
|
|
740
|
+
out["@type"] = "PostalAddress";
|
|
741
|
+
if (location.country !== void 0) out.addressCountry = location.country;
|
|
742
|
+
if (location.region !== void 0) out.addressRegion = location.region;
|
|
743
|
+
if (location.locality !== void 0) out.addressLocality = location.locality;
|
|
744
|
+
return out;
|
|
745
|
+
}
|
|
746
|
+
function partitionCareers(careers) {
|
|
747
|
+
const current = [];
|
|
748
|
+
const past = [];
|
|
749
|
+
for (const c of careers) {
|
|
750
|
+
if (c.isCurrent === true) current.push(c);
|
|
751
|
+
else past.push(c);
|
|
752
|
+
}
|
|
753
|
+
return { current, past };
|
|
754
|
+
}
|
|
755
|
+
function buildCurrentRoleFields(current) {
|
|
756
|
+
const head = current[0];
|
|
757
|
+
if (head === void 0) return {};
|
|
758
|
+
const out = {};
|
|
759
|
+
if (head.role !== "") out.jobTitle = head.role;
|
|
760
|
+
out.worksFor = buildWorksFor(head);
|
|
761
|
+
return out;
|
|
762
|
+
}
|
|
763
|
+
function buildWorksFor(career) {
|
|
764
|
+
const out = {};
|
|
765
|
+
out["@type"] = "Organization";
|
|
766
|
+
out.name = career.organization;
|
|
767
|
+
if (career.url !== void 0) out.url = career.url;
|
|
768
|
+
return out;
|
|
769
|
+
}
|
|
770
|
+
function buildPastRoles(past) {
|
|
771
|
+
return past.map((c) => {
|
|
772
|
+
const out = {};
|
|
773
|
+
out["@type"] = "WorkRole";
|
|
774
|
+
out.name = c.role;
|
|
775
|
+
out.memberOf = { "@type": "Organization", name: c.organization };
|
|
776
|
+
out.startDate = c.startDate;
|
|
777
|
+
if (c.endDate !== void 0 && c.endDate !== null) out.endDate = c.endDate;
|
|
778
|
+
return out;
|
|
779
|
+
});
|
|
780
|
+
}
|
|
781
|
+
function buildProjects(projects) {
|
|
782
|
+
return projects.map((p) => {
|
|
783
|
+
const out = {};
|
|
784
|
+
out["@type"] = "CreativeWork";
|
|
785
|
+
out.name = p.title;
|
|
786
|
+
if (p.url !== void 0) out.url = p.url;
|
|
787
|
+
if (p.startDate !== void 0) out.datePublished = p.startDate;
|
|
788
|
+
if (p.tags !== void 0 && p.tags.length > 0) out.about = p.tags;
|
|
789
|
+
return out;
|
|
790
|
+
});
|
|
791
|
+
}
|
|
792
|
+
function buildSameAs(links) {
|
|
793
|
+
const out = links.filter((l) => SAMEAS_IDENTITY_TYPES.has(l.type)).map((l) => l.url);
|
|
794
|
+
return out.length === 0 ? void 0 : out;
|
|
795
|
+
}
|
|
796
|
+
function buildKnowsAbout(skills) {
|
|
797
|
+
if (skills.length === 0) return void 0;
|
|
798
|
+
return skills.map((s) => s.label);
|
|
799
|
+
}
|
|
800
|
+
function buildEmail(contact) {
|
|
801
|
+
if (contact.showEmail !== true) return void 0;
|
|
802
|
+
if (typeof contact.email !== "string" || contact.email.length === 0) return void 0;
|
|
803
|
+
return contact.email;
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
// src/export.ts
|
|
807
|
+
var ImportError = class extends Error {
|
|
808
|
+
errors;
|
|
809
|
+
constructor(message, options) {
|
|
810
|
+
super(message, { cause: options?.cause });
|
|
811
|
+
this.name = "ImportError";
|
|
812
|
+
this.errors = options?.errors;
|
|
813
|
+
}
|
|
814
|
+
};
|
|
815
|
+
function exportTakuhon(data, options = {}) {
|
|
816
|
+
const out = JSON.parse(JSON.stringify(data));
|
|
817
|
+
if (options.updateTimestamp !== false) {
|
|
818
|
+
out.meta = { ...out.meta, updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
819
|
+
}
|
|
820
|
+
return out;
|
|
821
|
+
}
|
|
822
|
+
function importTakuhon(data) {
|
|
823
|
+
const result = validate(data);
|
|
824
|
+
if (!result.ok) {
|
|
825
|
+
throw new ImportError("imported document failed schema validation", { errors: result.errors });
|
|
826
|
+
}
|
|
827
|
+
return JSON.parse(JSON.stringify(result.data));
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
// src/migrations/_chain.ts
|
|
831
|
+
function findMigrationChain(from, to, registry) {
|
|
832
|
+
if (from === to) return [];
|
|
833
|
+
const byFrom = /* @__PURE__ */ new Map();
|
|
834
|
+
for (const m of registry) byFrom.set(m.from, m);
|
|
835
|
+
const chain = [];
|
|
836
|
+
const visited = /* @__PURE__ */ new Set([from]);
|
|
837
|
+
let cur = from;
|
|
838
|
+
while (cur !== to) {
|
|
839
|
+
const next = byFrom.get(cur);
|
|
840
|
+
if (!next) return null;
|
|
841
|
+
if (visited.has(next.to)) return null;
|
|
842
|
+
chain.push(next);
|
|
843
|
+
visited.add(next.to);
|
|
844
|
+
cur = next.to;
|
|
845
|
+
}
|
|
846
|
+
return chain;
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
// src/migrations/index.ts
|
|
850
|
+
var migrations = [];
|
|
851
|
+
|
|
852
|
+
// src/migrate.ts
|
|
853
|
+
var MigrationError = class extends Error {
|
|
854
|
+
constructor(message, options) {
|
|
855
|
+
super(message, options);
|
|
856
|
+
this.name = "MigrationError";
|
|
857
|
+
}
|
|
858
|
+
};
|
|
859
|
+
function migrateTakuhon(data, targetVersion) {
|
|
860
|
+
const sourceVersion = data.schemaVersion;
|
|
861
|
+
if (sourceVersion === targetVersion) {
|
|
862
|
+
return JSON.parse(JSON.stringify(data));
|
|
863
|
+
}
|
|
864
|
+
const chain = findMigrationChain(sourceVersion, targetVersion, migrations);
|
|
865
|
+
if (!chain) {
|
|
866
|
+
throw new MigrationError(`No migration path from ${sourceVersion} to ${targetVersion}`);
|
|
867
|
+
}
|
|
868
|
+
let current = JSON.parse(JSON.stringify(data));
|
|
869
|
+
for (const step of chain) {
|
|
870
|
+
current = step.migrate(current);
|
|
871
|
+
}
|
|
872
|
+
return current;
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
// src/storage-interface.ts
|
|
876
|
+
var StorageError = class extends Error {
|
|
877
|
+
constructor(message, options) {
|
|
878
|
+
super(message, options);
|
|
879
|
+
this.name = "StorageError";
|
|
880
|
+
}
|
|
881
|
+
};
|
|
882
|
+
var NotFoundError = class extends StorageError {
|
|
883
|
+
constructor(message, options) {
|
|
884
|
+
super(message, options);
|
|
885
|
+
this.name = "NotFoundError";
|
|
886
|
+
}
|
|
887
|
+
};
|
|
888
|
+
var ConflictError = class extends StorageError {
|
|
889
|
+
currentVersion;
|
|
890
|
+
constructor(message, options) {
|
|
891
|
+
super(message, { cause: options?.cause });
|
|
892
|
+
this.name = "ConflictError";
|
|
893
|
+
this.currentVersion = options?.currentVersion;
|
|
894
|
+
}
|
|
895
|
+
};
|
|
896
|
+
|
|
897
|
+
// src/index.ts
|
|
898
|
+
var SCHEMA_VERSION = "0.1.0";
|
|
899
|
+
export {
|
|
900
|
+
ConflictError,
|
|
901
|
+
ImportError,
|
|
902
|
+
MigrationError,
|
|
903
|
+
NotFoundError,
|
|
904
|
+
SCHEMA_VERSION,
|
|
905
|
+
SUPPORTED_SCHEMA_VERSIONS,
|
|
906
|
+
StorageError,
|
|
907
|
+
exportTakuhon,
|
|
908
|
+
generateJsonLd,
|
|
909
|
+
generatePersonJsonLd,
|
|
910
|
+
generateProfilePageJsonLd,
|
|
911
|
+
importTakuhon,
|
|
912
|
+
migrateTakuhon,
|
|
913
|
+
migrations,
|
|
914
|
+
normalize,
|
|
915
|
+
resolveLocale,
|
|
916
|
+
schema,
|
|
917
|
+
validate
|
|
918
|
+
};
|
|
919
|
+
//# sourceMappingURL=index.js.map
|