Skip to content

Commit 404e804

Browse files
committed
fix: site base from Astro config and horizontal Bopomofo in UX
Read href/img base from astro.config for snapshots; rebase snapshot HTML to import.meta.env.BASE_URL at serve time. Map vertical 丨 to ㄧ in legacyPlainText (volume ranges, rails). Remove volume hub split notice; add site-base tests.
1 parent 30e6853 commit 404e804

9 files changed

Lines changed: 81 additions & 16 deletions

File tree

lib/site/legacy-text.ts

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { stripPe2Tags, wrapKaiFont } from "../dic/pe2-text.ts";
22
import { jadeUnescapeLine, loadFontMaps } from "../dic/unescape.ts";
3+
import { normalizeSiteBase } from "./site-base.ts";
34

45
const fontMaps = loadFontMaps(process.cwd());
56
const ASTRAL_PUA = /[\u{f0000}-\u{fffff}]/gu;
@@ -10,12 +11,17 @@ function stripKaiTagFragments(text: string): string {
1011
return text.replace(RAW_K_TAG, "").replace(STRAY_K_BEFORE_CJK, "$1");
1112
}
1213

13-
/** Site-root absolute paths for k/m3 glyph PNGs (section pages are nested under /NN/M/). */
14-
export function absolutizeDictionaryImgSrc(html: string, base = "/koktai/"): string {
15-
const b = base.endsWith("/") ? base : `${base}/`;
14+
/** Site-root paths for k/m3 glyph PNGs (nested section pages need a base prefix). */
15+
export function absolutizeDictionaryImgSrc(html: string, base: string): string {
16+
const b = normalizeSiteBase(base);
1617
return html.replace(/src=(["'])img\/(k|m3)\//g, `src=$1${b}img/$2/`);
1718
}
1819

20+
/** UX: show horizontal Bopomofo (ㄧ), not vertical stroke forms (丨) from legacy data. */
21+
export function horizontalBopomofo(text: string): string {
22+
return text.replaceAll("丨", "ㄧ").replaceAll("ㆳ", "ㆪ");
23+
}
24+
1925

2026
function puaCode(ch: string): string {
2127
return (ch.codePointAt(0)! - 0xf0000).toString(16).padStart(4, "0");
@@ -35,10 +41,11 @@ export function legacyPlainText(text: string): string {
3541
out = out.replace(ASTRAL_PUA, plainPua);
3642
}
3743
ASTRAL_PUA.lastIndex = 0;
38-
return stripKaiTagFragments(out.replace(ASTRAL_PUA, "□"));
44+
out = stripKaiTagFragments(out.replace(ASTRAL_PUA, "□"));
45+
return horizontalBopomofo(out);
3946
}
4047

41-
export function renderLegacyText(text: string, assetBase = "/koktai/"): string {
48+
export function renderLegacyText(text: string, assetBase: string): string {
4249
const raw = stripKaiTagFragments(jadeUnescapeLine(stripPe2Tags(wrapKaiFont(text)), fontMaps, true));
4350
return absolutizeDictionaryImgSrc(raw, assetBase);
4451
}

lib/site/search-data.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { legacyPlainText, renderLegacyText } from "./legacy-text.ts";
22
import type { Reading, SinogramEntry, Token, WordRecord } from "../extract/types.ts";
33
import { VOLUME_IDS } from "../dic/pipeline.ts";
44
import type { Corpus } from "./corpus.ts";
5+
import { readAstroBaseFromConfig } from "./site-base.ts";
56

67

78
export type SuggestRow = [t: string, z: string, v: string, l: number, k: 0 | 1, s: number, h?: string];
@@ -33,7 +34,7 @@ export function stripHeadwordMarkup(t: string): string {
3334
function headwordFields(raw: string): { text: string; html?: string } {
3435
const stripped = stripHeadwordMarkup(raw);
3536
const text = legacyPlainText(stripped);
36-
const html = renderLegacyText(stripped);
37+
const html = renderLegacyText(stripped, readAstroBaseFromConfig(process.cwd()));
3738
return html.includes("<img") ? { text, html } : { text };
3839
}
3940

lib/site/site-base.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { readFileSync } from "node:fs";
2+
import { join } from "node:path";
3+
4+
/** Trailing slash, for concatenating `base + "01/3/index.html"`. */
5+
export function normalizeSiteBase(base: string): string {
6+
if (!base || base === "/") return "/";
7+
return base.endsWith("/") ? base : `${base}/`;
8+
}
9+
10+
/** Read `base` from `astro.config.mjs` (build-time / snapshot scripts). */
11+
export function readAstroBaseFromConfig(root: string): string {
12+
const raw = readFileSync(join(root, "astro.config.mjs"), "utf8");
13+
const m = raw.match(/base:\s*["']([^"']*)["']/);
14+
const configured = m?.[1] ?? "/";
15+
return normalizeSiteBase(configured === "" ? "/" : configured.startsWith("/") ? configured : `/${configured}`);
16+
}
17+
18+
/**
19+
* Rebase dictionary snapshot markup to the active site prefix (Astro `import.meta.env.BASE_URL`).
20+
* Snapshots may have been generated with a different configured base.
21+
*/
22+
export function rebaseDictionarySnapshotHtml(html: string, targetBase: string): string {
23+
const b = normalizeSiteBase(targetBase);
24+
let out = html;
25+
out = out.replace(/href="\/koktai\//g, `href="${b}`);
26+
out = out.replace(/src="\/koktai\/img\//g, `src="${b}img/`);
27+
if (b !== "/") {
28+
out = out.replace(/href="\/(0[1-9]|1[0-9]|2[0-6])\//g, `href="${b}$1/`);
29+
out = out.replace(/href="\/(0[1-9]|1[0-9]|2[0-6])\/index\.html/g, `href="${b}$1/index.html`);
30+
}
31+
return out;
32+
}

lib/site/volume-rail.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { getVolumeData } from "../compile-pug.ts";
2+
import { horizontalBopomofo } from "./legacy-text.ts";
23
import type { SectionInfo } from "../compile-pug.ts";
34
import { getStructuredVolume } from "./structured-volume.ts";
45

@@ -11,7 +12,7 @@ export interface RailSection {
1112
sinogramCount: number;
1213
}
1314

14-
const horizontalI = (s: string) => s.replaceAll("丨", "ㄧ");
15+
const horizontalI = horizontalBopomofo;
1516

1617
export function buildRailSections(root: string, base: string): RailSection[] {
1718
const { sections: pugSections } = getVolumeData(root, base);

scripts/write-section-snapshots.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,13 @@ import {
1414
buildSectionEntryIndex,
1515
volumeSectionPath,
1616
} from "../lib/site/volume-paths.ts";
17+
import { readAstroBaseFromConfig } from "../lib/site/site-base.ts";
1718
import { renderStructuredSection } from "../lib/site/structured-render.ts";
1819

1920
const root = process.cwd();
2021
const outRoot = join(root, "public", "sections");
2122
const corpus = getCorpus(root);
22-
const hrefBase = "/koktai/";
23+
const hrefBase = readAstroBaseFromConfig(root);
2324

2425
mkdirSync(outRoot, { recursive: true });
2526

src/pages/[volume]/[section]/index.astro

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { buildRailSections } from "../../../../lib/site/volume-rail.ts";
88
import { legacyPlainText } from "../../../../lib/site/structured-render.ts";
99
import { volumeHubPath, volumeSectionPath } from "../../../../lib/site/volume-paths.ts";
1010
import { VOLUME_IDS } from "../../../../lib/dic/pipeline.ts";
11+
import { rebaseDictionarySnapshotHtml } from "../../../../lib/site/site-base.ts";
1112
import { volumeNav, volumeRangeText } from "../../../../lib/site/volume-astro-shared.ts";
1213
1314
export function getStaticPaths() {
@@ -36,11 +37,11 @@ const snapshotPath = join(root, "public", "sections", volumeSectionPath(volume,
3637
if (!existsSync(snapshotPath)) {
3738
throw new Error(`missing section snapshot ${snapshotPath} — run bun run section:snapshots`);
3839
}
39-
const sectionHtml = readFileSync(snapshotPath, "utf8");
4040
const baseUrl = import.meta.env.BASE_URL.endsWith("/")
4141
? import.meta.env.BASE_URL
4242
: `${import.meta.env.BASE_URL}/`;
4343
const href = (path = "") => `${baseUrl}${path}`;
44+
const sectionHtml = rebaseDictionarySnapshotHtml(readFileSync(snapshotPath, "utf8"), baseUrl);
4445
const { prev, next, volName } = volumeNav(volNo, href);
4546
const sylText = legacyPlainText(rail.syllable);
4647
const title = `${volName} ${sylText}・國臺對照活用辭典`;

src/pages/[volume]/index.astro

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,9 +57,6 @@ const description = `國臺對照活用辭典${volName}(${rangeText}),共
5757
<p class="vol-meta">
5858
{railSections.length} 音節・{structured.entryCount.toLocaleString("en-US")} 條目
5959
</p>
60-
<p class="vol-hub-note">
61-
本卷依音節拆頁以加快載入;請由左側索引或下方連結進入各音節。
62-
</p>
6360
<nav class="vol-nav" aria-label="卷次">
6461
{prev && <a href={href(volumeHubPath(prev))}>◀ 卷{chineseNumeral(volNo - 1)}</a>}
6562
<a href={href("#volumes")}>卷目</a>

tests/site/site-base.test.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { describe, expect, test } from "bun:test";
2+
import { rebaseDictionarySnapshotHtml } from "../../lib/site/site-base.ts";
3+
4+
describe("rebaseDictionarySnapshotHtml", () => {
5+
test("rewrites /koktai/ links to configured base", () => {
6+
const html =
7+
'<a href="/koktai/21/24/index.html#c-1"><img src="/koktai/img/k/faca.png">';
8+
expect(rebaseDictionarySnapshotHtml(html, "/")).toBe(
9+
'<a href="/21/24/index.html#c-1"><img src="/img/k/faca.png">',
10+
);
11+
});
12+
13+
test("leaves already-correct base unchanged", () => {
14+
const html = '<a href="/koktai/01/1/index.html#w-1">';
15+
expect(rebaseDictionarySnapshotHtml(html, "/koktai/")).toBe(html);
16+
});
17+
});
18+
19+
import { legacyPlainText } from "../../lib/site/legacy-text.ts";
20+
21+
describe("horizontalBopomofo via legacyPlainText", () => {
22+
test("maps vertical 丨 to ㄧ in plain syllable labels", () => {
23+
expect(legacyPlainText("丨~丨ㄥˋ")).toBe("ㄧ~ㄧㄥˋ");
24+
});
25+
});

tests/site/structured-render.test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ describe("structured dictionary render", () => {
126126
test("legacy text consumes k tags and emits horizontal zhuyin ruby", () => {
127127
const kai = String.fromCodePoint(0xf8d44);
128128
const horizontalI = String.fromCodePoint(0xf8265);
129-
const html = renderLegacyText(`祀<k>${kai}</k>人${horizontalI}`);
129+
const html = renderLegacyText(`祀<k>${kai}</k>人${horizontalI}`, "/koktai/");
130130

131131
expect(html).not.toContain("&lt;k&gt;");
132132
expect(html).not.toContain("&lt;/k&gt;");
@@ -137,7 +137,7 @@ describe("structured dictionary render", () => {
137137

138138
test("legacy text renders high k PUA fallback as bitmap image", () => {
139139
const highK = String.fromCodePoint(0xffa44);
140-
const html = renderLegacyText(`<k>${highK}</k>`);
140+
const html = renderLegacyText(`<k>${highK}</k>`, "/koktai/");
141141

142142
expect(html).toContain('src="/koktai/img/k/fa44.png"');
143143
expect(html).not.toContain("&#xffa44;");
@@ -186,7 +186,7 @@ describe("structured dictionary render", () => {
186186

187187
test("legacy text strips raw PE2 control tags from prose", () => {
188188
const raw = "~fk;;~fm3;;食~fk;ㄆ~fm3;;~fk;;~fm3;;緊(等)煮較爛咧則(/即)即付汝~bt180;·~bt0;;食。枵鬼氐氐。";
189-
const html = renderLegacyText(raw);
189+
const html = renderLegacyText(raw, "/koktai/");
190190

191191
expect(html).toContain("食");
192192
expect(html).toContain("枵鬼氐氐");
@@ -232,7 +232,7 @@ describe("structured dictionary render", () => {
232232
});
233233

234234
test("standalone zhuyin fragments get an explicit class", () => {
235-
const html = renderLegacyText("<rt>ㄧ</rt>");
235+
const html = renderLegacyText("<rt>ㄧ</rt>", "/koktai/");
236236

237237
expect(html).toBe('<ruby class="zhuyin zhuyin-standalone"><rt>ㄧ</rt></ruby>');
238238
});

0 commit comments

Comments
 (0)