@moloodev/platform 0.9.1 → 0.11.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/package.json +4 -4
- package/src/asset-types.ts +43 -144
- package/src/editor-appearance.ts +7 -0
- package/src/index.ts +3 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@moloodev/platform",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": "./src/index.ts",
|
|
@@ -21,9 +21,9 @@
|
|
|
21
21
|
},
|
|
22
22
|
"devDependencies": {
|
|
23
23
|
"@types/node": "^24.13.3",
|
|
24
|
-
"
|
|
24
|
+
"vitest": "5.1.0",
|
|
25
25
|
"typescript": "^7.0.2",
|
|
26
|
-
"@moloodev/typescript-config": "0.
|
|
26
|
+
"@moloodev/typescript-config": "0.11.0"
|
|
27
27
|
},
|
|
28
28
|
"publishConfig": {
|
|
29
29
|
"access": "public"
|
|
@@ -35,6 +35,6 @@
|
|
|
35
35
|
],
|
|
36
36
|
"scripts": {
|
|
37
37
|
"typecheck": "tsc --noEmit",
|
|
38
|
-
"test": "
|
|
38
|
+
"test": "vitest run --root ../.. --project @moloodev/platform"
|
|
39
39
|
}
|
|
40
40
|
}
|
package/src/asset-types.ts
CHANGED
|
@@ -1,25 +1,5 @@
|
|
|
1
|
-
/**
|
|
2
|
-
export
|
|
3
|
-
"tabler:folder",
|
|
4
|
-
"tabler:hierarchy",
|
|
5
|
-
"tabler:layout",
|
|
6
|
-
"tabler:table",
|
|
7
|
-
"tabler:markdown",
|
|
8
|
-
"tabler:photo",
|
|
9
|
-
"tabler:movie",
|
|
10
|
-
"tabler:music",
|
|
11
|
-
"tabler:file-text",
|
|
12
|
-
"tabler:database",
|
|
13
|
-
"tabler:code",
|
|
14
|
-
"tabler:archive",
|
|
15
|
-
"tabler:typography",
|
|
16
|
-
"tabler:cube",
|
|
17
|
-
"tabler:file",
|
|
18
|
-
"tabler:package",
|
|
19
|
-
"tabler:help",
|
|
20
|
-
] as const;
|
|
21
|
-
|
|
22
|
-
export type AssetTypeIcon = (typeof FILE_APPEARANCE_ICONS)[number];
|
|
1
|
+
/** Tabler URI for a file glyph; hosts use a neutral glyph for unknown names. */
|
|
2
|
+
export type AssetTypeIcon = `tabler:${string}`;
|
|
23
3
|
|
|
24
4
|
/**
|
|
25
5
|
* Primitive storage and CRDT collaboration mode for an asset.
|
|
@@ -56,6 +36,8 @@ export type FileAppearance = {
|
|
|
56
36
|
|
|
57
37
|
/** Optional file matcher rule declared by an installed `EditorManifest`. */
|
|
58
38
|
export type FileAppearanceMatcher = {
|
|
39
|
+
/** Explicit asset kind id for type-only surfaces; never inferred from an editor. */
|
|
40
|
+
type?: string;
|
|
59
41
|
extensions?: readonly string[];
|
|
60
42
|
mimeTypes?: readonly string[];
|
|
61
43
|
schema?: string;
|
|
@@ -71,31 +53,32 @@ export const UNKNOWN_FILE_APPEARANCE: FileAppearance = {
|
|
|
71
53
|
icon: "tabler:help",
|
|
72
54
|
};
|
|
73
55
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
"moloo.
|
|
77
|
-
"moloo.
|
|
78
|
-
"moloo.
|
|
79
|
-
"moloo.
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
"moloo.image": { label: "이미지", color: "#a5c65b", icon: "tabler:photo" },
|
|
83
|
-
"moloo.video": { label: "비디오", color: "#8f7fe0", icon: "tabler:movie" },
|
|
84
|
-
"moloo.audio": { label: "오디오", color: "#c78cd9", icon: "tabler:music" },
|
|
85
|
-
"moloo.document": { label: "문서", color: "#89b4e8", icon: "tabler:file-text" },
|
|
86
|
-
"moloo.data": { label: "데이터", color: "#66b06f", icon: "tabler:database" },
|
|
87
|
-
"moloo.code": { label: "코드", color: "#5c73c9", icon: "tabler:code" },
|
|
88
|
-
"moloo.archive": { label: "압축", color: "#b3a27a", icon: "tabler:archive" },
|
|
89
|
-
"moloo.font": { label: "글꼴", color: "#b09bbf", icon: "tabler:typography" },
|
|
90
|
-
"moloo.model": { label: "3D 모델", color: "#d1b24f", icon: "tabler:cube" },
|
|
91
|
-
"moloo.binary": { label: "바이너리", color: "#a7adba", icon: "tabler:file" },
|
|
92
|
-
} as const satisfies Record<string, FileAppearance>;
|
|
56
|
+
/** Classification identifiers, independent of display identity. */
|
|
57
|
+
export const KNOWN_PATTERN_IDS = [
|
|
58
|
+
"moloo.folder", "moloo.loop", "moloo.table", "moloo.markdown",
|
|
59
|
+
"moloo.bundle", "moloo.layout", "moloo.catalog", "moloo.image",
|
|
60
|
+
"moloo.video", "moloo.audio", "moloo.document", "moloo.data",
|
|
61
|
+
"moloo.code", "moloo.archive", "moloo.font", "moloo.model", "moloo.binary",
|
|
62
|
+
] as const;
|
|
63
|
+
export type KnownPatternId = (typeof KNOWN_PATTERN_IDS)[number];
|
|
93
64
|
|
|
94
|
-
|
|
65
|
+
const DIRECTORY_APPEARANCE: FileAppearance = {
|
|
66
|
+
label: "폴더", color: UNKNOWN_FILE_APPEARANCE.color, icon: "tabler:folder",
|
|
67
|
+
};
|
|
95
68
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
69
|
+
/** Type-only surfaces use the same explicit file-type declaration as file surfaces. */
|
|
70
|
+
export function resolveTypeAppearance(
|
|
71
|
+
type: string,
|
|
72
|
+
matchers: readonly FileAppearanceMatcher[] = [],
|
|
73
|
+
): FileAppearance {
|
|
74
|
+
if (type === "moloo.folder") return DIRECTORY_APPEARANCE;
|
|
75
|
+
const matcher = matchers.find((entry) => entry.type === type);
|
|
76
|
+
return {
|
|
77
|
+
label: matcher?.label ?? (type || UNKNOWN_FILE_APPEARANCE.label),
|
|
78
|
+
icon: matcher?.icon ?? UNKNOWN_FILE_APPEARANCE.icon,
|
|
79
|
+
color: matcher?.color ?? UNKNOWN_FILE_APPEARANCE.color,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
99
82
|
|
|
100
83
|
const DOCUMENT_SUBTYPES = new Set([
|
|
101
84
|
"pdf",
|
|
@@ -340,14 +323,6 @@ export function classifyFile(input: {
|
|
|
340
323
|
};
|
|
341
324
|
}
|
|
342
325
|
|
|
343
|
-
const FIRST_PARTY_SCHEMA_APPEARANCE: Record<string, FileAppearance> = {
|
|
344
|
-
[FIRST_PARTY_SCHEMAS.layout]: PATTERN_APPEARANCE_MAP["moloo.layout"],
|
|
345
|
-
[FIRST_PARTY_SCHEMAS.catalog]: PATTERN_APPEARANCE_MAP["moloo.catalog"],
|
|
346
|
-
[FIRST_PARTY_SCHEMAS.table]: PATTERN_APPEARANCE_MAP["moloo.table"],
|
|
347
|
-
[FIRST_PARTY_SCHEMAS.loop]: PATTERN_APPEARANCE_MAP["moloo.loop"],
|
|
348
|
-
[FIRST_PARTY_SCHEMAS.bundle]: PATTERN_APPEARANCE_MAP["moloo.bundle"],
|
|
349
|
-
};
|
|
350
|
-
|
|
351
326
|
function isCompoundExtension(ext: string): boolean {
|
|
352
327
|
const withoutLeadingDot = ext.startsWith(".") ? ext.slice(1) : ext;
|
|
353
328
|
return withoutLeadingDot.includes(".");
|
|
@@ -365,7 +340,8 @@ function scoreMatcher(
|
|
|
365
340
|
if (matcher.storageMode && file.storageMode && matcher.storageMode !== file.storageMode) {
|
|
366
341
|
return 0;
|
|
367
342
|
}
|
|
368
|
-
if (matcher.schema && file.schema
|
|
343
|
+
if (matcher.schema && file.schema) {
|
|
344
|
+
if (matcher.schema !== file.schema) return 0;
|
|
369
345
|
return 400;
|
|
370
346
|
}
|
|
371
347
|
const lowerName = file.name.toLowerCase();
|
|
@@ -396,119 +372,42 @@ function scoreMatcher(
|
|
|
396
372
|
}
|
|
397
373
|
|
|
398
374
|
/**
|
|
399
|
-
*
|
|
400
|
-
*
|
|
401
|
-
*
|
|
375
|
+
* File identity comes only from manifest fileTypes. Highest specificity wins:
|
|
376
|
+
* schema, compound extension, extension, exact MIME, MIME family. Ties keep the
|
|
377
|
+
* first declaration (hosts sort manifests by editor id). Missing fields are
|
|
378
|
+
* neutral, never borrowed from a different rule or the owning editor.
|
|
402
379
|
*/
|
|
403
380
|
export function resolveFileAppearance(
|
|
404
381
|
file: {
|
|
405
382
|
name: string;
|
|
406
383
|
storageMode?: string;
|
|
407
|
-
contentType
|
|
384
|
+
contentType?: string | null;
|
|
408
385
|
schema?: string | null;
|
|
409
386
|
},
|
|
410
387
|
matchers: readonly FileAppearanceMatcher[] = [],
|
|
411
388
|
): FileAppearance {
|
|
412
389
|
if (file.name === ".keep" || normalizeMimeType(file.contentType) === "application/x-directory") {
|
|
413
|
-
return
|
|
390
|
+
return DIRECTORY_APPEARANCE;
|
|
414
391
|
}
|
|
415
|
-
|
|
416
|
-
const classified = classifyFile({
|
|
417
|
-
fileName: file.name,
|
|
418
|
-
contentType: file.contentType,
|
|
419
|
-
});
|
|
420
|
-
const effectiveSchema = file.schema ?? classified.defaultSchema;
|
|
392
|
+
const classified = classifyFile({ fileName: file.name, contentType: file.contentType });
|
|
421
393
|
const effectiveFile = {
|
|
422
394
|
...file,
|
|
423
395
|
storageMode: file.storageMode ?? classified.storageMode,
|
|
424
396
|
contentType: normalizeMimeType(file.contentType) || classified.contentType,
|
|
425
|
-
schema:
|
|
397
|
+
schema: file.schema ?? classified.defaultSchema,
|
|
426
398
|
};
|
|
427
|
-
|
|
428
399
|
let bestScore = 0;
|
|
429
|
-
let bestMatcher: FileAppearanceMatcher |
|
|
400
|
+
let bestMatcher: FileAppearanceMatcher | undefined;
|
|
430
401
|
for (const matcher of matchers) {
|
|
431
402
|
const score = scoreMatcher(effectiveFile, matcher);
|
|
432
|
-
if (score > bestScore
|
|
403
|
+
if (score > bestScore) {
|
|
433
404
|
bestScore = score;
|
|
434
405
|
bestMatcher = matcher;
|
|
435
406
|
}
|
|
436
407
|
}
|
|
437
|
-
|
|
438
|
-
if (effectiveSchema && FIRST_PARTY_SCHEMA_APPEARANCE[effectiveSchema]) {
|
|
439
|
-
const base = FIRST_PARTY_SCHEMA_APPEARANCE[effectiveSchema]!;
|
|
440
|
-
// Only let a matcher override a first-party schema/compound-extension appearance
|
|
441
|
-
// when it matched at schema (>= 400) or compound-extension (>= 300) specificity,
|
|
442
|
-
// never on a generic .json or application/json rule.
|
|
443
|
-
const override = bestScore >= 300 ? bestMatcher : null;
|
|
444
|
-
return {
|
|
445
|
-
label: override?.label ?? base.label,
|
|
446
|
-
color: override?.color ?? base.color,
|
|
447
|
-
icon: override?.icon ?? base.icon,
|
|
448
|
-
};
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
const lowerName = file.name.toLowerCase();
|
|
452
|
-
const mime = effectiveFile.contentType;
|
|
453
|
-
if (
|
|
454
|
-
mime === "text/markdown" ||
|
|
455
|
-
lowerName.endsWith(".md") ||
|
|
456
|
-
lowerName.endsWith(".markdown")
|
|
457
|
-
) {
|
|
458
|
-
const md = PATTERN_APPEARANCE_MAP["moloo.markdown"];
|
|
459
|
-
const override = bestScore >= 150 ? bestMatcher : null;
|
|
460
|
-
return {
|
|
461
|
-
label: override?.label ?? md.label,
|
|
462
|
-
color: override?.color ?? md.color,
|
|
463
|
-
icon: override?.icon ?? md.icon,
|
|
464
|
-
};
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
const fallbackKind = patternForContentType(mime);
|
|
468
|
-
const fallback = PATTERN_APPEARANCE_MAP[fallbackKind];
|
|
469
408
|
return {
|
|
470
|
-
label: bestMatcher?.label ??
|
|
471
|
-
|
|
472
|
-
|
|
409
|
+
label: bestMatcher?.label ?? UNKNOWN_FILE_APPEARANCE.label,
|
|
410
|
+
icon: bestMatcher?.icon ?? UNKNOWN_FILE_APPEARANCE.icon,
|
|
411
|
+
color: bestMatcher?.color ?? UNKNOWN_FILE_APPEARANCE.color,
|
|
473
412
|
};
|
|
474
413
|
}
|
|
475
|
-
|
|
476
|
-
/**
|
|
477
|
-
* Resolve cosmetic appearance for either a pattern/kind identifier (`moloo.*`)
|
|
478
|
-
* or an asset summary with `name`, `storageMode`, `contentType`, and `schema`.
|
|
479
|
-
* When file fields (`storageMode`, `schema`, or `contentType` + `name`) are
|
|
480
|
-
* present, they take precedence over a legacy `type` string.
|
|
481
|
-
*/
|
|
482
|
-
export function resolvePatternAppearance(
|
|
483
|
-
input:
|
|
484
|
-
| string
|
|
485
|
-
| {
|
|
486
|
-
type?: string;
|
|
487
|
-
name?: string;
|
|
488
|
-
storageMode?: StorageMode;
|
|
489
|
-
contentType?: string | null;
|
|
490
|
-
schema?: string | null;
|
|
491
|
-
},
|
|
492
|
-
): FileAppearance {
|
|
493
|
-
if (typeof input === "string") {
|
|
494
|
-
return Object.hasOwn(PATTERN_APPEARANCE_MAP, input)
|
|
495
|
-
? PATTERN_APPEARANCE_MAP[input as KnownPatternId]
|
|
496
|
-
: UNKNOWN_FILE_APPEARANCE;
|
|
497
|
-
}
|
|
498
|
-
if (
|
|
499
|
-
input.storageMode ||
|
|
500
|
-
input.schema ||
|
|
501
|
-
(input.contentType && input.name)
|
|
502
|
-
) {
|
|
503
|
-
return resolveFileAppearance({
|
|
504
|
-
name: input.name ?? "",
|
|
505
|
-
storageMode: input.storageMode,
|
|
506
|
-
contentType: input.contentType ?? "application/octet-stream",
|
|
507
|
-
schema: input.schema ?? null,
|
|
508
|
-
});
|
|
509
|
-
}
|
|
510
|
-
if (input.type && Object.hasOwn(PATTERN_APPEARANCE_MAP, input.type)) {
|
|
511
|
-
return PATTERN_APPEARANCE_MAP[input.type as KnownPatternId];
|
|
512
|
-
}
|
|
513
|
-
return UNKNOWN_FILE_APPEARANCE;
|
|
514
|
-
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** Editor identity is declared at the manifest top level, independently of files. */
|
|
2
|
+
export function resolveEditorAppearance(editor: { icon?: string; color?: string }): {
|
|
3
|
+
icon: string;
|
|
4
|
+
color: string;
|
|
5
|
+
} {
|
|
6
|
+
return { icon: editor.icon ?? "tabler:help", color: editor.color ?? "#94a3b8" };
|
|
7
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -7,7 +7,6 @@ export {
|
|
|
7
7
|
} from "./qualified-name";
|
|
8
8
|
|
|
9
9
|
export {
|
|
10
|
-
FILE_APPEARANCE_ICONS,
|
|
11
10
|
FIRST_PARTY_SCHEMAS,
|
|
12
11
|
KNOWN_PATTERN_IDS,
|
|
13
12
|
STORAGE_MODES,
|
|
@@ -16,7 +15,7 @@ export {
|
|
|
16
15
|
isStorageMode,
|
|
17
16
|
patternForContentType,
|
|
18
17
|
resolveFileAppearance,
|
|
19
|
-
|
|
18
|
+
resolveTypeAppearance,
|
|
20
19
|
type AssetTypeIcon,
|
|
21
20
|
type FileAppearance,
|
|
22
21
|
type FileAppearanceMatcher,
|
|
@@ -94,3 +93,5 @@ export { EDITOR_TRACKS, isEditorTrack, type EditorTrack } from "./editor-track";
|
|
|
94
93
|
export type { PublishTarget } from "./publish-target";
|
|
95
94
|
export { PUBLISH_ERROR_MESSAGES, publishErrorBody, parsePublishName } from "./publish-target";
|
|
96
95
|
export { isPublisherHandle, isReservedPublisherHandle, reservedPublishers } from "./publisher-handle";
|
|
96
|
+
|
|
97
|
+
export { resolveEditorAppearance } from "./editor-appearance";
|