@terrafirmagreg-team/guide 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 +57 -3
- package/src/components/book-switcher.tsx +52 -0
- package/src/components/mdx/guide-entry.tsx +48 -0
- package/src/components/mdx/multiblock.tsx +86 -0
- package/src/components/tree/children.tsx +75 -0
- package/src/components/tree/index.tsx +127 -0
- package/src/components/views/browser.tsx +133 -0
- package/src/components/views/entry/pages.tsx +188 -0
- package/src/components/views/entry/view.tsx +62 -0
- package/src/components/views/kind/links.tsx +112 -0
- package/src/components/views/kind/media.tsx +168 -0
- package/src/components/views/kind/recipe.tsx +89 -0
- package/src/components/views/kind/table.tsx +140 -0
- package/src/components/views/kind/unknown.tsx +24 -0
- package/src/components/views/page/category-body.tsx +58 -0
- package/src/components/views/page/entry-body.tsx +81 -0
- package/src/components/views/page/overview-body.tsx +99 -0
- package/src/components/views/page/text.tsx +58 -0
- package/src/components/views/page/view.tsx +279 -0
- package/src/components/views/text.tsx +132 -0
- package/src/hooks/use-left-sidebar-tree.ts +28 -0
- package/src/hooks/use-tree.ts +35 -0
- package/src/lib/book/convert.ts +75 -0
- package/src/lib/book/index-rows.ts +136 -0
- package/src/lib/book/index.ts +3 -0
- package/src/lib/book/localize.ts +77 -0
- package/src/lib/book/raw.ts +81 -0
- package/src/lib/books.ts +31 -0
- package/src/lib/breadcrumb.ts +56 -0
- package/src/lib/client/guides.ts +262 -0
- package/src/lib/client-payload/extras.ts +103 -0
- package/src/lib/client-payload/index.ts +113 -0
- package/src/lib/client-payload/links.ts +103 -0
- package/src/lib/config.ts +47 -0
- package/src/lib/data/dto.ts +78 -0
- package/src/lib/data/parse.ts +94 -0
- package/src/lib/dump.ts +6 -0
- package/src/lib/embeds.ts +12 -0
- package/src/lib/format.ts +232 -0
- package/src/lib/links.ts +2 -0
- package/src/lib/markdown/book.ts +74 -0
- package/src/lib/markdown/ctx.ts +28 -0
- package/src/lib/markdown/index.ts +9 -0
- package/src/lib/markdown/page.ts +153 -0
- package/src/lib/markdown/table.ts +36 -0
- package/src/lib/markdown/text.ts +40 -0
- package/src/lib/markdown-page.ts +74 -0
- package/src/lib/messages.ts +14 -0
- package/src/lib/meta.ts +10 -0
- package/src/lib/multiblock/index.ts +9 -0
- package/src/lib/multiblock/url.ts +75 -0
- package/src/lib/names.ts +37 -0
- package/src/lib/page-kind.ts +118 -0
- package/src/lib/paths.ts +54 -0
- package/src/lib/preview.ts +37 -0
- package/src/lib/query.ts +29 -0
- package/src/lib/recipe.ts +31 -0
- package/src/lib/reference/index.ts +41 -0
- package/src/lib/reference/resolver.ts +88 -0
- package/src/lib/search/filter.ts +18 -0
- package/src/lib/search/index.ts +183 -0
- package/src/lib/sprites.ts +18 -0
- package/src/lib/table.ts +11 -0
- package/src/lib/toc.ts +138 -0
- package/src/locales/en_us.json +14 -0
- package/src/locales/ru_ru.json +14 -0
- package/src/locales.ts +6 -0
- package/src/types/index.ts +217 -0
- package/README.md +0 -3
package/src/lib/paths.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { DatasetRef } from '@terrafirmagreg-team/sites/location';
|
|
2
|
+
import type { Lang } from '@terrafirmagreg-team/translate/types/locale';
|
|
3
|
+
import type { GuideCategoryRef } from '@terrafirmagreg-team/guide/types';
|
|
4
|
+
import { guideHref as guideQueryHref } from '@terrafirmagreg-team/guide/lib/links';
|
|
5
|
+
|
|
6
|
+
export function bareId(id: string): string {
|
|
7
|
+
const trimmed = id.replace(/^\/+|\/+$/g, '');
|
|
8
|
+
return trimmed.includes(':')
|
|
9
|
+
? trimmed.slice(trimmed.indexOf(':') + 1)
|
|
10
|
+
: trimmed;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function bookOptionId(modId: string, bookPath: string): string {
|
|
14
|
+
return `${modId}:${bookPath}`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function guideBookHref(
|
|
18
|
+
dataset: DatasetRef,
|
|
19
|
+
lang: Lang | undefined,
|
|
20
|
+
modId?: string,
|
|
21
|
+
bookPath?: string,
|
|
22
|
+
entryId?: string,
|
|
23
|
+
hash?: string,
|
|
24
|
+
categoryId?: string,
|
|
25
|
+
): string {
|
|
26
|
+
const bookId = modId && bookPath ? bookOptionId(modId, bookPath) : undefined;
|
|
27
|
+
return guideQueryHref(dataset, lang, bookId, entryId, hash, categoryId);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function guideCategoryHref(
|
|
31
|
+
dataset: DatasetRef,
|
|
32
|
+
lang: Lang | undefined,
|
|
33
|
+
modId: string,
|
|
34
|
+
bookPath: string,
|
|
35
|
+
category: { id: string },
|
|
36
|
+
): string {
|
|
37
|
+
return guideBookHref(
|
|
38
|
+
dataset,
|
|
39
|
+
lang,
|
|
40
|
+
modId,
|
|
41
|
+
bookPath,
|
|
42
|
+
undefined,
|
|
43
|
+
undefined,
|
|
44
|
+
category.id,
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function isRootCategory(
|
|
49
|
+
_category: GuideCategoryRef,
|
|
50
|
+
_all: GuideCategoryRef[],
|
|
51
|
+
): boolean {
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { registerPreviewResolver } from '@terrafirmagreg-team/preview/lib';
|
|
2
|
+
import { loadClientGuideBook } from '@terrafirmagreg-team/guide/lib/client/guides';
|
|
3
|
+
import { entryPreviewIcon } from '@terrafirmagreg-team/game/lib/preview';
|
|
4
|
+
import type { PreviewResolverCtx } from '@terrafirmagreg-team/preview/lib';
|
|
5
|
+
import type { PagePreviewData } from '@terrafirmagreg-team/ui/types/preview';
|
|
6
|
+
|
|
7
|
+
export async function guideLinkPreview({
|
|
8
|
+
parts,
|
|
9
|
+
query,
|
|
10
|
+
lang,
|
|
11
|
+
}: PreviewResolverCtx): Promise<PagePreviewData | undefined> {
|
|
12
|
+
const dataset = {
|
|
13
|
+
id: parts[1] ?? '',
|
|
14
|
+
version: query.get('version') ?? undefined,
|
|
15
|
+
};
|
|
16
|
+
const bookId = query.get('book');
|
|
17
|
+
if (!dataset.id || !bookId) return undefined;
|
|
18
|
+
const index = await loadClientGuideBook(dataset, lang, bookId);
|
|
19
|
+
if (!index) return undefined;
|
|
20
|
+
const entryId = query.get('entry');
|
|
21
|
+
if (entryId) {
|
|
22
|
+
const entry = index.entries.find((row) => row.id === entryId);
|
|
23
|
+
if (!entry) return undefined;
|
|
24
|
+
return {
|
|
25
|
+
title: entry.name ?? entry.id,
|
|
26
|
+
icon: entryPreviewIcon(entry.icon, dataset),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
return {
|
|
30
|
+
title: index.book?.name ?? bookId,
|
|
31
|
+
description: index.book?.landingText,
|
|
32
|
+
icon: entryPreviewIcon(index.book?.cover ?? undefined, dataset),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
registerPreviewResolver('guide', guideLinkPreview);
|
|
37
|
+
|
package/src/lib/query.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { GuideIndex } from '@terrafirmagreg-team/guide/types';
|
|
2
|
+
|
|
3
|
+
type GuideQuery = {
|
|
4
|
+
bookId?: string;
|
|
5
|
+
entryId?: string;
|
|
6
|
+
categoryId?: string;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export function guideQueryFromParams(params: {
|
|
10
|
+
get(name: string): string | null;
|
|
11
|
+
}): GuideQuery {
|
|
12
|
+
const book = params.get('book') ?? undefined;
|
|
13
|
+
const entry = params.get('entry') ?? undefined;
|
|
14
|
+
const category = params.get('cat') ?? undefined;
|
|
15
|
+
return {
|
|
16
|
+
...(book ? { bookId: book } : {}),
|
|
17
|
+
...(entry ? { entryId: entry } : {}),
|
|
18
|
+
...(category ? { categoryId: category } : {}),
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function firstGuideEntryId(index: GuideIndex): string | undefined {
|
|
23
|
+
return (
|
|
24
|
+
index.categories.flatMap((category) =>
|
|
25
|
+
category.entries.map((entry) => entry.id),
|
|
26
|
+
)[0] ?? index.entries[0]?.id
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { GuidePage } from '@terrafirmagreg-team/guide/types';
|
|
2
|
+
import type { EmbedRecipe } from '@terrafirmagreg-team/guide/lib/embeds';
|
|
3
|
+
import { stackDisplayName } from '@terrafirmagreg-team/game/lib/entry/display';
|
|
4
|
+
|
|
5
|
+
export function recipeIds(page: GuidePage): string[] {
|
|
6
|
+
return (page.recipes ?? []).filter(
|
|
7
|
+
(id): id is string => typeof id === 'string' && Boolean(id),
|
|
8
|
+
);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function recipeNameScope(
|
|
12
|
+
langMap: Record<string, string>,
|
|
13
|
+
recipe: EmbedRecipe,
|
|
14
|
+
): Record<string, string> {
|
|
15
|
+
const ids = new Set<string>([recipe.id]);
|
|
16
|
+
for (const ing of [...recipe.inputs, ...recipe.outputs]) ids.add(ing.id);
|
|
17
|
+
for (const fill of Object.values(recipe.slots ?? {})) {
|
|
18
|
+
ids.add(fill.id);
|
|
19
|
+
for (const alt of fill.alternatives ?? []) ids.add(alt.id);
|
|
20
|
+
}
|
|
21
|
+
for (const widget of recipe.widgets ?? []) {
|
|
22
|
+
if (widget.type === 'slot') {
|
|
23
|
+
ids.add(widget.id);
|
|
24
|
+
for (const alt of widget.alternatives ?? []) ids.add(alt);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
const names: Record<string, string> = { ...langMap };
|
|
28
|
+
for (const id of ids) names[id] = stackDisplayName(langMap, id);
|
|
29
|
+
return names;
|
|
30
|
+
}
|
|
31
|
+
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import {
|
|
2
|
+
parseGuideReference,
|
|
3
|
+
resolveGuideReference,
|
|
4
|
+
} from '@terrafirmagreg-team/guide/lib/reference/resolver';
|
|
5
|
+
import type {
|
|
6
|
+
GuideReferenceContext,
|
|
7
|
+
PageRendererProps,
|
|
8
|
+
} from '@terrafirmagreg-team/guide/types';
|
|
9
|
+
|
|
10
|
+
export function referenceHref(
|
|
11
|
+
props: PageRendererProps,
|
|
12
|
+
target: string,
|
|
13
|
+
): { href: string; external: boolean } | null {
|
|
14
|
+
if (/^https?:\/\//i.test(target)) return { href: target, external: true };
|
|
15
|
+
const reference = resolveGuideReference(parseGuideReference(target), {
|
|
16
|
+
dataset: props.dataset,
|
|
17
|
+
lang: props.lang,
|
|
18
|
+
modId: props.bookModId,
|
|
19
|
+
bookPath: props.bookPath,
|
|
20
|
+
categoryByEntry: props.categoryByEntry,
|
|
21
|
+
questLinks: props.questLinks,
|
|
22
|
+
});
|
|
23
|
+
return reference.href && reference.available
|
|
24
|
+
? { href: reference.href, external: false }
|
|
25
|
+
: null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function guideLink(
|
|
29
|
+
node: { link?: string },
|
|
30
|
+
ctx: GuideReferenceContext,
|
|
31
|
+
): { href: string | undefined; external: boolean } {
|
|
32
|
+
const target = node.link;
|
|
33
|
+
if (!target) return { href: undefined, external: false };
|
|
34
|
+
if (/^https?:\/\//i.test(target)) return { href: target, external: true };
|
|
35
|
+
const resolved = resolveGuideReference(parseGuideReference(target), ctx);
|
|
36
|
+
return {
|
|
37
|
+
href: resolved.available ? resolved.href : undefined,
|
|
38
|
+
external: false,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { guideEmbeds } from '@terrafirmagreg-team/guide/lib/embeds';
|
|
2
|
+
import {
|
|
3
|
+
bareId,
|
|
4
|
+
guideBookHref,
|
|
5
|
+
} from '@terrafirmagreg-team/guide/lib/paths';
|
|
6
|
+
import type {
|
|
7
|
+
GuideReference,
|
|
8
|
+
GuideReferenceContext,
|
|
9
|
+
ResolvedGuideReference,
|
|
10
|
+
} from '@terrafirmagreg-team/guide/types';
|
|
11
|
+
|
|
12
|
+
export function parseGuideReference(target: string): GuideReference {
|
|
13
|
+
const [rawId, anchor] = target.replace(/\.json$/i, '').split('#', 2);
|
|
14
|
+
const [prefix, ...rest] = rawId.split(':');
|
|
15
|
+
if ((prefix === 'recipe' || prefix === 'quest') && rest.length) {
|
|
16
|
+
return { kind: prefix, id: rest.join(':'), anchor };
|
|
17
|
+
}
|
|
18
|
+
return { kind: 'guide', id: rawId.replace(/^guide:/, ''), anchor };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function matchGuideEntryKey(
|
|
22
|
+
ids: Iterable<string>,
|
|
23
|
+
query: string,
|
|
24
|
+
): string | undefined {
|
|
25
|
+
const keys = ids instanceof Set ? ids : new Set(ids);
|
|
26
|
+
if (keys.has(query)) return query;
|
|
27
|
+
const bare = bareId(query);
|
|
28
|
+
for (const key of keys) {
|
|
29
|
+
if (bareId(key) === bare) return key;
|
|
30
|
+
}
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function resolveGuideReference(
|
|
35
|
+
reference: GuideReference,
|
|
36
|
+
context: GuideReferenceContext,
|
|
37
|
+
): ResolvedGuideReference {
|
|
38
|
+
switch (reference.kind) {
|
|
39
|
+
case 'guide': {
|
|
40
|
+
const entryId = matchGuideEntryKey(
|
|
41
|
+
context.categoryByEntry.keys(),
|
|
42
|
+
reference.id,
|
|
43
|
+
);
|
|
44
|
+
return {
|
|
45
|
+
...reference,
|
|
46
|
+
href: entryId
|
|
47
|
+
? guideBookHref(
|
|
48
|
+
context.dataset,
|
|
49
|
+
undefined,
|
|
50
|
+
context.modId,
|
|
51
|
+
context.bookPath,
|
|
52
|
+
entryId,
|
|
53
|
+
reference.anchor,
|
|
54
|
+
)
|
|
55
|
+
: guideBookHref(
|
|
56
|
+
context.dataset,
|
|
57
|
+
undefined,
|
|
58
|
+
context.modId,
|
|
59
|
+
context.bookPath,
|
|
60
|
+
bareId(reference.id),
|
|
61
|
+
reference.anchor,
|
|
62
|
+
),
|
|
63
|
+
available: Boolean(entryId ?? reference.id),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
case 'recipe': {
|
|
67
|
+
const href =
|
|
68
|
+
context.targets?.recipe?.(reference.id) ??
|
|
69
|
+
guideEmbeds().recipe?.href(context.dataset, undefined, { id: reference.id });
|
|
70
|
+
return { ...reference, href, available: Boolean(href) };
|
|
71
|
+
}
|
|
72
|
+
case 'quest': {
|
|
73
|
+
const hit = context.questLinks?.[reference.id];
|
|
74
|
+
const href = hit
|
|
75
|
+
? guideEmbeds().quest?.href(context.dataset, undefined, {
|
|
76
|
+
category: hit.filename,
|
|
77
|
+
id: hit.questId,
|
|
78
|
+
})
|
|
79
|
+
: context.targets?.quest?.(reference.id);
|
|
80
|
+
return { ...reference, href, available: Boolean(href) };
|
|
81
|
+
}
|
|
82
|
+
default: {
|
|
83
|
+
const _exhaustive: never = reference.kind;
|
|
84
|
+
return _exhaustive;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { registerSearchHitFilter } from '@terrafirmagreg-team/search/lib';
|
|
2
|
+
import type { SortedResult } from '@terrafirmagreg-team/ui/types/search';
|
|
3
|
+
|
|
4
|
+
export function guideHitFilter(
|
|
5
|
+
params: URLSearchParams,
|
|
6
|
+
): ((hit: SortedResult) => boolean) | undefined {
|
|
7
|
+
const book = params.get('book');
|
|
8
|
+
if (!book) return undefined;
|
|
9
|
+
return (hit) => {
|
|
10
|
+
const queryString = hit.url.split('?')[1];
|
|
11
|
+
if (!queryString) return true;
|
|
12
|
+
const docBook = new URLSearchParams(queryString).get('book');
|
|
13
|
+
return docBook === null || docBook === book;
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
registerSearchHitFilter('guide', guideHitFilter);
|
|
18
|
+
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import type { DatasetRef } from '@terrafirmagreg-team/sites/location';
|
|
2
|
+
import type { AdvancedIndex } from 'fumadocs-core/search/server';
|
|
3
|
+
import { dumpTextString, langString } from '@terrafirmagreg-team/dump/lib/dump/assets/lang/names';
|
|
4
|
+
import { formatGuide } from '@terrafirmagreg-team/guide/lib/format';
|
|
5
|
+
import { isIndexPage } from '@terrafirmagreg-team/guide/lib/page-kind';
|
|
6
|
+
import type { DumpIndex } from '@terrafirmagreg-team/dump/lib/dump/mods/dto';
|
|
7
|
+
|
|
8
|
+
export type GuideIndex = AdvancedIndex & { locale: string };
|
|
9
|
+
|
|
10
|
+
export interface DumpSource {
|
|
11
|
+
getJson(rel: string): Promise<unknown | undefined>;
|
|
12
|
+
getLang(lang: string): Promise<Record<string, string>>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
type DumpTextNode = Parameters<typeof dumpTextString>[1];
|
|
16
|
+
|
|
17
|
+
function plainText(raw: string, macros?: Record<string, string>): string {
|
|
18
|
+
let out = '';
|
|
19
|
+
for (const node of formatGuide(raw, { macros })) {
|
|
20
|
+
if (node.type === 'break') out += '\n';
|
|
21
|
+
else out += (node.type === 'command' ? node.label : undefined) ?? node.value;
|
|
22
|
+
}
|
|
23
|
+
return out
|
|
24
|
+
.replace(/\s*\$\([^)]*$/, '')
|
|
25
|
+
.replace(/[ \t]*\n[ \t]*/g, '\n')
|
|
26
|
+
.replace(/\n{3,}/g, '\n\n')
|
|
27
|
+
.trim();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function resolveText(
|
|
31
|
+
map: Record<string, string>,
|
|
32
|
+
node: unknown,
|
|
33
|
+
macros?: Record<string, string>,
|
|
34
|
+
): string | undefined {
|
|
35
|
+
if (typeof node === 'string') {
|
|
36
|
+
const bare =
|
|
37
|
+
node.startsWith('{') && node.endsWith('}')
|
|
38
|
+
? node.slice(1, -1).trim()
|
|
39
|
+
: undefined;
|
|
40
|
+
const resolved = langString(map, node) ?? bare ?? (bare ? undefined : node);
|
|
41
|
+
return resolved ? plainText(resolved, macros) || undefined : undefined;
|
|
42
|
+
}
|
|
43
|
+
if (Array.isArray(node)) {
|
|
44
|
+
const parts = node
|
|
45
|
+
.map((item) => resolveText(map, item, macros))
|
|
46
|
+
.filter((part): part is string => Boolean(part));
|
|
47
|
+
return parts.length ? parts.join('\n') : undefined;
|
|
48
|
+
}
|
|
49
|
+
if (node && typeof node === 'object') {
|
|
50
|
+
const resolved = dumpTextString(map, node as DumpTextNode);
|
|
51
|
+
return resolved ? plainText(resolved, macros) || undefined : undefined;
|
|
52
|
+
}
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
interface RawPage {
|
|
57
|
+
anchor?: unknown;
|
|
58
|
+
title?: unknown;
|
|
59
|
+
texts?: unknown;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
interface RawEntry {
|
|
63
|
+
display?: { name?: unknown };
|
|
64
|
+
pages?: RawPage[];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
interface RawBook {
|
|
68
|
+
id?: unknown;
|
|
69
|
+
entries?: Record<string, string>;
|
|
70
|
+
macros?: Record<string, string>;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function entryIndex(
|
|
74
|
+
dataset: DatasetRef,
|
|
75
|
+
bookId: string,
|
|
76
|
+
entryId: string,
|
|
77
|
+
entry: RawEntry,
|
|
78
|
+
map: Record<string, string>,
|
|
79
|
+
lang: string,
|
|
80
|
+
macros?: Record<string, string>,
|
|
81
|
+
) {
|
|
82
|
+
const title =
|
|
83
|
+
resolveText(map, entry.display?.name, macros) ??
|
|
84
|
+
entryId.split('/').pop() ??
|
|
85
|
+
entryId;
|
|
86
|
+
const contents = (entry.pages ?? [])
|
|
87
|
+
.map((page) => {
|
|
88
|
+
if (isIndexPage(page, map)) return null;
|
|
89
|
+
const heading = resolveText(map, page.title, macros);
|
|
90
|
+
const content = resolveText(map, page.texts, macros);
|
|
91
|
+
return content ? { heading: heading ?? title, content } : null;
|
|
92
|
+
})
|
|
93
|
+
.filter((c): c is { heading: string; content: string } => c !== null);
|
|
94
|
+
const body = contents.map((c) => c.content).join('\n');
|
|
95
|
+
if (!body && !title) return null;
|
|
96
|
+
const params = new URLSearchParams({ book: bookId, entry: entryId });
|
|
97
|
+
if (dataset.version) params.set('version', dataset.version);
|
|
98
|
+
const refKey = `${dataset.id}/${dataset.version ?? ''}`;
|
|
99
|
+
return {
|
|
100
|
+
title,
|
|
101
|
+
url: `/guide/${dataset.id}?${params}`,
|
|
102
|
+
id: `guide:${refKey}:${bookId}:${entryId}:${lang}`,
|
|
103
|
+
tag: `guide:${refKey}`,
|
|
104
|
+
locale: lang,
|
|
105
|
+
structuredData: { headings: [], contents },
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async function bookIndexes(
|
|
110
|
+
dataset: DatasetRef,
|
|
111
|
+
dir: string,
|
|
112
|
+
bookId: string,
|
|
113
|
+
langs: string[],
|
|
114
|
+
source: DumpSource,
|
|
115
|
+
): Promise<GuideIndex[]> {
|
|
116
|
+
const book = (await source.getJson(`${dir}/book.json`)) as
|
|
117
|
+
| RawBook
|
|
118
|
+
| undefined;
|
|
119
|
+
const buckets = book?.entries;
|
|
120
|
+
if (!buckets || typeof buckets !== 'object') return [];
|
|
121
|
+
const maps = new Map(
|
|
122
|
+
await Promise.all(
|
|
123
|
+
langs.map(
|
|
124
|
+
async (lang) => [lang, await source.getLang(lang)] as const,
|
|
125
|
+
),
|
|
126
|
+
),
|
|
127
|
+
);
|
|
128
|
+
const bucketDocs = new Map<string, Record<string, RawEntry> | undefined>();
|
|
129
|
+
await Promise.all(
|
|
130
|
+
[...new Set(Object.values(buckets))].map(async (rel) => {
|
|
131
|
+
bucketDocs.set(
|
|
132
|
+
rel,
|
|
133
|
+
(await source.getJson(`${dir}/${rel}`)) as
|
|
134
|
+
| Record<string, RawEntry>
|
|
135
|
+
| undefined,
|
|
136
|
+
);
|
|
137
|
+
}),
|
|
138
|
+
);
|
|
139
|
+
const out: GuideIndex[] = [];
|
|
140
|
+
for (const [entryId, rel] of Object.entries(buckets)) {
|
|
141
|
+
const entry = bucketDocs.get(rel)?.[entryId];
|
|
142
|
+
if (!entry) continue;
|
|
143
|
+
for (const lang of langs) {
|
|
144
|
+
const index = entryIndex(
|
|
145
|
+
dataset,
|
|
146
|
+
bookId,
|
|
147
|
+
entryId,
|
|
148
|
+
entry,
|
|
149
|
+
maps.get(lang) ?? {},
|
|
150
|
+
lang,
|
|
151
|
+
book.macros,
|
|
152
|
+
);
|
|
153
|
+
if (index) out.push(index);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return out;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export async function guideSearchIndexes(
|
|
160
|
+
dataset: DatasetRef,
|
|
161
|
+
dump: DumpIndex,
|
|
162
|
+
source: DumpSource,
|
|
163
|
+
): Promise<GuideIndex[]> {
|
|
164
|
+
const langs = dump.manifest.languages?.length
|
|
165
|
+
? dump.manifest.languages
|
|
166
|
+
: ['en_us'];
|
|
167
|
+
const out: GuideIndex[] = [];
|
|
168
|
+
for (const mod of dump.mods) {
|
|
169
|
+
for (const book of mod.books) {
|
|
170
|
+
out.push(
|
|
171
|
+
...(await bookIndexes(dataset, book.dir, book.bookId, langs, source)),
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
const seen = new Set<string>();
|
|
176
|
+
return out.filter((index) => {
|
|
177
|
+
const key = `${index.locale}:${index.id}`;
|
|
178
|
+
if (seen.has(key)) return false;
|
|
179
|
+
seen.add(key);
|
|
180
|
+
return true;
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { collectItemIds } from '@terrafirmagreg-team/guide/lib/data/parse';
|
|
2
|
+
import type { GuideEntry } from '@terrafirmagreg-team/guide/types';
|
|
3
|
+
|
|
4
|
+
export function spriteIdsFromEntry(entry: GuideEntry): string[] {
|
|
5
|
+
const ids: string[] = [];
|
|
6
|
+
if (entry.icon && entry.icon.kind !== 'texture') ids.push(entry.icon.id);
|
|
7
|
+
for (const page of entry.pages ?? []) {
|
|
8
|
+
ids.push(...collectItemIds(page.item), ...collectItemIds(page.icon));
|
|
9
|
+
if (typeof page.entity === 'string') ids.push(page.entity);
|
|
10
|
+
ids.push(
|
|
11
|
+
...(page.recipes ?? []).filter(
|
|
12
|
+
(id): id is string => typeof id === 'string' && Boolean(id),
|
|
13
|
+
),
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
return ids;
|
|
17
|
+
}
|
|
18
|
+
|
package/src/lib/table.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { GuideTableCell } from '@terrafirmagreg-team/guide/types';
|
|
2
|
+
|
|
3
|
+
export function cssColor(value: string | undefined): string | undefined {
|
|
4
|
+
return value && /^0x[0-9a-f]{6}$/i.test(value) ? `#${value.slice(2)}` : value;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function cellTexts(cell: GuideTableCell): string[] {
|
|
8
|
+
if (Array.isArray(cell.texts)) return cell.texts.filter(Boolean);
|
|
9
|
+
return typeof cell.text === 'string' && cell.text ? [cell.text] : [];
|
|
10
|
+
}
|
|
11
|
+
|
package/src/lib/toc.ts
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import type { TOCItemType } from 'fumadocs-core/toc';
|
|
2
|
+
import type { TaggedReference } from '@terrafirmagreg-team/dump/lib/dump/contract/ref';
|
|
3
|
+
import { langString } from '@terrafirmagreg-team/dump/lib/dump/assets/lang/names';
|
|
4
|
+
import { stackDisplayName } from '@terrafirmagreg-team/game/lib/entry/display';
|
|
5
|
+
import {
|
|
6
|
+
classifyGuidePageType,
|
|
7
|
+
collectItemIds,
|
|
8
|
+
parseGuideIcon,
|
|
9
|
+
parseGuideItems,
|
|
10
|
+
} from '@terrafirmagreg-team/guide/lib/data/parse';
|
|
11
|
+
import type { GuidePage } from '@terrafirmagreg-team/guide/lib/data/dto';
|
|
12
|
+
import { isIndexPage } from '@terrafirmagreg-team/guide/lib/page-kind';
|
|
13
|
+
import type { GuideTocLabels } from '@terrafirmagreg-team/guide/types';
|
|
14
|
+
|
|
15
|
+
function resourceIds(page: GuidePage): string[] {
|
|
16
|
+
return [
|
|
17
|
+
...collectItemIds(page.item),
|
|
18
|
+
...collectItemIds(page.icon),
|
|
19
|
+
page.entity,
|
|
20
|
+
...(page.recipes ?? []),
|
|
21
|
+
].filter((id): id is string => typeof id === 'string' && Boolean(id));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function localizedField(
|
|
25
|
+
map: Record<string, string>,
|
|
26
|
+
value: unknown,
|
|
27
|
+
): string | undefined {
|
|
28
|
+
if (typeof value !== 'string' || !value) return undefined;
|
|
29
|
+
const resolved = langString(map, value);
|
|
30
|
+
if (resolved) return resolved;
|
|
31
|
+
const trimmed = value.trim();
|
|
32
|
+
if (!trimmed) return undefined;
|
|
33
|
+
if (trimmed.startsWith('{') && trimmed.endsWith('}')) return undefined;
|
|
34
|
+
return value;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function typeOf(page: GuidePage): string {
|
|
38
|
+
return (page.type ?? '').toLowerCase().trim();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function pageTitle(
|
|
42
|
+
page: GuidePage,
|
|
43
|
+
langMap: Record<string, string> = {},
|
|
44
|
+
labels: GuideTocLabels = {},
|
|
45
|
+
entityNames?: Record<string, string>,
|
|
46
|
+
): string | undefined {
|
|
47
|
+
const display =
|
|
48
|
+
page.display &&
|
|
49
|
+
typeof page.display === 'object' &&
|
|
50
|
+
!Array.isArray(page.display)
|
|
51
|
+
? (page.display as Record<string, unknown>)
|
|
52
|
+
: undefined;
|
|
53
|
+
const fromFields =
|
|
54
|
+
localizedField(langMap, page.title) ??
|
|
55
|
+
localizedField(
|
|
56
|
+
langMap,
|
|
57
|
+
typeof display?.name === 'string' ? display.name : undefined,
|
|
58
|
+
) ??
|
|
59
|
+
localizedField(langMap, page.name);
|
|
60
|
+
if (fromFields && fromFields !== page.entity) return fromFields;
|
|
61
|
+
|
|
62
|
+
const type = typeOf(page);
|
|
63
|
+
if (type === 'relations') return labels.related;
|
|
64
|
+
if (type === 'quest') return labels.objective;
|
|
65
|
+
|
|
66
|
+
for (const id of resourceIds(page)) {
|
|
67
|
+
const name = entityNames?.[id] ?? stackDisplayName(langMap, id);
|
|
68
|
+
if (name !== id) return name;
|
|
69
|
+
}
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function pageAnchor(
|
|
74
|
+
page: GuidePage,
|
|
75
|
+
index: number,
|
|
76
|
+
taken?: Set<string>,
|
|
77
|
+
): string {
|
|
78
|
+
const anchor = typeof page.anchor === 'string' ? page.anchor.trim() : '';
|
|
79
|
+
if (anchor && !taken?.has(anchor)) {
|
|
80
|
+
taken?.add(anchor);
|
|
81
|
+
return anchor;
|
|
82
|
+
}
|
|
83
|
+
return `page-${index + 1}`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function pageAnchors(pages: GuidePage[] | undefined): string[] {
|
|
87
|
+
if (!pages?.length) return [];
|
|
88
|
+
const taken = new Set<string>();
|
|
89
|
+
return pages.map((page, i) => pageAnchor(page, i, taken));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function pageIcon(page: GuidePage): TaggedReference | undefined {
|
|
93
|
+
return pageIcons(page)[0];
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function pageIcons(page: GuidePage): TaggedReference[] {
|
|
97
|
+
const kind = classifyGuidePageType(page.type);
|
|
98
|
+
if (kind === 'entity') return [];
|
|
99
|
+
const icon = parseGuideIcon(page.icon);
|
|
100
|
+
if (icon) return [icon];
|
|
101
|
+
return parseGuideItems(page.item);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function guidePageTitles(
|
|
105
|
+
pages: GuidePage[] | undefined,
|
|
106
|
+
langMap: Record<string, string> = {},
|
|
107
|
+
labels: GuideTocLabels = {},
|
|
108
|
+
entityNames?: Record<string, string>,
|
|
109
|
+
): (string | undefined)[] {
|
|
110
|
+
if (!pages?.length) return [];
|
|
111
|
+
return pages.map((page) =>
|
|
112
|
+
isIndexPage(page, langMap)
|
|
113
|
+
? undefined
|
|
114
|
+
: pageTitle(page, langMap, labels, entityNames),
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function repeatedTitle(
|
|
119
|
+
titles: (string | undefined)[],
|
|
120
|
+
index: number,
|
|
121
|
+
): boolean {
|
|
122
|
+
return index > 0 && titles[index] !== undefined && titles[index] === titles[index - 1];
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function guideToc(
|
|
126
|
+
pages: GuidePage[] | undefined,
|
|
127
|
+
langMap: Record<string, string> = {},
|
|
128
|
+
labels: GuideTocLabels = {},
|
|
129
|
+
entityNames?: Record<string, string>,
|
|
130
|
+
): TOCItemType[] {
|
|
131
|
+
const titles = guidePageTitles(pages, langMap, labels, entityNames);
|
|
132
|
+
const anchors = pageAnchors(pages);
|
|
133
|
+
return titles.flatMap((title, i) => {
|
|
134
|
+
if (!title || repeatedTitle(titles, i)) return [];
|
|
135
|
+
return [{ title, url: `#${anchors[i]}`, depth: 2 }];
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"Entity preview unavailable(guide.entity_unavailable)": "Entity preview unavailable",
|
|
3
|
+
"Entry not found(guide.entry_missing)": "Entry not found",
|
|
4
|
+
"Gameplay guide(sections.desc.guide)": "Gameplay guide",
|
|
5
|
+
"Guide(sections.guide)": "Guide",
|
|
6
|
+
"No recipes(guide.recipe_empty)": "No recipes",
|
|
7
|
+
"Objective(guide.objective)": "Objective",
|
|
8
|
+
"Open(guide.link)": "Open",
|
|
9
|
+
"Quest(guide.quest)": "Quest",
|
|
10
|
+
"Related(guide.related)": "Related",
|
|
11
|
+
"Select book(ui.select_book)": "Select book",
|
|
12
|
+
"{count} entries(guide.entry_count)": "{count} entries",
|
|
13
|
+
"{count} pages(guide.page_count)": "{count} pages"
|
|
14
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"Entity preview unavailable(guide.entity_unavailable)": "Превью сущности недоступно",
|
|
3
|
+
"Entry not found(guide.entry_missing)": "Запись не найдена",
|
|
4
|
+
"Gameplay guide(sections.desc.guide)": "Игровой гайд",
|
|
5
|
+
"Guide(sections.guide)": "Гайды",
|
|
6
|
+
"No recipes(guide.recipe_empty)": "Нет рецептов",
|
|
7
|
+
"Objective(guide.objective)": "Задача",
|
|
8
|
+
"Open(guide.link)": "Открыть",
|
|
9
|
+
"Quest(guide.quest)": "Квест",
|
|
10
|
+
"Related(guide.related)": "Связанное",
|
|
11
|
+
"Select book(ui.select_book)": "Выберите книгу",
|
|
12
|
+
"{count} entries(guide.entry_count)": "Записей: {count}",
|
|
13
|
+
"{count} pages(guide.page_count)": "Страниц: {count}"
|
|
14
|
+
}
|