@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
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { langString } from '@terrafirmagreg-team/dump/lib/dump/assets/lang/names';
|
|
2
|
+
import type { DumpText } from '@terrafirmagreg-team/dump/lib/dump/contract/text';
|
|
3
|
+
|
|
4
|
+
export type FallbackTracker = {
|
|
5
|
+
mark(key: string | undefined): void;
|
|
6
|
+
readonly flag: 'untranslated' | 'partial' | undefined;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export function fallbackTracker(
|
|
10
|
+
rawLang: Record<string, string> | undefined,
|
|
11
|
+
): FallbackTracker | undefined {
|
|
12
|
+
if (!rawLang) return undefined;
|
|
13
|
+
let total = 0;
|
|
14
|
+
let missing = 0;
|
|
15
|
+
return {
|
|
16
|
+
mark(key) {
|
|
17
|
+
if (!key) return;
|
|
18
|
+
total += 1;
|
|
19
|
+
if (!langString(rawLang, key)) missing += 1;
|
|
20
|
+
},
|
|
21
|
+
get flag() {
|
|
22
|
+
if (!missing) return undefined;
|
|
23
|
+
return missing === total ? 'untranslated' : 'partial';
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function markTextKeys(
|
|
29
|
+
texts: DumpText[] | undefined,
|
|
30
|
+
tracker: FallbackTracker | undefined,
|
|
31
|
+
) {
|
|
32
|
+
if (!tracker) return;
|
|
33
|
+
for (const text of texts ?? []) {
|
|
34
|
+
tracker.mark(text.translate);
|
|
35
|
+
markTextKeys(text.with, tracker);
|
|
36
|
+
markTextKeys(text.extra, tracker);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function localize(
|
|
41
|
+
map: Record<string, string>,
|
|
42
|
+
key: string | undefined,
|
|
43
|
+
fallback: string,
|
|
44
|
+
tracker?: FallbackTracker,
|
|
45
|
+
): string {
|
|
46
|
+
if (!key) return fallback;
|
|
47
|
+
tracker?.mark(key);
|
|
48
|
+
const bare =
|
|
49
|
+
key.startsWith('{') && key.endsWith('}') ? key.slice(1, -1).trim() : key;
|
|
50
|
+
return langString(map, key) ?? (bare !== key ? bare : fallback);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const LANG_KEY_WRAP = /^\{[^{}]+\}$/;
|
|
54
|
+
|
|
55
|
+
export function localizeGuideValue(
|
|
56
|
+
value: unknown,
|
|
57
|
+
map: Record<string, string>,
|
|
58
|
+
tracker?: FallbackTracker,
|
|
59
|
+
): unknown {
|
|
60
|
+
if (typeof value === 'string') {
|
|
61
|
+
if (!LANG_KEY_WRAP.test(value)) return value;
|
|
62
|
+
tracker?.mark(value);
|
|
63
|
+
return langString(map, value) ?? value;
|
|
64
|
+
}
|
|
65
|
+
if (Array.isArray(value)) {
|
|
66
|
+
return value.map((item) => localizeGuideValue(item, map, tracker));
|
|
67
|
+
}
|
|
68
|
+
if (value && typeof value === 'object') {
|
|
69
|
+
const out: Record<string, unknown> = {};
|
|
70
|
+
for (const [key, item] of Object.entries(value as Record<string, unknown>)) {
|
|
71
|
+
out[key] = localizeGuideValue(item, map, tracker);
|
|
72
|
+
}
|
|
73
|
+
return out;
|
|
74
|
+
}
|
|
75
|
+
return value;
|
|
76
|
+
}
|
|
77
|
+
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { asTaggedReference } from '@terrafirmagreg-team/dump/lib/dump/contract/ref';
|
|
2
|
+
import { asDumpTextList } from '@terrafirmagreg-team/dump/lib/dump/contract/text';
|
|
3
|
+
import type { DumpText } from '@terrafirmagreg-team/dump/lib/dump/contract/text';
|
|
4
|
+
import type { TaggedReference } from '@terrafirmagreg-team/dump/lib/dump/contract/ref';
|
|
5
|
+
import { bareId } from '@terrafirmagreg-team/guide/lib/paths';
|
|
6
|
+
|
|
7
|
+
export type RawRecord = Record<string, unknown>;
|
|
8
|
+
|
|
9
|
+
export function numeric(value: unknown, fallback = 0): number {
|
|
10
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : fallback;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function compareGuideOrder(
|
|
14
|
+
left: { id: string; order?: unknown },
|
|
15
|
+
right: { id: string; order?: unknown },
|
|
16
|
+
) {
|
|
17
|
+
const sort = numeric(left.order) - numeric(right.order);
|
|
18
|
+
if (sort !== 0) return sort;
|
|
19
|
+
return left.id.localeCompare(right.id);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function rawId(raw: RawRecord, fallback: string) {
|
|
23
|
+
return typeof raw.id === 'string' && raw.id ? raw.id : fallback;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function stringField(
|
|
27
|
+
raw: RawRecord | undefined,
|
|
28
|
+
key: string,
|
|
29
|
+
): string | undefined {
|
|
30
|
+
const value = raw?.[key];
|
|
31
|
+
return typeof value === 'string' && value ? value : undefined;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function displayRecord(
|
|
35
|
+
raw: RawRecord | undefined,
|
|
36
|
+
): RawRecord | undefined {
|
|
37
|
+
const display = raw?.display;
|
|
38
|
+
return display && typeof display === 'object' && !Array.isArray(display)
|
|
39
|
+
? (display as RawRecord)
|
|
40
|
+
: undefined;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function urlSafeSlug(value: string): string {
|
|
44
|
+
return value
|
|
45
|
+
.trim()
|
|
46
|
+
.replaceAll('\\', '/')
|
|
47
|
+
.split('/')
|
|
48
|
+
.filter(Boolean)
|
|
49
|
+
.join('-')
|
|
50
|
+
.replace(/:/g, '-')
|
|
51
|
+
.replace(/[^a-zA-Z0-9._-]+/g, '-')
|
|
52
|
+
.replace(/^-+|-+$/g, '')
|
|
53
|
+
.toLowerCase();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function macrosFrom(
|
|
57
|
+
raw: RawRecord | undefined,
|
|
58
|
+
): Record<string, string> | undefined {
|
|
59
|
+
if (!raw?.macros || typeof raw.macros !== 'object') return undefined;
|
|
60
|
+
const entries = Object.entries(raw.macros as RawRecord).filter(
|
|
61
|
+
(entry): entry is [string, string] => typeof entry[1] === 'string',
|
|
62
|
+
);
|
|
63
|
+
return entries.length ? Object.fromEntries(entries) : undefined;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function asCover(value: unknown): TaggedReference | null {
|
|
67
|
+
return asTaggedReference(value) ?? null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function asLanding(value: unknown): DumpText[] {
|
|
71
|
+
return asDumpTextList(value) ?? [];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function categoryMatches(
|
|
75
|
+
categoryId: string,
|
|
76
|
+
entryCategory: string | undefined,
|
|
77
|
+
) {
|
|
78
|
+
if (!entryCategory) return false;
|
|
79
|
+
return bareId(entryCategory) === bareId(categoryId);
|
|
80
|
+
}
|
|
81
|
+
|
package/src/lib/books.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { DatasetRef } from '@terrafirmagreg-team/sites/location';
|
|
2
|
+
import { sectionConfig } from '@terrafirmagreg-team/kit/lib/config';
|
|
3
|
+
import { listClientGuideBookSummaries } from '@terrafirmagreg-team/guide/lib/client/guides';
|
|
4
|
+
import type { Lang } from '@terrafirmagreg-team/translate/types/locale';
|
|
5
|
+
|
|
6
|
+
type GuideBookSummary = Awaited<
|
|
7
|
+
ReturnType<typeof listClientGuideBookSummaries>
|
|
8
|
+
>[number];
|
|
9
|
+
|
|
10
|
+
function orderGuideBooks<T extends { bookId: string }>(
|
|
11
|
+
books: T[],
|
|
12
|
+
order: readonly string[] | undefined,
|
|
13
|
+
): T[] {
|
|
14
|
+
if (!order?.length) return books;
|
|
15
|
+
const rank = new Map(order.map((id, i) => [id, i]));
|
|
16
|
+
return [...books].sort(
|
|
17
|
+
(a, b) =>
|
|
18
|
+
(rank.get(a.bookId) ?? order.length) - (rank.get(b.bookId) ?? order.length) ||
|
|
19
|
+
a.bookId.localeCompare(b.bookId),
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function listGuideBooks(
|
|
24
|
+
dataset: DatasetRef,
|
|
25
|
+
lang: Lang,
|
|
26
|
+
): Promise<GuideBookSummary[]> {
|
|
27
|
+
return listClientGuideBookSummaries(dataset, lang).then((books) =>
|
|
28
|
+
orderGuideBooks(books, sectionConfig().datasetConfig[dataset.id]?.guide?.bookOrder),
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { DatasetRef } from '@terrafirmagreg-team/sites/location';
|
|
2
|
+
import { guideBookHref, guideCategoryHref } from '@terrafirmagreg-team/guide/lib/paths';
|
|
3
|
+
import type { GuideCategory, GuideIndex, GuidePageKind } from '@terrafirmagreg-team/guide/types';
|
|
4
|
+
|
|
5
|
+
export function categoryMap(
|
|
6
|
+
categories: GuideCategory[],
|
|
7
|
+
): Map<string, string> {
|
|
8
|
+
const map = new Map<string, string>();
|
|
9
|
+
for (const cat of categories) {
|
|
10
|
+
for (const entry of cat.entries) {
|
|
11
|
+
map.set(entry.id, cat.id);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
return map;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function guideBreadcrumbItems({
|
|
18
|
+
kind,
|
|
19
|
+
title,
|
|
20
|
+
dataset,
|
|
21
|
+
modId,
|
|
22
|
+
bookPath,
|
|
23
|
+
overview,
|
|
24
|
+
categoryId,
|
|
25
|
+
}: {
|
|
26
|
+
kind: GuidePageKind;
|
|
27
|
+
title: string;
|
|
28
|
+
dataset: DatasetRef;
|
|
29
|
+
modId?: string;
|
|
30
|
+
bookPath?: string;
|
|
31
|
+
overview: GuideIndex | undefined;
|
|
32
|
+
categoryId?: string;
|
|
33
|
+
}): Array<{ name: string; url?: string }> {
|
|
34
|
+
if (kind === 'pack') return [];
|
|
35
|
+
const bookName = overview?.book?.name ?? bookPath ?? '';
|
|
36
|
+
const bookHref =
|
|
37
|
+
modId && bookPath
|
|
38
|
+
? guideBookHref(dataset, undefined, modId, bookPath)
|
|
39
|
+
: undefined;
|
|
40
|
+
if (kind === 'overview') return [];
|
|
41
|
+
const items: Array<{ name: string; url?: string }> = [];
|
|
42
|
+
if (bookName && bookHref) items.push({ name: bookName, url: bookHref });
|
|
43
|
+
const cat = overview?.categories.find((entry) => entry.id === categoryId);
|
|
44
|
+
if (cat && modId && bookPath) {
|
|
45
|
+
items.push({
|
|
46
|
+
name: cat.name,
|
|
47
|
+
url:
|
|
48
|
+
kind === 'entry'
|
|
49
|
+
? guideCategoryHref(dataset, undefined, modId, bookPath, cat)
|
|
50
|
+
: undefined,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
items.push({ name: title });
|
|
54
|
+
return items;
|
|
55
|
+
}
|
|
56
|
+
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import type { DatasetRef } from '@terrafirmagreg-team/sites/location';
|
|
2
|
+
import { asRecord, asString } from '@terrafirmagreg-team/dump/lib/dump/contract/read';
|
|
3
|
+
import { joinDumpRel } from '@terrafirmagreg-team/dump/lib/dump/paths';
|
|
4
|
+
import { bareId } from '@terrafirmagreg-team/guide/lib/paths';
|
|
5
|
+
import type { GuideEntry } from '@terrafirmagreg-team/guide/types';
|
|
6
|
+
import type { GuideMultiblock } from '@terrafirmagreg-team/guide/lib/data/dto';
|
|
7
|
+
import type {
|
|
8
|
+
GuideBookJson,
|
|
9
|
+
GuideBookRef,
|
|
10
|
+
} from '@terrafirmagreg-team/guide/lib/data/dto';
|
|
11
|
+
import {
|
|
12
|
+
bookSlug,
|
|
13
|
+
indexBookRows,
|
|
14
|
+
toBook,
|
|
15
|
+
type BookRows,
|
|
16
|
+
} from '@terrafirmagreg-team/guide/lib/book';
|
|
17
|
+
import type { Lang } from '@terrafirmagreg-team/translate/types/locale';
|
|
18
|
+
import type { GuideIndex } from '@terrafirmagreg-team/guide/types';
|
|
19
|
+
import {
|
|
20
|
+
clientDumpJson,
|
|
21
|
+
clientDumpLang,
|
|
22
|
+
clientDumpLangRaw,
|
|
23
|
+
} from '@terrafirmagreg-team/dump/lib/dump/client/dump';
|
|
24
|
+
import { loadDatasetDump } from '@terrafirmagreg-team/dump/lib/dump/mods/load';
|
|
25
|
+
import { createDatasetCache } from '@terrafirmagreg-team/dump/lib/dump/reader';
|
|
26
|
+
|
|
27
|
+
type BookRow = {
|
|
28
|
+
ref: GuideBookRef;
|
|
29
|
+
slug: string;
|
|
30
|
+
raw?: GuideBookJson;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const listedBookRows = createDatasetCache(
|
|
34
|
+
async (dataset: DatasetRef): Promise<BookRow[]> => {
|
|
35
|
+
const dump = await loadDatasetDump(dataset);
|
|
36
|
+
if (!dump) return [];
|
|
37
|
+
const raws = await Promise.all(
|
|
38
|
+
dump.mods.flatMap((mod) =>
|
|
39
|
+
mod.books.map(async (ref) => ({
|
|
40
|
+
ref,
|
|
41
|
+
raw: asRecord(
|
|
42
|
+
await clientDumpJson(dataset, `${ref.dir}/book.json`),
|
|
43
|
+
) as GuideBookJson | undefined,
|
|
44
|
+
})),
|
|
45
|
+
),
|
|
46
|
+
);
|
|
47
|
+
const rows: BookRow[] = [];
|
|
48
|
+
const taken = new Set<string>();
|
|
49
|
+
for (const { ref, raw } of raws) {
|
|
50
|
+
if (!raw) continue;
|
|
51
|
+
let slug = bookSlug(raw, ref);
|
|
52
|
+
if (taken.has(slug)) {
|
|
53
|
+
let n = 2;
|
|
54
|
+
while (taken.has(`${slug}-${n}`)) n += 1;
|
|
55
|
+
slug = `${slug}-${n}`;
|
|
56
|
+
}
|
|
57
|
+
taken.add(slug);
|
|
58
|
+
rows.push({ ref, slug, raw });
|
|
59
|
+
}
|
|
60
|
+
return rows;
|
|
61
|
+
},
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
function listedBodies(
|
|
65
|
+
bookDir: string,
|
|
66
|
+
index: Record<string, unknown> | undefined,
|
|
67
|
+
): { files: string[]; ids: string[] } {
|
|
68
|
+
const files: string[] = [];
|
|
69
|
+
const ids: string[] = [];
|
|
70
|
+
const seen = new Set<string>();
|
|
71
|
+
for (const [id, value] of Object.entries(index ?? {})) {
|
|
72
|
+
if (!id || typeof value !== 'string' || !value) continue;
|
|
73
|
+
const rel = joinDumpRel(`${bookDir}/x`, value);
|
|
74
|
+
if (!rel) continue;
|
|
75
|
+
ids.push(id);
|
|
76
|
+
if (!seen.has(rel)) {
|
|
77
|
+
seen.add(rel);
|
|
78
|
+
files.push(rel);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return { files, ids };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function bookRows(
|
|
85
|
+
dataset: DatasetRef,
|
|
86
|
+
row: BookRow,
|
|
87
|
+
lang: Lang,
|
|
88
|
+
): Promise<BookRows | null> {
|
|
89
|
+
if (!row.raw) return null;
|
|
90
|
+
const bookDir = row.ref.dir;
|
|
91
|
+
const listed = row.raw as Record<string, unknown>;
|
|
92
|
+
const entryList = listedBodies(
|
|
93
|
+
bookDir,
|
|
94
|
+
listed.entries as Record<string, unknown> | undefined,
|
|
95
|
+
);
|
|
96
|
+
const categoryList = listedBodies(
|
|
97
|
+
bookDir,
|
|
98
|
+
listed.categories as Record<string, unknown> | undefined,
|
|
99
|
+
);
|
|
100
|
+
const bucketDocs = new Map<string, Record<string, unknown>>();
|
|
101
|
+
const categoryDocs = new Map<string, Record<string, unknown>>();
|
|
102
|
+
await Promise.all([
|
|
103
|
+
...entryList.files.map(async (rel) => {
|
|
104
|
+
const doc = asRecord(await clientDumpJson(dataset, rel));
|
|
105
|
+
if (doc) bucketDocs.set(rel, doc);
|
|
106
|
+
}),
|
|
107
|
+
...categoryList.files.map(async (rel) => {
|
|
108
|
+
const doc = asRecord(await clientDumpJson(dataset, rel));
|
|
109
|
+
if (doc) categoryDocs.set(rel, doc);
|
|
110
|
+
}),
|
|
111
|
+
]);
|
|
112
|
+
const entries: BookRows['entries'] = [];
|
|
113
|
+
for (const [id, value] of Object.entries(
|
|
114
|
+
(listed.entries as Record<string, unknown> | undefined) ?? {},
|
|
115
|
+
)) {
|
|
116
|
+
if (!id || typeof value !== 'string' || !value) continue;
|
|
117
|
+
const rel = joinDumpRel(`${bookDir}/x`, value);
|
|
118
|
+
const body = rel ? bucketDocs.get(rel)?.[id] : undefined;
|
|
119
|
+
if (body && typeof body === 'object' && !Array.isArray(body)) {
|
|
120
|
+
entries.push({ id, raw: body as Record<string, unknown> });
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
entries.sort((a, b) => a.id.localeCompare(b.id));
|
|
124
|
+
const categories: BookRows['categories'] = [];
|
|
125
|
+
for (const [id, value] of Object.entries(
|
|
126
|
+
(listed.categories as Record<string, unknown> | undefined) ?? {},
|
|
127
|
+
)) {
|
|
128
|
+
if (!id || typeof value !== 'string' || !value) continue;
|
|
129
|
+
const rel = joinDumpRel(`${bookDir}/x`, value);
|
|
130
|
+
const raw = rel ? categoryDocs.get(rel) : undefined;
|
|
131
|
+
if (raw) categories.push({ id, raw });
|
|
132
|
+
}
|
|
133
|
+
categories.sort((a, b) => a.id.localeCompare(b.id));
|
|
134
|
+
return {
|
|
135
|
+
ref: row.ref,
|
|
136
|
+
slug: row.slug,
|
|
137
|
+
raw: row.raw,
|
|
138
|
+
entries,
|
|
139
|
+
categories,
|
|
140
|
+
contentLanguage: lang,
|
|
141
|
+
isFallback: false,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export const loadClientGuideBook = createDatasetCache(
|
|
146
|
+
async (
|
|
147
|
+
dataset: DatasetRef,
|
|
148
|
+
lang: Lang,
|
|
149
|
+
bookSlugValue: string,
|
|
150
|
+
): Promise<GuideIndex | null> => {
|
|
151
|
+
const row = (await listedBookRows(dataset)).find(
|
|
152
|
+
(entry) =>
|
|
153
|
+
entry.slug === bookSlugValue || entry.ref.bookId === bookSlugValue,
|
|
154
|
+
);
|
|
155
|
+
if (!row) return null;
|
|
156
|
+
const [rows, langMap, rawLang] = await Promise.all([
|
|
157
|
+
bookRows(dataset, row, lang),
|
|
158
|
+
clientDumpLang(dataset, lang),
|
|
159
|
+
clientDumpLangRaw(dataset, lang),
|
|
160
|
+
]);
|
|
161
|
+
if (!rows) return null;
|
|
162
|
+
return indexBookRows(rows, dataset, lang, langMap, rawLang);
|
|
163
|
+
},
|
|
164
|
+
(dataset, lang, bookSlugValue) => `${dataset.id}/${dataset.version ?? ''}${lang}${bookSlugValue}`,
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
export type ClientGuideEntryHit = {
|
|
168
|
+
entry: GuideEntry;
|
|
169
|
+
modId?: string;
|
|
170
|
+
bookPath?: string;
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
export async function findClientGuideEntry(
|
|
174
|
+
dataset: DatasetRef,
|
|
175
|
+
lang: Lang,
|
|
176
|
+
entryId: string,
|
|
177
|
+
): Promise<ClientGuideEntryHit | undefined> {
|
|
178
|
+
const needle = bareId(entryId);
|
|
179
|
+
const nameNeedle = entryId.trim().toLowerCase();
|
|
180
|
+
for (const row of await listedBookRows(dataset)) {
|
|
181
|
+
const entries = (row.raw as Record<string, unknown> | undefined)?.entries;
|
|
182
|
+
const overview = await loadClientGuideBook(dataset, lang, row.slug);
|
|
183
|
+
const match =
|
|
184
|
+
entries && typeof entries === 'object'
|
|
185
|
+
? Object.keys(entries).find(
|
|
186
|
+
(key) => bareId(key) === needle || key === entryId,
|
|
187
|
+
)
|
|
188
|
+
: undefined;
|
|
189
|
+
const entry = overview?.entries.find(
|
|
190
|
+
(item) =>
|
|
191
|
+
item.id === match ||
|
|
192
|
+
bareId(item.id) === needle ||
|
|
193
|
+
item.name?.trim().toLowerCase() === nameNeedle,
|
|
194
|
+
);
|
|
195
|
+
if (entry) {
|
|
196
|
+
return {
|
|
197
|
+
entry,
|
|
198
|
+
modId: overview?.modId,
|
|
199
|
+
bookPath: overview?.bookPath,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return undefined;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export const loadMultiblockNameIndex = createDatasetCache(
|
|
207
|
+
async (
|
|
208
|
+
dataset: DatasetRef,
|
|
209
|
+
lang: Lang,
|
|
210
|
+
): Promise<Record<string, GuideMultiblock>> => {
|
|
211
|
+
const index: Record<string, GuideMultiblock> = {};
|
|
212
|
+
const put = (key: string | undefined, spec: GuideMultiblock) => {
|
|
213
|
+
const normalized = key?.trim().toLowerCase();
|
|
214
|
+
if (normalized) index[normalized] ??= spec;
|
|
215
|
+
};
|
|
216
|
+
for (const row of await listedBookRows(dataset)) {
|
|
217
|
+
const overview = await loadClientGuideBook(dataset, lang, row.slug);
|
|
218
|
+
for (const entry of overview?.entries ?? []) {
|
|
219
|
+
for (const page of entry.pages ?? []) {
|
|
220
|
+
const spec = page?.multiblock;
|
|
221
|
+
if (!spec) continue;
|
|
222
|
+
const display = asRecord(
|
|
223
|
+
(page as Record<string, unknown>).display,
|
|
224
|
+
);
|
|
225
|
+
put(asString(display?.name) ?? page.name ?? page.title, spec);
|
|
226
|
+
put(entry.id, spec);
|
|
227
|
+
put(entry.name, spec);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return index;
|
|
232
|
+
},
|
|
233
|
+
);
|
|
234
|
+
|
|
235
|
+
export async function listClientGuideBookSummaries(
|
|
236
|
+
dataset: DatasetRef,
|
|
237
|
+
lang: Lang,
|
|
238
|
+
): Promise<
|
|
239
|
+
Array<{
|
|
240
|
+
modId: string;
|
|
241
|
+
bookId: string;
|
|
242
|
+
slug: string;
|
|
243
|
+
name: string;
|
|
244
|
+
landingText?: string;
|
|
245
|
+
}>
|
|
246
|
+
> {
|
|
247
|
+
const langMap = await clientDumpLang(dataset, lang);
|
|
248
|
+
const rows = await listedBookRows(dataset);
|
|
249
|
+
return rows
|
|
250
|
+
.map((row) => {
|
|
251
|
+
const book = toBook(row.raw, row.ref, row.slug, langMap);
|
|
252
|
+
return {
|
|
253
|
+
modId: row.ref.modId,
|
|
254
|
+
bookId: row.ref.bookId,
|
|
255
|
+
slug: row.slug,
|
|
256
|
+
name: book.name,
|
|
257
|
+
...(book.landingText ? { landingText: book.landingText } : {}),
|
|
258
|
+
};
|
|
259
|
+
})
|
|
260
|
+
.sort((a, b) => a.bookId.localeCompare(b.bookId));
|
|
261
|
+
}
|
|
262
|
+
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import type { DatasetRef } from '@terrafirmagreg-team/sites/location';
|
|
2
|
+
import { stackDisplayName } from '@terrafirmagreg-team/dump/lib/dump/assets/lang/names';
|
|
3
|
+
import { loadClientModAtlases } from '@terrafirmagreg-team/dump/lib/dump/client/atlases';
|
|
4
|
+
import { loadClientKeybinds } from '@terrafirmagreg-team/dump/lib/dump/client/keybinds';
|
|
5
|
+
import { guideEmbeds } from '@terrafirmagreg-team/guide/lib/embeds';
|
|
6
|
+
import { loadClientRegistry } from '@terrafirmagreg-team/dump/lib/dump/client/registry';
|
|
7
|
+
import { parseStackId } from '@terrafirmagreg-team/dump/lib/dump/contract/ref';
|
|
8
|
+
import { spriteIdsFromEntry } from '@terrafirmagreg-team/guide/lib/sprites';
|
|
9
|
+
import { recipeIds } from '@terrafirmagreg-team/guide/lib/recipe';
|
|
10
|
+
import { asMultiblock } from '@terrafirmagreg-team/guide/lib/meta';
|
|
11
|
+
import { glbFor } from '@terrafirmagreg-team/guide/lib/multiblock/url';
|
|
12
|
+
import { multiblockKey } from '@terrafirmagreg-team/guide/lib/multiblock';
|
|
13
|
+
import type { Lang } from '@terrafirmagreg-team/translate/types/locale';
|
|
14
|
+
import type { GuideEntry, GuideEntryExtras } from '@terrafirmagreg-team/guide/types';
|
|
15
|
+
import { type QuestLinks, collectPageLinks } from './links';
|
|
16
|
+
|
|
17
|
+
export async function entryExtras(
|
|
18
|
+
dataset: DatasetRef,
|
|
19
|
+
lang: Lang,
|
|
20
|
+
langMap: Record<string, string>,
|
|
21
|
+
bookModId: string,
|
|
22
|
+
bookPath: string,
|
|
23
|
+
categoryByEntry: Map<string, string>,
|
|
24
|
+
questLinks: QuestLinks,
|
|
25
|
+
hrefs: Set<string>,
|
|
26
|
+
entry: GuideEntry,
|
|
27
|
+
): Promise<GuideEntryExtras> {
|
|
28
|
+
const pages = entry.pages ?? [];
|
|
29
|
+
const recipes: GuideEntryExtras['recipes'] = {};
|
|
30
|
+
const entities: GuideEntryExtras['entities'] = {};
|
|
31
|
+
const entityNames: GuideEntryExtras['entityNames'] = {};
|
|
32
|
+
const glbs: GuideEntryExtras['glbs'] = {};
|
|
33
|
+
const registry = await loadClientRegistry(dataset, 'entities');
|
|
34
|
+
|
|
35
|
+
await Promise.all(
|
|
36
|
+
pages.map(async (page) => {
|
|
37
|
+
await Promise.all(
|
|
38
|
+
recipeIds(page).map(async (id) => {
|
|
39
|
+
const recipe = await guideEmbeds().recipe?.load(dataset, id);
|
|
40
|
+
if (recipe) recipes[id] = recipe;
|
|
41
|
+
}),
|
|
42
|
+
);
|
|
43
|
+
const entityId =
|
|
44
|
+
typeof page.entity === 'string' ? page.entity : undefined;
|
|
45
|
+
if (entityId) {
|
|
46
|
+
entityNames[entityId] = stackDisplayName(
|
|
47
|
+
langMap,
|
|
48
|
+
entityId,
|
|
49
|
+
'entity',
|
|
50
|
+
registry.get(entityId)?.display?.name,
|
|
51
|
+
);
|
|
52
|
+
const parsed = parseStackId(entityId);
|
|
53
|
+
const record = parsed?.id ? registry.get(parsed.id) : undefined;
|
|
54
|
+
if (parsed?.id && record) entities[parsed.id] = record;
|
|
55
|
+
}
|
|
56
|
+
await collectPageLinks(
|
|
57
|
+
dataset,
|
|
58
|
+
lang,
|
|
59
|
+
bookModId,
|
|
60
|
+
bookPath,
|
|
61
|
+
categoryByEntry,
|
|
62
|
+
questLinks,
|
|
63
|
+
hrefs,
|
|
64
|
+
page,
|
|
65
|
+
);
|
|
66
|
+
await Promise.all(
|
|
67
|
+
[asMultiblock(page.model ?? page.multiblock), ...(page.multiblocks ?? []).map(asMultiblock)].map(
|
|
68
|
+
async (spec) => {
|
|
69
|
+
const key = multiblockKey(spec);
|
|
70
|
+
if (!key || glbs[key]) return;
|
|
71
|
+
const url = await glbFor(dataset, bookModId, spec);
|
|
72
|
+
if (url) glbs[key] = url;
|
|
73
|
+
},
|
|
74
|
+
),
|
|
75
|
+
);
|
|
76
|
+
}),
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
const spriteIds = spriteIdsFromEntry(entry);
|
|
80
|
+
const atlasMods = [
|
|
81
|
+
...new Set(
|
|
82
|
+
spriteIds
|
|
83
|
+
.map((id) => id.replace(/^#/, '').split(':', 2)[0])
|
|
84
|
+
.filter(Boolean),
|
|
85
|
+
),
|
|
86
|
+
];
|
|
87
|
+
const atlasEntries = await Promise.all(
|
|
88
|
+
atlasMods.map(
|
|
89
|
+
async (modId) =>
|
|
90
|
+
[modId, await loadClientModAtlases(dataset, modId)] as const,
|
|
91
|
+
),
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
atlases: Object.fromEntries(atlasEntries),
|
|
96
|
+
keybinds: await loadClientKeybinds(dataset),
|
|
97
|
+
recipes,
|
|
98
|
+
entities,
|
|
99
|
+
entityNames,
|
|
100
|
+
glbs,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|