@terrafirmagreg-team/game 0.0.0-stage → 1.2.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 +59 -3
- package/src/components/entry/atlas-sprite.tsx +44 -0
- package/src/components/entry/copy-id-menu.tsx +48 -0
- package/src/components/entry/entry.tsx +308 -0
- package/src/components/entry/group.tsx +161 -0
- package/src/components/entry/icon.tsx +181 -0
- package/src/components/entry/list.tsx +67 -0
- package/src/components/entry/mc-text.tsx +6 -0
- package/src/components/entry/stack-count.tsx +18 -0
- package/src/components/entry/tooltip.tsx +137 -0
- package/src/components/mdx/item-icon.tsx +77 -0
- package/src/components/preview-icon.tsx +26 -0
- package/src/hooks/use-dump-location.ts +20 -0
- package/src/hooks/use-entry-context.ts +37 -0
- package/src/hooks/use-icon-source.ts +306 -0
- package/src/hooks/use-lang-map.ts +20 -0
- package/src/lib/entry/display.ts +5 -0
- package/src/lib/entry/entry.ts +59 -0
- package/src/lib/entry/formula.ts +227 -0
- package/src/lib/entry/href.ts +156 -0
- package/src/lib/entry/icon-load.ts +231 -0
- package/src/lib/entry/icon.ts +11 -0
- package/src/lib/entry/tooltip.ts +62 -0
- package/src/lib/preview.ts +20 -0
- package/src/lib/search.ts +30 -0
- package/src/locales/en_us.json +5 -0
- package/src/locales/ru_ru.json +5 -0
- package/src/locales.ts +6 -0
- package/src/types/entry.ts +91 -0
- package/styles/fonts.css +31 -0
- package/styles/tokens.css +15 -0
- package/styles.css +2 -0
- package/README.md +0 -3
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useAsyncResult } from '@terrafirmagreg-team/ui/hooks/use-async-value';
|
|
4
|
+
import { useCycleIndex } from '@terrafirmagreg-team/ui/hooks/use-cycle-index';
|
|
5
|
+
import {
|
|
6
|
+
classifyRefId,
|
|
7
|
+
isTagKind,
|
|
8
|
+
parseStackId,
|
|
9
|
+
tagKindOf,
|
|
10
|
+
} from '@terrafirmagreg-team/dump/lib/dump/contract/ref';
|
|
11
|
+
import { texturePublicUrl } from '@terrafirmagreg-team/dump/lib/dump/assets/urls';
|
|
12
|
+
import { useDumpLocation } from '@terrafirmagreg-team/game/hooks/use-dump-location';
|
|
13
|
+
import type { DatasetRef } from '@terrafirmagreg-team/sites/location';
|
|
14
|
+
import { lookupSpriteInBundle } from '@terrafirmagreg-team/dump/lib/dump/assets/atlas/lookup';
|
|
15
|
+
import type { AtlasLookup } from '@terrafirmagreg-team/dump/lib/dump/assets/atlas/dto';
|
|
16
|
+
import {
|
|
17
|
+
blockItemId,
|
|
18
|
+
fetchAtlases,
|
|
19
|
+
fetchEntityTexture,
|
|
20
|
+
fetchTagMembers,
|
|
21
|
+
peekBlockItem,
|
|
22
|
+
peekEntityTexture,
|
|
23
|
+
peekTagMembers,
|
|
24
|
+
} from '@terrafirmagreg-team/game/lib/entry/icon-load';
|
|
25
|
+
import { loadClientModAtlases } from '@terrafirmagreg-team/dump/lib/dump/client/atlases';
|
|
26
|
+
import { stackKindOf } from '@terrafirmagreg-team/game/lib/entry/icon';
|
|
27
|
+
import type {
|
|
28
|
+
DumpNbt,
|
|
29
|
+
ReferenceKind,
|
|
30
|
+
TaggedReference,
|
|
31
|
+
} from '@terrafirmagreg-team/dump/lib/dump/contract/ref';
|
|
32
|
+
|
|
33
|
+
export const ENTRY_CYCLE_MS = 1500;
|
|
34
|
+
|
|
35
|
+
export type IconSource = {
|
|
36
|
+
|
|
37
|
+
rawId: string;
|
|
38
|
+
|
|
39
|
+
cycleRef: TaggedReference | null;
|
|
40
|
+
|
|
41
|
+
lookup: AtlasLookup | null;
|
|
42
|
+
|
|
43
|
+
missingLookup: AtlasLookup | null;
|
|
44
|
+
missingPending: boolean;
|
|
45
|
+
|
|
46
|
+
imgSrc: string | undefined;
|
|
47
|
+
|
|
48
|
+
sourceKey: string;
|
|
49
|
+
|
|
50
|
+
pending: boolean;
|
|
51
|
+
awaitingAtlas: boolean;
|
|
52
|
+
awaitingTexture: boolean;
|
|
53
|
+
usesAtlas: boolean;
|
|
54
|
+
isEntity: boolean;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export function useIconSource({
|
|
58
|
+
dataset,
|
|
59
|
+
reference,
|
|
60
|
+
id,
|
|
61
|
+
kind,
|
|
62
|
+
tag,
|
|
63
|
+
missing,
|
|
64
|
+
preferVariant,
|
|
65
|
+
entityView = 'auto',
|
|
66
|
+
}: {
|
|
67
|
+
dataset?: DatasetRef;
|
|
68
|
+
reference?: TaggedReference;
|
|
69
|
+
id?: string;
|
|
70
|
+
kind?: ReferenceKind;
|
|
71
|
+
tag?: DumpNbt;
|
|
72
|
+
missing: 'placeholder' | 'item' | 'hide';
|
|
73
|
+
preferVariant?: boolean;
|
|
74
|
+
entityView?: 'auto' | 'texture' | 'egg';
|
|
75
|
+
}): IconSource {
|
|
76
|
+
const ref = reference ?? (id ? classifyRefId(id, kind) : undefined);
|
|
77
|
+
const refKind = ref?.kind;
|
|
78
|
+
const rawId = ref?.id ?? id ?? '';
|
|
79
|
+
const nbt = tag ?? ref?.tag;
|
|
80
|
+
const tagMemberKind = tagKindOf(refKind);
|
|
81
|
+
const tagKey =
|
|
82
|
+
dataset && rawId && isTagKind(refKind)
|
|
83
|
+
? `${dataset.id}/${dataset.version ?? ''}/${tagMemberKind ?? 'item'}/${rawId}`
|
|
84
|
+
: null;
|
|
85
|
+
const tagMembersPeek = tagKey
|
|
86
|
+
? peekTagMembers(dataset!, rawId, tagMemberKind ?? 'item')
|
|
87
|
+
: undefined;
|
|
88
|
+
const tagHit = useAsyncResult(
|
|
89
|
+
tagKey && tagMembersPeek === undefined
|
|
90
|
+
? async () => ({
|
|
91
|
+
key: tagKey,
|
|
92
|
+
members: await fetchTagMembers(
|
|
93
|
+
dataset!,
|
|
94
|
+
rawId,
|
|
95
|
+
tagMemberKind ?? 'item',
|
|
96
|
+
),
|
|
97
|
+
})
|
|
98
|
+
: false,
|
|
99
|
+
[dataset, rawId, tagMemberKind, tagKey, tagMembersPeek],
|
|
100
|
+
);
|
|
101
|
+
const tagMembers = !tagKey
|
|
102
|
+
? undefined
|
|
103
|
+
: tagMembersPeek !== undefined
|
|
104
|
+
? tagMembersPeek
|
|
105
|
+
: tagHit.pending
|
|
106
|
+
? undefined
|
|
107
|
+
: tagHit.value?.key === tagKey
|
|
108
|
+
? (tagHit.value.members ?? null)
|
|
109
|
+
: null;
|
|
110
|
+
const cycleRefs =
|
|
111
|
+
tagMembers && tagMembers.length > 1
|
|
112
|
+
? tagMembers.map((member) => ({
|
|
113
|
+
kind: (tagMemberKind ?? 'item') as ReferenceKind,
|
|
114
|
+
id: member,
|
|
115
|
+
}))
|
|
116
|
+
: null;
|
|
117
|
+
const [cycleIndex] = useCycleIndex(cycleRefs?.length ?? 0, {
|
|
118
|
+
enabled: cycleRefs !== null,
|
|
119
|
+
intervalMs: ENTRY_CYCLE_MS,
|
|
120
|
+
});
|
|
121
|
+
const itemId = !rawId
|
|
122
|
+
? ''
|
|
123
|
+
: tagKey
|
|
124
|
+
? tagMembers === undefined
|
|
125
|
+
? undefined
|
|
126
|
+
: tagMembers === null
|
|
127
|
+
? null
|
|
128
|
+
: (tagMembers[0] ?? null)
|
|
129
|
+
: rawId;
|
|
130
|
+
const memberKind =
|
|
131
|
+
isTagKind(refKind) && itemId && !itemId.startsWith('#')
|
|
132
|
+
? (tagMemberKind ?? 'item')
|
|
133
|
+
: refKind;
|
|
134
|
+
const blockKey =
|
|
135
|
+
dataset && memberKind === 'block' && itemId && !itemId.startsWith('#')
|
|
136
|
+
? `${dataset.id}/${dataset.version ?? ''}/${itemId}`
|
|
137
|
+
: null;
|
|
138
|
+
const blockPeek = blockKey ? peekBlockItem(dataset!, itemId!) : undefined;
|
|
139
|
+
const blockHit = useAsyncResult(
|
|
140
|
+
blockKey && blockPeek === undefined
|
|
141
|
+
? async () => ({
|
|
142
|
+
key: blockKey,
|
|
143
|
+
id: await blockItemId(dataset!, itemId!),
|
|
144
|
+
})
|
|
145
|
+
: false,
|
|
146
|
+
[dataset, itemId, blockKey, blockPeek],
|
|
147
|
+
);
|
|
148
|
+
const blockResolved = !blockKey
|
|
149
|
+
? undefined
|
|
150
|
+
: blockPeek !== undefined
|
|
151
|
+
? blockPeek
|
|
152
|
+
: blockHit.pending
|
|
153
|
+
? undefined
|
|
154
|
+
: blockHit.value?.key === blockKey
|
|
155
|
+
? (blockHit.value.id ?? null)
|
|
156
|
+
: null;
|
|
157
|
+
const atlasId = blockKey ? (blockResolved ?? undefined) : itemId;
|
|
158
|
+
const displayKind = blockKey && blockResolved ? 'item' : memberKind;
|
|
159
|
+
const resolvedId = atlasId ?? rawId;
|
|
160
|
+
const isTexture = displayKind === 'texture';
|
|
161
|
+
const isEntity = displayKind === 'entity';
|
|
162
|
+
const parsed = parseStackId(resolvedId.replace(/^#/, ''));
|
|
163
|
+
const spriteId = parsed.id;
|
|
164
|
+
const eggId =
|
|
165
|
+
isEntity && entityView !== 'texture' && spriteId && !spriteId.startsWith('#')
|
|
166
|
+
? `${spriteId}_spawn_egg`
|
|
167
|
+
: undefined;
|
|
168
|
+
const usesAtlas =
|
|
169
|
+
Boolean(dataset && rawId) &&
|
|
170
|
+
(displayKind === 'item' ||
|
|
171
|
+
displayKind === 'block' ||
|
|
172
|
+
displayKind === 'fluid' ||
|
|
173
|
+
Boolean(eggId));
|
|
174
|
+
const modId = spriteId.includes(':')
|
|
175
|
+
? spriteId.slice(0, spriteId.indexOf(':'))
|
|
176
|
+
: '';
|
|
177
|
+
const atlasNeedsLoad =
|
|
178
|
+
dataset && usesAtlas && modId && spriteId && !spriteId.startsWith('#');
|
|
179
|
+
const atlasPeek = atlasNeedsLoad
|
|
180
|
+
? loadClientModAtlases.peek(dataset, modId)
|
|
181
|
+
: undefined;
|
|
182
|
+
const atlasBundle =
|
|
183
|
+
atlasPeek && (atlasPeek.items || atlasPeek.blocks) ? atlasPeek : null;
|
|
184
|
+
const atlases = useAsyncResult(
|
|
185
|
+
atlasNeedsLoad && atlasPeek === undefined
|
|
186
|
+
? () => fetchAtlases(dataset, modId)
|
|
187
|
+
: false,
|
|
188
|
+
[atlasNeedsLoad, atlasPeek, dataset, modId],
|
|
189
|
+
);
|
|
190
|
+
const atlasValue = atlasPeek === undefined ? atlases.value : atlasBundle;
|
|
191
|
+
const sourceKey = `${dataset?.id}/${dataset?.version ?? ''}:${displayKind}:${resolvedId}`;
|
|
192
|
+
const dumpLoc = useDumpLocation(dataset);
|
|
193
|
+
const wantEntityTexture = isEntity && entityView !== 'egg';
|
|
194
|
+
const texturePeek =
|
|
195
|
+
dataset && isTexture && rawId
|
|
196
|
+
? texturePublicUrl(dataset, rawId)
|
|
197
|
+
: dataset && wantEntityTexture && resolvedId
|
|
198
|
+
? peekEntityTexture(dataset, resolvedId, nbt)
|
|
199
|
+
: null;
|
|
200
|
+
const textureSrc = useAsyncResult(
|
|
201
|
+
dataset && isTexture && rawId
|
|
202
|
+
? false
|
|
203
|
+
: dataset &&
|
|
204
|
+
wantEntityTexture &&
|
|
205
|
+
resolvedId &&
|
|
206
|
+
texturePeek === undefined
|
|
207
|
+
? async () => ({
|
|
208
|
+
key: sourceKey,
|
|
209
|
+
url:
|
|
210
|
+
(await fetchEntityTexture(dataset, resolvedId, nbt)) ?? undefined,
|
|
211
|
+
})
|
|
212
|
+
: false,
|
|
213
|
+
[
|
|
214
|
+
isTexture,
|
|
215
|
+
isEntity,
|
|
216
|
+
wantEntityTexture,
|
|
217
|
+
dataset,
|
|
218
|
+
rawId,
|
|
219
|
+
resolvedId,
|
|
220
|
+
sourceKey,
|
|
221
|
+
nbt,
|
|
222
|
+
texturePeek,
|
|
223
|
+
],
|
|
224
|
+
);
|
|
225
|
+
const resolvedTexture =
|
|
226
|
+
texturePeek !== undefined
|
|
227
|
+
? (texturePeek ?? undefined)
|
|
228
|
+
: textureSrc.value?.key === sourceKey
|
|
229
|
+
? textureSrc.value.url
|
|
230
|
+
: undefined;
|
|
231
|
+
const wantMissingItem =
|
|
232
|
+
missing === 'item' && dataset !== undefined && usesAtlas;
|
|
233
|
+
const missingPeek = wantMissingItem
|
|
234
|
+
? loadClientModAtlases.peek(dataset, 'ftbquests')
|
|
235
|
+
: undefined;
|
|
236
|
+
const missingBundleValue =
|
|
237
|
+
missingPeek !== undefined
|
|
238
|
+
? missingPeek.items || missingPeek.blocks
|
|
239
|
+
? missingPeek
|
|
240
|
+
: null
|
|
241
|
+
: undefined;
|
|
242
|
+
const missingBundle = useAsyncResult(
|
|
243
|
+
wantMissingItem && missingPeek === undefined
|
|
244
|
+
? () => fetchAtlases(dataset, 'ftbquests')
|
|
245
|
+
: false,
|
|
246
|
+
[wantMissingItem, missingPeek, dataset],
|
|
247
|
+
);
|
|
248
|
+
const missingLookup = wantMissingItem
|
|
249
|
+
? lookupSpriteInBundle(
|
|
250
|
+
missingBundleValue === undefined
|
|
251
|
+
? missingBundle.value
|
|
252
|
+
: missingBundleValue,
|
|
253
|
+
{
|
|
254
|
+
id: 'ftbquests:missing_item',
|
|
255
|
+
kind: 'item',
|
|
256
|
+
},
|
|
257
|
+
)
|
|
258
|
+
: null;
|
|
259
|
+
|
|
260
|
+
const lookupKind = stackKindOf(displayKind) ?? (eggId ? 'item' : undefined);
|
|
261
|
+
const lookup = usesAtlas
|
|
262
|
+
? lookupSpriteInBundle(
|
|
263
|
+
atlasValue,
|
|
264
|
+
{
|
|
265
|
+
id: eggId ?? spriteId,
|
|
266
|
+
...(lookupKind ? { kind: lookupKind } : {}),
|
|
267
|
+
...(nbt !== undefined
|
|
268
|
+
? { tag: nbt }
|
|
269
|
+
: parsed.nbt !== undefined
|
|
270
|
+
? { tag: parsed.nbt }
|
|
271
|
+
: {}),
|
|
272
|
+
},
|
|
273
|
+
lookupKind,
|
|
274
|
+
preferVariant,
|
|
275
|
+
)
|
|
276
|
+
: null;
|
|
277
|
+
const pending =
|
|
278
|
+
(isTagKind(refKind) && itemId === undefined) ||
|
|
279
|
+
(blockKey !== null && blockResolved === undefined);
|
|
280
|
+
const awaitingAtlas =
|
|
281
|
+
usesAtlas &&
|
|
282
|
+
atlasId != null &&
|
|
283
|
+
!atlasId.startsWith('#') &&
|
|
284
|
+
atlases.pending;
|
|
285
|
+
const awaitingTexture =
|
|
286
|
+
(isTexture || isEntity) &&
|
|
287
|
+
texturePeek === undefined &&
|
|
288
|
+
(textureSrc.pending || (isTexture && !dumpLoc));
|
|
289
|
+
|
|
290
|
+
return {
|
|
291
|
+
rawId,
|
|
292
|
+
cycleRef:
|
|
293
|
+
cycleRefs && !pending ? (cycleRefs[cycleIndex] ?? cycleRefs[0]!) : null,
|
|
294
|
+
lookup: lookup ?? null,
|
|
295
|
+
missingLookup: missingLookup ?? null,
|
|
296
|
+
missingPending: missingBundle.pending,
|
|
297
|
+
imgSrc: isTexture || isEntity ? resolvedTexture : undefined,
|
|
298
|
+
sourceKey,
|
|
299
|
+
pending,
|
|
300
|
+
awaitingAtlas,
|
|
301
|
+
awaitingTexture,
|
|
302
|
+
usesAtlas,
|
|
303
|
+
isEntity,
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import type { DatasetRef } from '@terrafirmagreg-team/sites/location';
|
|
3
|
+
|
|
4
|
+
import { useAsyncResult } from '@terrafirmagreg-team/ui/hooks/use-async-value';
|
|
5
|
+
import { dumpReader } from '@terrafirmagreg-team/dump/lib/dump/reader';
|
|
6
|
+
|
|
7
|
+
export function useLangMap(
|
|
8
|
+
dataset: DatasetRef | undefined,
|
|
9
|
+
lang: string | undefined,
|
|
10
|
+
enabled = true,
|
|
11
|
+
): Record<string, string> | undefined {
|
|
12
|
+
const { value, pending } = useAsyncResult(
|
|
13
|
+
enabled && dataset && lang
|
|
14
|
+
? () => dumpReader(dataset).then((reader) => reader.getLang(lang))
|
|
15
|
+
: false,
|
|
16
|
+
[dataset, lang, enabled],
|
|
17
|
+
);
|
|
18
|
+
return pending ? undefined : value;
|
|
19
|
+
}
|
|
20
|
+
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { DatasetRef } from '@terrafirmagreg-team/sites/location';
|
|
2
|
+
import { entryHref } from '@terrafirmagreg-team/game/lib/entry/href';
|
|
3
|
+
import { tagKindOf } from '@terrafirmagreg-team/dump/lib/dump/contract/ref';
|
|
4
|
+
import type { ReferenceKind } from '@terrafirmagreg-team/dump/lib/dump/contract/ref';
|
|
5
|
+
import type { EntryProps } from '@terrafirmagreg-team/game/types/entry';
|
|
6
|
+
|
|
7
|
+
export function usesStackTooltip(kind: ReferenceKind | undefined): boolean {
|
|
8
|
+
switch (kind) {
|
|
9
|
+
case 'texture':
|
|
10
|
+
case 'model':
|
|
11
|
+
case 'recipe':
|
|
12
|
+
case 'quest':
|
|
13
|
+
case 'guide':
|
|
14
|
+
case 'chapter':
|
|
15
|
+
return false;
|
|
16
|
+
default:
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function tooltipLoadKind(
|
|
22
|
+
kind: ReferenceKind | undefined,
|
|
23
|
+
): 'item' | 'fluid' | 'entity' | undefined {
|
|
24
|
+
const member = tagKindOf(kind) ?? kind;
|
|
25
|
+
if (member === 'fluid') return 'fluid';
|
|
26
|
+
if (member === 'entity') return 'entity';
|
|
27
|
+
return 'item';
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function resolveEntryHref(
|
|
31
|
+
props: EntryProps & { dataset: DatasetRef },
|
|
32
|
+
): string | undefined {
|
|
33
|
+
if (props.href === false) return undefined;
|
|
34
|
+
if (typeof props.href === 'string') return props.href;
|
|
35
|
+
if (!props.lang) return undefined;
|
|
36
|
+
const kind = props.reference?.kind ?? props.kind ?? 'item';
|
|
37
|
+
const id = props.reference?.id ?? props.id;
|
|
38
|
+
if (!id) return undefined;
|
|
39
|
+
if (kind === 'entity' || kind === 'texture' || kind === 'model') {
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
return entryHref(
|
|
43
|
+
props.dataset,
|
|
44
|
+
props.lang,
|
|
45
|
+
props.reference ?? { kind, id, ...(props.tag !== undefined ? { tag: props.tag } : {}) },
|
|
46
|
+
{
|
|
47
|
+
tab: props.tab,
|
|
48
|
+
type: props.type,
|
|
49
|
+
search: props.search,
|
|
50
|
+
rpage: props.rpage,
|
|
51
|
+
filename: props.filename,
|
|
52
|
+
modId: props.modId,
|
|
53
|
+
bookPath: props.bookPath,
|
|
54
|
+
entry: props.entry,
|
|
55
|
+
hash: props.hash,
|
|
56
|
+
},
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
@@ -0,0 +1,227 @@
|
|
|
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
|
+
}
|
|
227
|
+
|