@xingwangzhe/stalux 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/LICENSE +21 -0
- package/README.md +192 -0
- package/package.json +108 -0
- package/scripts/cli.mjs +103 -0
- package/src/assets/BiCcCircle.svg +1 -0
- package/src/assets/astro.svg +1 -0
- package/src/assets/background/README.md +3 -0
- package/src/assets/background/pattern-1.min.svg +1 -0
- package/src/assets/background/pattern-10.min.svg +1 -0
- package/src/assets/background/pattern-11.min.svg +1 -0
- package/src/assets/background/pattern-12.min.svg +1 -0
- package/src/assets/background/pattern-13.min.svg +1 -0
- package/src/assets/background/pattern-14.min.svg +1 -0
- package/src/assets/background/pattern-15.min.svg +1 -0
- package/src/assets/background/pattern-16.min.svg +1 -0
- package/src/assets/background/pattern-17.min.svg +1 -0
- package/src/assets/background/pattern-18.min.svg +1 -0
- package/src/assets/background/pattern-19.min.svg +1 -0
- package/src/assets/background/pattern-2.min.svg +1 -0
- package/src/assets/background/pattern-20.min.svg +1 -0
- package/src/assets/background/pattern-21.min.svg +1 -0
- package/src/assets/background/pattern-22.min.svg +1 -0
- package/src/assets/background/pattern-23.min.svg +1 -0
- package/src/assets/background/pattern-24.min.svg +1 -0
- package/src/assets/background/pattern-25.min.svg +1 -0
- package/src/assets/background/pattern-26.min.svg +1 -0
- package/src/assets/background/pattern-27.min.svg +1 -0
- package/src/assets/background/pattern-28.min.svg +1 -0
- package/src/assets/background/pattern-29.min.svg +1 -0
- package/src/assets/background/pattern-3.min.svg +1 -0
- package/src/assets/background/pattern-30.min.svg +1 -0
- package/src/assets/background/pattern-31.min.svg +1 -0
- package/src/assets/background/pattern-32.min.svg +1 -0
- package/src/assets/background/pattern-33.min.svg +1 -0
- package/src/assets/background/pattern-34.min.svg +1 -0
- package/src/assets/background/pattern-35.min.svg +1 -0
- package/src/assets/background/pattern-36.min.svg +1 -0
- package/src/assets/background/pattern-37.min.svg +1 -0
- package/src/assets/background/pattern-38.min.svg +1 -0
- package/src/assets/background/pattern-39.min.svg +1 -0
- package/src/assets/background/pattern-4.min.svg +1 -0
- package/src/assets/background/pattern-40.min.svg +1 -0
- package/src/assets/background/pattern-41.min.svg +1 -0
- package/src/assets/background/pattern-42.min.svg +1 -0
- package/src/assets/background/pattern-5.min.svg +1 -0
- package/src/assets/background/pattern-6.min.svg +1 -0
- package/src/assets/background/pattern-7.min.svg +1 -0
- package/src/assets/background/pattern-8.min.svg +1 -0
- package/src/assets/background/pattern-9.min.svg +1 -0
- package/src/assets/background.svg +1 -0
- package/src/assets/cc-icon.svg +4 -0
- package/src/assets/fonts/GoogleSansCode-Italic.woff2 +0 -0
- package/src/assets/fonts/GoogleSansCode.woff2 +0 -0
- package/src/assets/fonts/LXGWWenKai-Regular.ttf +0 -0
- package/src/assets/fonts/LXGWWenKai-Regular.woff2 +0 -0
- package/src/components/stalux/analytics/clarity.astro +25 -0
- package/src/components/stalux/analytics/google.astro +31 -0
- package/src/components/stalux/analytics/umami.astro +16 -0
- package/src/components/stalux/archives/archivesList.astro +116 -0
- package/src/components/stalux/archives/toArchive.astro +14 -0
- package/src/components/stalux/categories/categoriesCard.astro +49 -0
- package/src/components/stalux/categories/categoriesList.astro +17 -0
- package/src/components/stalux/common/author.astro +35 -0
- package/src/components/stalux/common/date.astro +59 -0
- package/src/components/stalux/common/search.astro +14 -0
- package/src/components/stalux/common/textType.astro +16 -0
- package/src/components/stalux/common/verCount.astro +10 -0
- package/src/components/stalux/footer/FooterBadges.astro +150 -0
- package/src/components/stalux/footer/FooterBeian.astro +91 -0
- package/src/components/stalux/footer/FooterCopyright.astro +40 -0
- package/src/components/stalux/footer/FooterStats.astro +108 -0
- package/src/components/stalux/footer/FooterThemeInfo.astro +67 -0
- package/src/components/stalux/layout/footer.astro +87 -0
- package/src/components/stalux/layout/head.astro +194 -0
- package/src/components/stalux/layout/listPage.astro +26 -0
- package/src/components/stalux/layout/navs.astro +174 -0
- package/src/components/stalux/links/linkCard.astro +50 -0
- package/src/components/stalux/links/linkList.astro +20 -0
- package/src/components/stalux/links/socialMedia.astro +102 -0
- package/src/components/stalux/posts/PhotoSwipeStyle.astro +3 -0
- package/src/components/stalux/posts/PostTaxonomy.astro +65 -0
- package/src/components/stalux/posts/WalineComment.astro +609 -0
- package/src/components/stalux/posts/postAuthor.astro +23 -0
- package/src/components/stalux/posts/postCC.astro +52 -0
- package/src/components/stalux/posts/postCard.astro +83 -0
- package/src/components/stalux/posts/postContent.astro +174 -0
- package/src/components/stalux/posts/postLeft.astro +41 -0
- package/src/components/stalux/posts/postList.astro +25 -0
- package/src/components/stalux/posts/postNavigation.astro +137 -0
- package/src/components/stalux/posts/postRight.astro +33 -0
- package/src/components/stalux/posts/postToc.astro +88 -0
- package/src/components/stalux/posts/randomPost.astro +134 -0
- package/src/components/stalux/tags/tagHeader.astro +50 -0
- package/src/components/stalux/tags/tagsCloud.astro +100 -0
- package/src/components/stalux/words/wordCard.astro +62 -0
- package/src/components/stalux/words/wordList.astro +70 -0
- package/src/config.ts +58 -0
- package/src/i18n/en.json +193 -0
- package/src/i18n/zh-CN.json +193 -0
- package/src/index.ts +215 -0
- package/src/internal/components-plugin.ts +54 -0
- package/src/internal/dev-toolbar.ts +33 -0
- package/src/internal/override.ts +99 -0
- package/src/internal/pagefind.ts +40 -0
- package/src/layouts/PostLayout.astro +90 -0
- package/src/layouts/Stalux.astro +91 -0
- package/src/pages/404.astro +103 -0
- package/src/pages/about.astro +59 -0
- package/src/pages/about.md.ts +15 -0
- package/src/pages/api/post.abbrlink.json.ts +20 -0
- package/src/pages/archives.astro +23 -0
- package/src/pages/archives.md.ts +24 -0
- package/src/pages/atom.xml.ts +29 -0
- package/src/pages/categories/[category].astro +35 -0
- package/src/pages/categories/[category].md.ts +34 -0
- package/src/pages/categories/index.astro +23 -0
- package/src/pages/categories/index.md.ts +27 -0
- package/src/pages/index.astro +30 -0
- package/src/pages/index.md.ts +18 -0
- package/src/pages/links.astro +23 -0
- package/src/pages/links.md.ts +18 -0
- package/src/pages/llms-full.txt.ts +25 -0
- package/src/pages/llms.txt.ts +26 -0
- package/src/pages/posts/[post].astro +69 -0
- package/src/pages/posts/[post].md.ts +109 -0
- package/src/pages/rss.xml.ts +29 -0
- package/src/pages/tags/[tag].astro +37 -0
- package/src/pages/tags/[tag].md.ts +34 -0
- package/src/pages/tags/index.astro +23 -0
- package/src/pages/tags/index.md.ts +27 -0
- package/src/pages/words.astro +24 -0
- package/src/pages/words.md.ts +14 -0
- package/src/plugins/feature-flags.ts +68 -0
- package/src/plugins/satteri-temml.ts +81 -0
- package/src/schemas/collections.ts +140 -0
- package/src/schemas/config.ts +226 -0
- package/src/scripts/background.ts +59 -0
- package/src/styles/base/init.css +286 -0
- package/src/styles/base/math.css +411 -0
- package/src/styles/components/archives/archivesList.module.css +378 -0
- package/src/styles/components/archives/toArchive.module.css +3 -0
- package/src/styles/components/categories/categoriesCard.module.css +87 -0
- package/src/styles/components/categories/categoriesList.module.css +27 -0
- package/src/styles/components/common/author.module.css +73 -0
- package/src/styles/components/common/date.module.css +15 -0
- package/src/styles/components/common/textType.module.css +10 -0
- package/src/styles/components/footer/footer.module.css +44 -0
- package/src/styles/components/layout/navs.module.css +250 -0
- package/src/styles/components/links/linkCard.module.css +99 -0
- package/src/styles/components/links/linkList.module.css +67 -0
- package/src/styles/components/links/socialLinks.module.css +22 -0
- package/src/styles/components/markdown.css +528 -0
- package/src/styles/components/posts/postAuthor.module.css +32 -0
- package/src/styles/components/posts/postCC.module.css +138 -0
- package/src/styles/components/posts/postCard.module.css +156 -0
- package/src/styles/components/posts/postCategories.module.css +50 -0
- package/src/styles/components/posts/postContent.module.css +230 -0
- package/src/styles/components/posts/postLayout.module.css +142 -0
- package/src/styles/components/posts/postLeft.module.css +38 -0
- package/src/styles/components/posts/postList.module.css +24 -0
- package/src/styles/components/posts/postNavigation.module.css +187 -0
- package/src/styles/components/posts/postRight.module.css +60 -0
- package/src/styles/components/posts/postSidebar.module.css +57 -0
- package/src/styles/components/posts/postTags.module.css +51 -0
- package/src/styles/components/posts/postTaxonomy.module.css +87 -0
- package/src/styles/components/posts/postToc.module.css +74 -0
- package/src/styles/components/posts/randomPost.module.css +131 -0
- package/src/styles/components/tags/tagHeader.module.css +44 -0
- package/src/styles/components/tags/tagsCloud.module.css +124 -0
- package/src/styles/components/words/wordCard.module.css +130 -0
- package/src/styles/components/words/wordList.module.css +39 -0
- package/src/styles/pages/archives.module.css +18 -0
- package/src/styles/pages/categories.module.css +13 -0
- package/src/styles/pages/home.module.css +51 -0
- package/src/styles/pages/listPage.module.css +13 -0
- package/src/styles/pages/tags.module.css +12 -0
- package/src/styles/pages/words.module.css +13 -0
- package/src/styles/shared/animations.css +87 -0
- package/src/styles/shared/stagger.css +20 -0
- package/src/utils/ai-discovery.ts +547 -0
- package/src/utils/badge-generator.ts +68 -0
- package/src/utils/cc.ts +46 -0
- package/src/utils/collections-stats.ts +48 -0
- package/src/utils/config-utils.ts +107 -0
- package/src/utils/count-words.ts +20 -0
- package/src/utils/dayjs.ts +106 -0
- package/src/utils/feed.ts +70 -0
- package/src/utils/i18n.ts +64 -0
- package/src/utils/lucide-icons.ts +52 -0
- package/src/utils/semantic-time.ts +11 -0
- package/src/utils/taxonomy.ts +44 -0
- package/src/utils/toc-scrollspy.ts +95 -0
- package/src/utils/waline.ts +27 -0
- package/src/utils/word-count-utils.ts +83 -0
- package/template/astro.config.mjs +51 -0
- package/template/package.json +27 -0
- package/template/public/robots.txt +3 -0
- package/template/src/content.config.ts +11 -0
- package/template/stalux/about/index.md +14 -0
- package/template/stalux/config/ai-discovery.yml +2 -0
- package/template/stalux/config/author.yml +4 -0
- package/template/stalux/config/comment.yml +5 -0
- package/template/stalux/config/footer.yml +13 -0
- package/template/stalux/config/head.yml +7 -0
- package/template/stalux/config/links.yml +4 -0
- package/template/stalux/config/media-links.yml +8 -0
- package/template/stalux/config/navs.yml +23 -0
- package/template/stalux/config/promote.yml +2 -0
- package/template/stalux/config/site.yml +7 -0
- package/template/stalux/config/typetexts.yml +5 -0
- package/template/stalux/posts/hello-stalux.md +47 -0
- package/template/stalux/words/_template.md +12 -0
- package/template/stalux/words/einstein-imagination.md +8 -0
- package/template/tsconfig.json +9 -0
|
@@ -0,0 +1,547 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AI 发现文件(llms.txt / llms-full.txt)生成工具
|
|
3
|
+
*
|
|
4
|
+
* 为 /llms.txt、/llms-full.txt 以及所有页面 .md 导出提供共享数据与格式化函数。
|
|
5
|
+
*/
|
|
6
|
+
import type { CollectionEntry } from "astro:content";
|
|
7
|
+
import { getCollection } from "astro:content";
|
|
8
|
+
|
|
9
|
+
import { toTimestamp } from "./dayjs";
|
|
10
|
+
import { createTranslator } from "./i18n";
|
|
11
|
+
|
|
12
|
+
export type ConfigMap = Map<string, Record<string, unknown>>;
|
|
13
|
+
export type Post = CollectionEntry<"posts">;
|
|
14
|
+
type About = CollectionEntry<"about">;
|
|
15
|
+
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
// AI 发现文件配置
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
|
|
20
|
+
export type AiFileName = "llms.txt" | "llms-full.txt";
|
|
21
|
+
|
|
22
|
+
const conformanceFiles: Record<string, AiFileName[]> = {
|
|
23
|
+
disabled: [],
|
|
24
|
+
essential: ["llms.txt", "llms-full.txt"],
|
|
25
|
+
recommended: ["llms.txt", "llms-full.txt"],
|
|
26
|
+
complete: ["llms.txt", "llms-full.txt"],
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/** 根据 conformance 等级判断指定 AI 文件是否应生成 */
|
|
30
|
+
export function isAiFileEnabled(conformance: string | undefined, file: AiFileName): boolean {
|
|
31
|
+
const files = conformanceFiles[conformance ?? "essential"];
|
|
32
|
+
if (!files) return true; // unknown conformance → default to enabled
|
|
33
|
+
return files.includes(file);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Markdown 导出仅在 promote.export_md 严格为 true 时启用 */
|
|
37
|
+
export function isMarkdownExportEnabled(config: ConfigMap): boolean {
|
|
38
|
+
return config.get("promote")?.export_md === true;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** 加载 config 集合,返回按 id 索引的 Map */
|
|
42
|
+
export async function loadConfig(): Promise<ConfigMap> {
|
|
43
|
+
const configCollection = await getCollection("config");
|
|
44
|
+
return new Map(configCollection.map((entry) => [entry.id, entry.data]));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** 快捷获取 site 对象 */
|
|
48
|
+
function getSiteConfig(config: ConfigMap): Record<string, unknown> {
|
|
49
|
+
return config.get("site") ?? {};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** 解析站点根 URL */
|
|
53
|
+
export function getSite(config: ConfigMap, contextSite?: string): string {
|
|
54
|
+
const siteData = getSiteConfig(config);
|
|
55
|
+
return (contextSite || (siteData.url as string) || "").replace(/\/$/, "");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** 将任意空白压缩为单行 */
|
|
59
|
+
function inlineText(text: string | undefined): string {
|
|
60
|
+
return (text || "").replace(/\s+/g, " ").trim();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** 转义 markdown 文本中的特殊字符(用于描述) */
|
|
64
|
+
function escapeMd(text: string | undefined): string {
|
|
65
|
+
return inlineText(text).replace(/([\\[*_`#])/g, "\\$1");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** 转义 markdown 链接标题中的特殊字符 */
|
|
69
|
+
function escapeMdLinkTitle(text: string | undefined): string {
|
|
70
|
+
return inlineText(text).replace(/([\\[\]])/g, "\\$1");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** 构建 markdown 列表项 */
|
|
74
|
+
function buildMdLink(title: string, url: string, desc?: string): string {
|
|
75
|
+
const line = `- [${escapeMdLinkTitle(title)}](${url})`;
|
|
76
|
+
return desc ? `${line}: ${escapeMd(desc)}` : line;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** 生成页面链接,支持 HTML 或 .md 版本 */
|
|
80
|
+
function pageUrl(site: string, path: string, exportMd: boolean): string {
|
|
81
|
+
return exportMd ? `${site}/${path}.md` : `${site}/${path}/`;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** 生成文章链接,可切换 .md 源码版本 */
|
|
85
|
+
export function postUrl(site: string, abbrlink: string | number, exportMd: boolean): string {
|
|
86
|
+
return exportMd && abbrlink ? `${site}/posts/${abbrlink}.md` : `${site}/posts/${abbrlink}/`;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** 获取已发布文章并按日期降序排列 */
|
|
90
|
+
export async function getPublishedPosts(): Promise<Post[]> {
|
|
91
|
+
const posts = await getCollection("posts", ({ data }) => !data.draft);
|
|
92
|
+
return posts.sort((a, b) => {
|
|
93
|
+
const aTime = toTimestamp(a.data.date || 0);
|
|
94
|
+
const bTime = toTimestamp(b.data.date || 0);
|
|
95
|
+
return bTime - aTime; // newest first
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** 获取 about 页面 */
|
|
100
|
+
async function loadAbout(): Promise<About | undefined> {
|
|
101
|
+
const aboutPages = await getCollection("about");
|
|
102
|
+
return aboutPages[0];
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** 从 media-links section 中解析社交媒体链接 */
|
|
106
|
+
function getMediaLinks(config: ConfigMap): Array<{ name: string; url: string }> {
|
|
107
|
+
const mediaSection = config.get("media-links");
|
|
108
|
+
if (!mediaSection) return [];
|
|
109
|
+
const items = (mediaSection as Record<string, unknown>).items as
|
|
110
|
+
| Array<{ icon: string; link: string }>
|
|
111
|
+
| undefined;
|
|
112
|
+
if (!items) return [];
|
|
113
|
+
return items
|
|
114
|
+
.filter((link) => link.link && !link.link.startsWith("mailto:"))
|
|
115
|
+
.map((link) => {
|
|
116
|
+
const url = link.link || "";
|
|
117
|
+
let name = "Link";
|
|
118
|
+
try {
|
|
119
|
+
const hostname = new URL(url.startsWith("http") ? url : `https://example.com${url}`)
|
|
120
|
+
.hostname;
|
|
121
|
+
const domain = hostname.replace(/^www\./, "").split(".")[0];
|
|
122
|
+
name = domain ? domain.charAt(0).toUpperCase() + domain.slice(1) : "Link";
|
|
123
|
+
} catch {
|
|
124
|
+
// 无法解析 URL 时保持默认
|
|
125
|
+
}
|
|
126
|
+
return { name, url };
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** 尝试从 media-links 中找出邮箱地址 */
|
|
131
|
+
function getEmail(config: ConfigMap): string | undefined {
|
|
132
|
+
const mediaSection = config.get("media-links");
|
|
133
|
+
if (!mediaSection) return undefined;
|
|
134
|
+
const items = (mediaSection as Record<string, unknown>).items as
|
|
135
|
+
| Array<{ icon: string; link: string }>
|
|
136
|
+
| undefined;
|
|
137
|
+
if (!items) return undefined;
|
|
138
|
+
const mail = items.find((l) => l.link?.startsWith("mailto:"))?.link;
|
|
139
|
+
if (mail) return mail.replace(/^mailto:/, "");
|
|
140
|
+
return undefined;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// ---------------------------------------------------------------------------
|
|
144
|
+
// 页面 Markdown 渲染函数(用于 .md 端点和 llms-full.txt)
|
|
145
|
+
// ---------------------------------------------------------------------------
|
|
146
|
+
|
|
147
|
+
/** 构建分类/标签名 → 文章列表的映射 */
|
|
148
|
+
export function buildTaxonomyMap(posts: Post[], key: "tags" | "categories"): Map<string, Post[]> {
|
|
149
|
+
const map = new Map<string, Post[]>();
|
|
150
|
+
for (const post of posts) {
|
|
151
|
+
for (const name of (post.data[key] ?? []) as string[]) {
|
|
152
|
+
if (!map.has(name)) map.set(name, []);
|
|
153
|
+
map.get(name)!.push(post);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return map;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** 渲染文章列表 Markdown */
|
|
160
|
+
export function renderPostListMd(
|
|
161
|
+
posts: Post[],
|
|
162
|
+
site: string,
|
|
163
|
+
exportMd: boolean,
|
|
164
|
+
title?: string,
|
|
165
|
+
): string {
|
|
166
|
+
const lines: string[] = [];
|
|
167
|
+
if (title) {
|
|
168
|
+
lines.push(`## ${title}`);
|
|
169
|
+
lines.push("");
|
|
170
|
+
}
|
|
171
|
+
if (posts.length === 0) {
|
|
172
|
+
lines.push("- No posts");
|
|
173
|
+
} else {
|
|
174
|
+
for (const post of posts) {
|
|
175
|
+
const postTitle = post.data.title || "Untitled";
|
|
176
|
+
const url = postUrl(site, post.data.abbrlink, exportMd);
|
|
177
|
+
const desc = post.data.desc || "";
|
|
178
|
+
lines.push(buildMdLink(postTitle, url, desc));
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
lines.push("");
|
|
182
|
+
return lines.join("\n");
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** 渲染分类/标签索引列表 Markdown */
|
|
186
|
+
export function renderTaxonomyListMd(
|
|
187
|
+
map: Map<string, Post[]>,
|
|
188
|
+
site: string,
|
|
189
|
+
type: "categories" | "tags",
|
|
190
|
+
title?: string,
|
|
191
|
+
): string {
|
|
192
|
+
const lines: string[] = [];
|
|
193
|
+
if (title) {
|
|
194
|
+
lines.push(`# ${title}`);
|
|
195
|
+
lines.push("");
|
|
196
|
+
}
|
|
197
|
+
lines.push(`URL: ${site}/${type}/`);
|
|
198
|
+
lines.push("");
|
|
199
|
+
const sorted = [...map].sort((a, b) => b[1].length - a[1].length);
|
|
200
|
+
for (const [name, posts] of sorted) {
|
|
201
|
+
const slug = encodeURIComponent(name);
|
|
202
|
+
const url = `${site}/${type}/${slug}/`;
|
|
203
|
+
lines.push(buildMdLink(name, url, `${posts.length} posts`));
|
|
204
|
+
}
|
|
205
|
+
lines.push("");
|
|
206
|
+
return lines.join("\n").trimEnd() + "\n";
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** 渲染单个分类/标签页 Markdown */
|
|
210
|
+
export function renderTaxonomyPageMd(
|
|
211
|
+
name: string,
|
|
212
|
+
posts: Post[],
|
|
213
|
+
site: string,
|
|
214
|
+
type: "categories" | "tags",
|
|
215
|
+
exportMd: boolean,
|
|
216
|
+
): string {
|
|
217
|
+
const label = type === "categories" ? "Category" : "Tag";
|
|
218
|
+
const slug = encodeURIComponent(name);
|
|
219
|
+
const pageUrl = `${site}/${type}/${slug}/`;
|
|
220
|
+
const lines: string[] = [];
|
|
221
|
+
lines.push(`# ${label}: ${name}`);
|
|
222
|
+
lines.push("");
|
|
223
|
+
lines.push(`URL: ${pageUrl}`);
|
|
224
|
+
lines.push("");
|
|
225
|
+
lines.push(renderPostListMd(posts, site, exportMd, `${posts.length} posts`));
|
|
226
|
+
return lines.join("\n").trimEnd() + "\n";
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** 渲染 About 页 Markdown */
|
|
230
|
+
export async function renderAboutMd(site: string): Promise<string> {
|
|
231
|
+
const aboutPages = await getCollection("about");
|
|
232
|
+
const about = aboutPages[0];
|
|
233
|
+
if (!about) return "";
|
|
234
|
+
const lines: string[] = [];
|
|
235
|
+
lines.push(`# ${about.data.title || "About"}`);
|
|
236
|
+
lines.push("");
|
|
237
|
+
lines.push(`URL: ${site}/about/`);
|
|
238
|
+
lines.push("");
|
|
239
|
+
lines.push((about.body as string) || "");
|
|
240
|
+
lines.push("");
|
|
241
|
+
return lines.join("\n").trimEnd() + "\n";
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/** 渲染 Words 页 Markdown */
|
|
245
|
+
export async function renderWordsMd(): Promise<string> {
|
|
246
|
+
const words = await getCollection("words", ({ data }) => !data.draft);
|
|
247
|
+
words.sort((a, b) => toTimestamp(b.data.date || 0) - toTimestamp(a.data.date || 0));
|
|
248
|
+
const lines: string[] = [];
|
|
249
|
+
lines.push("# Words");
|
|
250
|
+
lines.push("");
|
|
251
|
+
lines.push("URL: /words/");
|
|
252
|
+
lines.push("");
|
|
253
|
+
for (const word of words) {
|
|
254
|
+
const title = word.data.title || "Untitled";
|
|
255
|
+
lines.push(`## ${title}`);
|
|
256
|
+
lines.push((word.body as string) || "");
|
|
257
|
+
lines.push("");
|
|
258
|
+
}
|
|
259
|
+
return lines.join("\n").trimEnd() + "\n";
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/** 渲染 Links 页 Markdown */
|
|
263
|
+
export function renderLinksMd(config: ConfigMap, site: string, t: (key: string) => string): string {
|
|
264
|
+
const linksSection = config.get("links") as Record<string, unknown> | undefined;
|
|
265
|
+
const lines: string[] = [];
|
|
266
|
+
lines.push(`# ${(linksSection?.title as string) || t("ai.links")}`);
|
|
267
|
+
lines.push("");
|
|
268
|
+
lines.push(`URL: ${site}/links/`);
|
|
269
|
+
lines.push("");
|
|
270
|
+
if (linksSection?.sites) {
|
|
271
|
+
const sites = linksSection.sites as Array<{
|
|
272
|
+
name: string;
|
|
273
|
+
link: string;
|
|
274
|
+
description: string;
|
|
275
|
+
}>;
|
|
276
|
+
for (const s of sites) {
|
|
277
|
+
lines.push(buildMdLink(s.name, s.link, s.description));
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
lines.push("");
|
|
281
|
+
return lines.join("\n").trimEnd() + "\n";
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/** 渲染 Archives 页 Markdown */
|
|
285
|
+
export async function renderArchivesMd(
|
|
286
|
+
site: string,
|
|
287
|
+
exportMd: boolean,
|
|
288
|
+
t: (key: string) => string,
|
|
289
|
+
): Promise<string> {
|
|
290
|
+
const posts = await getPublishedPosts();
|
|
291
|
+
const lines: string[] = [];
|
|
292
|
+
lines.push(`# ${t("archives.title")}`);
|
|
293
|
+
lines.push("");
|
|
294
|
+
lines.push(`URL: ${site}/archives/`);
|
|
295
|
+
lines.push("");
|
|
296
|
+
lines.push(renderPostListMd(posts, site, exportMd));
|
|
297
|
+
return lines.join("\n").trimEnd() + "\n";
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/** 渲染首页 Markdown */
|
|
301
|
+
export async function renderIndexMd(
|
|
302
|
+
config: ConfigMap,
|
|
303
|
+
site: string,
|
|
304
|
+
t: (key: string) => string,
|
|
305
|
+
): Promise<string> {
|
|
306
|
+
const posts = await getPublishedPosts();
|
|
307
|
+
const siteData = getSiteConfig(config);
|
|
308
|
+
const lang = (siteData.lang as string) || "zh-CN";
|
|
309
|
+
const about = await loadAbout();
|
|
310
|
+
const lines: string[] = [];
|
|
311
|
+
|
|
312
|
+
lines.push(`# ${(siteData.title as string) || t("ai.blog")}`);
|
|
313
|
+
lines.push("");
|
|
314
|
+
lines.push(`URL: ${site}/`);
|
|
315
|
+
lines.push("");
|
|
316
|
+
lines.push(`> ${escapeMd(siteData.description as string)}`);
|
|
317
|
+
lines.push("");
|
|
318
|
+
lines.push(`Lang: ${lang}`);
|
|
319
|
+
lines.push("");
|
|
320
|
+
|
|
321
|
+
lines.push(`## ${t("ai.markdownSources")}`);
|
|
322
|
+
lines.push(buildMdLink("Home", `${site}/index.md`));
|
|
323
|
+
lines.push(buildMdLink("About", `${site}/about.md`));
|
|
324
|
+
lines.push(buildMdLink("Words", `${site}/words.md`));
|
|
325
|
+
lines.push(buildMdLink("Links", `${site}/links.md`));
|
|
326
|
+
lines.push(buildMdLink("Archives", `${site}/archives.md`));
|
|
327
|
+
lines.push(buildMdLink("Categories", `${site}/categories.md`));
|
|
328
|
+
lines.push(buildMdLink("Tags", `${site}/tags.md`));
|
|
329
|
+
lines.push(buildMdLink("LLMs.txt", `${site}/llms.txt`));
|
|
330
|
+
lines.push(buildMdLink("LLMs-full.txt", `${site}/llms-full.txt`));
|
|
331
|
+
lines.push("");
|
|
332
|
+
|
|
333
|
+
if (about) {
|
|
334
|
+
lines.push(`## ${t("ai.about")}`);
|
|
335
|
+
lines.push((about.body as string) || "");
|
|
336
|
+
lines.push("");
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
lines.push(`## ${t("ai.latestPosts")}`);
|
|
340
|
+
lines.push(renderPostListMd(posts.slice(0, 10), site, true));
|
|
341
|
+
return lines.join("\n").trimEnd() + "\n";
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// ---------------------------------------------------------------------------
|
|
345
|
+
// llms.txt / llms-full.txt 渲染
|
|
346
|
+
// ---------------------------------------------------------------------------
|
|
347
|
+
|
|
348
|
+
/** 生成 llms.txt 核心 markdown 内容 */
|
|
349
|
+
export async function renderLlmsTxt(config: ConfigMap, site: string): Promise<string> {
|
|
350
|
+
const posts = await getPublishedPosts();
|
|
351
|
+
const about = await loadAbout();
|
|
352
|
+
const siteData = getSiteConfig(config);
|
|
353
|
+
const authorSection = config.get("author") as Record<string, unknown> | undefined;
|
|
354
|
+
const linksSection = config.get("links") as Record<string, unknown> | undefined;
|
|
355
|
+
const mediaLinks = getMediaLinks(config);
|
|
356
|
+
const email = getEmail(config);
|
|
357
|
+
const lang = (siteData.lang as string) || "zh-CN";
|
|
358
|
+
const { t } = createTranslator(lang);
|
|
359
|
+
const exportMd = isMarkdownExportEnabled(config);
|
|
360
|
+
|
|
361
|
+
const lines: string[] = [];
|
|
362
|
+
|
|
363
|
+
// Lang 头部
|
|
364
|
+
lines.push(`Lang: ${lang}`);
|
|
365
|
+
lines.push("");
|
|
366
|
+
|
|
367
|
+
// H1 与 blockquote
|
|
368
|
+
lines.push(`# ${(siteData.title as string) || t("ai.blog")}`);
|
|
369
|
+
lines.push("");
|
|
370
|
+
lines.push(`> ${escapeMd(siteData.description as string)}`);
|
|
371
|
+
lines.push("");
|
|
372
|
+
lines.push(`> ${t("ai.siteLicense")}`);
|
|
373
|
+
lines.push("");
|
|
374
|
+
|
|
375
|
+
// 简介段落
|
|
376
|
+
const authorName = authorSection?.name as string | undefined;
|
|
377
|
+
const authorBio = authorSection?.bio as string | undefined;
|
|
378
|
+
if (authorName) {
|
|
379
|
+
lines.push(`${t("ai.author")}${authorName}`);
|
|
380
|
+
if (authorBio) lines.push(inlineText(authorBio));
|
|
381
|
+
lines.push("");
|
|
382
|
+
} else if (authorBio) {
|
|
383
|
+
lines.push(inlineText(authorBio));
|
|
384
|
+
lines.push("");
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// Contact
|
|
388
|
+
lines.push(`## ${t("ai.contact")}`);
|
|
389
|
+
if (authorName) {
|
|
390
|
+
lines.push(`- ${t("ai.author")}: ${escapeMd(authorName)}`);
|
|
391
|
+
}
|
|
392
|
+
if (email) {
|
|
393
|
+
lines.push(`- ${t("ai.email")}: ${email}`);
|
|
394
|
+
}
|
|
395
|
+
for (const { name, url } of mediaLinks) {
|
|
396
|
+
lines.push(`- ${t("ai.socialMedia")} (${escapeMd(name)}): ${url}`);
|
|
397
|
+
}
|
|
398
|
+
if (!authorName && !email && mediaLinks.length === 0) {
|
|
399
|
+
lines.push(`- ${t("ai.noContact")}`);
|
|
400
|
+
}
|
|
401
|
+
lines.push("");
|
|
402
|
+
|
|
403
|
+
// Posts
|
|
404
|
+
lines.push(`## ${t("ai.posts")}`);
|
|
405
|
+
if (posts.length === 0) {
|
|
406
|
+
lines.push(`- ${t("ai.noPosts")}`);
|
|
407
|
+
} else {
|
|
408
|
+
for (const post of posts) {
|
|
409
|
+
const title = post.data.title || t("ai.untitled");
|
|
410
|
+
const url = postUrl(site, post.data.abbrlink, exportMd);
|
|
411
|
+
const desc = post.data.desc || "";
|
|
412
|
+
lines.push(buildMdLink(title, url, desc));
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
lines.push("");
|
|
416
|
+
|
|
417
|
+
// Pages
|
|
418
|
+
lines.push(`## ${t("ai.pages")}`);
|
|
419
|
+
if (about) {
|
|
420
|
+
lines.push(
|
|
421
|
+
buildMdLink(
|
|
422
|
+
about.data.title || t("ai.about"),
|
|
423
|
+
pageUrl(site, "about", exportMd),
|
|
424
|
+
about.data.description || t("ai.aboutDesc"),
|
|
425
|
+
),
|
|
426
|
+
);
|
|
427
|
+
}
|
|
428
|
+
lines.push(
|
|
429
|
+
buildMdLink(t("ai.archives"), pageUrl(site, "archives", exportMd), t("ai.archivesDesc")),
|
|
430
|
+
);
|
|
431
|
+
lines.push(buildMdLink(t("ai.tags"), pageUrl(site, "tags", exportMd), t("ai.tagsDesc")));
|
|
432
|
+
lines.push(
|
|
433
|
+
buildMdLink(
|
|
434
|
+
t("ai.categories"),
|
|
435
|
+
pageUrl(site, "categories", exportMd),
|
|
436
|
+
t("ai.categoriesDesc"),
|
|
437
|
+
),
|
|
438
|
+
);
|
|
439
|
+
lines.push(buildMdLink(t("ai.words"), pageUrl(site, "words", exportMd), t("ai.wordsDesc")));
|
|
440
|
+
lines.push(buildMdLink(t("ai.links"), pageUrl(site, "links", exportMd), t("ai.links")));
|
|
441
|
+
lines.push("");
|
|
442
|
+
|
|
443
|
+
// Links(友情链接)
|
|
444
|
+
if (linksSection?.sites) {
|
|
445
|
+
const sites = linksSection.sites as Array<{
|
|
446
|
+
name: string;
|
|
447
|
+
link: string;
|
|
448
|
+
description: string;
|
|
449
|
+
}>;
|
|
450
|
+
if (sites.length > 0) {
|
|
451
|
+
lines.push(`## ${(linksSection.title as string) || t("ai.links")}`);
|
|
452
|
+
for (const siteLink of sites) {
|
|
453
|
+
lines.push(buildMdLink(siteLink.name, siteLink.link, siteLink.description));
|
|
454
|
+
}
|
|
455
|
+
lines.push("");
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// AI Discovery Files
|
|
460
|
+
lines.push(`## ${t("ai.aiDiscoveryFiles")}`);
|
|
461
|
+
lines.push(buildMdLink("llms.txt", `${site}/llms.txt`, t("ai.llmsTxtDesc")));
|
|
462
|
+
lines.push(buildMdLink("llms-full.txt", `${site}/llms-full.txt`, t("ai.llmsFullTxtDesc")));
|
|
463
|
+
lines.push("");
|
|
464
|
+
|
|
465
|
+
// Optional
|
|
466
|
+
lines.push(`## ${t("ai.optional")}`);
|
|
467
|
+
lines.push(buildMdLink(t("ai.rss"), `${site}/rss.xml`));
|
|
468
|
+
lines.push(buildMdLink(t("ai.atom"), `${site}/atom.xml`));
|
|
469
|
+
if (exportMd) {
|
|
470
|
+
lines.push(`- ${t("ai.mdAvailable")}`);
|
|
471
|
+
}
|
|
472
|
+
lines.push("");
|
|
473
|
+
|
|
474
|
+
return lines.join("\n").trimEnd() + "\n";
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/** 生成 llms-full.txt 内容:全站 Markdown 镜像 */
|
|
478
|
+
export async function renderLlmsFullTxt(config: ConfigMap, site: string): Promise<string> {
|
|
479
|
+
const siteData = getSiteConfig(config);
|
|
480
|
+
const lang = (siteData.lang as string) || "zh-CN";
|
|
481
|
+
const { t } = createTranslator(lang);
|
|
482
|
+
const posts = await getPublishedPosts();
|
|
483
|
+
const categoryMap = buildTaxonomyMap(posts, "categories");
|
|
484
|
+
const tagMap = buildTaxonomyMap(posts, "tags");
|
|
485
|
+
const exportMd = isMarkdownExportEnabled(config);
|
|
486
|
+
|
|
487
|
+
const sections: string[] = [];
|
|
488
|
+
|
|
489
|
+
sections.push(`Lang: ${lang}`);
|
|
490
|
+
sections.push("");
|
|
491
|
+
sections.push(`# ${t("ai.fullDataset")}`);
|
|
492
|
+
sections.push("");
|
|
493
|
+
sections.push(`> ${t("ai.fullDatasetDesc")}`);
|
|
494
|
+
sections.push("");
|
|
495
|
+
sections.push(`- ${t("ai.license")}: CC-BY-NC-SA-4.0`);
|
|
496
|
+
sections.push(`- ${t("ai.postCount")}: ${posts.length}`);
|
|
497
|
+
sections.push("");
|
|
498
|
+
|
|
499
|
+
sections.push("---");
|
|
500
|
+
sections.push("");
|
|
501
|
+
sections.push(await renderIndexMd(config, site, t));
|
|
502
|
+
sections.push("---");
|
|
503
|
+
sections.push("");
|
|
504
|
+
sections.push(await renderAboutMd(site));
|
|
505
|
+
sections.push("---");
|
|
506
|
+
sections.push("");
|
|
507
|
+
sections.push(await renderWordsMd());
|
|
508
|
+
sections.push("---");
|
|
509
|
+
sections.push("");
|
|
510
|
+
sections.push(renderLinksMd(config, site, t));
|
|
511
|
+
sections.push("---");
|
|
512
|
+
sections.push("");
|
|
513
|
+
sections.push(await renderArchivesMd(site, exportMd, t));
|
|
514
|
+
sections.push("---");
|
|
515
|
+
sections.push("");
|
|
516
|
+
sections.push(renderTaxonomyListMd(categoryMap, site, "categories", t("ai.allCategories")));
|
|
517
|
+
sections.push("---");
|
|
518
|
+
sections.push("");
|
|
519
|
+
for (const [name, catPosts] of [...categoryMap].sort((a, b) => b[1].length - a[1].length)) {
|
|
520
|
+
sections.push(renderTaxonomyPageMd(name, catPosts, site, "categories", exportMd));
|
|
521
|
+
sections.push("---");
|
|
522
|
+
sections.push("");
|
|
523
|
+
}
|
|
524
|
+
sections.push(renderTaxonomyListMd(tagMap, site, "tags", t("ai.allTags")));
|
|
525
|
+
sections.push("---");
|
|
526
|
+
sections.push("");
|
|
527
|
+
for (const [name, tagPosts] of [...tagMap].sort((a, b) => b[1].length - a[1].length)) {
|
|
528
|
+
sections.push(renderTaxonomyPageMd(name, tagPosts, site, "tags", exportMd));
|
|
529
|
+
sections.push("---");
|
|
530
|
+
sections.push("");
|
|
531
|
+
}
|
|
532
|
+
for (const post of posts) {
|
|
533
|
+
const title = post.data.title || t("ai.untitled");
|
|
534
|
+
const url = postUrl(site, post.data.abbrlink, false);
|
|
535
|
+
const cc = post.data.cc || "CC-BY-NC-SA-4.0";
|
|
536
|
+
sections.push(`## ${escapeMd(title)}`);
|
|
537
|
+
sections.push(`URL: ${url}`);
|
|
538
|
+
sections.push(`License: ${cc}`);
|
|
539
|
+
sections.push("");
|
|
540
|
+
sections.push((post.body as string) || "");
|
|
541
|
+
sections.push("");
|
|
542
|
+
sections.push("---");
|
|
543
|
+
sections.push("");
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
return sections.join("\n").trimEnd() + "\n";
|
|
547
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 徽章生成器工具
|
|
3
|
+
* 基于 badge-maker 库生成徽章
|
|
4
|
+
*/
|
|
5
|
+
import { makeBadge } from "badge-maker";
|
|
6
|
+
|
|
7
|
+
export interface BadgeOptions {
|
|
8
|
+
label: string;
|
|
9
|
+
message: string;
|
|
10
|
+
color?: string;
|
|
11
|
+
style?: "plastic" | "flat" | "flat-square" | "for-the-badge" | "social";
|
|
12
|
+
labelColor?: string;
|
|
13
|
+
logo?: string;
|
|
14
|
+
logoWidth?: number;
|
|
15
|
+
alt?: string;
|
|
16
|
+
href?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface BadgeGroup {
|
|
20
|
+
title: string;
|
|
21
|
+
collapsed?: boolean;
|
|
22
|
+
items: BadgeOptions[];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** 单个 badge 或 badge 分组 */
|
|
26
|
+
export type BadgeItem = BadgeOptions | BadgeGroup;
|
|
27
|
+
|
|
28
|
+
/** 判断是否为 badge 分组 */
|
|
29
|
+
export function isBadgeGroup(item: BadgeItem): item is BadgeGroup {
|
|
30
|
+
return "title" in item && "items" in item;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* 生成徽章SVG
|
|
35
|
+
* @param options 徽章选项
|
|
36
|
+
* @returns 返回徽章SVG字符串
|
|
37
|
+
*/
|
|
38
|
+
export function generateBadge(options: BadgeOptions): string {
|
|
39
|
+
try {
|
|
40
|
+
const format = Object.fromEntries(
|
|
41
|
+
Object.entries({
|
|
42
|
+
label: options.label,
|
|
43
|
+
message: options.message,
|
|
44
|
+
color: options.color ?? "blue",
|
|
45
|
+
style: options.style ?? "flat",
|
|
46
|
+
labelColor: options.labelColor,
|
|
47
|
+
logo: options.logo,
|
|
48
|
+
logoWidth: options.logoWidth,
|
|
49
|
+
}).filter(([, v]) => v !== undefined),
|
|
50
|
+
) as Parameters<typeof makeBadge>[0];
|
|
51
|
+
|
|
52
|
+
return makeBadge(format);
|
|
53
|
+
} catch (error) {
|
|
54
|
+
console.error("Badge generation error:", error);
|
|
55
|
+
return "";
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* 将SVG字符串转为Data URL
|
|
61
|
+
* @param svg SVG字符串
|
|
62
|
+
* @returns data URL
|
|
63
|
+
*/
|
|
64
|
+
export function svgToDataUrl(svg: string): string {
|
|
65
|
+
if (!svg) return "";
|
|
66
|
+
const encoded = encodeURIComponent(svg).replaceAll("'", "%27").replaceAll('"', "%22");
|
|
67
|
+
return `data:image/svg+xml,${encoded}`;
|
|
68
|
+
}
|
package/src/utils/cc.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { Translator } from "@utils/i18n";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 从许可证代码(如 "CC-BY-NC-SA-4.0")推导 CC 官网链接。
|
|
5
|
+
*
|
|
6
|
+
* "CC-BY-NC-SA-4.0" → "https://creativecommons.org/licenses/by-nc-sa/4.0/"
|
|
7
|
+
* "CC0-1.0" → "https://creativecommons.org/publicdomain/zero/1.0/"
|
|
8
|
+
*/
|
|
9
|
+
export function buildCCLink(licenseCode: string): string {
|
|
10
|
+
if (licenseCode === "CC0-1.0") {
|
|
11
|
+
return "https://creativecommons.org/publicdomain/zero/1.0/";
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const match = licenseCode.match(/^CC-?(.+)-(\d+(?:\.\d+))$/);
|
|
15
|
+
if (!match) {
|
|
16
|
+
return "https://creativecommons.org/licenses/by-nc-sa/4.0/";
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const [, pathPart, version] = match;
|
|
20
|
+
return `https://creativecommons.org/licenses/${pathPart.toLowerCase()}/${version}/`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* 用 i18n 翻译组装许可证显示名称。
|
|
25
|
+
*
|
|
26
|
+
* "CC-BY-NC-SA-4.0" + zh-CN → "知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议"
|
|
27
|
+
* "CC-BY-NC-SA-4.0" + en → "Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License"
|
|
28
|
+
* "CC0-1.0" + zh-CN → "CC0 1.0 通用"
|
|
29
|
+
*/
|
|
30
|
+
export function buildCCName(licenseCode: string, t: Translator["t"]): string {
|
|
31
|
+
if (licenseCode === "CC0-1.0") {
|
|
32
|
+
return t("cc.zero");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const match = licenseCode.match(/^CC-?(.+)-(\d+(?:\.\d+))$/);
|
|
36
|
+
if (!match) {
|
|
37
|
+
// 无法解析时回退为默认 CC-BY-NC-SA-4.0
|
|
38
|
+
const elements = ["BY", "NC", "SA"].map((code) => t("cc." + code)).join("-");
|
|
39
|
+
return t("cc.format", { elements, version: "4.0" });
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const [, pathPart, version] = match;
|
|
43
|
+
const codes = pathPart.split("-");
|
|
44
|
+
const elements = codes.map((code) => t("cc." + code)).join("-");
|
|
45
|
+
return t("cc.format", { elements, version });
|
|
46
|
+
}
|