@terrafirmagreg-team/guide 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/book-switcher.tsx +46 -0
- package/src/components/views/browser.tsx +124 -0
- package/src/components/views/entry/pages.tsx +186 -0
- package/src/components/views/entry/view.tsx +60 -0
- package/src/components/views/kind/links.tsx +111 -0
- package/src/components/views/kind/media.tsx +167 -0
- package/src/components/views/kind/recipe.tsx +134 -0
- package/src/components/views/kind/table.tsx +138 -0
- package/src/components/views/kind/unknown.tsx +23 -0
- package/src/components/views/page/category-body.tsx +56 -0
- package/src/components/views/page/entry-body.tsx +79 -0
- package/src/components/views/page/overview-body.tsx +97 -0
- package/src/components/views/page/text.tsx +57 -0
- package/src/components/views/page/view.tsx +279 -0
- package/src/components/views/text.tsx +130 -0
- package/src/hooks/use-left-sidebar-tree.ts +19 -0
- package/src/hooks/use-tree.ts +33 -0
- package/src/lib/book/convert.ts +74 -0
- package/src/lib/book/index-rows.ts +134 -0
- package/src/lib/book/index.ts +2 -0
- package/src/lib/book/localize.ts +76 -0
- package/src/lib/book/raw.ts +80 -0
- package/src/lib/books.ts +29 -0
- package/src/lib/breadcrumb.ts +54 -0
- package/src/lib/client/guides.ts +189 -0
- package/src/lib/client-payload/extras.ts +101 -0
- package/src/lib/client-payload/index.ts +112 -0
- package/src/lib/client-payload/links.ts +101 -0
- package/src/lib/config.ts +46 -0
- package/src/lib/data/dto.ts +77 -0
- package/src/lib/data/parse.ts +93 -0
- package/src/lib/dump.ts +5 -0
- package/src/lib/format.ts +231 -0
- package/src/lib/links.ts +2 -0
- package/src/lib/markdown/book.ts +73 -0
- package/src/lib/markdown/ctx.ts +26 -0
- package/src/lib/markdown/index.ts +8 -0
- package/src/lib/markdown/page.ts +154 -0
- package/src/lib/markdown/table.ts +35 -0
- package/src/lib/markdown/text.ts +39 -0
- package/src/lib/markdown-page.ts +72 -0
- package/src/lib/messages.ts +13 -0
- package/src/lib/meta.ts +9 -0
- package/src/lib/multiblock/index.ts +8 -0
- package/src/lib/multiblock/url.ts +61 -0
- package/src/lib/names.ts +35 -0
- package/src/lib/page-kind.ts +117 -0
- package/src/lib/paths.ts +52 -0
- package/src/lib/preview.ts +33 -0
- package/src/lib/query.ts +28 -0
- package/src/lib/recipe.ts +30 -0
- package/src/lib/reference/index.ts +40 -0
- package/src/lib/reference/resolver.ts +88 -0
- package/src/lib/search/filter.ts +17 -0
- package/src/lib/search/index.ts +156 -0
- package/src/lib/sprites.ts +17 -0
- package/src/lib/table.ts +10 -0
- package/src/lib/toc.ts +137 -0
- package/src/lib/tree/children.ts +70 -0
- package/src/lib/tree/index.ts +117 -0
- package/src/locales/en_us.json +22 -0
- package/src/locales/ru_ru.json +22 -0
- package/src/locales.ts +5 -0
- package/src/types/index.ts +215 -0
- package/README.md +0 -3
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { loadClientGuideBook } from '@terrafirmagreg-team/guide/lib/client/guides';
|
|
2
|
+
import { uiConfig } from '@terrafirmagreg-team/ui/lib/config';
|
|
3
|
+
import { categoryMap } from '@terrafirmagreg-team/guide/lib/breadcrumb';
|
|
4
|
+
import { bareId, guideBookHref } from '@terrafirmagreg-team/guide/lib/paths';
|
|
5
|
+
import { questHref } from '@terrafirmagreg-team/quest/lib/links';
|
|
6
|
+
import type { Lang } from '@terrafirmagreg-team/translate/types/locale';
|
|
7
|
+
import type {
|
|
8
|
+
GuideMessages,
|
|
9
|
+
GuidePagePayload,
|
|
10
|
+
} from '@terrafirmagreg-team/guide/types';
|
|
11
|
+
import { entryExtras } from './extras';
|
|
12
|
+
import { type QuestLinks, collectTextLinks } from './links';
|
|
13
|
+
|
|
14
|
+
export async function loadClientGuidePayload(
|
|
15
|
+
dataset: string,
|
|
16
|
+
lang: Lang,
|
|
17
|
+
bookId: string | undefined,
|
|
18
|
+
entryId: string | undefined,
|
|
19
|
+
categoryId: string | undefined,
|
|
20
|
+
langMap: Record<string, string>,
|
|
21
|
+
messages: GuideMessages,
|
|
22
|
+
): Promise<GuidePagePayload | undefined> {
|
|
23
|
+
if (!bookId) return undefined;
|
|
24
|
+
const overview = await loadClientGuideBook(dataset, lang, bookId);
|
|
25
|
+
if (!overview || overview.status !== 'ready') return undefined;
|
|
26
|
+
|
|
27
|
+
const entry = entryId
|
|
28
|
+
? overview.entries.find(
|
|
29
|
+
(item) => item.id === entryId || bareId(item.id) === bareId(entryId),
|
|
30
|
+
)
|
|
31
|
+
: undefined;
|
|
32
|
+
const category =
|
|
33
|
+
!entry && categoryId
|
|
34
|
+
? overview.categories.find((item) => item.id === categoryId)
|
|
35
|
+
: undefined;
|
|
36
|
+
const kind = entry ? 'entry' : category ? 'category' : 'overview';
|
|
37
|
+
const categoryByEntry = categoryMap(overview.categories);
|
|
38
|
+
const bookModId = overview.modId;
|
|
39
|
+
const bookPath = overview.bookPath ?? '';
|
|
40
|
+
|
|
41
|
+
const questLinks: QuestLinks = {};
|
|
42
|
+
const hrefs = new Set<string>();
|
|
43
|
+
const linkCtx = { lang, modId: bookModId, bookPath, categoryByEntry };
|
|
44
|
+
await collectTextLinks(
|
|
45
|
+
dataset,
|
|
46
|
+
questLinks,
|
|
47
|
+
hrefs,
|
|
48
|
+
linkCtx,
|
|
49
|
+
category?.description ?? overview.book?.landingText,
|
|
50
|
+
);
|
|
51
|
+
hrefs.add(guideBookHref(dataset, undefined, bookModId, bookPath));
|
|
52
|
+
const entryCat = entry
|
|
53
|
+
? overview.categories.find((cat) =>
|
|
54
|
+
cat.entries.some((row) => row.id === entry.id),
|
|
55
|
+
)
|
|
56
|
+
: undefined;
|
|
57
|
+
const firstEntry = entryCat?.entries[0]?.id;
|
|
58
|
+
if (firstEntry) {
|
|
59
|
+
hrefs.add(
|
|
60
|
+
guideBookHref(dataset, undefined, bookModId, bookPath, firstEntry),
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const extras = entry
|
|
65
|
+
? await entryExtras(
|
|
66
|
+
dataset,
|
|
67
|
+
lang,
|
|
68
|
+
langMap,
|
|
69
|
+
bookModId,
|
|
70
|
+
bookPath,
|
|
71
|
+
categoryByEntry,
|
|
72
|
+
questLinks,
|
|
73
|
+
hrefs,
|
|
74
|
+
entry,
|
|
75
|
+
)
|
|
76
|
+
: undefined;
|
|
77
|
+
for (const hit of Object.values(questLinks)) {
|
|
78
|
+
hrefs.add(
|
|
79
|
+
questHref(dataset, undefined, {
|
|
80
|
+
category: hit.filename,
|
|
81
|
+
id: hit.questId,
|
|
82
|
+
}),
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const previews = await uiConfig().loadPagePreviews?.(lang, langMap, hrefs) ?? {};
|
|
87
|
+
const payloadCategoryId = entry ? entryCat?.id : category?.id;
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
kind,
|
|
91
|
+
dataset,
|
|
92
|
+
lang,
|
|
93
|
+
title: entry?.name ?? category?.name ?? overview.book?.name ?? bookId,
|
|
94
|
+
modId: bookModId,
|
|
95
|
+
bookPath,
|
|
96
|
+
description: entry
|
|
97
|
+
? undefined
|
|
98
|
+
: (category?.description ?? overview.book?.landingText),
|
|
99
|
+
previewIcon:
|
|
100
|
+
entry?.icon?.id ?? category?.icon?.id ?? overview.book?.cover?.id,
|
|
101
|
+
previewKind: entry?.icon?.kind ?? category?.icon?.kind,
|
|
102
|
+
categoryId: payloadCategoryId,
|
|
103
|
+
bookModId,
|
|
104
|
+
book: overview.book,
|
|
105
|
+
categories: overview.categories,
|
|
106
|
+
entries: overview.entries.map(({ pages: _pages, ...stub }) => stub),
|
|
107
|
+
...(entry ? { entry, extras } : {}),
|
|
108
|
+
...(Object.keys(questLinks).length ? { questLinks } : {}),
|
|
109
|
+
...(Object.keys(previews).length ? { previews } : {}),
|
|
110
|
+
messages,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import {
|
|
2
|
+
findClientQuestLocation,
|
|
3
|
+
type ClientQuestLocation,
|
|
4
|
+
} from '@terrafirmagreg-team/quest/lib/client/quest';
|
|
5
|
+
import { classify, pageTexts } from '@terrafirmagreg-team/guide/lib/page-kind';
|
|
6
|
+
import { formatGuide } from '@terrafirmagreg-team/guide/lib/format';
|
|
7
|
+
import {
|
|
8
|
+
parseGuideReference,
|
|
9
|
+
resolveGuideReference,
|
|
10
|
+
} from '@terrafirmagreg-team/guide/lib/reference/resolver';
|
|
11
|
+
import type { Lang } from '@terrafirmagreg-team/translate/types/locale';
|
|
12
|
+
import type { GuidePage } from '@terrafirmagreg-team/guide/types';
|
|
13
|
+
|
|
14
|
+
export type QuestLinks = Record<string, ClientQuestLocation>;
|
|
15
|
+
|
|
16
|
+
async function collectQuestLink(
|
|
17
|
+
dataset: string,
|
|
18
|
+
questLinks: QuestLinks,
|
|
19
|
+
target: string | undefined,
|
|
20
|
+
force = false,
|
|
21
|
+
): Promise<void> {
|
|
22
|
+
if (!target || /^https?:\/\//i.test(target)) return;
|
|
23
|
+
const ref = parseGuideReference(target);
|
|
24
|
+
if (!force && ref.kind !== 'quest') return;
|
|
25
|
+
if (questLinks[ref.id]) return;
|
|
26
|
+
const hit = await findClientQuestLocation(dataset, ref.id);
|
|
27
|
+
if (hit) questLinks[ref.id] = hit;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function collectTextLinks(
|
|
31
|
+
dataset: string,
|
|
32
|
+
questLinks: QuestLinks,
|
|
33
|
+
hrefs: Set<string>,
|
|
34
|
+
ctx: {
|
|
35
|
+
lang: Lang;
|
|
36
|
+
modId?: string;
|
|
37
|
+
bookPath?: string;
|
|
38
|
+
categoryByEntry: Map<string, string>;
|
|
39
|
+
},
|
|
40
|
+
text: string | undefined,
|
|
41
|
+
): Promise<void> {
|
|
42
|
+
if (!text) return;
|
|
43
|
+
for (const node of formatGuide(text, {})) {
|
|
44
|
+
const link = 'link' in node ? node.link : undefined;
|
|
45
|
+
if (!link) continue;
|
|
46
|
+
await collectQuestLink(dataset, questLinks, link);
|
|
47
|
+
if (/^https?:\/\//i.test(link)) continue;
|
|
48
|
+
const resolved = resolveGuideReference(parseGuideReference(link), {
|
|
49
|
+
dataset,
|
|
50
|
+
lang: ctx.lang,
|
|
51
|
+
modId: ctx.modId ?? '',
|
|
52
|
+
bookPath: ctx.bookPath ?? '',
|
|
53
|
+
categoryByEntry: ctx.categoryByEntry,
|
|
54
|
+
questLinks,
|
|
55
|
+
});
|
|
56
|
+
if (resolved.href) hrefs.add(resolved.href);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export async function collectPageLinks(
|
|
61
|
+
dataset: string,
|
|
62
|
+
lang: Lang,
|
|
63
|
+
bookModId: string,
|
|
64
|
+
bookPath: string,
|
|
65
|
+
categoryByEntry: Map<string, string>,
|
|
66
|
+
questLinks: QuestLinks,
|
|
67
|
+
hrefs: Set<string>,
|
|
68
|
+
page: GuidePage,
|
|
69
|
+
): Promise<void> {
|
|
70
|
+
const ctx = { lang, modId: bookModId, bookPath, categoryByEntry };
|
|
71
|
+
if (classify(page.type) === 'quest') {
|
|
72
|
+
await collectQuestLink(
|
|
73
|
+
dataset,
|
|
74
|
+
questLinks,
|
|
75
|
+
page.trigger || page.quest || page.link,
|
|
76
|
+
true,
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
await collectTextLinks(dataset, questLinks, hrefs, ctx, page.title);
|
|
80
|
+
for (const text of pageTexts(page)) {
|
|
81
|
+
await collectTextLinks(dataset, questLinks, hrefs, ctx, text);
|
|
82
|
+
}
|
|
83
|
+
for (const target of [
|
|
84
|
+
page.url,
|
|
85
|
+
page.link,
|
|
86
|
+
page.entry,
|
|
87
|
+
...(page.entries ?? []),
|
|
88
|
+
]) {
|
|
89
|
+
if (!target || /^https?:\/\//i.test(target)) continue;
|
|
90
|
+
await collectQuestLink(dataset, questLinks, target);
|
|
91
|
+
const resolved = resolveGuideReference(parseGuideReference(target), {
|
|
92
|
+
dataset,
|
|
93
|
+
lang,
|
|
94
|
+
modId: bookModId,
|
|
95
|
+
bookPath,
|
|
96
|
+
categoryByEntry,
|
|
97
|
+
questLinks,
|
|
98
|
+
});
|
|
99
|
+
if (resolved.href) hrefs.add(resolved.href);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { ComponentType } from 'react';
|
|
2
|
+
import type { TranslationState } from '@terrafirmagreg-team/translate/server';
|
|
3
|
+
import { createConfigRegistry, uiConfig } from '@terrafirmagreg-team/ui/lib/config';
|
|
4
|
+
import '@terrafirmagreg-team/guide/lib/preview';
|
|
5
|
+
import '@terrafirmagreg-team/guide/lib/search/filter';
|
|
6
|
+
|
|
7
|
+
export type GuidePageActionsProps = {
|
|
8
|
+
copied?: TranslationState;
|
|
9
|
+
crowdinUrl?: string;
|
|
10
|
+
title: string;
|
|
11
|
+
mdUrl?: string;
|
|
12
|
+
githubUrl?: string;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export type GuideConfigInput = {
|
|
16
|
+
appName?: string;
|
|
17
|
+
components?: {
|
|
18
|
+
PageActions?: ComponentType<GuidePageActionsProps>;
|
|
19
|
+
UntranslatedCallout?: ComponentType<{ copied?: TranslationState }>;
|
|
20
|
+
};
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export type GuideConfig = {
|
|
24
|
+
appName: string;
|
|
25
|
+
components: {
|
|
26
|
+
PageActions: ComponentType<GuidePageActionsProps>;
|
|
27
|
+
UntranslatedCallout: ComponentType<{ copied?: TranslationState }>;
|
|
28
|
+
};
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const { register: registerGuideConfig, get: guideConfig } = createConfigRegistry<
|
|
32
|
+
GuideConfigInput,
|
|
33
|
+
GuideConfig
|
|
34
|
+
>((overrides) => ({
|
|
35
|
+
appName: overrides.appName ?? uiConfig().brand?.name ?? '',
|
|
36
|
+
components: {
|
|
37
|
+
PageActions: (overrides.components?.PageActions ??
|
|
38
|
+
uiConfig().components?.PageActions) as ComponentType<GuidePageActionsProps>,
|
|
39
|
+
UntranslatedCallout: (overrides.components?.UntranslatedCallout ??
|
|
40
|
+
uiConfig().components?.UntranslatedCallout) as ComponentType<{
|
|
41
|
+
copied?: TranslationState;
|
|
42
|
+
}>,
|
|
43
|
+
},
|
|
44
|
+
}));
|
|
45
|
+
|
|
46
|
+
export { registerGuideConfig, guideConfig };
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import type { TaggedReference } from '@terrafirmagreg-team/game/lib/dump/contract/ref';
|
|
2
|
+
import type { DumpText } from '@terrafirmagreg-team/game/lib/dump/contract/text';
|
|
3
|
+
import type { GuideBookRef } from '@terrafirmagreg-team/game/lib/dump/mods/dto';
|
|
4
|
+
import type { DataGuidesBook } from '@terrafirmagreg-team/game/lib/dump/gen';
|
|
5
|
+
|
|
6
|
+
export type { GuideBookRef };
|
|
7
|
+
|
|
8
|
+
export type GuideBookJson = Omit<
|
|
9
|
+
DataGuidesBook,
|
|
10
|
+
'categories' | 'cover' | 'display' | 'entries' | 'macros' | 'order'
|
|
11
|
+
> & {
|
|
12
|
+
display?: {
|
|
13
|
+
name?: string;
|
|
14
|
+
landing?: DumpText[];
|
|
15
|
+
};
|
|
16
|
+
cover?: TaggedReference | null;
|
|
17
|
+
order?: number;
|
|
18
|
+
macros?: Record<string, unknown>;
|
|
19
|
+
categories?: Record<string, unknown>;
|
|
20
|
+
entries?: Record<string, unknown>;
|
|
21
|
+
[key: string]: unknown;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export type GuideMultiblock = {
|
|
25
|
+
pattern?: string[][];
|
|
26
|
+
mapping?: Record<string, unknown>;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export type GuideTableLegend = {
|
|
30
|
+
color?: string;
|
|
31
|
+
texts?: string[];
|
|
32
|
+
text?: string;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export type GuideTableCell = {
|
|
36
|
+
fill?: string;
|
|
37
|
+
bold?: boolean;
|
|
38
|
+
texts?: string[];
|
|
39
|
+
text?: string;
|
|
40
|
+
item?: unknown;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export type GuidePage = {
|
|
44
|
+
type?: string;
|
|
45
|
+
text?: string;
|
|
46
|
+
texts?: string[];
|
|
47
|
+
title?: string;
|
|
48
|
+
name?: string;
|
|
49
|
+
images?: string[];
|
|
50
|
+
image?: string;
|
|
51
|
+
recipes?: string[];
|
|
52
|
+
item?: unknown;
|
|
53
|
+
icon?: unknown;
|
|
54
|
+
entity?: string;
|
|
55
|
+
url?: string;
|
|
56
|
+
link?: string;
|
|
57
|
+
link_text?: string;
|
|
58
|
+
entry?: string;
|
|
59
|
+
entries?: string[];
|
|
60
|
+
trigger?: string;
|
|
61
|
+
quest?: string;
|
|
62
|
+
multiblock?: GuideMultiblock;
|
|
63
|
+
model?: string;
|
|
64
|
+
multiblocks?: Array<string | GuideMultiblock>;
|
|
65
|
+
columns?: string[] | number;
|
|
66
|
+
rows?: string[][];
|
|
67
|
+
strings?: Array<string | GuideTableCell | Array<string | GuideTableCell>>;
|
|
68
|
+
legend?: GuideTableLegend[];
|
|
69
|
+
fill?: boolean;
|
|
70
|
+
enable_visualize?: boolean;
|
|
71
|
+
border?: boolean;
|
|
72
|
+
anchor?: string;
|
|
73
|
+
scale?: number;
|
|
74
|
+
offset?: number;
|
|
75
|
+
rotate?: boolean;
|
|
76
|
+
[key: string]: unknown;
|
|
77
|
+
};
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import {
|
|
2
|
+
asTaggedReference,
|
|
3
|
+
stackIdFromRef,
|
|
4
|
+
type TaggedReference,
|
|
5
|
+
} from '@terrafirmagreg-team/game/lib/dump/contract/ref';
|
|
6
|
+
import { asString } from '@terrafirmagreg-team/game/lib/dump/contract/read';
|
|
7
|
+
import { referenceFrom, type ResourceKind } from '@terrafirmagreg-team/game/lib/dump/contract/kind';
|
|
8
|
+
|
|
9
|
+
function itemstackIds(value: string): string[] {
|
|
10
|
+
return value
|
|
11
|
+
.split(',')
|
|
12
|
+
.map((part) => part.trim())
|
|
13
|
+
.map((part) => (part.startsWith('tag:') ? `#${part.slice(4)}` : part))
|
|
14
|
+
.filter(
|
|
15
|
+
(part) =>
|
|
16
|
+
part && (part.startsWith('#') || part.includes(':')) && !part.endsWith('.png'),
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function collectItemIds(value: unknown): string[] {
|
|
21
|
+
if (!value) return [];
|
|
22
|
+
if (Array.isArray(value)) return value.flatMap(collectItemIds);
|
|
23
|
+
if (typeof value === 'string') return itemstackIds(value);
|
|
24
|
+
const tagged = asTaggedReference(value);
|
|
25
|
+
if (tagged) {
|
|
26
|
+
if (
|
|
27
|
+
tagged.kind === 'item' ||
|
|
28
|
+
tagged.kind === 'itemTag' ||
|
|
29
|
+
tagged.kind === 'block' ||
|
|
30
|
+
tagged.kind === 'blockTag'
|
|
31
|
+
) {
|
|
32
|
+
return [stackIdFromRef(tagged)];
|
|
33
|
+
}
|
|
34
|
+
return [];
|
|
35
|
+
}
|
|
36
|
+
if (typeof value === 'object') {
|
|
37
|
+
const row = value as Record<string, unknown>;
|
|
38
|
+
if ('item' in row) return collectItemIds(row.item);
|
|
39
|
+
const tag = asString(row.tag);
|
|
40
|
+
if (tag) return collectItemIds(`#${tag}`);
|
|
41
|
+
const id = asString(row.id);
|
|
42
|
+
if (id) return collectItemIds(id);
|
|
43
|
+
}
|
|
44
|
+
return [];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function parseGuideIcon(value: unknown): TaggedReference | undefined {
|
|
48
|
+
if (!value) return undefined;
|
|
49
|
+
const ref = referenceFrom(value);
|
|
50
|
+
if (ref) return ref;
|
|
51
|
+
if (typeof value !== 'string') return undefined;
|
|
52
|
+
const id = value.trim();
|
|
53
|
+
if (!id) return undefined;
|
|
54
|
+
return { kind: id.endsWith('.png') ? 'texture' : 'item', id };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const GUIDE_ITEM_KINDS: readonly TaggedReference['kind'][] = [
|
|
58
|
+
'item',
|
|
59
|
+
'block',
|
|
60
|
+
'itemTag',
|
|
61
|
+
'blockTag',
|
|
62
|
+
'fluidTag',
|
|
63
|
+
'entityTag',
|
|
64
|
+
];
|
|
65
|
+
|
|
66
|
+
export function parseGuideItems(value: unknown): TaggedReference[] {
|
|
67
|
+
if (!value) return [];
|
|
68
|
+
if (Array.isArray(value)) return value.flatMap(parseGuideItems);
|
|
69
|
+
if (typeof value === 'string') {
|
|
70
|
+
return itemstackIds(value).map((id) =>
|
|
71
|
+
id.startsWith('#')
|
|
72
|
+
? { kind: 'itemTag' as const, id }
|
|
73
|
+
: { kind: 'item' as const, id },
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
const ref = parseGuideIcon(value);
|
|
77
|
+
return ref && GUIDE_ITEM_KINDS.includes(ref.kind) ? [ref] : [];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function classifyGuidePageType(type: string | undefined): ResourceKind | undefined {
|
|
81
|
+
switch ((type ?? '').toLowerCase().trim()) {
|
|
82
|
+
case 'entity':
|
|
83
|
+
return 'entity';
|
|
84
|
+
case 'item':
|
|
85
|
+
return 'item';
|
|
86
|
+
case 'block':
|
|
87
|
+
return 'block';
|
|
88
|
+
case 'fluid':
|
|
89
|
+
return 'fluid';
|
|
90
|
+
default:
|
|
91
|
+
return undefined;
|
|
92
|
+
}
|
|
93
|
+
}
|
package/src/lib/dump.ts
ADDED
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
GuideFormatNode,
|
|
3
|
+
GuideFormatOptions,
|
|
4
|
+
PageRendererProps,
|
|
5
|
+
} from '@terrafirmagreg-team/guide/types';
|
|
6
|
+
import { MC_FORMAT_COLORS } from '@terrafirmagreg-team/game/lib/dump/contract/text';
|
|
7
|
+
import { langString } from '@terrafirmagreg-team/game/lib/dump/assets/lang/names';
|
|
8
|
+
|
|
9
|
+
type State = {
|
|
10
|
+
styles: string[];
|
|
11
|
+
color?: string;
|
|
12
|
+
link?: string;
|
|
13
|
+
tooltip?: string;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
function macroReplacement(
|
|
17
|
+
name: string,
|
|
18
|
+
token: string,
|
|
19
|
+
macros: Record<string, string>,
|
|
20
|
+
): string | undefined {
|
|
21
|
+
const found = macros[name] ?? macros[token] ?? macros[`$(${name})`];
|
|
22
|
+
if (found !== undefined && found !== token) return found;
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function expandMacros(
|
|
27
|
+
raw: string,
|
|
28
|
+
macros: Record<string, string> | undefined,
|
|
29
|
+
) {
|
|
30
|
+
let value = raw;
|
|
31
|
+
const replacements = macros ?? {};
|
|
32
|
+
for (let depth = 0; depth < 16; depth++) {
|
|
33
|
+
let changed = false;
|
|
34
|
+
value = value.replace(/\$\(([A-Za-z0-9_.-]+)\)/g, (token, name) => {
|
|
35
|
+
const replacement = macroReplacement(name, token, replacements);
|
|
36
|
+
if (replacement === undefined) return token;
|
|
37
|
+
changed = true;
|
|
38
|
+
return replacement;
|
|
39
|
+
});
|
|
40
|
+
if (!changed || value.length > 100_000) return value.slice(0, 100_000);
|
|
41
|
+
}
|
|
42
|
+
return value;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function appendText(out: GuideFormatNode[], value: string, state: State) {
|
|
46
|
+
if (!value) return;
|
|
47
|
+
value = value.replace(/[◼■⬛]/g, '$&︎');
|
|
48
|
+
const parts = value.split(/(§[0-9a-fk-or])/i);
|
|
49
|
+
let active = { ...state, styles: [...state.styles] };
|
|
50
|
+
for (const part of parts) {
|
|
51
|
+
const code = /^§([0-9a-fk-or])$/i.exec(part)?.[1]?.toLowerCase();
|
|
52
|
+
if (!code) {
|
|
53
|
+
if (part) out.push({ type: 'text', value: part, ...active });
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
if (code === 'r') {
|
|
57
|
+
active = { styles: [] };
|
|
58
|
+
} else if (code in MC_FORMAT_COLORS) {
|
|
59
|
+
active.color = MC_FORMAT_COLORS[code];
|
|
60
|
+
} else {
|
|
61
|
+
const style =
|
|
62
|
+
code === 'l'
|
|
63
|
+
? 'bold'
|
|
64
|
+
: code === 'o'
|
|
65
|
+
? 'italic'
|
|
66
|
+
: code === 'm'
|
|
67
|
+
? 'strike'
|
|
68
|
+
: code === 'n'
|
|
69
|
+
? 'underline'
|
|
70
|
+
: code === 'k'
|
|
71
|
+
? 'obfuscated'
|
|
72
|
+
: undefined;
|
|
73
|
+
if (style && !active.styles.includes(style)) active.styles.push(style);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function pushStyle(state: State, style: string) {
|
|
79
|
+
return state.styles.includes(style)
|
|
80
|
+
? state
|
|
81
|
+
: { ...state, styles: [...state.styles, style] };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function colorFor(token: string): string | undefined {
|
|
85
|
+
if (/^#[0-9a-f]{6}$/i.test(token)) return token;
|
|
86
|
+
if (/^#[0-9a-f]{3}$/i.test(token)) {
|
|
87
|
+
return `#${token[1]}${token[1]}${token[2]}${token[2]}${token[3]}${token[3]}`;
|
|
88
|
+
}
|
|
89
|
+
if (/^0x[0-9a-f]{6}$/i.test(token)) return `#${token.slice(2)}`;
|
|
90
|
+
if (token.length === 1) return MC_FORMAT_COLORS[token.toLowerCase()];
|
|
91
|
+
const named: Record<string, string> = {
|
|
92
|
+
thing: '#449900',
|
|
93
|
+
item: '#bb00bb',
|
|
94
|
+
header: 'var(--color-foreground)',
|
|
95
|
+
};
|
|
96
|
+
return named[token];
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const styleAliases: Record<string, string> = {
|
|
100
|
+
bold: 'bold',
|
|
101
|
+
italic: 'italic',
|
|
102
|
+
strike: 'strike',
|
|
103
|
+
strikethrough: 'strike',
|
|
104
|
+
obfuscated: 'obfuscated',
|
|
105
|
+
underline: 'underline',
|
|
106
|
+
l: 'bold',
|
|
107
|
+
o: 'italic',
|
|
108
|
+
m: 'strike',
|
|
109
|
+
n: 'underline',
|
|
110
|
+
k: 'obfuscated',
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
export function formatGuide(
|
|
114
|
+
raw: string,
|
|
115
|
+
options: GuideFormatOptions = {},
|
|
116
|
+
): GuideFormatNode[] {
|
|
117
|
+
const out: GuideFormatNode[] = [];
|
|
118
|
+
const source = expandMacros(raw, options.macros);
|
|
119
|
+
let state: State = { styles: [] };
|
|
120
|
+
let last = 0;
|
|
121
|
+
const tokens = /\$\(([^)]*)\)/g;
|
|
122
|
+
let match: RegExpExecArray | null;
|
|
123
|
+
|
|
124
|
+
while ((match = tokens.exec(source)) !== null) {
|
|
125
|
+
appendText(out, source.slice(last, match.index), state);
|
|
126
|
+
last = tokens.lastIndex;
|
|
127
|
+
const token = (match[1] ?? '').trim();
|
|
128
|
+
if (!token || token === 'reset' || token === 'clear') {
|
|
129
|
+
state = { styles: [] };
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
if (token === 'br' || token === 'br2') {
|
|
133
|
+
const breaks = token === 'br2' ? 2 : 1;
|
|
134
|
+
for (let index = 0; index < breaks; index++)
|
|
135
|
+
out.push({ type: 'break' });
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
const li = /^li(\d*)$/.exec(token);
|
|
139
|
+
if (li) {
|
|
140
|
+
out.push({ type: 'break' });
|
|
141
|
+
const level = li[1] ? Number(li[1]) : 1;
|
|
142
|
+
appendText(out, `${' '.repeat(Math.max(level, 1) - 1)}• `, state);
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
if (token === '/l') {
|
|
146
|
+
state = { ...state, link: undefined };
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
if (token === '/t') {
|
|
150
|
+
state = { ...state, tooltip: undefined };
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
if (token.startsWith('k:')) {
|
|
154
|
+
out.push({ type: 'keybind', value: token.slice(2) });
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
if (token.startsWith('command:')) {
|
|
158
|
+
out.push({ type: 'command', value: token.slice(8) });
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
if (token.startsWith('l:') || token.startsWith('url:')) {
|
|
162
|
+
state = { ...state, link: token.slice(token.indexOf(':') + 1) };
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
if (token.startsWith('t:') || token.startsWith('tooltip:')) {
|
|
166
|
+
state = {
|
|
167
|
+
...state,
|
|
168
|
+
tooltip: token.slice(token.indexOf(':') + 1),
|
|
169
|
+
};
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
if (token === 'playername') {
|
|
173
|
+
appendText(out, 'Player', state);
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
if (token === 'p') {
|
|
177
|
+
out.push({ type: 'break' });
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
const color = colorFor(token);
|
|
181
|
+
if (color) {
|
|
182
|
+
state = { ...state, color };
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
const style = styleAliases[token];
|
|
186
|
+
if (style) {
|
|
187
|
+
state = pushStyle(state, style);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
appendText(out, source.slice(last), state);
|
|
191
|
+
return out;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export function guideFormatOpts(props: PageRendererProps) {
|
|
195
|
+
return {
|
|
196
|
+
dataset: props.dataset,
|
|
197
|
+
lang: props.lang,
|
|
198
|
+
modId: props.bookModId,
|
|
199
|
+
bookPath: props.bookPath,
|
|
200
|
+
categoryByEntry: props.categoryByEntry,
|
|
201
|
+
macros: props.book?.macros,
|
|
202
|
+
langMap: props.langMap,
|
|
203
|
+
keybinds: props.keybinds,
|
|
204
|
+
questLinks: props.questLinks,
|
|
205
|
+
previews: props.previews,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function prettyKeyId(id: string): string {
|
|
210
|
+
return id
|
|
211
|
+
.replace(/^key\.(?:keyboard\.|mouse\.|categories\.)?/i, '')
|
|
212
|
+
.replace(/[._]+/g, ' ')
|
|
213
|
+
.trim()
|
|
214
|
+
.replace(/\b[a-z]/g, (ch) => ch.toUpperCase());
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export function keybindLabels(
|
|
218
|
+
bindId: string,
|
|
219
|
+
langMap: Record<string, string>,
|
|
220
|
+
keybinds: Record<string, string>,
|
|
221
|
+
): { label: string; title: string } {
|
|
222
|
+
const defaultKey = keybinds[bindId];
|
|
223
|
+
const keyName = defaultKey
|
|
224
|
+
? (langString(langMap, defaultKey) ?? prettyKeyId(defaultKey))
|
|
225
|
+
: undefined;
|
|
226
|
+
const bindName = langString(langMap, bindId) ?? prettyKeyId(bindId);
|
|
227
|
+
return {
|
|
228
|
+
label: keyName ?? bindName,
|
|
229
|
+
title: bindName,
|
|
230
|
+
};
|
|
231
|
+
}
|
package/src/lib/links.ts
ADDED