@terrafirmagreg-team/changelog 0.1.0 → 1.2.1
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 +25 -11
- package/src/components/entries.tsx +75 -0
- package/src/components/month-label.tsx +9 -0
- package/src/components/now-bar.tsx +95 -0
- package/src/components/timeline.tsx +155 -0
- package/src/components/views/timeline.tsx +25 -0
- package/src/lib/format.ts +20 -0
- package/src/lib/remark.ts +38 -0
- package/src/lib/trees.ts +83 -0
- package/src/locales/en_us.json +2 -6
- package/src/locales/ru_ru.json +2 -6
- package/src/locales.ts +1 -0
- package/src/lib/route.ts +0 -2
- package/src/lib/tree.ts +0 -41
package/package.json
CHANGED
|
@@ -1,23 +1,40 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@terrafirmagreg-team/changelog",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "1.2.1",
|
|
4
4
|
"type": "module",
|
|
5
|
+
"description": "Changelog section",
|
|
6
|
+
"license": "LGPL-3.0-only",
|
|
5
7
|
"exports": {
|
|
8
|
+
"./components/*": [
|
|
9
|
+
"./src/components/*.tsx",
|
|
10
|
+
"./src/components/*/index.tsx",
|
|
11
|
+
"./src/components/*/index.ts"
|
|
12
|
+
],
|
|
6
13
|
"./lib/*": [
|
|
7
14
|
"./src/lib/*.ts",
|
|
8
|
-
"./src/lib
|
|
9
|
-
"./src/lib/*/index.ts",
|
|
10
|
-
"./src/lib/*/index.tsx"
|
|
15
|
+
"./src/lib/*/index.ts"
|
|
11
16
|
],
|
|
17
|
+
"./types/*": "./src/types/*.ts",
|
|
18
|
+
"./types": "./src/types/index.ts",
|
|
12
19
|
"./locales": "./src/locales.ts"
|
|
13
20
|
},
|
|
14
21
|
"dependencies": {
|
|
22
|
+
"@terrafirmagreg-team/kit": "1.2.1",
|
|
23
|
+
"@terrafirmagreg-team/mdx": "1.2.1",
|
|
24
|
+
"@terrafirmagreg-team/translate": "1.2.1",
|
|
25
|
+
"@terrafirmagreg-team/ui": "1.2.1",
|
|
15
26
|
"fumadocs-core": "16.17.8",
|
|
16
|
-
"
|
|
17
|
-
|
|
27
|
+
"lucide-react": "^0.577.0"
|
|
28
|
+
},
|
|
29
|
+
"peerDependencies": {
|
|
30
|
+
"next": ">=15",
|
|
31
|
+
"react": ">=19"
|
|
18
32
|
},
|
|
19
33
|
"devDependencies": {
|
|
20
|
-
"@types/
|
|
34
|
+
"@types/mdx": "^2.0.14",
|
|
35
|
+
"@types/node": "^26.6.5",
|
|
36
|
+
"@types/react": "^19.3.0",
|
|
37
|
+
"@types/react-dom": "^19.3.0"
|
|
21
38
|
},
|
|
22
39
|
"repository": {
|
|
23
40
|
"type": "git",
|
|
@@ -28,9 +45,6 @@
|
|
|
28
45
|
"access": "public"
|
|
29
46
|
},
|
|
30
47
|
"files": [
|
|
31
|
-
"src"
|
|
32
|
-
"styles",
|
|
33
|
-
"styles.css",
|
|
34
|
-
"locales"
|
|
48
|
+
"src"
|
|
35
49
|
]
|
|
36
50
|
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { createRelativeLink } from '@terrafirmagreg-team/kit/components/relative-link';
|
|
2
|
+
import { getMDXComponents } from '@terrafirmagreg-team/mdx/components/mdx-components';
|
|
3
|
+
import { datasetCatalog } from '@terrafirmagreg-team/kit/lib/datasets';
|
|
4
|
+
import { loadLocaleRegistry } from '@terrafirmagreg-team/translate/server';
|
|
5
|
+
import { lookupPagePreview } from '@terrafirmagreg-team/kit/lib/preview/lookup';
|
|
6
|
+
import { LinkPreview } from '@terrafirmagreg-team/ui/components/blocks/link/link-preview';
|
|
7
|
+
import type { SectionSource } from '@terrafirmagreg-team/kit/lib/page';
|
|
8
|
+
import type { SectionPage } from '@terrafirmagreg-team/kit/types';
|
|
9
|
+
import type { LoaderConfig, LoaderOutput, Page } from 'fumadocs-core/source';
|
|
10
|
+
import type { MDXContent } from 'mdx/types';
|
|
11
|
+
import type { Lang } from '@terrafirmagreg-team/translate/types/locale';
|
|
12
|
+
import type { ChangelogTimelineEntry } from './timeline';
|
|
13
|
+
|
|
14
|
+
export async function timelineEntries(
|
|
15
|
+
source: SectionSource,
|
|
16
|
+
route: string,
|
|
17
|
+
dataset: string,
|
|
18
|
+
lang: Lang,
|
|
19
|
+
repo?: `${string}/${string}`,
|
|
20
|
+
): Promise<ChangelogTimelineEntry[]> {
|
|
21
|
+
const catalog = await datasetCatalog();
|
|
22
|
+
const { defaultLanguage } = loadLocaleRegistry();
|
|
23
|
+
const byFile = new Map<string, { own?: SectionPage; fallback?: SectionPage }>();
|
|
24
|
+
for (const raw of source.getPages()) {
|
|
25
|
+
const page = raw as unknown as SectionPage;
|
|
26
|
+
if (page.slugs[0] !== dataset) continue;
|
|
27
|
+
const pageLang = (page.locale ?? page.slugs[1]) as Lang;
|
|
28
|
+
const key = page.slugs.slice(2).join('/');
|
|
29
|
+
const slot = byFile.get(key) ?? {};
|
|
30
|
+
if (pageLang === lang) slot.own = page;
|
|
31
|
+
else if (pageLang === defaultLanguage) slot.fallback = page;
|
|
32
|
+
byFile.set(key, slot);
|
|
33
|
+
}
|
|
34
|
+
const entries: ChangelogTimelineEntry[] = [];
|
|
35
|
+
for (const { own, fallback } of byFile.values()) {
|
|
36
|
+
const page = own ?? fallback;
|
|
37
|
+
if (!page) continue;
|
|
38
|
+
const pageLang = (page.locale ?? page.slugs[1] ?? lang) as Lang;
|
|
39
|
+
const data = page.data as {
|
|
40
|
+
date?: string | Date;
|
|
41
|
+
frontmatter?: { date?: string | Date };
|
|
42
|
+
};
|
|
43
|
+
const rawDate = data.date ?? data.frontmatter?.date;
|
|
44
|
+
if (!rawDate) continue;
|
|
45
|
+
const { body: MDX } = (await (
|
|
46
|
+
page.data as unknown as {
|
|
47
|
+
load(): Promise<{ body: MDXContent }>;
|
|
48
|
+
}
|
|
49
|
+
).load()) as { body: MDXContent };
|
|
50
|
+
const link = createRelativeLink(
|
|
51
|
+
source as LoaderOutput<LoaderConfig>,
|
|
52
|
+
page as unknown as Page,
|
|
53
|
+
(props) => (
|
|
54
|
+
<LinkPreview
|
|
55
|
+
preview={lookupPagePreview(catalog, props.href, pageLang)}
|
|
56
|
+
{...props}
|
|
57
|
+
/>
|
|
58
|
+
),
|
|
59
|
+
);
|
|
60
|
+
const body = <MDX components={getMDXComponents({ a: link })} />;
|
|
61
|
+
const fileSlug = page.slugs.slice(2);
|
|
62
|
+
entries.push({
|
|
63
|
+
id: fileSlug.join('/'),
|
|
64
|
+
title: page.data.title,
|
|
65
|
+
date: new Date(rawDate),
|
|
66
|
+
url: `${route}/${dataset}/${fileSlug.join('/')}`,
|
|
67
|
+
releaseUrl: repo
|
|
68
|
+
? `https://github.com/${repo}/releases/tag/${page.data.title}`
|
|
69
|
+
: undefined,
|
|
70
|
+
untranslated: !own,
|
|
71
|
+
body,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
return entries;
|
|
75
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { useResolvedLang } from '@terrafirmagreg-team/translate/hooks/use-resolved-lang';
|
|
3
|
+
import { monthLabel } from '@terrafirmagreg-team/changelog/lib/format';
|
|
4
|
+
|
|
5
|
+
export function ChangelogMonthLabel({ monthKey }: { monthKey: string }) {
|
|
6
|
+
const lang = useResolvedLang();
|
|
7
|
+
const [year, month] = monthKey.split('-').map(Number);
|
|
8
|
+
return <>{monthLabel(lang, new Date(Date.UTC(year, month - 1)))}</>;
|
|
9
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
3
|
+
import { UntranslatedChip } from '@terrafirmagreg-team/ui/components/blocks/lang/untranslated-chip';
|
|
4
|
+
import {
|
|
5
|
+
FloatBar,
|
|
6
|
+
FloatBarItem,
|
|
7
|
+
} from '@terrafirmagreg-team/ui/components/blocks/viewer/float-bar';
|
|
8
|
+
|
|
9
|
+
export type ChangelogNowItem = {
|
|
10
|
+
id: string;
|
|
11
|
+
untranslated?: boolean;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export function ChangelogNow({ items }: { items: ChangelogNowItem[] }) {
|
|
15
|
+
const ref = useRef<HTMLDivElement>(null);
|
|
16
|
+
const [activeId, setActiveId] = useState(items[0]?.id ?? null);
|
|
17
|
+
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
const bar = ref.current;
|
|
20
|
+
const root = bar?.closest('[data-changelog-timeline]');
|
|
21
|
+
if (!bar || !root) return;
|
|
22
|
+
|
|
23
|
+
let tops: { id: string; top: number }[] = [];
|
|
24
|
+
const measure = () => {
|
|
25
|
+
const base = root.getBoundingClientRect().top;
|
|
26
|
+
tops = [...root.querySelectorAll<HTMLElement>('[data-release]')].map(
|
|
27
|
+
(li) => ({
|
|
28
|
+
id: li.id,
|
|
29
|
+
top: li.getBoundingClientRect().top - base,
|
|
30
|
+
}),
|
|
31
|
+
);
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
let raf = 0;
|
|
35
|
+
const update = () => {
|
|
36
|
+
raf = 0;
|
|
37
|
+
const y =
|
|
38
|
+
bar.getBoundingClientRect().top - root.getBoundingClientRect().top;
|
|
39
|
+
let active = tops[0]?.id ?? null;
|
|
40
|
+
for (const { id, top } of tops) {
|
|
41
|
+
if (top <= y) active = id;
|
|
42
|
+
else break;
|
|
43
|
+
}
|
|
44
|
+
setActiveId((prev) => (prev === active ? prev : active));
|
|
45
|
+
};
|
|
46
|
+
const schedule = () => {
|
|
47
|
+
if (!raf) raf = requestAnimationFrame(update);
|
|
48
|
+
};
|
|
49
|
+
const remeasure = () => {
|
|
50
|
+
measure();
|
|
51
|
+
schedule();
|
|
52
|
+
};
|
|
53
|
+
measure();
|
|
54
|
+
update();
|
|
55
|
+
document.addEventListener('scroll', schedule, {
|
|
56
|
+
capture: true,
|
|
57
|
+
passive: true,
|
|
58
|
+
});
|
|
59
|
+
window.addEventListener('resize', remeasure);
|
|
60
|
+
const observer = new ResizeObserver(remeasure);
|
|
61
|
+
observer.observe(root);
|
|
62
|
+
return () => {
|
|
63
|
+
document.removeEventListener('scroll', schedule, { capture: true });
|
|
64
|
+
window.removeEventListener('resize', remeasure);
|
|
65
|
+
observer.disconnect();
|
|
66
|
+
cancelAnimationFrame(raf);
|
|
67
|
+
};
|
|
68
|
+
}, []);
|
|
69
|
+
|
|
70
|
+
const current = useMemo(
|
|
71
|
+
() => items.find((entry) => entry.id === activeId) ?? null,
|
|
72
|
+
[items, activeId],
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
return (
|
|
76
|
+
<div
|
|
77
|
+
ref={ref}
|
|
78
|
+
className="not-prose pointer-events-none sticky top-[calc(var(--header-height,0px)+0.75rem)] z-10 h-0"
|
|
79
|
+
>
|
|
80
|
+
{current && (
|
|
81
|
+
<div className="absolute start-[7px] flex -translate-y-1/2 items-center gap-2">
|
|
82
|
+
<span className="size-3 shrink-0 rounded-full border-2 border-primary bg-background" />
|
|
83
|
+
<FloatBar className="pointer-events-auto">
|
|
84
|
+
<FloatBarItem className="gap-1.5">
|
|
85
|
+
<span className="font-semibold">{current.id}</span>
|
|
86
|
+
<span className={current.untranslated ? undefined : 'hidden'}>
|
|
87
|
+
<UntranslatedChip />
|
|
88
|
+
</span>
|
|
89
|
+
</FloatBarItem>
|
|
90
|
+
</FloatBar>
|
|
91
|
+
</div>
|
|
92
|
+
)}
|
|
93
|
+
</div>
|
|
94
|
+
);
|
|
95
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import Link from 'next/link';
|
|
2
|
+
import { ExternalLink, Link as LinkIcon } from 'lucide-react';
|
|
3
|
+
import type { ReactNode } from 'react';
|
|
4
|
+
import { dayFormat, monthLabel } from '@terrafirmagreg-team/changelog/lib/format';
|
|
5
|
+
import { ChangelogNow } from './now-bar';
|
|
6
|
+
|
|
7
|
+
export interface ChangelogTimelineEntry {
|
|
8
|
+
id: string;
|
|
9
|
+
title: string;
|
|
10
|
+
date: Date;
|
|
11
|
+
url?: string;
|
|
12
|
+
releaseUrl?: string;
|
|
13
|
+
untranslated?: boolean;
|
|
14
|
+
body: ReactNode;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export type ChangelogMonth = {
|
|
18
|
+
key: string;
|
|
19
|
+
label: string;
|
|
20
|
+
items: ChangelogTimelineEntry[];
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export function groupByMonth(
|
|
24
|
+
entries: ChangelogTimelineEntry[],
|
|
25
|
+
lang = 'en_us',
|
|
26
|
+
): ChangelogMonth[] {
|
|
27
|
+
const months = new Map<string, ChangelogMonth>();
|
|
28
|
+
for (const entry of [...entries].sort(
|
|
29
|
+
(a, b) => b.date.getTime() - a.date.getTime(),
|
|
30
|
+
)) {
|
|
31
|
+
const key = `${entry.date.getUTCFullYear()}-${String(entry.date.getUTCMonth() + 1).padStart(2, '0')}`;
|
|
32
|
+
const month = months.get(key);
|
|
33
|
+
if (month) month.items.push(entry);
|
|
34
|
+
else
|
|
35
|
+
months.set(key, {
|
|
36
|
+
key,
|
|
37
|
+
label: monthLabel(lang, entry.date),
|
|
38
|
+
items: [entry],
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
return [...months.values()];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export const monthAnchor = (monthKey: string) => monthKey;
|
|
45
|
+
|
|
46
|
+
export function ChangelogTimeline({
|
|
47
|
+
entries,
|
|
48
|
+
lang = 'en_us',
|
|
49
|
+
}: {
|
|
50
|
+
entries: ChangelogTimelineEntry[];
|
|
51
|
+
lang?: string;
|
|
52
|
+
}) {
|
|
53
|
+
const day = dayFormat(lang);
|
|
54
|
+
const latestId = entries.length
|
|
55
|
+
? entries.reduce((a, b) => (b.date > a.date ? b : a)).id
|
|
56
|
+
: undefined;
|
|
57
|
+
const months = groupByMonth(entries, lang);
|
|
58
|
+
return (
|
|
59
|
+
<div data-changelog-timeline>
|
|
60
|
+
<ChangelogNow
|
|
61
|
+
items={months.flatMap((month) =>
|
|
62
|
+
month.items.map((entry) => ({
|
|
63
|
+
id: entry.id,
|
|
64
|
+
untranslated: entry.untranslated,
|
|
65
|
+
})),
|
|
66
|
+
)}
|
|
67
|
+
/>
|
|
68
|
+
{months.map(({ key, label, items }) => (
|
|
69
|
+
<section
|
|
70
|
+
key={key}
|
|
71
|
+
className="ms-3 border-s-2 border-muted-foreground/25 ps-6 pb-8"
|
|
72
|
+
>
|
|
73
|
+
<h2
|
|
74
|
+
id={monthAnchor(key)}
|
|
75
|
+
className="not-prose scroll-mt-28 py-2 text-[11px] font-semibold tracking-[0.05em] text-muted-foreground/60"
|
|
76
|
+
>
|
|
77
|
+
{label}
|
|
78
|
+
</h2>
|
|
79
|
+
<ol className="mt-4 mb-0 list-none ps-0">
|
|
80
|
+
{items.map((entry) => (
|
|
81
|
+
<ChangelogItem
|
|
82
|
+
key={entry.id}
|
|
83
|
+
entry={entry}
|
|
84
|
+
day={day}
|
|
85
|
+
latest={entry.id === latestId}
|
|
86
|
+
/>
|
|
87
|
+
))}
|
|
88
|
+
</ol>
|
|
89
|
+
</section>
|
|
90
|
+
))}
|
|
91
|
+
</div>
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function ChangelogItem({
|
|
96
|
+
entry,
|
|
97
|
+
day,
|
|
98
|
+
latest,
|
|
99
|
+
}: {
|
|
100
|
+
entry: ChangelogTimelineEntry;
|
|
101
|
+
day: Intl.DateTimeFormat;
|
|
102
|
+
latest?: boolean;
|
|
103
|
+
}) {
|
|
104
|
+
return (
|
|
105
|
+
<li
|
|
106
|
+
id={entry.id}
|
|
107
|
+
data-release
|
|
108
|
+
className="group scroll-mt-28 ps-0 pb-12 last:pb-0"
|
|
109
|
+
>
|
|
110
|
+
<time
|
|
111
|
+
dateTime={entry.date.toISOString()}
|
|
112
|
+
className="relative mb-1.5 block text-xs font-medium text-muted-foreground tabular-nums"
|
|
113
|
+
>
|
|
114
|
+
<span className="absolute -start-[25px] top-1/2 size-2.5 -translate-x-1/2 -translate-y-1/2 rounded-full border-2 border-primary bg-background rtl:translate-x-1/2" />
|
|
115
|
+
{day.format(entry.date)}
|
|
116
|
+
</time>
|
|
117
|
+
<h3 className="not-prose mb-3 flex items-center gap-2 text-lg font-semibold tracking-tight">
|
|
118
|
+
{latest && (
|
|
119
|
+
<span className="rounded-md bg-primary/10 px-1.5 py-0.5 text-[10px] font-semibold uppercase tracking-wide text-primary">
|
|
120
|
+
Latest
|
|
121
|
+
</span>
|
|
122
|
+
)}
|
|
123
|
+
{entry.url ? (
|
|
124
|
+
<Link
|
|
125
|
+
href={entry.url}
|
|
126
|
+
className="underline-offset-4 hover:text-primary hover:underline"
|
|
127
|
+
>
|
|
128
|
+
{entry.title}
|
|
129
|
+
</Link>
|
|
130
|
+
) : (
|
|
131
|
+
entry.title
|
|
132
|
+
)}
|
|
133
|
+
{entry.releaseUrl && (
|
|
134
|
+
<a
|
|
135
|
+
href={entry.releaseUrl}
|
|
136
|
+
target="_blank"
|
|
137
|
+
rel="noreferrer"
|
|
138
|
+
aria-label="GitHub release"
|
|
139
|
+
className="text-muted-foreground hover:text-primary"
|
|
140
|
+
>
|
|
141
|
+
<ExternalLink className="size-3.5" />
|
|
142
|
+
</a>
|
|
143
|
+
)}
|
|
144
|
+
<a
|
|
145
|
+
href={`#${entry.id}`}
|
|
146
|
+
aria-label="Link to this release"
|
|
147
|
+
className="text-muted-foreground opacity-0 transition-opacity hover:text-primary group-hover:opacity-100"
|
|
148
|
+
>
|
|
149
|
+
<LinkIcon className="size-3.5" />
|
|
150
|
+
</a>
|
|
151
|
+
</h3>
|
|
152
|
+
<div className="text-sm text-muted-foreground">{entry.body}</div>
|
|
153
|
+
</li>
|
|
154
|
+
);
|
|
155
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { Lang } from '@terrafirmagreg-team/translate/types/locale';
|
|
2
|
+
import type { SectionSource } from '@terrafirmagreg-team/kit/lib/page';
|
|
3
|
+
import { timelineEntries } from '../entries';
|
|
4
|
+
import { ChangelogTimeline } from '../timeline';
|
|
5
|
+
|
|
6
|
+
export async function ChangelogTimelineView({
|
|
7
|
+
source,
|
|
8
|
+
route,
|
|
9
|
+
dataset,
|
|
10
|
+
lang,
|
|
11
|
+
repo,
|
|
12
|
+
}: {
|
|
13
|
+
source: SectionSource;
|
|
14
|
+
route: string;
|
|
15
|
+
dataset: string;
|
|
16
|
+
lang: Lang;
|
|
17
|
+
repo?: `${string}/${string}`;
|
|
18
|
+
}) {
|
|
19
|
+
return (
|
|
20
|
+
<ChangelogTimeline
|
|
21
|
+
entries={await timelineEntries(source, route, dataset, lang, repo)}
|
|
22
|
+
lang={lang}
|
|
23
|
+
/>
|
|
24
|
+
);
|
|
25
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export const bcp47 = (lang: string) => lang.replace('_', '-');
|
|
2
|
+
|
|
3
|
+
const capitalize = (s: string) => s.charAt(0).toUpperCase() + s.slice(1);
|
|
4
|
+
|
|
5
|
+
export const monthFormat = (lang: string) =>
|
|
6
|
+
new Intl.DateTimeFormat(bcp47(lang), {
|
|
7
|
+
month: 'long',
|
|
8
|
+
year: 'numeric',
|
|
9
|
+
timeZone: 'UTC',
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
export const monthLabel = (lang: string, date: Date) =>
|
|
13
|
+
capitalize(monthFormat(lang).format(date));
|
|
14
|
+
|
|
15
|
+
export const dayFormat = (lang: string) =>
|
|
16
|
+
new Intl.DateTimeFormat(bcp47(lang), {
|
|
17
|
+
month: 'short',
|
|
18
|
+
day: 'numeric',
|
|
19
|
+
timeZone: 'UTC',
|
|
20
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
type MdNode = {
|
|
2
|
+
type: string;
|
|
3
|
+
depth?: number;
|
|
4
|
+
children?: { type?: string; value?: string; children?: unknown[] }[];
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
const STRIP_HEADING = /^(what'?s changed|new contributors)$/i;
|
|
8
|
+
const FULL_CHANGELOG = /^\*?\*?full changelog\*?\*?/i;
|
|
9
|
+
|
|
10
|
+
const text = (node: MdNode): string =>
|
|
11
|
+
(node.children ?? [])
|
|
12
|
+
.map((child) => child.value ?? text(child as MdNode))
|
|
13
|
+
.join('')
|
|
14
|
+
.trim();
|
|
15
|
+
|
|
16
|
+
export const remarkStripAutoSections =
|
|
17
|
+
() =>
|
|
18
|
+
(tree: { children: MdNode[] }) => {
|
|
19
|
+
const out: MdNode[] = [];
|
|
20
|
+
let skipDepth = 0;
|
|
21
|
+
for (const node of tree.children) {
|
|
22
|
+
if (node.type === 'heading') {
|
|
23
|
+
const depth = node.depth ?? 2;
|
|
24
|
+
if (STRIP_HEADING.test(text(node))) {
|
|
25
|
+
skipDepth = depth;
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
if (skipDepth && depth <= skipDepth) skipDepth = 0;
|
|
29
|
+
if (skipDepth) continue;
|
|
30
|
+
} else if (skipDepth) {
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
if (node.type === 'paragraph' && FULL_CHANGELOG.test(text(node)))
|
|
34
|
+
continue;
|
|
35
|
+
out.push(node);
|
|
36
|
+
}
|
|
37
|
+
tree.children = out;
|
|
38
|
+
};
|
package/src/lib/trees.ts
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import type * as PageTree from 'fumadocs-core/page-tree';
|
|
2
|
+
import type { SectionTrees } from '@terrafirmagreg-team/kit/types';
|
|
3
|
+
|
|
4
|
+
const GROUP_NAME = /^(\d+)\.(\d+)\.x$/;
|
|
5
|
+
|
|
6
|
+
const numbers = (value: string) => value.match(/\d+/g)?.map(Number) ?? [];
|
|
7
|
+
|
|
8
|
+
const descByVersion = (a: PageTree.Node, b: PageTree.Node) => {
|
|
9
|
+
const an = numbers(String(a.name ?? ''));
|
|
10
|
+
const bn = numbers(String(b.name ?? ''));
|
|
11
|
+
for (let i = 0; i < Math.max(an.length, bn.length); i++) {
|
|
12
|
+
const diff = (bn[i] ?? 0) - (an[i] ?? 0);
|
|
13
|
+
if (diff) return diff;
|
|
14
|
+
}
|
|
15
|
+
return 0;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
function groupVersionItems(root: PageTree.Root): PageTree.Root {
|
|
19
|
+
const children: PageTree.Node[] = [];
|
|
20
|
+
const groups = new Map<string, PageTree.Folder>();
|
|
21
|
+
for (const node of root.children) {
|
|
22
|
+
const title = node.type === 'page' ? String(node.name ?? '') : '';
|
|
23
|
+
const series = /^v?(\d+)\.(\d+)/.exec(title)?.slice(1, 3).join('.');
|
|
24
|
+
if (node.type !== 'page' || !series) {
|
|
25
|
+
children.push(node);
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
let folder = groups.get(series);
|
|
29
|
+
if (!folder) {
|
|
30
|
+
folder = {
|
|
31
|
+
$id: `${root.$id ?? 'root'}:${series}`,
|
|
32
|
+
type: 'folder',
|
|
33
|
+
name: `${series}.x`,
|
|
34
|
+
children: [],
|
|
35
|
+
};
|
|
36
|
+
groups.set(series, folder);
|
|
37
|
+
children.push(folder);
|
|
38
|
+
}
|
|
39
|
+
folder.children.push(node);
|
|
40
|
+
}
|
|
41
|
+
for (const folder of groups.values())
|
|
42
|
+
folder.children.sort(descByVersion);
|
|
43
|
+
const seriesOf = (node: PageTree.Node) =>
|
|
44
|
+
node.type === 'folder' && typeof node.name === 'string'
|
|
45
|
+
? GROUP_NAME.exec(node.name)
|
|
46
|
+
: null;
|
|
47
|
+
children.sort((a, b) => {
|
|
48
|
+
const sa = seriesOf(a);
|
|
49
|
+
const sb = seriesOf(b);
|
|
50
|
+
return sa && sb
|
|
51
|
+
? Number(sb[1]) - Number(sa[1]) || Number(sb[2]) - Number(sa[2])
|
|
52
|
+
: 0;
|
|
53
|
+
});
|
|
54
|
+
return { ...root, children };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const TIMELINE_NAMES: Record<string, string> = {
|
|
58
|
+
en_us: 'Timeline',
|
|
59
|
+
ru_ru: 'Таймлайн',
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export const groupTrees =
|
|
63
|
+
(route: string, names: Record<string, string> = TIMELINE_NAMES) =>
|
|
64
|
+
(trees: SectionTrees): SectionTrees =>
|
|
65
|
+
Object.fromEntries(
|
|
66
|
+
Object.entries(trees).map(([lang, byDataset]) => [
|
|
67
|
+
lang,
|
|
68
|
+
Object.fromEntries(
|
|
69
|
+
Object.entries(byDataset).map(([id, root]) => {
|
|
70
|
+
const grouped = groupVersionItems(root);
|
|
71
|
+
const timeline: PageTree.Item = {
|
|
72
|
+
type: 'page',
|
|
73
|
+
name: names[lang] ?? 'Timeline',
|
|
74
|
+
url: `${route}/${id}`,
|
|
75
|
+
};
|
|
76
|
+
return [
|
|
77
|
+
id,
|
|
78
|
+
{ ...grouped, children: [timeline, ...grouped.children] },
|
|
79
|
+
];
|
|
80
|
+
}),
|
|
81
|
+
),
|
|
82
|
+
]),
|
|
83
|
+
);
|
package/src/locales/en_us.json
CHANGED
|
@@ -1,8 +1,4 @@
|
|
|
1
1
|
{
|
|
2
|
-
"sections":
|
|
3
|
-
|
|
4
|
-
"desc": {
|
|
5
|
-
"changelog": "Release history and version notes"
|
|
6
|
-
}
|
|
7
|
-
}
|
|
2
|
+
"Changelog(sections.changelog)": "Changelog",
|
|
3
|
+
"Release history and version notes(sections.desc.changelog)": "Release history and version notes"
|
|
8
4
|
}
|
package/src/locales/ru_ru.json
CHANGED
|
@@ -1,8 +1,4 @@
|
|
|
1
1
|
{
|
|
2
|
-
"sections":
|
|
3
|
-
|
|
4
|
-
"desc": {
|
|
5
|
-
"changelog": "История релизов и заметки к версиям"
|
|
6
|
-
}
|
|
7
|
-
}
|
|
2
|
+
"Changelog(sections.changelog)": "Список изменений",
|
|
3
|
+
"Release history and version notes(sections.desc.changelog)": "История релизов и заметки к версиям"
|
|
8
4
|
}
|
package/src/locales.ts
CHANGED
package/src/lib/route.ts
DELETED
package/src/lib/tree.ts
DELETED
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
import type * as PageTree from 'fumadocs-core/page-tree';
|
|
2
|
-
import type { SectionTrees } from '@terrafirmagreg-team/kit/types';
|
|
3
|
-
|
|
4
|
-
function groupVersionItems(root: PageTree.Root): PageTree.Root {
|
|
5
|
-
const children: PageTree.Node[] = [];
|
|
6
|
-
const groups = new Map<string, PageTree.Folder>();
|
|
7
|
-
for (const node of root.children) {
|
|
8
|
-
const title = node.type === 'page' ? String(node.name ?? '') : '';
|
|
9
|
-
const series = /^v?(\d+)\.(\d+)/.exec(title)?.slice(1, 3).join('.');
|
|
10
|
-
if (node.type !== 'page' || !series) {
|
|
11
|
-
children.push(node);
|
|
12
|
-
continue;
|
|
13
|
-
}
|
|
14
|
-
let folder = groups.get(series);
|
|
15
|
-
if (!folder) {
|
|
16
|
-
folder = {
|
|
17
|
-
$id: `${root.$id ?? 'root'}:${series}`,
|
|
18
|
-
type: 'folder',
|
|
19
|
-
name: `${series}.x`,
|
|
20
|
-
children: [],
|
|
21
|
-
};
|
|
22
|
-
groups.set(series, folder);
|
|
23
|
-
children.push(folder);
|
|
24
|
-
}
|
|
25
|
-
folder.children.push(node);
|
|
26
|
-
}
|
|
27
|
-
return { ...root, children };
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export const groupTrees = (trees: SectionTrees): SectionTrees =>
|
|
31
|
-
Object.fromEntries(
|
|
32
|
-
Object.entries(trees).map(([lang, byDataset]) => [
|
|
33
|
-
lang,
|
|
34
|
-
Object.fromEntries(
|
|
35
|
-
Object.entries(byDataset).map(([id, root]) => [
|
|
36
|
-
id,
|
|
37
|
-
groupVersionItems(root),
|
|
38
|
-
]),
|
|
39
|
-
),
|
|
40
|
-
]),
|
|
41
|
-
);
|