@terrafirmagreg-team/game 0.0.0-stage → 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/package.json +58 -3
- package/src/components/entry/atlas-sprite.tsx +57 -0
- package/src/components/entry/copy-id-menu.tsx +55 -0
- package/src/components/entry/entry.tsx +229 -0
- package/src/components/entry/frame.tsx +32 -0
- package/src/components/entry/group.tsx +180 -0
- package/src/components/entry/icon/tabs.tsx +118 -0
- package/src/components/entry/icon.tsx +187 -0
- package/src/components/entry/list.tsx +70 -0
- package/src/components/entry/mc-text.tsx +92 -0
- package/src/components/entry/stack-count.tsx +14 -0
- package/src/components/entry/tooltip.tsx +129 -0
- package/src/components/mdx/item-icon.tsx +69 -0
- package/src/components/preview-icon.tsx +25 -0
- package/src/hooks/use-dump-location.ts +18 -0
- package/src/hooks/use-entry-context.ts +35 -0
- package/src/hooks/use-icon-source.ts +297 -0
- package/src/hooks/use-lang-map.ts +18 -0
- package/src/lib/dump/assets/atlas/dto.ts +49 -0
- package/src/lib/dump/assets/atlas/lookup.ts +206 -0
- package/src/lib/dump/assets/atlas/sprites.ts +130 -0
- package/src/lib/dump/assets/lang/load.ts +17 -0
- package/src/lib/dump/assets/lang/names.ts +244 -0
- package/src/lib/dump/assets/urls.ts +143 -0
- package/src/lib/dump/client/atlases.ts +83 -0
- package/src/lib/dump/client/dump.ts +188 -0
- package/src/lib/dump/client/groups.ts +31 -0
- package/src/lib/dump/client/keybinds.ts +11 -0
- package/src/lib/dump/client/mods.ts +16 -0
- package/src/lib/dump/client/registry.ts +125 -0
- package/src/lib/dump/client/tabs.ts +27 -0
- package/src/lib/dump/client/tags.ts +64 -0
- package/src/lib/dump/contract/kind.ts +61 -0
- package/src/lib/dump/contract/read.ts +77 -0
- package/src/lib/dump/contract/ref.ts +262 -0
- package/src/lib/dump/contract/text.ts +86 -0
- package/src/lib/dump/data/registry/dto.ts +56 -0
- package/src/lib/dump/data/registry/parse.ts +291 -0
- package/src/lib/dump/data/tab/dto.ts +13 -0
- package/src/lib/dump/data/tab/parse.ts +26 -0
- package/src/lib/dump/data/tag/dto.ts +7 -0
- package/src/lib/dump/data/tag/parse.ts +57 -0
- package/src/lib/dump/gen/data/guides/book.ts +56 -0
- package/src/lib/dump/gen/data/guides/category.ts +30 -0
- package/src/lib/dump/gen/data/guides/entry.ts +48 -0
- package/src/lib/dump/gen/data/models/model.ts +3 -0
- package/src/lib/dump/gen/data/quests/chapter.ts +126 -0
- package/src/lib/dump/gen/data/quests/chapter_groups.ts +11 -0
- package/src/lib/dump/gen/data/quests/data.ts +40 -0
- package/src/lib/dump/gen/data/recipes/recipe.ts +78 -0
- package/src/lib/dump/gen/index.ts +20 -0
- package/src/lib/dump/gen/keybinds.ts +3 -0
- package/src/lib/dump/gen/lang.ts +3 -0
- package/src/lib/dump/gen/manifest.ts +59 -0
- package/src/lib/dump/gen/registry/blocks.ts +60 -0
- package/src/lib/dump/gen/registry/entities.ts +49 -0
- package/src/lib/dump/gen/registry/fluids.ts +48 -0
- package/src/lib/dump/gen/registry/groups.ts +14 -0
- package/src/lib/dump/gen/registry/items.ts +51 -0
- package/src/lib/dump/gen/registry/recipe_categories.ts +64 -0
- package/src/lib/dump/gen/registry/recipes.ts +3 -0
- package/src/lib/dump/gen/registry/tabs.ts +46 -0
- package/src/lib/dump/gen/registry/tags.ts +11 -0
- package/src/lib/dump/json-reader.ts +231 -0
- package/src/lib/dump/manifest/dto.ts +11 -0
- package/src/lib/dump/manifest/parse.ts +167 -0
- package/src/lib/dump/mods/dto.ts +40 -0
- package/src/lib/dump/mods/index.ts +81 -0
- package/src/lib/dump/mods/load.ts +61 -0
- package/src/lib/dump/paths.ts +171 -0
- package/src/lib/dump/reader.ts +15 -0
- package/src/lib/entry/display.ts +4 -0
- package/src/lib/entry/entry.ts +57 -0
- package/src/lib/entry/formula.ts +226 -0
- package/src/lib/entry/href.ts +148 -0
- package/src/lib/entry/icon-load.ts +195 -0
- package/src/lib/entry/icon.ts +10 -0
- package/src/lib/entry/tooltip.ts +60 -0
- package/src/lib/preview.ts +18 -0
- package/src/lib/search.ts +31 -0
- package/src/lib/snbt.ts +246 -0
- package/src/locales/en_us.json +9 -0
- package/src/locales/ru_ru.json +9 -0
- package/src/locales.ts +5 -0
- package/src/types/entry.ts +52 -0
- package/styles/fonts.css +15 -0
- package/styles/tokens.css +15 -0
- package/styles.css +2 -0
- package/README.md +0 -3
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
DumpManifest,
|
|
3
|
+
NamespaceDisplay,
|
|
4
|
+
} from '@terrafirmagreg-team/game/lib/dump/manifest/dto';
|
|
5
|
+
import type { DumpIndex, DumpMod, GuideBookRef } from '@terrafirmagreg-team/game/lib/dump/mods/dto';
|
|
6
|
+
|
|
7
|
+
function registryRel(
|
|
8
|
+
meta: NamespaceDisplay,
|
|
9
|
+
file: string,
|
|
10
|
+
): string | null {
|
|
11
|
+
return meta.registry.includes(file) ? `registry/${file}.json` : null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function loadBooks(
|
|
15
|
+
namespace: string,
|
|
16
|
+
guides: string[],
|
|
17
|
+
locales: string[],
|
|
18
|
+
): { guides: Record<string, string>; books: GuideBookRef[] } {
|
|
19
|
+
const out: { guides: Record<string, string>; books: GuideBookRef[] } = {
|
|
20
|
+
guides: {},
|
|
21
|
+
books: [],
|
|
22
|
+
};
|
|
23
|
+
for (const name of guides) {
|
|
24
|
+
if (!name || name.includes('..') || name.includes('/')) continue;
|
|
25
|
+
const bookId = `${namespace}:${name}`;
|
|
26
|
+
const dir = `data/${namespace}/guides/${name}`;
|
|
27
|
+
out.guides[bookId] = dir;
|
|
28
|
+
out.books.push({
|
|
29
|
+
namespace,
|
|
30
|
+
modId: namespace,
|
|
31
|
+
bookPath: name,
|
|
32
|
+
bookId,
|
|
33
|
+
dir,
|
|
34
|
+
locales,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
return out;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function loadMod(
|
|
41
|
+
namespace: string,
|
|
42
|
+
meta: NamespaceDisplay,
|
|
43
|
+
manifest: DumpManifest,
|
|
44
|
+
): DumpMod {
|
|
45
|
+
const dumpLangs = new Set(manifest.languages);
|
|
46
|
+
const languages: Record<string, string> = {};
|
|
47
|
+
for (const code of meta.languages) {
|
|
48
|
+
if (!dumpLangs.has(code)) continue;
|
|
49
|
+
languages[code] = `lang/${code}.json`;
|
|
50
|
+
}
|
|
51
|
+
return {
|
|
52
|
+
namespace,
|
|
53
|
+
languages,
|
|
54
|
+
keybindsFile: meta.keybinds ? 'keybinds.json' : null,
|
|
55
|
+
items: registryRel(meta, 'items'),
|
|
56
|
+
blocks: registryRel(meta, 'blocks'),
|
|
57
|
+
fluids: registryRel(meta, 'fluids'),
|
|
58
|
+
entities: registryRel(meta, 'entities'),
|
|
59
|
+
tabs: registryRel(meta, 'tabs'),
|
|
60
|
+
groups: registryRel(meta, 'groups'),
|
|
61
|
+
recipeCategories: registryRel(meta, 'recipe_categories'),
|
|
62
|
+
tags: registryRel(meta, 'tags'),
|
|
63
|
+
recipeIndex: registryRel(meta, 'recipes'),
|
|
64
|
+
modelIndex: null,
|
|
65
|
+
questData: meta.quests ? `data/${namespace}/quests/data.json` : null,
|
|
66
|
+
...loadBooks(namespace, meta.guides, manifest.languages),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function dumpIndexFromManifest(
|
|
71
|
+
manifest: DumpManifest,
|
|
72
|
+
root: string,
|
|
73
|
+
): DumpIndex {
|
|
74
|
+
const mods = Object.keys(manifest.namespaces)
|
|
75
|
+
.sort((a, b) => a.localeCompare(b))
|
|
76
|
+
.flatMap((namespace) => {
|
|
77
|
+
const row = manifest.namespaces[namespace];
|
|
78
|
+
return row ? [loadMod(namespace, row, manifest)] : [];
|
|
79
|
+
});
|
|
80
|
+
return { root, manifest, mods };
|
|
81
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import 'server-only';
|
|
2
|
+
import { asRecord, asString } from '@terrafirmagreg-team/game/lib/dump/contract/read';
|
|
3
|
+
import { dumpLocation } from '@terrafirmagreg-team/game/lib/dump/paths';
|
|
4
|
+
import { dumpReader } from '@terrafirmagreg-team/game/lib/dump/reader';
|
|
5
|
+
import { parseDumpManifest } from '@terrafirmagreg-team/game/lib/dump/manifest/parse';
|
|
6
|
+
import { parseDatasetRef } from '@terrafirmagreg-team/kit/lib/location';
|
|
7
|
+
import { sectionConfig } from '@terrafirmagreg-team/kit/lib/config';
|
|
8
|
+
import { createAsyncCache } from '@terrafirmagreg-team/ui/lib/utils/async-cache';
|
|
9
|
+
import { dumpIndexFromManifest } from '@terrafirmagreg-team/game/lib/dump/mods/index';
|
|
10
|
+
import type { DumpIndex } from '@terrafirmagreg-team/game/lib/dump/mods/dto';
|
|
11
|
+
|
|
12
|
+
const dumpCache = createAsyncCache(
|
|
13
|
+
async (dataset: string): Promise<DumpIndex | null> => {
|
|
14
|
+
const reader = await dumpReader(dataset);
|
|
15
|
+
const manifest = parseDumpManifest(await reader.getJson('manifest.json'));
|
|
16
|
+
if (!manifest) return null;
|
|
17
|
+
return dumpIndexFromManifest(
|
|
18
|
+
manifest,
|
|
19
|
+
dumpLocation(dataset)?.root ?? parseDatasetRef(dataset).id,
|
|
20
|
+
);
|
|
21
|
+
},
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
export function loadDatasetDump(
|
|
25
|
+
dataset: string,
|
|
26
|
+
): Promise<DumpIndex | null> {
|
|
27
|
+
return dumpCache(dataset);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function discoverDumpDatasets(): Promise<string[]> {
|
|
31
|
+
const refs = await Promise.all(
|
|
32
|
+
Object.keys(sectionConfig().datasetConfig).map(async (id) => {
|
|
33
|
+
const versions = asRecord(
|
|
34
|
+
await dumpReader(id).then((reader) =>
|
|
35
|
+
reader.getJson('versions.json'),
|
|
36
|
+
),
|
|
37
|
+
);
|
|
38
|
+
const latest = asString(versions?.latest);
|
|
39
|
+
if (!versions || !latest) return [];
|
|
40
|
+
const manifest = await dumpReader(id).then((reader) =>
|
|
41
|
+
reader.getJson('manifest.json'),
|
|
42
|
+
);
|
|
43
|
+
if (!manifest) return [];
|
|
44
|
+
const listed = Array.isArray(versions.versions)
|
|
45
|
+
? versions.versions.filter(
|
|
46
|
+
(row): row is string => typeof row === 'string',
|
|
47
|
+
)
|
|
48
|
+
: [];
|
|
49
|
+
const published = await Promise.all(
|
|
50
|
+
listed.map(async (version) => {
|
|
51
|
+
const ok = await dumpReader(`${id}@${version}`).then((reader) =>
|
|
52
|
+
reader.getJson('manifest.json'),
|
|
53
|
+
);
|
|
54
|
+
return ok ? `${id}@${version}` : null;
|
|
55
|
+
}),
|
|
56
|
+
);
|
|
57
|
+
return [id, ...published.filter((row): row is string => row !== null)];
|
|
58
|
+
}),
|
|
59
|
+
);
|
|
60
|
+
return refs.flat().sort();
|
|
61
|
+
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
import { stackIdBase } from '@terrafirmagreg-team/game/lib/dump/contract/ref';
|
|
4
|
+
|
|
5
|
+
export type DumpLocation = {
|
|
6
|
+
|
|
7
|
+
root: string;
|
|
8
|
+
|
|
9
|
+
files?: Record<string, string>;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const dumpLocations = new Map<string, DumpLocation>();
|
|
13
|
+
const dumpLocationListeners = new Set<() => void>();
|
|
14
|
+
|
|
15
|
+
export function registerDumpLocation(
|
|
16
|
+
dataset: string,
|
|
17
|
+
location: DumpLocation,
|
|
18
|
+
): void {
|
|
19
|
+
dumpLocations.set(dataset, location);
|
|
20
|
+
for (const listener of dumpLocationListeners) listener();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function subscribeDumpLocations(listener: () => void): () => void {
|
|
24
|
+
dumpLocationListeners.add(listener);
|
|
25
|
+
return () => dumpLocationListeners.delete(listener);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function dumpLocation(dataset: string): DumpLocation | undefined {
|
|
29
|
+
return dumpLocations.get(dataset);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function dumpStorageUrl(storageRel: string): string {
|
|
33
|
+
const origin = (process.env.NEXT_PUBLIC_DUMP_STORAGE_URL ?? '').replace(
|
|
34
|
+
/\/+$/,
|
|
35
|
+
'',
|
|
36
|
+
);
|
|
37
|
+
if (!origin) {
|
|
38
|
+
throw new Error(
|
|
39
|
+
'NEXT_PUBLIC_DUMP_STORAGE_URL is not set — dump storage is required.',
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
return `${origin}/${storageRel}`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function dumpPublicUrl(
|
|
46
|
+
dataset: string,
|
|
47
|
+
...segments: string[]
|
|
48
|
+
): string | undefined {
|
|
49
|
+
const ds = cleanDumpSegment(dataset);
|
|
50
|
+
if (!ds || ds.includes('/')) {
|
|
51
|
+
throw new Error(`Invalid dump dataset segment: ${dataset}`);
|
|
52
|
+
}
|
|
53
|
+
const rel = segments
|
|
54
|
+
.map(cleanDumpSegment)
|
|
55
|
+
.filter((s): s is string => Boolean(s))
|
|
56
|
+
.join('/');
|
|
57
|
+
const loc = dumpLocation(dataset);
|
|
58
|
+
if (!loc) return undefined;
|
|
59
|
+
const key = rel ? loc.files?.[rel] : undefined;
|
|
60
|
+
const storageRel = key ?? `${loc.root}${rel ? `/${rel}` : ''}`;
|
|
61
|
+
return dumpStorageUrl(storageRel);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function cleanDumpSegment(segment: string): string | null {
|
|
65
|
+
const cleaned = segment.replaceAll('\\', '/').replace(/^\/+/, '');
|
|
66
|
+
if (!cleaned || cleaned.includes('..')) return null;
|
|
67
|
+
return cleaned;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function cleanDumpRel(rel: string): string | null {
|
|
71
|
+
return cleanDumpSegment(rel);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function dumpDirOf(rel: string): string {
|
|
75
|
+
const cleaned = cleanDumpRel(rel) ?? '';
|
|
76
|
+
const slash = cleaned.lastIndexOf('/');
|
|
77
|
+
return slash >= 0 ? cleaned.slice(0, slash) : '';
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function joinDumpRel(
|
|
81
|
+
baseFileRel: string,
|
|
82
|
+
childRel: string,
|
|
83
|
+
): string | null {
|
|
84
|
+
const child = cleanDumpRel(childRel);
|
|
85
|
+
if (!child) return null;
|
|
86
|
+
const dir = dumpDirOf(baseFileRel);
|
|
87
|
+
return dir ? `${dir}/${child}` : child;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const TAG_REGISTRIES = ['items', 'blocks', 'fluids', 'entities'] as const;
|
|
91
|
+
|
|
92
|
+
export function tagHashId(tagId: string): string {
|
|
93
|
+
return `#${tagId.replace(/^#+/, '')}`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function tagIdForms(tagId: string): string[] {
|
|
97
|
+
const raw = tagId.startsWith('#') ? tagId.slice(1) : tagId;
|
|
98
|
+
return [raw, `#${raw}`];
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function tagRegistryKeys(
|
|
102
|
+
tagId: string,
|
|
103
|
+
preferredRegistry?: string,
|
|
104
|
+
): string[] {
|
|
105
|
+
const raw = tagId.startsWith('#') ? tagId.slice(1) : tagId;
|
|
106
|
+
const parts = splitDumpId(raw);
|
|
107
|
+
if (!parts) return [];
|
|
108
|
+
const registries = preferredRegistry
|
|
109
|
+
? [
|
|
110
|
+
preferredRegistry,
|
|
111
|
+
...TAG_REGISTRIES.filter((registry) => registry !== preferredRegistry),
|
|
112
|
+
]
|
|
113
|
+
: TAG_REGISTRIES;
|
|
114
|
+
return registries.map((registry) => `${parts.ns}:${registry}/${parts.path}`);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function matchTagIndexKey(
|
|
118
|
+
entries: Record<string, unknown> | undefined,
|
|
119
|
+
tagId: string,
|
|
120
|
+
preferredRegistry?: string,
|
|
121
|
+
): string | undefined {
|
|
122
|
+
if (!entries) return undefined;
|
|
123
|
+
for (const form of tagIdForms(tagId)) {
|
|
124
|
+
if (entries[form] !== undefined) return form;
|
|
125
|
+
}
|
|
126
|
+
for (const key of tagRegistryKeys(tagId, preferredRegistry)) {
|
|
127
|
+
if (entries[key] !== undefined) return key;
|
|
128
|
+
}
|
|
129
|
+
const raw = tagId.startsWith('#') ? tagId.slice(1) : tagId;
|
|
130
|
+
const parts = splitDumpId(raw);
|
|
131
|
+
if (!parts) return undefined;
|
|
132
|
+
const candidates = Object.keys(entries).filter(
|
|
133
|
+
(key) => key.startsWith(`${parts.ns}:`) && key.endsWith(`/${parts.path}`),
|
|
134
|
+
);
|
|
135
|
+
return candidates.sort()[0];
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function splitDumpId(
|
|
139
|
+
id: string,
|
|
140
|
+
): { ns: string; path: string } | undefined {
|
|
141
|
+
const sep = id.indexOf(':');
|
|
142
|
+
if (sep < 1) return undefined;
|
|
143
|
+
const ns = id.slice(0, sep);
|
|
144
|
+
const path = id
|
|
145
|
+
.slice(sep + 1)
|
|
146
|
+
.replaceAll('\\', '/')
|
|
147
|
+
.replace(/\/{2,}/g, '/')
|
|
148
|
+
.replace(/^\/+|\/+$/g, '');
|
|
149
|
+
if (!ns || !path || ns.includes('/') || ns.includes('..') || path.includes('..')) {
|
|
150
|
+
return undefined;
|
|
151
|
+
}
|
|
152
|
+
return { ns, path };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function registryId(id: string): string {
|
|
156
|
+
return stackIdBase(id);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function tagPseudoId(tag: string, kind?: string): string {
|
|
160
|
+
const raw = tagIdForms(tag)[0]!;
|
|
161
|
+
const parts = splitDumpId(raw);
|
|
162
|
+
return `emi:tag/${kind ?? 'item'}/${parts ? `${parts.ns}/${parts.path}` : raw}`;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function dumpRelPublicUrl(
|
|
166
|
+
dataset: string,
|
|
167
|
+
rel: string | null | undefined,
|
|
168
|
+
): string | undefined {
|
|
169
|
+
const cleaned = typeof rel === 'string' ? cleanDumpRel(rel) : null;
|
|
170
|
+
return cleaned ? dumpPublicUrl(dataset, cleaned) : undefined;
|
|
171
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { JsonDumpReader } from '@terrafirmagreg-team/game/lib/dump/json-reader';
|
|
2
|
+
import { createAsyncCache } from '@terrafirmagreg-team/ui/lib/utils/async-cache';
|
|
3
|
+
|
|
4
|
+
export interface DumpReader {
|
|
5
|
+
|
|
6
|
+
getJson(path: string, priority?: 'high'): Promise<unknown | undefined>;
|
|
7
|
+
|
|
8
|
+
getLang(lang: string): Promise<Record<string, string>>;
|
|
9
|
+
|
|
10
|
+
getLangRaw(lang: string): Promise<Record<string, string>>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const dumpReader = createAsyncCache(
|
|
14
|
+
async (dataset: string): Promise<DumpReader> => new JsonDumpReader(dataset),
|
|
15
|
+
);
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { entryHref } from '@terrafirmagreg-team/game/lib/entry/href';
|
|
2
|
+
import { tagKindOf } from '@terrafirmagreg-team/game/lib/dump/contract/ref';
|
|
3
|
+
import type { ReferenceKind } from '@terrafirmagreg-team/game/lib/dump/contract/ref';
|
|
4
|
+
import type { EntryProps } from '@terrafirmagreg-team/game/types/entry';
|
|
5
|
+
|
|
6
|
+
export function usesStackTooltip(kind: ReferenceKind | undefined): boolean {
|
|
7
|
+
switch (kind) {
|
|
8
|
+
case 'texture':
|
|
9
|
+
case 'model':
|
|
10
|
+
case 'recipe':
|
|
11
|
+
case 'quest':
|
|
12
|
+
case 'guide':
|
|
13
|
+
case 'chapter':
|
|
14
|
+
return false;
|
|
15
|
+
default:
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function tooltipLoadKind(
|
|
21
|
+
kind: ReferenceKind | undefined,
|
|
22
|
+
): 'item' | 'fluid' | 'entity' | undefined {
|
|
23
|
+
const member = tagKindOf(kind) ?? kind;
|
|
24
|
+
if (member === 'fluid') return 'fluid';
|
|
25
|
+
if (member === 'entity') return 'entity';
|
|
26
|
+
return 'item';
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function resolveEntryHref(
|
|
30
|
+
props: EntryProps & { dataset: string },
|
|
31
|
+
): string | undefined {
|
|
32
|
+
if (props.href === false) return undefined;
|
|
33
|
+
if (typeof props.href === 'string') return props.href;
|
|
34
|
+
if (!props.lang) return undefined;
|
|
35
|
+
const kind = props.reference?.kind ?? props.kind ?? 'item';
|
|
36
|
+
const id = props.reference?.id ?? props.id;
|
|
37
|
+
if (!id) return undefined;
|
|
38
|
+
if (kind === 'entity' || kind === 'texture' || kind === 'model') {
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
return entryHref(
|
|
42
|
+
props.dataset,
|
|
43
|
+
props.lang,
|
|
44
|
+
props.reference ?? { kind, id, ...(props.tag !== undefined ? { tag: props.tag } : {}) },
|
|
45
|
+
{
|
|
46
|
+
tab: props.tab,
|
|
47
|
+
type: props.type,
|
|
48
|
+
search: props.search,
|
|
49
|
+
rpage: props.rpage,
|
|
50
|
+
filename: props.filename,
|
|
51
|
+
modId: props.modId,
|
|
52
|
+
bookPath: props.bookPath,
|
|
53
|
+
entry: props.entry,
|
|
54
|
+
hash: props.hash,
|
|
55
|
+
},
|
|
56
|
+
);
|
|
57
|
+
}
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
const ELEMENT_COLORS: Record<string, string> = {
|
|
2
|
+
H: '#FFFFFF',
|
|
3
|
+
D: '#FFFFC0',
|
|
4
|
+
T: '#FFFFA0',
|
|
5
|
+
He: '#D9FFFF',
|
|
6
|
+
Li: '#CC80FF',
|
|
7
|
+
Be: '#C2FF00',
|
|
8
|
+
B: '#FFB5B5',
|
|
9
|
+
C: '#909090',
|
|
10
|
+
N: '#3050F8',
|
|
11
|
+
O: '#FF0D0D',
|
|
12
|
+
F: '#90E050',
|
|
13
|
+
Ne: '#B3E3F5',
|
|
14
|
+
Na: '#AB5CF2',
|
|
15
|
+
Mg: '#8AFF00',
|
|
16
|
+
Al: '#BFA6A6',
|
|
17
|
+
Si: '#F0C8A0',
|
|
18
|
+
P: '#FF8000',
|
|
19
|
+
S: '#FFFF30',
|
|
20
|
+
Cl: '#1FF01F',
|
|
21
|
+
Ar: '#80D1E3',
|
|
22
|
+
K: '#8F40D4',
|
|
23
|
+
Ca: '#3DFF00',
|
|
24
|
+
Sc: '#E6E6E6',
|
|
25
|
+
Ti: '#BFC2C7',
|
|
26
|
+
V: '#A6A6AB',
|
|
27
|
+
Cr: '#8A99C7',
|
|
28
|
+
Mn: '#9C7AC7',
|
|
29
|
+
Fe: '#E06633',
|
|
30
|
+
Co: '#F090A0',
|
|
31
|
+
Ni: '#50D050',
|
|
32
|
+
Cu: '#C88033',
|
|
33
|
+
Zn: '#7D80B0',
|
|
34
|
+
Ga: '#C28F8F',
|
|
35
|
+
Ge: '#668F8F',
|
|
36
|
+
As: '#BD80E3',
|
|
37
|
+
Se: '#FFA100',
|
|
38
|
+
Br: '#A62929',
|
|
39
|
+
Kr: '#5CB8D1',
|
|
40
|
+
Rb: '#702EB0',
|
|
41
|
+
Sr: '#00FF00',
|
|
42
|
+
Y: '#94FFFF',
|
|
43
|
+
Zr: '#94E0E0',
|
|
44
|
+
Nb: '#73C2C9',
|
|
45
|
+
Mo: '#54B5B5',
|
|
46
|
+
Tc: '#3B9E9E',
|
|
47
|
+
Ru: '#248F8F',
|
|
48
|
+
Rh: '#0A7D8C',
|
|
49
|
+
Pd: '#000000',
|
|
50
|
+
Ag: '#C0C0C0',
|
|
51
|
+
Cd: '#FFD98F',
|
|
52
|
+
In: '#A67573',
|
|
53
|
+
Sn: '#668080',
|
|
54
|
+
Sb: '#9E63B5',
|
|
55
|
+
Te: '#D47A00',
|
|
56
|
+
I: '#940094',
|
|
57
|
+
Xe: '#429EB0',
|
|
58
|
+
Cs: '#57178F',
|
|
59
|
+
Ba: '#00C900',
|
|
60
|
+
La: '#70D4FF',
|
|
61
|
+
Ce: '#FFFFC7',
|
|
62
|
+
Pr: '#D9FFC7',
|
|
63
|
+
Nd: '#C7FFC7',
|
|
64
|
+
Pm: '#A3FFC7',
|
|
65
|
+
Sm: '#8FFFC7',
|
|
66
|
+
Eu: '#61FFC7',
|
|
67
|
+
Gd: '#45FFC7',
|
|
68
|
+
Tb: '#30FFC7',
|
|
69
|
+
Dy: '#1FFFC7',
|
|
70
|
+
Ho: '#00FF9C',
|
|
71
|
+
Er: '#00E675',
|
|
72
|
+
Tm: '#00D452',
|
|
73
|
+
Yb: '#00BF38',
|
|
74
|
+
Lu: '#00AB24',
|
|
75
|
+
Hf: '#4DC2FF',
|
|
76
|
+
Ta: '#4DA6FF',
|
|
77
|
+
W: '#2194D6',
|
|
78
|
+
Re: '#267DAB',
|
|
79
|
+
Os: '#266696',
|
|
80
|
+
Ir: '#175487',
|
|
81
|
+
Pt: '#D0D0E0',
|
|
82
|
+
Au: '#FFD123',
|
|
83
|
+
Hg: '#B8B8D0',
|
|
84
|
+
Tl: '#A6544D',
|
|
85
|
+
Pb: '#575961',
|
|
86
|
+
Bi: '#9E4FB5',
|
|
87
|
+
Po: '#AB5C00',
|
|
88
|
+
At: '#754F45',
|
|
89
|
+
Rn: '#428296',
|
|
90
|
+
Fr: '#420066',
|
|
91
|
+
Ra: '#000000',
|
|
92
|
+
Ac: '#70ABFA',
|
|
93
|
+
Th: '#00BAFF',
|
|
94
|
+
Pa: '#00A1FF',
|
|
95
|
+
U: '#008FFF',
|
|
96
|
+
Np: '#0080FF',
|
|
97
|
+
Pu: '#000000',
|
|
98
|
+
Am: '#545CF2',
|
|
99
|
+
Cm: '#785CE3',
|
|
100
|
+
Bk: '#8A4FE3',
|
|
101
|
+
Cf: '#A136D4',
|
|
102
|
+
Es: '#B31FD4',
|
|
103
|
+
Fm: '#B31FBA',
|
|
104
|
+
Md: '#B30DA6',
|
|
105
|
+
No: '#BD0D87',
|
|
106
|
+
Lr: '#C70066',
|
|
107
|
+
Rf: '#CC0059',
|
|
108
|
+
Db: '#D1004F',
|
|
109
|
+
Sg: '#D90045',
|
|
110
|
+
Bh: '#E00038',
|
|
111
|
+
Hs: '#E6002E',
|
|
112
|
+
Mt: '#EB0026',
|
|
113
|
+
Ds: '#EB0026',
|
|
114
|
+
Rg: '#FFFF55',
|
|
115
|
+
Cn: '#FFFF55',
|
|
116
|
+
Nh: '#FFFF55',
|
|
117
|
+
Fl: '#FFFF55',
|
|
118
|
+
Mc: '#FFFF55',
|
|
119
|
+
Lv: '#FFFF55',
|
|
120
|
+
Ts: '#FFFF55',
|
|
121
|
+
Og: '#FFFF55',
|
|
122
|
+
Nq: '#FFFF55',
|
|
123
|
+
Ke: '#FFFF55',
|
|
124
|
+
Tr: '#FFFF55',
|
|
125
|
+
Dr: '#FFFF55',
|
|
126
|
+
Nt: '#FFFF55',
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const MIN_BRIGHTNESS = 0.1;
|
|
130
|
+
|
|
131
|
+
function lightenHex(hex: string): string {
|
|
132
|
+
const value = Number.parseInt(hex.slice(1), 16);
|
|
133
|
+
const r = (value >> 16) & 0xff;
|
|
134
|
+
const g = (value >> 8) & 0xff;
|
|
135
|
+
const b = value & 0xff;
|
|
136
|
+
const max = Math.max(r, g, b) / 255;
|
|
137
|
+
const min = Math.min(r, g, b) / 255;
|
|
138
|
+
const delta = max - min;
|
|
139
|
+
let h = 0;
|
|
140
|
+
if (delta !== 0) {
|
|
141
|
+
if (max === r / 255) h = ((g / 255 - b / 255) / delta) % 6;
|
|
142
|
+
else if (max === g / 255) h = (b / 255 - r / 255) / delta + 2;
|
|
143
|
+
else h = (r / 255 - g / 255) / delta + 4;
|
|
144
|
+
h *= 60;
|
|
145
|
+
if (h < 0) h += 360;
|
|
146
|
+
}
|
|
147
|
+
const s = max === 0 ? 0 : delta / max;
|
|
148
|
+
const v = Math.max(max, MIN_BRIGHTNESS);
|
|
149
|
+
const c = v * s;
|
|
150
|
+
const x = c * (1 - Math.abs(((h / 60) % 2) - 1));
|
|
151
|
+
const m = v - c;
|
|
152
|
+
const [rp, gp, bp] =
|
|
153
|
+
h < 60
|
|
154
|
+
? [c, x, 0]
|
|
155
|
+
: h < 120
|
|
156
|
+
? [x, c, 0]
|
|
157
|
+
: h < 180
|
|
158
|
+
? [0, c, x]
|
|
159
|
+
: h < 240
|
|
160
|
+
? [0, x, c]
|
|
161
|
+
: h < 300
|
|
162
|
+
? [x, 0, c]
|
|
163
|
+
: [c, 0, x];
|
|
164
|
+
const to255 = (n: number) =>
|
|
165
|
+
Math.round((n + m) * 255)
|
|
166
|
+
.toString(16)
|
|
167
|
+
.padStart(2, '0');
|
|
168
|
+
return `#${to255(rp)}${to255(gp)}${to255(bp)}`;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const LIGHTENED = new Map<string, string>();
|
|
172
|
+
function elementColor(symbol: string): string | undefined {
|
|
173
|
+
const raw = ELEMENT_COLORS[symbol];
|
|
174
|
+
if (!raw) return undefined;
|
|
175
|
+
const cached = LIGHTENED.get(symbol);
|
|
176
|
+
if (cached) return cached;
|
|
177
|
+
const color = lightenHex(raw);
|
|
178
|
+
LIGHTENED.set(symbol, color);
|
|
179
|
+
return color;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const SUBSCRIPT = '₀₁₂₃₄₅₆₇₈₉';
|
|
183
|
+
const SUPERSCRIPT = '⁰¹²³⁴⁵⁶⁷⁸⁹⁺⁻';
|
|
184
|
+
const PUNCT = new Set('()[]{}·∙.*+?=-‐‑‒–—~≈≡,');
|
|
185
|
+
|
|
186
|
+
export type FormulaRun = { text: string; color?: string };
|
|
187
|
+
|
|
188
|
+
export function formulaRuns(plain: string): FormulaRun[] | null {
|
|
189
|
+
if (!plain || plain.length < 2) return null;
|
|
190
|
+
const runs: FormulaRun[] = [];
|
|
191
|
+
let i = 0;
|
|
192
|
+
let elements = 0;
|
|
193
|
+
let pushPlain = (text: string) => {
|
|
194
|
+
const last = runs[runs.length - 1];
|
|
195
|
+
if (last && !last.color) last.text += text;
|
|
196
|
+
else runs.push({ text });
|
|
197
|
+
};
|
|
198
|
+
while (i < plain.length) {
|
|
199
|
+
const ch = plain[i];
|
|
200
|
+
if (ch >= 'A' && ch <= 'Z') {
|
|
201
|
+
const two = plain.slice(i, i + 2);
|
|
202
|
+
const symbol = /^[a-z]$/.test(plain[i + 1] ?? '') ? two : ch;
|
|
203
|
+
const color = elementColor(symbol);
|
|
204
|
+
if (!color) return null;
|
|
205
|
+
runs.push({ text: symbol, color });
|
|
206
|
+
elements += 1;
|
|
207
|
+
i += symbol.length;
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
if ((ch >= '0' && ch <= '9') || SUBSCRIPT.includes(ch)) {
|
|
211
|
+
let j = i;
|
|
212
|
+
while (j < plain.length && /[0-9]/.test(plain[j])) j += 1;
|
|
213
|
+
while (j < plain.length && SUBSCRIPT.includes(plain[j])) j += 1;
|
|
214
|
+
pushPlain(plain.slice(i, j));
|
|
215
|
+
i = j;
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
if (SUPERSCRIPT.includes(ch) || PUNCT.has(ch)) {
|
|
219
|
+
pushPlain(ch);
|
|
220
|
+
i += 1;
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
return elements > 0 ? runs : null;
|
|
226
|
+
}
|