@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.
Files changed (66) hide show
  1. package/package.json +58 -3
  2. package/src/components/book-switcher.tsx +46 -0
  3. package/src/components/views/browser.tsx +124 -0
  4. package/src/components/views/entry/pages.tsx +186 -0
  5. package/src/components/views/entry/view.tsx +60 -0
  6. package/src/components/views/kind/links.tsx +111 -0
  7. package/src/components/views/kind/media.tsx +167 -0
  8. package/src/components/views/kind/recipe.tsx +134 -0
  9. package/src/components/views/kind/table.tsx +138 -0
  10. package/src/components/views/kind/unknown.tsx +23 -0
  11. package/src/components/views/page/category-body.tsx +56 -0
  12. package/src/components/views/page/entry-body.tsx +79 -0
  13. package/src/components/views/page/overview-body.tsx +97 -0
  14. package/src/components/views/page/text.tsx +57 -0
  15. package/src/components/views/page/view.tsx +279 -0
  16. package/src/components/views/text.tsx +130 -0
  17. package/src/hooks/use-left-sidebar-tree.ts +19 -0
  18. package/src/hooks/use-tree.ts +33 -0
  19. package/src/lib/book/convert.ts +74 -0
  20. package/src/lib/book/index-rows.ts +134 -0
  21. package/src/lib/book/index.ts +2 -0
  22. package/src/lib/book/localize.ts +76 -0
  23. package/src/lib/book/raw.ts +80 -0
  24. package/src/lib/books.ts +29 -0
  25. package/src/lib/breadcrumb.ts +54 -0
  26. package/src/lib/client/guides.ts +189 -0
  27. package/src/lib/client-payload/extras.ts +101 -0
  28. package/src/lib/client-payload/index.ts +112 -0
  29. package/src/lib/client-payload/links.ts +101 -0
  30. package/src/lib/config.ts +46 -0
  31. package/src/lib/data/dto.ts +77 -0
  32. package/src/lib/data/parse.ts +93 -0
  33. package/src/lib/dump.ts +5 -0
  34. package/src/lib/format.ts +231 -0
  35. package/src/lib/links.ts +2 -0
  36. package/src/lib/markdown/book.ts +73 -0
  37. package/src/lib/markdown/ctx.ts +26 -0
  38. package/src/lib/markdown/index.ts +8 -0
  39. package/src/lib/markdown/page.ts +154 -0
  40. package/src/lib/markdown/table.ts +35 -0
  41. package/src/lib/markdown/text.ts +39 -0
  42. package/src/lib/markdown-page.ts +72 -0
  43. package/src/lib/messages.ts +13 -0
  44. package/src/lib/meta.ts +9 -0
  45. package/src/lib/multiblock/index.ts +8 -0
  46. package/src/lib/multiblock/url.ts +61 -0
  47. package/src/lib/names.ts +35 -0
  48. package/src/lib/page-kind.ts +117 -0
  49. package/src/lib/paths.ts +52 -0
  50. package/src/lib/preview.ts +33 -0
  51. package/src/lib/query.ts +28 -0
  52. package/src/lib/recipe.ts +30 -0
  53. package/src/lib/reference/index.ts +40 -0
  54. package/src/lib/reference/resolver.ts +88 -0
  55. package/src/lib/search/filter.ts +17 -0
  56. package/src/lib/search/index.ts +156 -0
  57. package/src/lib/sprites.ts +17 -0
  58. package/src/lib/table.ts +10 -0
  59. package/src/lib/toc.ts +137 -0
  60. package/src/lib/tree/children.ts +70 -0
  61. package/src/lib/tree/index.ts +117 -0
  62. package/src/locales/en_us.json +22 -0
  63. package/src/locales/ru_ru.json +22 -0
  64. package/src/locales.ts +5 -0
  65. package/src/types/index.ts +215 -0
  66. 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
+ }
@@ -0,0 +1,5 @@
1
+ import type { DumpIndex } from '@terrafirmagreg-team/game/lib/dump/mods/dto';
2
+
3
+ export function guideDumpHasContent(dump: DumpIndex): boolean {
4
+ return dump.mods.some((mod) => mod.books.length > 0);
5
+ }
@@ -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
+ }
@@ -0,0 +1,2 @@
1
+
2
+ export { guideHref } from '@terrafirmagreg-team/game/lib/entry/href';