@xingwangzhe/stalux 1.46.2 → 1.47.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/README.md +4 -0
- package/package.json +3 -2
- package/src/components/stalux/archives/archivesList.astro +1 -1
- package/src/components/stalux/categories/categoriesList.astro +6 -1
- package/src/components/stalux/common/Weather.astro +1 -1
- package/src/components/stalux/common/date.astro +1 -1
- package/src/components/stalux/common/search.astro +2 -0
- package/src/components/stalux/footer/FooterBeian.astro +2 -0
- package/src/components/stalux/layout/head.astro +1 -1
- package/src/components/stalux/links/linkList.astro +1 -1
- package/src/components/stalux/posts/ArticleSummary.astro +8 -1
- package/src/components/stalux/posts/postContent.astro +20 -2
- package/src/components/stalux/posts/postList.astro +6 -1
- package/src/components/stalux/posts/postRight.astro +11 -4
- package/src/components/stalux/words/wordList.astro +11 -8
- package/src/index.ts +16 -4
- package/src/internal/cjk-search-index.ts +130 -0
- package/src/internal/compact-styles.ts +1 -1
- package/src/internal/font-slices.ts +3 -3
- package/src/internal/page-output-cache.ts +6 -7
- package/src/internal/search-style.ts +16 -0
- package/src/internal/social-card-renderer.ts +3 -5
- package/src/layouts/PostLayout.astro +2 -0
- package/src/pages/og/[image].[format].ts +1 -1
- package/src/pages/posts/[post].astro +1 -0
- package/src/schemas/collections.ts +1 -0
- package/src/schemas/config.ts +5 -5
- package/src/scripts/analytics-runtime.ts +1 -1
- package/src/scripts/archive-descriptions.ts +1 -1
- package/src/scripts/back-to-top.ts +7 -6
- package/src/scripts/background.ts +2 -2
- package/src/scripts/countdown.ts +1 -1
- package/src/scripts/date-time.ts +1 -1
- package/src/scripts/list-reveal.ts +39 -18
- package/src/scripts/module-motion.ts +5 -3
- package/src/scripts/navigation-font.ts +4 -1
- package/src/scripts/navigation.ts +1 -1
- package/src/scripts/page-runtime.ts +6 -4
- package/src/scripts/pagefind-api.ts +1 -1
- package/src/scripts/random-posts.ts +2 -2
- package/src/scripts/route-motion.ts +67 -56
- package/src/scripts/route-out.ts +12 -14
- package/src/scripts/runtime-counter.ts +1 -1
- package/src/scripts/search-engine-entry.ts +48 -0
- package/src/scripts/search-engine.ts +219 -0
- package/src/scripts/shell-hydration.ts +60 -0
- package/src/scripts/smooth-scroll.ts +4 -4
- package/src/scripts/swup-internals.d.ts +31 -0
- package/src/scripts/swup-router.ts +26 -11
- package/src/scripts/waline.ts +1 -1
- package/src/scripts/weather.ts +148 -82
- package/src/scripts/webmcp.ts +24 -20
- package/src/utils/ai-discovery.ts +1 -1
- package/src/utils/cc.ts +2 -2
- package/src/utils/cjk-search.ts +40 -0
- package/src/utils/collections-stats.ts +4 -2
- package/src/utils/dayjs.ts +2 -2
- package/src/utils/i18n.ts +2 -2
- package/src/utils/seo-document.ts +4 -4
- package/src/utils/taxonomy.ts +10 -12
package/src/scripts/weather.ts
CHANGED
|
@@ -8,22 +8,140 @@ export interface WeatherData {
|
|
|
8
8
|
code: number;
|
|
9
9
|
updated: number;
|
|
10
10
|
}
|
|
11
|
-
export
|
|
11
|
+
export interface WeatherState {
|
|
12
|
+
status: "loading" | "ready" | "error";
|
|
13
|
+
data?: WeatherData;
|
|
14
|
+
}
|
|
12
15
|
type Listener = (state: WeatherState) => void;
|
|
16
|
+
/** One provider request; the only shape callers may assume is `unknown`. */
|
|
17
|
+
type GeoRequest = (url: string) => Promise<unknown>;
|
|
18
|
+
|
|
19
|
+
interface ApproximateLocation {
|
|
20
|
+
city: string;
|
|
21
|
+
lat: number;
|
|
22
|
+
lon: number;
|
|
23
|
+
ip?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface WeatherReading {
|
|
27
|
+
temperature: number;
|
|
28
|
+
apparent: number;
|
|
29
|
+
code: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const invalidLocation = () => new Error("Invalid approximate location");
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Read one provider's answer into a location, or reject it.
|
|
36
|
+
*
|
|
37
|
+
* The two providers use different field names, so this is the only place that
|
|
38
|
+
* knows either wire format; everything downstream sees ApproximateLocation.
|
|
39
|
+
* Narrowing happens through `in` checks, so no value is ever asserted.
|
|
40
|
+
*/
|
|
41
|
+
function parseLocation(value: unknown): ApproximateLocation {
|
|
42
|
+
if (typeof value !== "object" || value === null) throw invalidLocation();
|
|
43
|
+
if ("error" in value && value.error) throw invalidLocation();
|
|
44
|
+
if (!("city" in value) || typeof value.city !== "string" || !value.city.trim())
|
|
45
|
+
throw invalidLocation();
|
|
46
|
+
if (!("latitude" in value) || typeof value.latitude !== "number") throw invalidLocation();
|
|
47
|
+
if (!("longitude" in value) || typeof value.longitude !== "number") throw invalidLocation();
|
|
48
|
+
const lat = value.latitude;
|
|
49
|
+
const lon = value.longitude;
|
|
50
|
+
if (
|
|
51
|
+
!(Number.isFinite(lat) && Number.isFinite(lon)) ||
|
|
52
|
+
Math.abs(lat) > 90 ||
|
|
53
|
+
Math.abs(lon) > 180
|
|
54
|
+
)
|
|
55
|
+
throw invalidLocation();
|
|
56
|
+
const usableIp =
|
|
57
|
+
"ip" in value &&
|
|
58
|
+
typeof value.ip === "string" &&
|
|
59
|
+
value.ip.length <= 45 &&
|
|
60
|
+
/^[0-9a-f:.]+$/i.test(value.ip);
|
|
61
|
+
return { city: value.city.trim(), lat, lon, ...(usableIp ? { ip: value.ip as string } : {}) };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Prefer the primary provider, fall back to the second one on any failure. */
|
|
65
|
+
async function resolveLocation(get: GeoRequest): Promise<ApproximateLocation> {
|
|
66
|
+
try {
|
|
67
|
+
return parseLocation(await get("https://api.ip.sb/geoip"));
|
|
68
|
+
} catch {
|
|
69
|
+
const fallback = await get("https://ipinfo.io/json");
|
|
70
|
+
if (typeof fallback !== "object" || fallback === null) throw invalidLocation();
|
|
71
|
+
const coordinates =
|
|
72
|
+
"loc" in fallback && typeof fallback.loc === "string" ? fallback.loc.split(",") : [];
|
|
73
|
+
const pair = coordinates.length === 2 ? coordinates : undefined;
|
|
74
|
+
return parseLocation({
|
|
75
|
+
city: "city" in fallback ? fallback.city : undefined,
|
|
76
|
+
ip: "ip" in fallback ? fallback.ip : undefined,
|
|
77
|
+
latitude: pair?.[0]?.trim() ? Number(pair[0]) : undefined,
|
|
78
|
+
longitude: pair?.[1]?.trim() ? Number(pair[1]) : undefined,
|
|
79
|
+
error: "error" in fallback ? fallback.error : undefined,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Parse an untrusted value into weather data, or reject it.
|
|
86
|
+
*
|
|
87
|
+
* `in` narrowing keeps this assertion-free: the value stays `unknown` until each
|
|
88
|
+
* field is proven, and the caller receives a domain type instead of a flag.
|
|
89
|
+
*/
|
|
90
|
+
function parseWeatherData(value: unknown): WeatherData | undefined {
|
|
91
|
+
if (typeof value !== "object" || value === null) return undefined;
|
|
92
|
+
if (!("city" in value) || typeof value.city !== "string") return undefined;
|
|
93
|
+
const city = value.city;
|
|
94
|
+
if (city.length === 0 || city.length >= 160) return undefined;
|
|
95
|
+
let ip: string | undefined;
|
|
96
|
+
if ("ip" in value && value.ip !== undefined) {
|
|
97
|
+
if (
|
|
98
|
+
typeof value.ip !== "string" ||
|
|
99
|
+
value.ip.length > 45 ||
|
|
100
|
+
!/^[0-9a-f:.]+$/i.test(value.ip)
|
|
101
|
+
)
|
|
102
|
+
return undefined;
|
|
103
|
+
ip = value.ip;
|
|
104
|
+
}
|
|
105
|
+
if (!("temperature" in value)) return undefined;
|
|
106
|
+
if (!("apparent" in value)) return undefined;
|
|
107
|
+
if (!("code" in value)) return undefined;
|
|
108
|
+
if (!("updated" in value)) return undefined;
|
|
109
|
+
const temperature = finite(value.temperature);
|
|
110
|
+
const apparent = finite(value.apparent);
|
|
111
|
+
const code = finite(value.code);
|
|
112
|
+
const updated = finite(value.updated);
|
|
113
|
+
if (
|
|
114
|
+
temperature === undefined ||
|
|
115
|
+
apparent === undefined ||
|
|
116
|
+
code === undefined ||
|
|
117
|
+
updated === undefined
|
|
118
|
+
)
|
|
119
|
+
return undefined;
|
|
120
|
+
return { city, ...(ip === undefined ? {} : { ip }), temperature, apparent, code, updated };
|
|
121
|
+
}
|
|
13
122
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
123
|
+
const finite = (value: unknown): number | undefined =>
|
|
124
|
+
typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
125
|
+
|
|
126
|
+
/** Read the three numbers the card shows, or reject the response. */
|
|
127
|
+
function parseReading(value: unknown): WeatherReading | undefined {
|
|
128
|
+
if (typeof value !== "object" || value === null) return undefined;
|
|
129
|
+
if (!("current" in value)) return undefined;
|
|
130
|
+
const current = value.current;
|
|
131
|
+
if (typeof current !== "object" || current === null) return undefined;
|
|
132
|
+
const number = (key: "temperature_2m" | "apparent_temperature" | "weather_code") => {
|
|
133
|
+
if (key === "temperature_2m" && "temperature_2m" in current)
|
|
134
|
+
return finite(current.temperature_2m);
|
|
135
|
+
if (key === "apparent_temperature" && "apparent_temperature" in current)
|
|
136
|
+
return finite(current.apparent_temperature);
|
|
137
|
+
if (key === "weather_code" && "weather_code" in current)
|
|
138
|
+
return finite(current.weather_code);
|
|
139
|
+
};
|
|
140
|
+
const temperature = number("temperature_2m");
|
|
141
|
+
const apparent = number("apparent_temperature");
|
|
142
|
+
const code = number("weather_code");
|
|
143
|
+
if (temperature === undefined || apparent === undefined || code === undefined) return undefined;
|
|
144
|
+
return { temperature, apparent, code };
|
|
27
145
|
}
|
|
28
146
|
|
|
29
147
|
export function createWeatherClient({
|
|
@@ -41,8 +159,8 @@ export function createWeatherClient({
|
|
|
41
159
|
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
42
160
|
let generation = 0;
|
|
43
161
|
try {
|
|
44
|
-
const cached
|
|
45
|
-
if (
|
|
162
|
+
const cached = parseWeatherData(JSON.parse(storage?.getItem(CACHE_KEY) ?? "null"));
|
|
163
|
+
if (cached && cached.updated <= now()) data = cached;
|
|
46
164
|
} catch {
|
|
47
165
|
/* Storage is optional. */
|
|
48
166
|
}
|
|
@@ -57,7 +175,7 @@ export function createWeatherClient({
|
|
|
57
175
|
timer = undefined;
|
|
58
176
|
};
|
|
59
177
|
const refresh = async (force = false) => {
|
|
60
|
-
if (
|
|
178
|
+
if (listeners.size === 0 || controller) return;
|
|
61
179
|
clearTimeout(timer);
|
|
62
180
|
if (!force && data && now() - data.updated < WEATHER_TTL) {
|
|
63
181
|
emit("ready");
|
|
@@ -71,9 +189,9 @@ export function createWeatherClient({
|
|
|
71
189
|
timer = timeout;
|
|
72
190
|
emit("loading");
|
|
73
191
|
try {
|
|
74
|
-
const get = async (
|
|
192
|
+
const get = async (endpoint: string): Promise<unknown> => {
|
|
75
193
|
if (request.signal.aborted) throw new Error("Request cancelled");
|
|
76
|
-
const response = await fetcher(
|
|
194
|
+
const response = await fetcher(endpoint, {
|
|
77
195
|
signal: request.signal,
|
|
78
196
|
credentials: "omit",
|
|
79
197
|
referrerPolicy: "no-referrer",
|
|
@@ -82,55 +200,7 @@ export function createWeatherClient({
|
|
|
82
200
|
throw new Error("Weather service unavailable");
|
|
83
201
|
return response.json();
|
|
84
202
|
};
|
|
85
|
-
const
|
|
86
|
-
city?: unknown;
|
|
87
|
-
latitude?: unknown;
|
|
88
|
-
longitude?: unknown;
|
|
89
|
-
ip?: unknown;
|
|
90
|
-
error?: unknown;
|
|
91
|
-
}) => {
|
|
92
|
-
const lat = location.latitude;
|
|
93
|
-
const lon = location.longitude;
|
|
94
|
-
if (
|
|
95
|
-
location.error ||
|
|
96
|
-
typeof location.city !== "string" ||
|
|
97
|
-
!location.city.trim() ||
|
|
98
|
-
typeof lat !== "number" ||
|
|
99
|
-
typeof lon !== "number" ||
|
|
100
|
-
!Number.isFinite(lat) ||
|
|
101
|
-
!Number.isFinite(lon) ||
|
|
102
|
-
Math.abs(lat) > 90 ||
|
|
103
|
-
Math.abs(lon) > 180
|
|
104
|
-
)
|
|
105
|
-
throw new Error("Invalid approximate location");
|
|
106
|
-
const ip =
|
|
107
|
-
typeof location.ip === "string" &&
|
|
108
|
-
location.ip.length <= 45 &&
|
|
109
|
-
/^[0-9a-f:.]+$/i.test(location.ip)
|
|
110
|
-
? location.ip
|
|
111
|
-
: undefined;
|
|
112
|
-
return { city: location.city.trim(), lat, lon, ip };
|
|
113
|
-
};
|
|
114
|
-
let location: ReturnType<typeof validateLocation>;
|
|
115
|
-
try {
|
|
116
|
-
location = validateLocation(await get("https://api.ip.sb/geoip"));
|
|
117
|
-
} catch {
|
|
118
|
-
const info = await get("https://ipinfo.io/json");
|
|
119
|
-
const coordinates = typeof info.loc === "string" ? info.loc.split(",") : [];
|
|
120
|
-
location = validateLocation({
|
|
121
|
-
city: info.city,
|
|
122
|
-
ip: info.ip,
|
|
123
|
-
latitude:
|
|
124
|
-
coordinates.length === 2 && coordinates[0]?.trim()
|
|
125
|
-
? Number(coordinates[0])
|
|
126
|
-
: undefined,
|
|
127
|
-
longitude:
|
|
128
|
-
coordinates.length === 2 && coordinates[1]?.trim()
|
|
129
|
-
? Number(coordinates[1])
|
|
130
|
-
: undefined,
|
|
131
|
-
error: info.error,
|
|
132
|
-
});
|
|
133
|
-
}
|
|
203
|
+
const location = await resolveLocation(get);
|
|
134
204
|
const { lat, lon } = location;
|
|
135
205
|
const url = new URL("https://api.open-meteo.com/v1/forecast");
|
|
136
206
|
url.search = new URLSearchParams({
|
|
@@ -140,19 +210,15 @@ export function createWeatherClient({
|
|
|
140
210
|
timezone: "auto",
|
|
141
211
|
forecast_days: "1",
|
|
142
212
|
}).toString();
|
|
143
|
-
const
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
code: current?.weather_code,
|
|
213
|
+
const reading = parseReading(await get(url.href));
|
|
214
|
+
if (!reading) throw new Error("Invalid weather response");
|
|
215
|
+
if (token !== generation || request.signal.aborted) return;
|
|
216
|
+
data = {
|
|
217
|
+
city: location.city,
|
|
218
|
+
...(location.ip === undefined ? {} : { ip: location.ip }),
|
|
219
|
+
...reading,
|
|
151
220
|
updated: now(),
|
|
152
221
|
};
|
|
153
|
-
if (!validData(next)) throw new Error("Invalid weather response");
|
|
154
|
-
if (token !== generation || request.signal.aborted) return;
|
|
155
|
-
data = next;
|
|
156
222
|
try {
|
|
157
223
|
storage?.setItem(CACHE_KEY, JSON.stringify(data));
|
|
158
224
|
} catch {
|
|
@@ -173,7 +239,7 @@ export function createWeatherClient({
|
|
|
173
239
|
void refresh();
|
|
174
240
|
return () => {
|
|
175
241
|
listeners.delete(listener);
|
|
176
|
-
if (
|
|
242
|
+
if (listeners.size === 0) stop();
|
|
177
243
|
};
|
|
178
244
|
},
|
|
179
245
|
refresh: () => void refresh(true),
|
|
@@ -222,7 +288,7 @@ export function weatherDescription(code: number, lang: string): string {
|
|
|
222
288
|
let client: ReturnType<typeof createWeatherClient> | undefined;
|
|
223
289
|
export function mountWeather() {
|
|
224
290
|
const elements = [...document.querySelectorAll<HTMLElement>("[data-stalux-weather]")];
|
|
225
|
-
if (
|
|
291
|
+
if (elements.length === 0) return;
|
|
226
292
|
if (!client) {
|
|
227
293
|
let storage: Storage | undefined;
|
|
228
294
|
try {
|
|
@@ -238,7 +304,7 @@ export function mountWeather() {
|
|
|
238
304
|
const locationOutput = element.querySelector<HTMLElement>("[data-weather-location]");
|
|
239
305
|
const status = element.querySelector<HTMLElement>("[data-weather-status]");
|
|
240
306
|
const retry = element.querySelector<HTMLButtonElement>("[data-weather-retry]");
|
|
241
|
-
if (!output
|
|
307
|
+
if (!(output && status && retry)) return () => undefined;
|
|
242
308
|
const zh = (element.dataset.lang ?? "zh-CN").startsWith("zh");
|
|
243
309
|
let visible = !globalThis.IntersectionObserver;
|
|
244
310
|
let unsubscribe: (() => void) | undefined;
|
package/src/scripts/webmcp.ts
CHANGED
|
@@ -65,9 +65,9 @@ type DocumentWithModelContext = { modelContext?: ModelContext };
|
|
|
65
65
|
// ---------------------------------------------------------------------------
|
|
66
66
|
|
|
67
67
|
const SITE_LANG =
|
|
68
|
-
(typeof document
|
|
69
|
-
?
|
|
70
|
-
:
|
|
68
|
+
(typeof document === "undefined"
|
|
69
|
+
? "zh-CN"
|
|
70
|
+
: document.body?.dataset.staluxSiteLang
|
|
71
71
|
)?.toLowerCase() ?? "zh-cn";
|
|
72
72
|
|
|
73
73
|
/** 站点为英文站时为 true */
|
|
@@ -141,22 +141,26 @@ function loadPostsMeta(): Promise<PostMeta[] | null> {
|
|
|
141
141
|
const attempt = fetchJSON(sitePath("api/posts.json"))
|
|
142
142
|
.then((data) => {
|
|
143
143
|
if (
|
|
144
|
-
!
|
|
145
|
-
|
|
146
|
-
(
|
|
147
|
-
post
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
(
|
|
154
|
-
post.categories
|
|
155
|
-
(
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
(
|
|
159
|
-
post.tags
|
|
144
|
+
!(
|
|
145
|
+
Array.isArray(data) &&
|
|
146
|
+
data.every(
|
|
147
|
+
(post) =>
|
|
148
|
+
post &&
|
|
149
|
+
typeof post.title === "string" &&
|
|
150
|
+
typeof post.abbrlink === "string" &&
|
|
151
|
+
typeof post.url === "string" &&
|
|
152
|
+
(post.desc === undefined || typeof post.desc === "string") &&
|
|
153
|
+
(!post.categories ||
|
|
154
|
+
(Array.isArray(post.categories) &&
|
|
155
|
+
post.categories.every(
|
|
156
|
+
(category: unknown) => typeof category === "string",
|
|
157
|
+
))) &&
|
|
158
|
+
(!post.tags ||
|
|
159
|
+
(Array.isArray(post.tags) &&
|
|
160
|
+
post.tags.every(
|
|
161
|
+
(tag: unknown) => typeof tag === "string",
|
|
162
|
+
))),
|
|
163
|
+
)
|
|
160
164
|
)
|
|
161
165
|
)
|
|
162
166
|
return null;
|
|
@@ -494,7 +498,7 @@ function searchPostsTool(): WebMCPTool {
|
|
|
494
498
|
if (!pagefind) return fallback("INDEX_UNAVAILABLE");
|
|
495
499
|
try {
|
|
496
500
|
const result = await pagefind.search(keyword);
|
|
497
|
-
if (!result
|
|
501
|
+
if (!(result && Array.isArray(result.results))) return fallback("SEARCH_FAILED");
|
|
498
502
|
const posts: Record<string, unknown>[] = [...metadataMatches];
|
|
499
503
|
const seen = new Set(metadataMatches.map((post) => post.abbrlink));
|
|
500
504
|
for (
|
|
@@ -413,7 +413,7 @@ export async function renderLlmsTxt(
|
|
|
413
413
|
for (const { name, url } of mediaLinks) {
|
|
414
414
|
lines.push(`- ${t("ai.socialMedia")} (${escapeMd(name)}): ${url}`);
|
|
415
415
|
}
|
|
416
|
-
if (!authorName
|
|
416
|
+
if (!(authorName || email) && mediaLinks.length === 0) {
|
|
417
417
|
lines.push(`- ${t("ai.noContact")}`);
|
|
418
418
|
}
|
|
419
419
|
lines.push("");
|
package/src/utils/cc.ts
CHANGED
|
@@ -18,7 +18,7 @@ export function buildCCLink(licenseCode: string): string {
|
|
|
18
18
|
|
|
19
19
|
const pathPart = match[1];
|
|
20
20
|
const version = match[2];
|
|
21
|
-
if (!pathPart
|
|
21
|
+
if (!(pathPart && version)) {
|
|
22
22
|
return "https://creativecommons.org/licenses/by-nc-sa/4.0/";
|
|
23
23
|
}
|
|
24
24
|
return `https://creativecommons.org/licenses/${pathPart.toLowerCase()}/${version}/`;
|
|
@@ -45,7 +45,7 @@ export function buildCCName(licenseCode: string, t: Translator["t"]): string {
|
|
|
45
45
|
|
|
46
46
|
const pathPart = match[1];
|
|
47
47
|
const version = match[2];
|
|
48
|
-
if (!pathPart
|
|
48
|
+
if (!(pathPart && version)) {
|
|
49
49
|
const elements = ["BY", "NC", "SA"].map((code) => t(`cc.${code}`)).join("-");
|
|
50
50
|
return t("cc.format", { elements, version: "4.0" });
|
|
51
51
|
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/** Shared by the indexer and browser: keep Unicode normalization identical. */
|
|
2
|
+
export const CJK = /[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Hangul}]/u;
|
|
3
|
+
|
|
4
|
+
export function normalizeSearch(text: string): string {
|
|
5
|
+
return text.normalize("NFKC").toLowerCase().replace(/\s+/gu, " ").trim();
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function cjkGrams(text: string): string[] {
|
|
9
|
+
const grams = new Set<string>();
|
|
10
|
+
let previous = "";
|
|
11
|
+
for (const char of normalizeSearch(text)) {
|
|
12
|
+
if (CJK.test(char)) {
|
|
13
|
+
grams.add(char);
|
|
14
|
+
if (previous) grams.add(previous + char);
|
|
15
|
+
previous = char;
|
|
16
|
+
} else previous = "";
|
|
17
|
+
}
|
|
18
|
+
return [...grams];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function gramShard(gram: string): string {
|
|
22
|
+
let hash = 0;
|
|
23
|
+
for (const char of gram) hash = (hash * 31 + (char.codePointAt(0) ?? 0)) >>> 0;
|
|
24
|
+
return (hash % 32).toString(16);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface CjkDocument {
|
|
28
|
+
url: string;
|
|
29
|
+
title: string;
|
|
30
|
+
description: string;
|
|
31
|
+
keywords: string;
|
|
32
|
+
language: string;
|
|
33
|
+
filters: Record<string, string[]>;
|
|
34
|
+
file: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface CjkManifest {
|
|
38
|
+
documents: CjkDocument[];
|
|
39
|
+
shards: Record<string, string>;
|
|
40
|
+
}
|
|
@@ -6,8 +6,10 @@ function countUncategorized(posts: PostEntry[]): number {
|
|
|
6
6
|
let count = 0;
|
|
7
7
|
for (const post of posts) {
|
|
8
8
|
const names = post.data.categories ?? [];
|
|
9
|
-
if (
|
|
10
|
-
|
|
9
|
+
if (names.length)
|
|
10
|
+
for (const name of names)
|
|
11
|
+
if (name === "uncategorized") count++;
|
|
12
|
+
else count++;
|
|
11
13
|
}
|
|
12
14
|
return count;
|
|
13
15
|
}
|
package/src/utils/dayjs.ts
CHANGED
|
@@ -138,7 +138,7 @@ export function format(date: Date | string | number, formatStr: string): string
|
|
|
138
138
|
export function formatInTimeZone(
|
|
139
139
|
date: Date | string | number,
|
|
140
140
|
timeZone: string,
|
|
141
|
-
formatStr
|
|
141
|
+
formatStr = "yyyy-MM-dd'T'HH:mm:ssXXX",
|
|
142
142
|
): string {
|
|
143
143
|
const d = date instanceof Date ? date : new Date(date);
|
|
144
144
|
if (Number.isNaN(d.getTime())) return "";
|
|
@@ -160,7 +160,7 @@ export function formatInTimeZone(
|
|
|
160
160
|
offset = "+00:00";
|
|
161
161
|
} else {
|
|
162
162
|
offset = offset.replace(/^GMT/, "").replace(/^UTC/, "");
|
|
163
|
-
if (!offset.startsWith("+")
|
|
163
|
+
if (!(offset.startsWith("+") || offset.startsWith("-"))) {
|
|
164
164
|
offset = `+${offset}`;
|
|
165
165
|
}
|
|
166
166
|
const m = offset.match(/^([+-])(\d{1,2})(?::?(\d{2}))?$/);
|
package/src/utils/i18n.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import en from "@i18n/en.json";
|
|
2
|
-
import zhCN from "@i18n/zh-CN.json";
|
|
1
|
+
import en from "@i18n/en.json" with { type: "json" };
|
|
2
|
+
import zhCN from "@i18n/zh-CN.json" with { type: "json" };
|
|
3
3
|
|
|
4
4
|
const dicts: Record<string, Record<string, unknown>> = {
|
|
5
5
|
"zh-CN": zhCN as Record<string, unknown>,
|
|
@@ -145,14 +145,14 @@ export function buildSeoDocument(input: SeoDocumentInput): SeoDocument {
|
|
|
145
145
|
{ name: "generator", content: "Stalux" },
|
|
146
146
|
{ name: "theme", content: "Stalux" },
|
|
147
147
|
{ name: "stalux-version", content: version },
|
|
148
|
-
...(
|
|
149
|
-
? [
|
|
148
|
+
...(noindex
|
|
149
|
+
? []
|
|
150
|
+
: [
|
|
150
151
|
{
|
|
151
152
|
name: "googlebot",
|
|
152
153
|
content: "max-image-preview:large, max-snippet:-1, max-video-preview:-1",
|
|
153
154
|
},
|
|
154
|
-
]
|
|
155
|
-
: []),
|
|
155
|
+
]),
|
|
156
156
|
];
|
|
157
157
|
|
|
158
158
|
const sameAs = input.mediaLinks.filter((link) => /^https?:\/\//i.test(link));
|
package/src/utils/taxonomy.ts
CHANGED
|
@@ -15,16 +15,14 @@ export async function buildTaxonomyStaticPaths(key: TaxonomyKey): Promise<GetSta
|
|
|
15
15
|
const paramName = key === "tags" ? "tag" : "category";
|
|
16
16
|
const taxonomy = index[key];
|
|
17
17
|
|
|
18
|
-
return [...taxonomy].map(([name, entry]) => {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
};
|
|
29
|
-
});
|
|
18
|
+
return [...taxonomy].map(([name, entry]) => ({
|
|
19
|
+
params: { [paramName]: name },
|
|
20
|
+
cacheKey: `${entry.cacheKey}|${getRuntimeCacheKey()}`,
|
|
21
|
+
props: {
|
|
22
|
+
[paramName]: name,
|
|
23
|
+
count: entry.posts.length,
|
|
24
|
+
posts: entry.posts,
|
|
25
|
+
lastUpdateDate: entry.latestTime,
|
|
26
|
+
},
|
|
27
|
+
}));
|
|
30
28
|
}
|