Repository navigation
Expand file tree
/
Copy pathsubpage-nav-html.js
More file actions
196 lines (180 loc) · 6.54 KB
/
Copy pathsubpage-nav-html.js
File metadata and controls
196 lines (180 loc) · 6.54 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
/**
* Shared subpage navigation HTML for encyclopedia, free-tier, etc.
* Data source: nav-links.json (single source of truth with plan-nav.js).
*/
const fs = require("fs");
const path = require("path");
const NAV_LINKS_PATH = path.join(__dirname, "nav-links.json");
function loadNavData() {
const raw = JSON.parse(fs.readFileSync(NAV_LINKS_PATH, "utf8"));
return {
links: Array.isArray(raw.links) ? raw.links : [],
sitemap: Array.isArray(raw.sitemap) ? raw.sitemap : [],
};
}
function getSubpageNavLinks() {
return loadNavData().links.filter((item) => {
if (item.type === "sep") return (item.nav || ["sub"]).includes("sub");
return (item.nav || ["sub", "plan"]).includes("sub");
});
}
function getPlanNavLinks() {
return loadNavData()
.links.filter((item) => item.type !== "sep" && (item.nav || ["sub", "plan"]).includes("plan"))
.map((item) => ({
href: item.href,
zh: item.zh,
en: item.en,
match: item.match || [item.href],
planGroup: item.planGroup || "plans",
}));
}
function getHomeTopPlanLinks() {
return loadNavData().links.filter(
(item) => item.type !== "sep" && (item.nav || []).includes("home") && item.homeGroup
);
}
function isActive(link, activePage) {
if (!activePage || !link.match) return false;
const page = activePage.replace(/^\//, "");
return link.match.some((m) => page === m || page.endsWith("/" + m) || page.endsWith(m));
}
function buildSubpageNav(activePage, lang) {
const useEn = lang === "en";
return getSubpageNavLinks()
.map((item) => {
if (item.type === "sep") return '<span class="subnav-sep">·</span>';
const label = useEn ? item.en : item.zh;
const active = isActive(item, activePage);
const style = active ? ' style="color:var(--accent2);font-weight:700"' : "";
return `<a href="${item.href}"${style}>${label}</a>`;
})
.join("\n ");
}
function buildHomeTopPlansHtml(esc) {
const escape =
typeof esc === "function"
? esc
: (s) =>
String(s == null ? "" : s)
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """);
const items = getHomeTopPlanLinks();
const groupOrder = [
{ key: "tools", highlight: true },
{ key: "models", highlight: false },
{ key: "plans", highlight: false },
{ key: "landing", highlight: false },
{ key: "method", highlight: false },
{ key: "highlight", highlight: true },
{ key: "compare", highlight: false },
];
const byGroup = {};
for (const g of groupOrder) byGroup[g.key] = [];
for (const item of items) {
const g = item.homeGroup || "plans";
if (!byGroup[g]) byGroup[g] = [];
byGroup[g].push(item);
}
function renderLink(item, opts) {
const zh = item.homeZh || item.zh;
const title = item.homeTitle || zh;
const highlight = opts && opts.highlight;
const i18n = item.homeI18n
? ` data-i18n="${escape(item.homeI18n)}" data-i18n-title="${escape(item.homeI18n)}Title"`
: "";
const style = highlight ? ' style="color:var(--accent2);font-weight:600"' : "";
return `<a href="${escape(item.href)}" title="${escape(title)}"${i18n}${style}>${escape(zh)}</a>`;
}
const parts = [];
const seen = new Set();
for (const g of groupOrder) {
if (seen.has(g.key)) continue;
seen.add(g.key);
const list = byGroup[g.key] || [];
if (!list.length) continue;
if (parts.length) parts.push('<span class="top-plans-sep" aria-hidden="true"></span>');
for (const item of list) parts.push(renderLink(item, { highlight: g.highlight }));
}
return parts.join("\n ");
}
function buildSitemapXml(extraUrls) {
const { sitemap } = loadNavData();
const extras = Array.isArray(extraUrls) ? extraUrls : [];
const seen = new Set();
const merged = [];
for (const u of [...sitemap, ...extras]) {
if (!u || !u.loc || seen.has(u.loc)) continue;
seen.add(u.loc);
merged.push(u);
}
const body = merged
.map(
(u) =>
` <url>\n <loc>${u.loc}</loc>\n <changefreq>${u.changefreq || "monthly"}</changefreq>\n <priority>${u.priority || "0.5"}</priority>\n </url>`
)
.join("\n");
return `<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n${body}\n</urlset>\n`;
}
/** 页脚合规链接(首页 / 子页共用文案) */
function buildLegalFooterLinks(opts) {
const o = opts || {};
const prefix = o.prefix != null ? String(o.prefix) : "";
const sep = o.sep != null ? o.sep : " · ";
const links = [
["about.html", "关于"],
["contact.html", "联系"],
["tools/index.html", "工具详情"],
["privacy.html", "隐私政策"],
["disclaimer.html", "免责声明"],
];
return links.map(([href, label]) => `<a href="${prefix}${href}">${label}</a>`).join(sep);
}
/** Rewrite plan-nav.js LINKS from nav-links.json so file:// pages stay self-contained. */
function syncPlanNavJs() {
const planNavPath = path.join(__dirname, "plan-nav.js");
let src = fs.readFileSync(planNavPath, "utf8");
const links = getPlanNavLinks();
const body = links
.map((item) => {
const match = JSON.stringify(item.match);
const group = item.planGroup ? `, group: ${JSON.stringify(item.planGroup)}` : "";
return ` { href: ${JSON.stringify(item.href)}, zh: ${JSON.stringify(item.zh)}, en: ${JSON.stringify(item.en)}, match: ${match}${group} }`;
})
.join(",\n");
const next = src.replace(/var LINKS = \[[\s\S]*?\];/, `var LINKS = [\n${body}\n ];`);
if (!/var LINKS = \[/.test(src)) {
console.warn("syncPlanNavJs: LINKS block not found in plan-nav.js");
return false;
}
if (next !== src) fs.writeFileSync(planNavPath, next, "utf8");
console.log("synced plan-nav.js from nav-links.json (", links.length, "links)");
return true;
}
module.exports = {
SUBPAGE_NAV_LINKS: null, // lazy; prefer getters
SITEMAP_URLS: null,
loadNavData,
getSubpageNavLinks,
getPlanNavLinks,
getHomeTopPlanLinks,
buildSubpageNav,
buildHomeTopPlansHtml,
buildSitemapXml,
buildLegalFooterLinks,
syncPlanNavJs,
isActive,
};
// Keep backward-compatible enumerable snapshots
Object.defineProperty(module.exports, "SUBPAGE_NAV_LINKS", {
get: getSubpageNavLinks,
enumerable: true,
});
Object.defineProperty(module.exports, "SITEMAP_URLS", {
get() {
return loadNavData().sitemap;
},
enumerable: true,
});