Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Build output and caches
dist/
.astro/
coverage/

# Tooling artifacts
playwright-report/
test-results/

# Generated files (regenerated by scripts — don't fight their formatting)
src/data/maps-catalogue.json
package-lock.json
public/maps/

# Legacy/reference content predating the Astro migration
_legacy-content/
docs/
posts/
map/
TODO.complete/

# prettier-plugin-astro breaks text↔tag line boundaries inside prose,
# and Astro trims those breaks (rendered spaces vanish — see the
# whitespace-collapse guard in test/site.test.ts). paragraphs with
# text↔tag boundaries (e.g. compare.astro's hero, the map page's
# preview deck) stay hand-wrapped — prettier-ignore corrupts files.
src/pages/compare.astro
src/pages/maps/\[systemCode\].astro
5 changes: 5 additions & 0 deletions .prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"semi": false,
"printWidth": 100,
"plugins": ["prettier-plugin-astro"]
}
47 changes: 47 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## What this is

interscript.org v2 — the Interscript website, rebuilt on Astro 7 (branch `astro-migration`). It replaces the legacy react-static site that still lives on the `main` branch — consult `main` (and `TODO.complete/`) as the reference for feature parity. Content claims must be factual: do not fabricate partner/authority/endorsement statements.

## Commands

```bash
npm run dev # dev server (localhost:4321; bumps port if taken — e2e uses 4322)
npm run build # static build to dist/
npm run preview # serve the build

npm run check # astro check (TypeScript across .astro/.ts/.vue)
npm test # vitest run (unit tests in test/)
npx vitest run test/site.test.ts # single test file
npm run test:e2e # Playwright (e2e/); local runs need `npm run dev` on 4322
npm run lint # eslint
npm run format # prettier --write .
npm run generate:catalogue # regenerate src/data/maps-catalogue.json from .isc sources
```

Dependency note: `interscript-ts` is a `file:` dependency on the sibling checkout `../interscript-ts`, and `npm run generate:catalogue` reads `../maps/maps/*.isc` — both sibling repos must exist locally.

## Architecture

- **Rendering**: Astro `output: "static"` + `@astrojs/node` adapter. Pages under `src/pages/` are prerendered by default; the real-time routes (`src/pages/api/{detect,systems,transliterate,transliterate/batch}.ts`) opt out with `export const prerender = false` and run `interscript-ts` server-side.
- **Interactive islands**: Vue 3 (`@astrojs/vue`) for tools — `MapExplorer`, `CompareMode`, `BatchProcessor`, `DiffViewer`, `DetectPanel`, `MarcTool`, `SubtitlesProcessor`, etc. Static pages are plain `.astro` importing these islands.
- **Map data**: ~289 compact `.isc` maps in `public/maps/`. The browsable catalogue `src/data/maps-catalogue.json` is generated by `scripts/generate-catalogue.mjs` (commit the regenerated file when maps change).
- **Map loading strategies** (why two modules): browser runtimes use `src/scripts/map-strategies.ts` — ISC files first, compiled-JSON HTTP fallback only for `.iml` libraries (posix, unicode, var-Cyrl, var-kor) which have no ISC form. The SSR API routes use `src/lib/server-map-strategies.ts` — same order but ISC loads from the filesystem, no HTTP roundtrip. Keep both in sync.
- **Transliteration worker**: `src/scripts/transliteration-worker.ts` runs interscript-ts off the main thread; `worker-client.ts` is the typed RPC client. Vite bundles the worker via `new Worker(new URL(...), { type: "module" })`.
- **Design system**: single source of truth in `src/styles/global.css` — Tailwind 4 CSS-first `@theme` tokens (colors, fonts, type scale, spacing) plus component classes (`.btn`, `.card`, `.prose`, `.eyebrow`). All pages consume these tokens; don't hardcode hex values. `src/layouts/Base.astro` owns the header nav / drawer / footer and carries its scoped styles inline.
- **Content**: AsciiDoc (`src/content/docs/`, `src/content/blog/`) loaded by `src/content/loaders/asciidoc.ts` (manual frontmatter parse + @asciidoctor/core), rendered into `.prose` wrappers.
- **Theme**: light/dark via `data-theme` on `<html>`; tokens flip in `src/styles/global.css`. A service worker (`public/sw.js`) provides offline access.

## Testing layout

- `test/*.test.ts` — vitest + happy-dom; covers pages, components, catalogue integrity, API endpoints.
- `e2e/*.spec.ts` — Playwright, Chromium only. In CI the config starts `npm run dev` itself; locally run the dev server on port 4322 first (config `baseURL`).

## Conventions

- Conventional Commits (`feat:`, `fix:`, `docs:`, …). All changes go through PRs to `main` (branch `astro-migration` is the active migration branch).
- Lint: `eslint.config.mjs` (ESLint 10 flat config; TS + Astro + Vue). Prettier: `.prettierrc` (no semicolons, 100 cols, astro plugin), `.prettierignore` for generated/legacy paths.
- **Astro whitespace trap:** Astro _trims_ the space at a text↔inline-tag line break — `Backed by\n<code>` renders as `byinterscript-ts`. `prettier-plugin-astro` reflows prose and will create such breaks; `test/site.test.ts` has a guard that fails on them. `prettier-ignore` comments corrupt `.astro` files — instead, hand-wrap the paragraph at text-only boundaries and add the file to `.prettierignore` (see `src/pages/compare.astro`).
7 changes: 6 additions & 1 deletion e2e/compare.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,12 @@ test.describe("compare systems", () => {
test("has system selectors", async ({ page }) => {
await page.goto("/compare")
const selects = page.locator("select")
if (await selects.first().isVisible({ timeout: 3000 }).catch(() => false)) {
if (
await selects
.first()
.isVisible({ timeout: 3000 })
.catch(() => false)
) {
const count = await selects.count()
expect(count).toBeGreaterThanOrEqual(1)
}
Expand Down
3 changes: 3 additions & 0 deletions e2e/demo-edge-cases.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ import { test, expect, type Page } from "@playwright/test"

async function ready(page: Page) {
await expect(page.locator(".rail-status")).toContainText("ready", { timeout: 20000 })
// The page default is the first catalogue entry (Chinese); these tests
// assert Cyrillic → Latin, so pin a Cyrillic system.
await page.locator("select.field-input").selectOption("odni-rus-Cyrl-Latn-2015")
}

test.describe("demo edge cases", () => {
Expand Down
11 changes: 8 additions & 3 deletions e2e/demo.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,14 @@ test.describe("demo page — transliteration explorer", () => {
const select = page.locator("select.field-input")
await expect(select).toBeVisible()
const options = select.locator("option")
await expect(options).toHaveCount(5)
await expect(options).toHaveCount(289)
await expect(select.locator("optgroup").first()).toHaveAttribute("label", "ACADSIN")
})

test("default system is BGN/PCGN Ukrainian", async ({ page }) => {
test("default system is the first catalogue entry", async ({ page }) => {
await page.goto("/demo")
const select = page.locator("select.field-input")
await expect(select).toHaveValue("bgnpcgn-ukr-Cyrl-Latn-2019")
await expect(select).toHaveValue("acadsin-zho-Hani-Latn-2002")
})

test("transliterates Ukrainian Cyrillic to Latin", async ({ page }) => {
Expand All @@ -37,6 +38,8 @@ test.describe("demo page — transliteration explorer", () => {
const status = page.locator(".rail-status")
await expect(status).toContainText("ready", { timeout: 20000 })

await page.locator("select.field-input").selectOption("bgnpcgn-ukr-Cyrl-Latn-2019")

// Type Cyrillic input
const textarea = page.locator("textarea.pane-body")
await textarea.fill("Антон")
Expand Down Expand Up @@ -158,6 +161,7 @@ test.describe("demo modes — API vs in-browser", () => {
const status = page.locator(".rail-status")
await expect(status).toContainText("ready", { timeout: 30000 })

await page.locator("select.field-input").selectOption("odni-rus-Cyrl-Latn-2015")
await page.locator("textarea.pane-body").fill("Антон")
await expect(page.locator(".pane-result")).toContainText("Anton", { timeout: 10000 })
})
Expand All @@ -182,6 +186,7 @@ test.describe("demo modes — API vs in-browser", () => {
// API mode first
const status = page.locator(".rail-status")
await expect(status).toContainText("ready", { timeout: 30000 })
await page.locator("select.field-input").selectOption("bgnpcgn-ukr-Cyrl-Latn-2019")
await page.locator("textarea.pane-body").fill("Київ")
await expect(page.locator(".pane-result")).toContainText("Kyiv", { timeout: 10000 })
const apiOutput = await page.locator(".pane-result").textContent()
Expand Down
4 changes: 3 additions & 1 deletion e2e/maps.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@ test.describe("maps browser", () => {
test("has search or filter controls", async ({ page }) => {
await page.goto("/maps")
// Look for search input, select, or filter buttons
const controls = page.locator("input[type='search'], input[placeholder*='search' i], select, button")
const controls = page.locator(
"input[type='search'], input[placeholder*='search' i], select, button",
)
await expect(controls.first()).toBeVisible({ timeout: 5000 })
})

Expand Down
61 changes: 61 additions & 0 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// @ts-check
import js from "@eslint/js"
import tseslint from "typescript-eslint"
import astro from "eslint-plugin-astro"
import vue from "eslint-plugin-vue"
import prettierConfig from "eslint-config-prettier"
import globals from "globals"

export default tseslint.config(
{
ignores: [
"dist/**",
".astro/**",
"coverage/**",
"playwright-report/**",
"test-results/**",
"public/**",
"_legacy-content/**",
"docs/**",
"posts/**",
"map/**",
"TODO.complete/**",
],
},
js.configs.recommended,
...tseslint.configs.recommended,
...astro.configs.recommended,
vue.configs["flat/essential"],
prettierConfig,
{
languageOptions: {
globals: { ...globals.browser, ...globals.node },
},
rules: {
"@typescript-eslint/no-unused-vars": [
"error",
{
argsIgnorePattern: "^_",
varsIgnorePattern: "^_",
caughtErrorsIgnorePattern: "^_",
},
],
// The ISC JSON IR and catalogue entries are untyped by design.
"@typescript-eslint/no-explicit-any": "off",
},
},
{
files: ["**/*.vue"],
languageOptions: {
parserOptions: { parser: tseslint.parser },
},
},
{
files: ["**/*.astro", "**/*.ts"],
rules: {
// tsc (astro check) owns undefined-variable detection; the
// Astro frontmatter processor trips no-undef on TS globals.
"no-undef": "off",
},
},
)
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,12 +47,15 @@
"eslint": "^10.0.0",
"eslint-config-prettier": "^10.0.0",
"eslint-plugin-astro": "^1.0.0",
"eslint-plugin-vue": "^10.10.0",
"globals": "^17.11.0",
"happy-dom": "^20.11.1",
"playwright": "^1.62.1",
"prettier": "^3.9.0",
"prettier-plugin-astro": "^0.14.0",
"tsx": "^4.23.7",
"typescript": "^5.6.0",
"typescript-eslint": "^8.67.0",
"vitest": "^4.1.10"
}
}
Binary file added public/img/partners/bas.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/img/partners/bgn.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions public/img/partners/calconnect.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/img/partners/icao.ico
Binary file not shown.
1 change: 1 addition & 0 deletions public/img/partners/icao.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions public/img/partners/ogc.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/img/partners/ungegn.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 2 additions & 3 deletions public/offline.html
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,8 @@
<img class="mark" src="/symbol.svg" alt="" />
<h1>You're offline.</h1>
<p>
The Interscript page you wanted isn't cached yet. Pages and map
data you've visited before are available — try one of those, or
reconnect to load the page fresh.
The Interscript page you wanted isn't cached yet. Pages and map data you've visited before
are available — try one of those, or reconnect to load the page fresh.
</p>
<p>
<a href="/">← Back to home</a>
Expand Down
17 changes: 5 additions & 12 deletions public/sw.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,7 @@ const VERSION = "isx-sw-v1"
const CORE_CACHE = `${VERSION}-core`
const MAP_CACHE = `${VERSION}-maps`

const CORE_ASSETS = [
"/",
"/offline.html",
"/symbol.svg",
"/favicon.svg",
]
const CORE_ASSETS = ["/", "/offline.html", "/symbol.svg", "/favicon.svg"]

self.addEventListener("install", (event) => {
event.waitUntil(
Expand All @@ -32,13 +27,11 @@ self.addEventListener("install", (event) => {

self.addEventListener("activate", (event) => {
event.waitUntil(
caches.keys().then((keys) =>
Promise.all(
keys
.filter((k) => !k.startsWith(VERSION))
.map((k) => caches.delete(k)),
caches
.keys()
.then((keys) =>
Promise.all(keys.filter((k) => !k.startsWith(VERSION)).map((k) => caches.delete(k))),
),
),
)
self.clients.claim()
})
Expand Down
4 changes: 3 additions & 1 deletion scripts/generate-catalogue.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,9 @@ function buildEntry(doc) {
}

function main() {
const files = readdirSync(MAPS_DIR).filter((f) => f.endsWith(".isc")).sort()
const files = readdirSync(MAPS_DIR)
.filter((f) => f.endsWith(".isc"))
.sort()
const catalogue = {}
let okCount = 0
const errors = []
Expand Down
Loading
Loading