diff --git a/.github/workflows/deploy-pages.yml b/.github/workflows/deploy-pages.yml index f37762e..71dfc46 100644 --- a/.github/workflows/deploy-pages.yml +++ b/.github/workflows/deploy-pages.yml @@ -24,57 +24,29 @@ jobs: - uses: Swatinem/rust-cache@v2 with: - workspaces: | - 2016/tools/xlsxread - 2017/tools/xlsxread - - - uses: pnpm/action-setup@v4 - with: - package_json_file: 2017/package.json + workspaces: parser - uses: actions/setup-node@v4 with: node-version: '24' - cache: 'pnpm' - cache-dependency-path: | - 2016/pnpm-lock.yaml - 2017/pnpm-lock.yaml + cache: 'npm' + cache-dependency-path: package-lock.json - - name: Build 2016 site - working-directory: '2016' - run: | - set -euo pipefail - cargo build --release --manifest-path tools/xlsxread/Cargo.toml - ./tools/xlsxread/target/release/xlsxread build \ - --schema tools/xlsxread/configs/thptqg2016-data.toml \ - --input data --output public/thptqg2016.db - pnpm install --frozen-lockfile - gzip -k -9 public/thptqg2016.db - pnpm build - rm -f dist/thptqg2016.db + - run: npm ci - - name: Build 2017 site variants - working-directory: '2017' + # One parser binary builds every dataset; build-db.js reads the dataset + # list from src/datasets.js and gzips each database in place, leaving no + # uncompressed file behind. + - name: Build databases run: | - set -euo pipefail - cargo build --release --manifest-path tools/xlsxread/Cargo.toml - ./tools/xlsxread/target/release/xlsxread build --schema tools/xlsxread/configs/thptqg2017-data.toml --input data --output public/thptqg2017.db - ./tools/xlsxread/target/release/xlsxread build --schema tools/xlsxread/configs/thptqg2017-data-old.toml --input data-old --output public-old/thptqg2017.db - ./tools/xlsxread/target/release/xlsxread build --schema tools/xlsxread/configs/thptqg2017-data-old2.toml --input data-old2 --output public-old2/thptqg2017.db - pnpm install --frozen-lockfile - gzip -kf -9 public/thptqg2017.db - gzip -kf -9 public-old/thptqg2017.db - gzip -kf -9 public-old2/thptqg2017.db - pnpm build:all - rm -f dist/thptqg2017.db dist/old/thptqg2017.db dist/old2/thptqg2017.db + npm run build:rust + npm run build:db - - name: Assemble site - run: | - set -euo pipefail - mkdir -p _site - cp index.html _site/ - cp -r 2016/dist _site/2016 - cp -r 2017/dist _site/2017 + # One Vite build produces every page. scripts/assemble-site.js copies the + # emitted index.html to each dataset path (and the legacy nested URLs), + # then fails the job if any uncompressed database reached the artifact. + - name: Build and assemble site + run: npm run build:site - uses: actions/upload-pages-artifact@v3 with: diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c67537c --- /dev/null +++ b/.gitignore @@ -0,0 +1,24 @@ +### Editors ### +.idea +*.iws +*.iml +*.ipr +.vscode/ + +### Mac OS ### +.DS_Store + +### Node ### +node_modules/ + +### Build output ### +dist/ + +### Generated databases and Vite publicDir staging ### +.build/ + +### Rust build artefacts ### +parser/target/ + +### Assembled Pages artifact ### +_site/ diff --git a/2016/.gitignore b/2016/.gitignore deleted file mode 100644 index 051b283..0000000 --- a/2016/.gitignore +++ /dev/null @@ -1,146 +0,0 @@ -# Logs -logs -*.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* -lerna-debug.log* - -# Diagnostic reports (https://nodejs.org/api/report.html) -report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json - -# Runtime data -pids -*.pid -*.seed -*.pid.lock - -# Directory for instrumented libs generated by jscoverage/JSCover -lib-cov - -# Coverage directory used by tools like istanbul -coverage -*.lcov - -# nyc test coverage -.nyc_output - -# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) -.grunt - -# Bower dependency directory (https://bower.io/) -bower_components - -# node-waf configuration -.lock-wscript - -# Compiled binary addons (https://nodejs.org/api/addons.html) -build/Release - -# Dependency directories -node_modules/ -jspm_packages/ - -# Snowpack dependency directory (https://snowpack.dev/) -web_modules/ - -# TypeScript cache -*.tsbuildinfo - -# Optional npm cache directory -.npm - -# Optional eslint cache -.eslintcache - -# Optional stylelint cache -.stylelintcache - -# Optional REPL history -.node_repl_history - -# Output of 'npm pack' -*.tgz - -# Yarn Integrity file -.yarn-integrity - -# dotenv environment variable files -.env -.env.* -!.env.example - -# parcel-bundler cache (https://parceljs.org/) -.cache -.parcel-cache - -# Next.js build output -.next -out - -# Nuxt.js build / generate output -.nuxt -dist - -# Gatsby files -.cache/ -# Comment in the public line in if your project uses Gatsby and not Next.js -# https://nextjs.org/blog/next-9-1#public-directory-support -# public - -# vuepress build output -.vuepress/dist - -# vuepress v2.x temp and cache directory -.temp -.cache - -# Sveltekit cache directory -.svelte-kit/ - -# vitepress build output -**/.vitepress/dist - -# vitepress cache directory -**/.vitepress/cache - -# Docusaurus cache and generated files -.docusaurus - -# Serverless directories -.serverless/ - -# FuseBox cache -.fusebox/ - -# DynamoDB Local files -.dynamodb/ - -# Firebase cache directory -.firebase/ - -# TernJS port file -.tern-port - -# Stores VSCode versions used for testing VSCode extensions -.vscode-test - -# yarn v3 -.pnp.* -.yarn/* -!.yarn/patches -!.yarn/plugins -!.yarn/releases -!.yarn/sdks -!.yarn/versions - -# Vite logs files -vite.config.js.timestamp-* -vite.config.ts.timestamp-* - -# Generated database files -public/*.db -public/*.db.gz - -# Rust build artifacts -tools/xlsxread/target/ diff --git a/2016/README.md b/2016/README.md deleted file mode 100644 index a086341..0000000 --- a/2016/README.md +++ /dev/null @@ -1,57 +0,0 @@ -# thptqg2016 - -Lookup tool for Vietnam's 2016 National High School Graduation Exam (THPT Quốc gia) scores — 877,461 candidates nationwide. - -Fully static app running entirely in the browser (SQLite via `sql.js`). No backend, no query logging. - -## Features - -- **Quick lookup** by exam ID (`số báo danh`) or full name (diacritics-insensitive) -- **Custom read-only SQL** (SELECT / PRAGMA / EXPLAIN / WITH) with 7 built-in preset queries -- **Complete data**: 12 subjects (Math, Literature, Physics, Chemistry, Biology, History, Geography, English, French, German, Japanese, Chinese), exam cluster, date of birth, gender -- Safety caps: 100 rows for lookup, 1000 rows for custom SQL -- Dark mode, `Ctrl+Enter` shortcut to run queries - -## Demo - - - -## Development - -```bash -# Build the SQLite database from source Excel files (requires Rust stable) -pnpm run build:db - -# Or build in two steps: -pnpm run build:rust # compile the xlsxread binary -./tools/xlsxread/target/release/xlsxread build \ - --schema tools/xlsxread/configs/thptqg2016-data.toml \ - --input data \ - --output public/thptqg2016.db - -pnpm run dev # Vite dev server -pnpm run build # Production bundle → dist/ -pnpm run lint # ESLint -``` - -The database is built by the `xlsxread` Rust binary (`tools/xlsxread/`), which -reads the 119 mixed `.xls`/`.xlsx` files and auto-detects the column layout per -file (`separate-scores`, `mapped`, or positional default). No Node.js Excel -library is required at build time. - -The GitHub Actions workflow (`.github/workflows/deploy.yml`) compiles -`xlsxread`, builds the DB, gzips it, and deploys to GitHub Pages on every push -to `main`. - -## Tech stack - -React 19 · Vite · sql.js (WASM) · xlsxread (Rust, build-time) · GitHub Pages - -## Documentation - -- [Project overview (PDR)](./docs/project-overview-pdr.md) -- [Codebase summary](./docs/codebase-summary.md) -- [System architecture](./docs/system-architecture.md) -- [Deployment guide](./docs/deployment-guide.md) - -**Source**: Originally published at (currently inaccessible). Data is for reference only. diff --git a/2016/docs/codebase-summary.md b/2016/docs/codebase-summary.md deleted file mode 100644 index 29626e7..0000000 --- a/2016/docs/codebase-summary.md +++ /dev/null @@ -1,63 +0,0 @@ -# Codebase Summary - -## Directory layout - -``` -thptqg2016/ -├── data/ # Source Excel files (~100, mixed formats) -├── scripts/ -│ └── build-database.js # Parse Excel → SQLite (build-time, Node + better-sqlite3) -├── public/ -│ └── thptqg2016.db # Generated DB, gzipped during CI -├── src/ -│ ├── main.jsx # React entry -│ ├── App.jsx # Root: tabs, lookup logic, useSqlite wiring -│ ├── App.css / index.css # Design tokens, dark mode, a11y styles -│ ├── hooks/ -│ │ └── use-sqlite.js # Fetch .db.gz + decompress + init sql.js -│ └── components/ -│ ├── search-form.jsx # Input for exam ID / full name -│ ├── score-table.jsx # Result table for lookups -│ └── custom-query.jsx # SQL editor + presets + result grid -├── .github/workflows/deploy.yml # CI: build db → gzip → vite build → Pages -├── vite.config.js # base: "/thptqg2016/" -└── eslint.config.js -``` - -## Key modules - -### `scripts/build-database.js` -Build-time only. Reads every `.xlsx/.xls` in `data/`, detects the header format (three variants), parses the `DIEM_THI` string via regex or separate score columns, normalizes gender, derives a diacritics-stripped `ho_ten_ascii` column for accent-insensitive search, and inserts into SQLite with three indexes (`ho_ten`, `ho_ten_ascii`, `ten_cum_thi`). - -### `src/hooks/use-sqlite.js` -Streams `.db.gz` with download progress, decompresses via `DecompressionStream("gzip")`, loads `sql.js` (WASM served from the `sql.js.org` CDN), and returns `{ db, loading, error, progress }`. - -### `src/App.jsx` -Two tabs: **Lookup** and **Custom SQL**. Lookup auto-detects exam IDs (regex `^[A-Z]{2,4}\d+$`) vs names and picks one of three query paths: exact exam ID / ASCII LIKE / original + ASCII LIKE. Capped at 100 rows. - -### `src/components/custom-query.jsx` -Whitelists leading keywords (`SELECT`, `PRAGMA`, `EXPLAIN`, `WITH`), auto-appends `LIMIT 1000` when missing, measures `performance.now()` execution time, and ships 7 preset analytics queries. - -## `student` table schema - -```sql -so_bao_danh TEXT PRIMARY KEY -- exam ID -ho_ten TEXT NOT NULL -- full name -ho_ten_ascii TEXT NOT NULL -- diacritics stripped, lowercased -ngay_sinh TEXT -- date of birth -ten_cum_thi TEXT -- exam cluster name -gioi_tinh TEXT -- "Nam" | "Nữ" | NULL -toan, ngu_van, vat_ly, hoa_hoc, -- REAL (nullable) subject scores -sinh_hoc, lich_su, dia_ly, -tieng_anh, tieng_phap, tieng_duc, -tieng_nhat, tieng_trung -``` - -Indexes: `idx_ho_ten`, `idx_ho_ten_ascii`, `idx_ten_cum_thi`. - -## Conventions - -- JS/JSX filenames: **kebab-case** (e.g., `search-form.jsx`, `use-sqlite.js`) -- React components: `PascalCase` named exports -- UI strings: Vietnamese (target audience) -- Code comments: English; explain *why*, not *what* diff --git a/2016/docs/deployment-guide.md b/2016/docs/deployment-guide.md deleted file mode 100644 index 87e4a03..0000000 --- a/2016/docs/deployment-guide.md +++ /dev/null @@ -1,50 +0,0 @@ -# Deployment Guide - -## Automatic (recommended) - -Push to `main` → GitHub Actions builds and deploys to GitHub Pages automatically. - -Workflow: `.github/workflows/deploy.yml` - -CI steps: -1. `npm ci` -2. `npm run build:db` — generate `public/thptqg2016.db` from `data/*.xlsx` -3. `gzip -k -9 public/thptqg2016.db` — max compression, keep original -4. `npm run build` — Vite bundles `dist/` -5. `rm -f dist/thptqg2016.db` — ship only the gzipped copy -6. `actions/upload-pages-artifact@v3` + `actions/deploy-pages@v4` - -One-time setup: **Settings → Pages → Source: GitHub Actions**. - -## Manual (local verification) - -```bash -npm install -npm run build:db -gzip -k -9 public/thptqg2016.db # Linux/macOS; on Windows use 7zip or WSL -npm run build -rm dist/thptqg2016.db # optional, shrinks artifact -npm run preview # serve dist/ locally -``` - -Open . - -## Base path - -`vite.config.js` sets `base: "/thptqg2016/"`. If you fork under a different repo name, update this to match `` so assets resolve correctly on GitHub Pages. - -## Updating data - -1. Add the new Excel file to `data/`. -2. If its header is unfamiliar, open `scripts/build-database.js` and extend `detectFormat()` or the `processSeparateScoresRow` / `processMappedRow` helpers. -3. Run `npm run build:db` locally to check row counts and error skips. -4. Commit + push → CI redeploys. - -## Troubleshooting - -| Symptom | Typical cause | -|---------|---------------| -| Blank page, 404 on assets | `base` in `vite.config.js` doesn't match the repo name | -| `Failed to fetch database: 404` | gzip step skipped, or `.db.gz` removed from `dist/` | -| WASM fails to load | `sql.js.org` blocked / offline — self-host `sql-wasm.wasm` in `public/` and update `SQL_WASM_URL` in `use-sqlite.js` | -| Missing rows after build | Excel file has an unknown header — check console for `Failed to read` or `errorCount` | diff --git a/2016/docs/project-overview-pdr.md b/2016/docs/project-overview-pdr.md deleted file mode 100644 index 9249e0a..0000000 --- a/2016/docs/project-overview-pdr.md +++ /dev/null @@ -1,34 +0,0 @@ -# Project Overview — thptqg2016 - -## Goal - -Provide a public lookup tool for Vietnam's 2016 National High School Graduation Exam scores (877,461 candidates), running entirely on the client, hosted for free on GitHub Pages. - -## Scope - -- Lookup by exam ID or full name (with Vietnamese diacritics handling) -- Read-only SQL queries against a single `student` table -- Static dataset — no updates (the 2016 exam is long over) - -## Target users - -- Former 2016 candidates checking their scores -- Education researchers / data journalists running aggregate stats -- Developers exploring SQL on a real-world dataset - -## Constraints - -- **Zero backend**: the full DB (tens of MB gzipped) is downloaded to the browser -- **Read-only**: INSERT/UPDATE/DELETE rejected to avoid the illusion that user edits persist -- **Row caps**: 100 rows (lookup), 1000 rows (SQL) to prevent browser hangs -- **Vietnamese-first UI**: app labels and data are Vietnamese; documentation is English - -## Data sources - -Excel files (`.xlsx`/`.xls`) collected from newspapers and exam clusters in 2016, stored in `data/`. One file per cluster, with several different column layouts (see `scripts/build-database.js`). - -Original aggregator link: — no longer accessible, so the raw Excel files are mirrored in this repo under `data/`. - -## Status - -Stable. Data is frozen. Recent work focuses on UX polish (dark mode, accessibility, diacritics-insensitive search). diff --git a/2016/docs/system-architecture.md b/2016/docs/system-architecture.md deleted file mode 100644 index 4c98b2d..0000000 --- a/2016/docs/system-architecture.md +++ /dev/null @@ -1,83 +0,0 @@ -# System Architecture - -## Overview - -A **static serverless** design: the entire dataset is packaged into a single SQLite file, gzip-compressed, and served as a static asset via GitHub Pages. The browser downloads it, decompresses it, and queries it in-process using `sql.js` (SQLite compiled to WebAssembly). - -``` -┌─────────────────┐ build ┌──────────────────────┐ -│ data/*.xlsx │ ───────────▶ │ scripts/ │ -│ (mixed formats)│ │ build-database.js │ -└─────────────────┘ │ (Node + xlsx + │ - │ better-sqlite3) │ - └──────────┬───────────┘ - │ - ▼ - ┌──────────────────────┐ - │ public/thptqg2016.db │ - └──────────┬───────────┘ - │ gzip -9 (CI) - ▼ - ┌──────────────────────┐ - │ dist/thptqg2016.db.gz│ - │ dist/assets/* │ ◀── Vite build - └──────────┬───────────┘ - │ upload-pages-artifact - ▼ - GitHub Pages CDN - │ - ▼ - ┌──────────────────────────────────┐ - │ Browser │ - │ ┌────────────────────────────┐ │ - │ │ useSqlite hook │ │ - │ │ fetch(.db.gz) + stream │ │ - │ │ DecompressionStream gzip │ │ - │ │ sql.js WASM (from CDN) │ │ - │ └─────────────┬──────────────┘ │ - │ ▼ │ - │ ┌────────────────────────────┐ │ - │ │ React UI │ │ - │ │ - SearchForm / ScoreTable │ │ - │ │ - CustomQuery (SQL editor)│ │ - │ └────────────────────────────┘ │ - └──────────────────────────────────┘ -``` - -## Build-time data flow - -1. Developer drops Excel files into `data/`. -2. `npm run build:db` reads every file; for each one: - - Detects the header row against a `KNOWN_HEADERS` set. - - Picks a format: `separate-scores` (one column per subject) vs. `mapped` (single `DIEM_THI` string). - - Parses each row into a canonical 18-column record. - - `INSERT OR REPLACE` into SQLite (primary key = `so_bao_danh` handles duplicates). -3. `VACUUM` shrinks the file. -4. CI runs `gzip -k -9` → `.db.gz`. - -## Runtime flow - -1. Page loads → React mounts → `useSqlite("thptqg2016.db.gz")`. -2. Streaming fetch with a progress bar (driven by `Content-Length`). -3. `DecompressionStream("gzip")` decompresses on the fly. -4. `sql.js` loads its WASM from `https://sql.js.org/dist/sql-wasm.wasm`. -5. `new SQL.Database(Uint8Array)` — DB is now in RAM. -6. Each search / query → `db.prepare()` + `stmt.step()` loop → render. - -## Design decisions - -| Concern | Choice | Rationale | -|---------|--------|-----------| -| Storage | Static SQLite file | No backend needed; dataset is frozen | -| Compression | gzip in CI, `DecompressionStream` in browser | Native browser API; no extra library | -| WASM hosting | `sql.js.org` CDN | Smaller self-hosted artifact | -| Diacritics search | Pre-computed `ho_ten_ascii` column | `LOWER(REPLACE(...))` at query time defeats the index | -| SQL safety | Leading-keyword allowlist | `sql.js` is in-memory so writes don't persist, but the allowlist prevents user confusion | -| Row caps | 100 (lookup), 1000 (SQL) | Keep DOM render sizes reasonable | - -## Risks and limitations - -- **DB size**: tens of MB gzipped — slow links have a visible wait; mitigated by the progress bar. -- **Browser memory**: the full DB lives in RAM; older mobile devices may OOM. -- **Dependency on `sql.js.org`**: if that CDN is unreachable, WASM fails to load. -- **Excel format drift**: a new source file with an unseen header layout needs a new branch in `detectFormat()`. diff --git a/2016/eslint.config.js b/2016/eslint.config.js deleted file mode 100644 index 1f236de..0000000 --- a/2016/eslint.config.js +++ /dev/null @@ -1,31 +0,0 @@ -import js from "@eslint/js"; -import globals from "globals"; -import reactHooks from "eslint-plugin-react-hooks"; -import reactRefresh from "eslint-plugin-react-refresh"; - -export default [ - { ignores: ["dist"] }, - { - files: ["**/*.{js,jsx}"], - languageOptions: { - ecmaVersion: 2024, - globals: globals.browser, - parserOptions: { - ecmaVersion: "latest", - ecmaFeatures: { jsx: true }, - sourceType: "module", - }, - }, - plugins: { - "react-hooks": reactHooks, - "react-refresh": reactRefresh, - }, - rules: { - ...reactHooks.configs.recommended.rules, - "react-refresh/only-export-components": [ - "warn", - { allowConstantExport: true }, - ], - }, - }, -]; diff --git a/2016/index.html b/2016/index.html deleted file mode 100644 index 172159e..0000000 --- a/2016/index.html +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - Tra cứu điểm thi THPT QG 2016 - - -
- - - diff --git a/2016/pnpm-lock.yaml b/2016/pnpm-lock.yaml deleted file mode 100644 index ce02b86..0000000 --- a/2016/pnpm-lock.yaml +++ /dev/null @@ -1,1624 +0,0 @@ -lockfileVersion: '9.0' - -settings: - autoInstallPeers: true - excludeLinksFromLockfile: false - -importers: - - .: - dependencies: - react: - specifier: ^19.2.4 - version: 19.2.6 - react-dom: - specifier: ^19.2.4 - version: 19.2.6(react@19.2.6) - sql.js: - specifier: ^1.14.1 - version: 1.14.1 - devDependencies: - '@eslint/js': - specifier: ^9.39.4 - version: 9.39.4 - '@types/react': - specifier: ^19.2.14 - version: 19.2.14 - '@types/react-dom': - specifier: ^19.2.3 - version: 19.2.3(@types/react@19.2.14) - '@vitejs/plugin-react': - specifier: ^6.0.1 - version: 6.0.1(vite@8.0.16) - eslint: - specifier: ^9.39.4 - version: 9.39.4 - eslint-plugin-react-hooks: - specifier: ^7.0.1 - version: 7.1.1(eslint@9.39.4) - eslint-plugin-react-refresh: - specifier: ^0.5.2 - version: 0.5.2(eslint@9.39.4) - globals: - specifier: ^17.4.0 - version: 17.6.0 - vite: - specifier: ^8.0.16 - version: 8.0.16 - -packages: - - '@babel/code-frame@7.29.0': - resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} - engines: {node: '>=6.9.0'} - - '@babel/compat-data@7.29.3': - resolution: {integrity: sha512-LIVqM46zQWZhj17qA8wb4nW/ixr2y1Nw+r1etiAWgRM6U1IqP+LNhL1yg440jYZR72jCWcWbLWzIosH+uP1fqg==} - engines: {node: '>=6.9.0'} - - '@babel/core@7.29.0': - resolution: {integrity: sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==} - engines: {node: '>=6.9.0'} - - '@babel/generator@7.29.1': - resolution: {integrity: sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==} - engines: {node: '>=6.9.0'} - - '@babel/helper-compilation-targets@7.28.6': - resolution: {integrity: sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==} - engines: {node: '>=6.9.0'} - - '@babel/helper-globals@7.28.0': - resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} - engines: {node: '>=6.9.0'} - - '@babel/helper-module-imports@7.28.6': - resolution: {integrity: sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==} - engines: {node: '>=6.9.0'} - - '@babel/helper-module-transforms@7.28.6': - resolution: {integrity: sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0 - - '@babel/helper-string-parser@7.27.1': - resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} - engines: {node: '>=6.9.0'} - - '@babel/helper-validator-identifier@7.28.5': - resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} - engines: {node: '>=6.9.0'} - - '@babel/helper-validator-option@7.27.1': - resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} - engines: {node: '>=6.9.0'} - - '@babel/helpers@7.29.2': - resolution: {integrity: sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==} - engines: {node: '>=6.9.0'} - - '@babel/parser@7.29.3': - resolution: {integrity: sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==} - engines: {node: '>=6.0.0'} - hasBin: true - - '@babel/template@7.28.6': - resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==} - engines: {node: '>=6.9.0'} - - '@babel/traverse@7.29.0': - resolution: {integrity: sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==} - engines: {node: '>=6.9.0'} - - '@babel/types@7.29.0': - resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} - engines: {node: '>=6.9.0'} - - '@emnapi/core@1.10.0': - resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==} - - '@emnapi/runtime@1.10.0': - resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==} - - '@emnapi/wasi-threads@1.2.1': - resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} - - '@eslint-community/eslint-utils@4.9.1': - resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 - - '@eslint-community/regexpp@4.12.2': - resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} - engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} - - '@eslint/config-array@0.21.2': - resolution: {integrity: sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@eslint/config-helpers@0.4.2': - resolution: {integrity: sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@eslint/core@0.17.0': - resolution: {integrity: sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@eslint/eslintrc@3.3.5': - resolution: {integrity: sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@eslint/js@9.39.4': - resolution: {integrity: sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@eslint/object-schema@2.1.7': - resolution: {integrity: sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@eslint/plugin-kit@0.4.1': - resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@humanfs/core@0.19.2': - resolution: {integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==} - engines: {node: '>=18.18.0'} - - '@humanfs/node@0.16.8': - resolution: {integrity: sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==} - engines: {node: '>=18.18.0'} - - '@humanfs/types@0.15.0': - resolution: {integrity: sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==} - engines: {node: '>=18.18.0'} - - '@humanwhocodes/module-importer@1.0.1': - resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} - engines: {node: '>=12.22'} - - '@humanwhocodes/retry@0.4.3': - resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} - engines: {node: '>=18.18'} - - '@jridgewell/gen-mapping@0.3.13': - resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} - - '@jridgewell/remapping@2.3.5': - resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} - - '@jridgewell/resolve-uri@3.1.2': - resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} - engines: {node: '>=6.0.0'} - - '@jridgewell/sourcemap-codec@1.5.5': - resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} - - '@jridgewell/trace-mapping@0.3.31': - resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} - - '@napi-rs/wasm-runtime@1.1.6': - resolution: {integrity: sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==} - peerDependencies: - '@emnapi/core': ^1.7.1 - '@emnapi/runtime': ^1.7.1 - - '@oxc-project/types@0.133.0': - resolution: {integrity: sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==} - - '@rolldown/binding-android-arm64@1.0.3': - resolution: {integrity: sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [android] - - '@rolldown/binding-darwin-arm64@1.0.3': - resolution: {integrity: sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [darwin] - - '@rolldown/binding-darwin-x64@1.0.3': - resolution: {integrity: sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [darwin] - - '@rolldown/binding-freebsd-x64@1.0.3': - resolution: {integrity: sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [freebsd] - - '@rolldown/binding-linux-arm-gnueabihf@1.0.3': - resolution: {integrity: sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm] - os: [linux] - - '@rolldown/binding-linux-arm64-gnu@1.0.3': - resolution: {integrity: sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [linux] - libc: [glibc] - - '@rolldown/binding-linux-arm64-musl@1.0.3': - resolution: {integrity: sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [linux] - libc: [musl] - - '@rolldown/binding-linux-ppc64-gnu@1.0.3': - resolution: {integrity: sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [ppc64] - os: [linux] - libc: [glibc] - - '@rolldown/binding-linux-s390x-gnu@1.0.3': - resolution: {integrity: sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [s390x] - os: [linux] - libc: [glibc] - - '@rolldown/binding-linux-x64-gnu@1.0.3': - resolution: {integrity: sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [linux] - libc: [glibc] - - '@rolldown/binding-linux-x64-musl@1.0.3': - resolution: {integrity: sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [linux] - libc: [musl] - - '@rolldown/binding-openharmony-arm64@1.0.3': - resolution: {integrity: sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [openharmony] - - '@rolldown/binding-wasm32-wasi@1.0.3': - resolution: {integrity: sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [wasm32] - - '@rolldown/binding-win32-arm64-msvc@1.0.3': - resolution: {integrity: sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [win32] - - '@rolldown/binding-win32-x64-msvc@1.0.3': - resolution: {integrity: sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [win32] - - '@rolldown/pluginutils@1.0.0-rc.7': - resolution: {integrity: sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA==} - - '@rolldown/pluginutils@1.0.1': - resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} - - '@tybys/wasm-util@0.10.3': - resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==} - - '@types/estree@1.0.9': - resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} - - '@types/json-schema@7.0.15': - resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} - - '@types/react-dom@19.2.3': - resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} - peerDependencies: - '@types/react': ^19.2.0 - - '@types/react@19.2.14': - resolution: {integrity: sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==} - - '@vitejs/plugin-react@6.0.1': - resolution: {integrity: sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ==} - engines: {node: ^20.19.0 || >=22.12.0} - peerDependencies: - '@rolldown/plugin-babel': ^0.1.7 || ^0.2.0 - babel-plugin-react-compiler: ^1.0.0 - vite: ^8.0.0 - peerDependenciesMeta: - '@rolldown/plugin-babel': - optional: true - babel-plugin-react-compiler: - optional: true - - acorn-jsx@5.3.2: - resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} - peerDependencies: - acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 - - acorn@8.16.0: - resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==} - engines: {node: '>=0.4.0'} - hasBin: true - - ajv@6.15.0: - resolution: {integrity: sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==} - - ansi-styles@4.3.0: - resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} - engines: {node: '>=8'} - - argparse@2.0.1: - resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} - - balanced-match@1.0.2: - resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} - - baseline-browser-mapping@2.10.29: - resolution: {integrity: sha512-Asa2krT+XTPZINCS+2QcyS8WTkObE77RwkydwF7h6DmnKqbvlalz93m/dnphUyCa6SWSP51VgtEUf2FN+gelFQ==} - engines: {node: '>=6.0.0'} - hasBin: true - - brace-expansion@1.1.14: - resolution: {integrity: sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==} - - browserslist@4.28.2: - resolution: {integrity: sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==} - engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} - hasBin: true - - callsites@3.1.0: - resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} - engines: {node: '>=6'} - - caniuse-lite@1.0.30001792: - resolution: {integrity: sha512-hVLMUZFgR4JJ6ACt1uEESvQN1/dBVqPAKY0hgrV70eN3391K6juAfTjKZLKvOMsx8PxA7gsY1/tLMMTcfFLLpw==} - - chalk@4.1.2: - resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} - engines: {node: '>=10'} - - color-convert@2.0.1: - resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} - engines: {node: '>=7.0.0'} - - color-name@1.1.4: - resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} - - concat-map@0.0.1: - resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} - - convert-source-map@2.0.0: - resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} - - cross-spawn@7.0.6: - resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} - engines: {node: '>= 8'} - - csstype@3.2.3: - resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} - - debug@4.4.3: - resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} - engines: {node: '>=6.0'} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - - deep-is@0.1.4: - resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} - - detect-libc@2.1.2: - resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} - engines: {node: '>=8'} - - electron-to-chromium@1.5.353: - resolution: {integrity: sha512-kOrWphBi8TOZyiJZqsgqIle0lw+tzmnQK83pV9dZUd01Nm2POECSyFQMAuarzZdYqQW7FH9RaYOuaRo3h+bQ3w==} - - escalade@3.2.0: - resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} - engines: {node: '>=6'} - - escape-string-regexp@4.0.0: - resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} - engines: {node: '>=10'} - - eslint-plugin-react-hooks@7.1.1: - resolution: {integrity: sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==} - engines: {node: '>=18'} - peerDependencies: - eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0 - - eslint-plugin-react-refresh@0.5.2: - resolution: {integrity: sha512-hmgTH57GfzoTFjVN0yBwTggnsVUF2tcqi7RJZHqi9lIezSs4eFyAMktA68YD4r5kNw1mxyY4dmkyoFDb3FIqrA==} - peerDependencies: - eslint: ^9 || ^10 - - eslint-scope@8.4.0: - resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - eslint-visitor-keys@3.4.3: - resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - - eslint-visitor-keys@4.2.1: - resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - eslint@9.39.4: - resolution: {integrity: sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - hasBin: true - peerDependencies: - jiti: '*' - peerDependenciesMeta: - jiti: - optional: true - - espree@10.4.0: - resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - esquery@1.7.0: - resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} - engines: {node: '>=0.10'} - - esrecurse@4.3.0: - resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} - engines: {node: '>=4.0'} - - estraverse@5.3.0: - resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} - engines: {node: '>=4.0'} - - esutils@2.0.3: - resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} - engines: {node: '>=0.10.0'} - - fast-deep-equal@3.1.3: - resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} - - fast-json-stable-stringify@2.1.0: - resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} - - fast-levenshtein@2.0.6: - resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} - - fdir@6.5.0: - resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} - engines: {node: '>=12.0.0'} - peerDependencies: - picomatch: ^3 || ^4 - peerDependenciesMeta: - picomatch: - optional: true - - file-entry-cache@8.0.0: - resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} - engines: {node: '>=16.0.0'} - - find-up@5.0.0: - resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} - engines: {node: '>=10'} - - flat-cache@4.0.1: - resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} - engines: {node: '>=16'} - - flatted@3.4.2: - resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==} - - fsevents@2.3.3: - resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} - engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} - os: [darwin] - - gensync@1.0.0-beta.2: - resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} - engines: {node: '>=6.9.0'} - - glob-parent@6.0.2: - resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} - engines: {node: '>=10.13.0'} - - globals@14.0.0: - resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} - engines: {node: '>=18'} - - globals@17.6.0: - resolution: {integrity: sha512-sepffkT8stwnIYbsMBpoCHJuJM5l98FUF2AnE07hfvE0m/qp3R586hw4jF4uadbhvg1ooIdzuu7CsfD2jzCaNA==} - engines: {node: '>=18'} - - has-flag@4.0.0: - resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} - engines: {node: '>=8'} - - hermes-estree@0.25.1: - resolution: {integrity: sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==} - - hermes-parser@0.25.1: - resolution: {integrity: sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==} - - ignore@5.3.2: - resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} - engines: {node: '>= 4'} - - import-fresh@3.3.1: - resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} - engines: {node: '>=6'} - - imurmurhash@0.1.4: - resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} - engines: {node: '>=0.8.19'} - - is-extglob@2.1.1: - resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} - engines: {node: '>=0.10.0'} - - is-glob@4.0.3: - resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} - engines: {node: '>=0.10.0'} - - isexe@2.0.0: - resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} - - js-tokens@4.0.0: - resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} - - js-yaml@4.1.1: - resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} - hasBin: true - - jsesc@3.1.0: - resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} - engines: {node: '>=6'} - hasBin: true - - json-buffer@3.0.1: - resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} - - json-schema-traverse@0.4.1: - resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} - - json-stable-stringify-without-jsonify@1.0.1: - resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} - - json5@2.2.3: - resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} - engines: {node: '>=6'} - hasBin: true - - keyv@4.5.4: - resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} - - levn@0.4.1: - resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} - engines: {node: '>= 0.8.0'} - - lightningcss-android-arm64@1.33.0: - resolution: {integrity: sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==} - engines: {node: '>= 12.0.0'} - cpu: [arm64] - os: [android] - - lightningcss-darwin-arm64@1.33.0: - resolution: {integrity: sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==} - engines: {node: '>= 12.0.0'} - cpu: [arm64] - os: [darwin] - - lightningcss-darwin-x64@1.33.0: - resolution: {integrity: sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==} - engines: {node: '>= 12.0.0'} - cpu: [x64] - os: [darwin] - - lightningcss-freebsd-x64@1.33.0: - resolution: {integrity: sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==} - engines: {node: '>= 12.0.0'} - cpu: [x64] - os: [freebsd] - - lightningcss-linux-arm-gnueabihf@1.33.0: - resolution: {integrity: sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==} - engines: {node: '>= 12.0.0'} - cpu: [arm] - os: [linux] - - lightningcss-linux-arm64-gnu@1.33.0: - resolution: {integrity: sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==} - engines: {node: '>= 12.0.0'} - cpu: [arm64] - os: [linux] - libc: [glibc] - - lightningcss-linux-arm64-musl@1.33.0: - resolution: {integrity: sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==} - engines: {node: '>= 12.0.0'} - cpu: [arm64] - os: [linux] - libc: [musl] - - lightningcss-linux-x64-gnu@1.33.0: - resolution: {integrity: sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==} - engines: {node: '>= 12.0.0'} - cpu: [x64] - os: [linux] - libc: [glibc] - - lightningcss-linux-x64-musl@1.33.0: - resolution: {integrity: sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==} - engines: {node: '>= 12.0.0'} - cpu: [x64] - os: [linux] - libc: [musl] - - lightningcss-win32-arm64-msvc@1.33.0: - resolution: {integrity: sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==} - engines: {node: '>= 12.0.0'} - cpu: [arm64] - os: [win32] - - lightningcss-win32-x64-msvc@1.33.0: - resolution: {integrity: sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==} - engines: {node: '>= 12.0.0'} - cpu: [x64] - os: [win32] - - lightningcss@1.33.0: - resolution: {integrity: sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==} - engines: {node: '>= 12.0.0'} - - locate-path@6.0.0: - resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} - engines: {node: '>=10'} - - lodash.merge@4.6.2: - resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} - - lru-cache@5.1.1: - resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} - - minimatch@3.1.5: - resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==} - - ms@2.1.3: - resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - - nanoid@3.3.16: - resolution: {integrity: sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==} - engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} - hasBin: true - - natural-compare@1.4.0: - resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} - - node-releases@2.0.38: - resolution: {integrity: sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw==} - - optionator@0.9.4: - resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} - engines: {node: '>= 0.8.0'} - - p-limit@3.1.0: - resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} - engines: {node: '>=10'} - - p-locate@5.0.0: - resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} - engines: {node: '>=10'} - - parent-module@1.0.1: - resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} - engines: {node: '>=6'} - - path-exists@4.0.0: - resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} - engines: {node: '>=8'} - - path-key@3.1.1: - resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} - engines: {node: '>=8'} - - picocolors@1.1.1: - resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} - - picomatch@4.0.5: - resolution: {integrity: sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==} - engines: {node: '>=12'} - - postcss@8.5.23: - resolution: {integrity: sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg==} - engines: {node: ^10 || ^12 || >=14} - - prelude-ls@1.2.1: - resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} - engines: {node: '>= 0.8.0'} - - punycode@2.3.1: - resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} - engines: {node: '>=6'} - - react-dom@19.2.6: - resolution: {integrity: sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g==} - peerDependencies: - react: ^19.2.6 - - react@19.2.6: - resolution: {integrity: sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q==} - engines: {node: '>=0.10.0'} - - resolve-from@4.0.0: - resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} - engines: {node: '>=4'} - - rolldown@1.0.3: - resolution: {integrity: sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==} - engines: {node: ^20.19.0 || >=22.12.0} - hasBin: true - - scheduler@0.27.0: - resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} - - semver@6.3.1: - resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} - hasBin: true - - shebang-command@2.0.0: - resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} - engines: {node: '>=8'} - - shebang-regex@3.0.0: - resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} - engines: {node: '>=8'} - - source-map-js@1.2.1: - resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} - engines: {node: '>=0.10.0'} - - sql.js@1.14.1: - resolution: {integrity: sha512-gcj8zBWU5cFsi9WUP+4bFNXAyF1iRpA3LLyS/DP5xlrNzGmPIizUeBggKa8DbDwdqaKwUcTEnChtd2grWo/x/A==} - - strip-json-comments@3.1.1: - resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} - engines: {node: '>=8'} - - supports-color@7.2.0: - resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} - engines: {node: '>=8'} - - tinyglobby@0.2.17: - resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} - engines: {node: '>=12.0.0'} - - tslib@2.8.1: - resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} - - type-check@0.4.0: - resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} - engines: {node: '>= 0.8.0'} - - update-browserslist-db@1.2.3: - resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} - hasBin: true - peerDependencies: - browserslist: '>= 4.21.0' - - uri-js@4.4.1: - resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} - - vite@8.0.16: - resolution: {integrity: sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==} - engines: {node: ^20.19.0 || >=22.12.0} - hasBin: true - peerDependencies: - '@types/node': ^20.19.0 || >=22.12.0 - '@vitejs/devtools': ^0.1.18 - esbuild: ^0.27.0 || ^0.28.0 - jiti: '>=1.21.0' - less: ^4.0.0 - sass: ^1.70.0 - sass-embedded: ^1.70.0 - stylus: '>=0.54.8' - sugarss: ^5.0.0 - terser: ^5.16.0 - tsx: ^4.8.1 - yaml: ^2.4.2 - peerDependenciesMeta: - '@types/node': - optional: true - '@vitejs/devtools': - optional: true - esbuild: - optional: true - jiti: - optional: true - less: - optional: true - sass: - optional: true - sass-embedded: - optional: true - stylus: - optional: true - sugarss: - optional: true - terser: - optional: true - tsx: - optional: true - yaml: - optional: true - - which@2.0.2: - resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} - engines: {node: '>= 8'} - hasBin: true - - word-wrap@1.2.5: - resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} - engines: {node: '>=0.10.0'} - - yallist@3.1.1: - resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} - - yocto-queue@0.1.0: - resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} - engines: {node: '>=10'} - - zod-validation-error@4.0.2: - resolution: {integrity: sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==} - engines: {node: '>=18.0.0'} - peerDependencies: - zod: ^3.25.0 || ^4.0.0 - - zod@4.4.3: - resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==} - -snapshots: - - '@babel/code-frame@7.29.0': - dependencies: - '@babel/helper-validator-identifier': 7.28.5 - js-tokens: 4.0.0 - picocolors: 1.1.1 - - '@babel/compat-data@7.29.3': {} - - '@babel/core@7.29.0': - dependencies: - '@babel/code-frame': 7.29.0 - '@babel/generator': 7.29.1 - '@babel/helper-compilation-targets': 7.28.6 - '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) - '@babel/helpers': 7.29.2 - '@babel/parser': 7.29.3 - '@babel/template': 7.28.6 - '@babel/traverse': 7.29.0 - '@babel/types': 7.29.0 - '@jridgewell/remapping': 2.3.5 - convert-source-map: 2.0.0 - debug: 4.4.3 - gensync: 1.0.0-beta.2 - json5: 2.2.3 - semver: 6.3.1 - transitivePeerDependencies: - - supports-color - - '@babel/generator@7.29.1': - dependencies: - '@babel/parser': 7.29.3 - '@babel/types': 7.29.0 - '@jridgewell/gen-mapping': 0.3.13 - '@jridgewell/trace-mapping': 0.3.31 - jsesc: 3.1.0 - - '@babel/helper-compilation-targets@7.28.6': - dependencies: - '@babel/compat-data': 7.29.3 - '@babel/helper-validator-option': 7.27.1 - browserslist: 4.28.2 - lru-cache: 5.1.1 - semver: 6.3.1 - - '@babel/helper-globals@7.28.0': {} - - '@babel/helper-module-imports@7.28.6': - dependencies: - '@babel/traverse': 7.29.0 - '@babel/types': 7.29.0 - transitivePeerDependencies: - - supports-color - - '@babel/helper-module-transforms@7.28.6(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-module-imports': 7.28.6 - '@babel/helper-validator-identifier': 7.28.5 - '@babel/traverse': 7.29.0 - transitivePeerDependencies: - - supports-color - - '@babel/helper-string-parser@7.27.1': {} - - '@babel/helper-validator-identifier@7.28.5': {} - - '@babel/helper-validator-option@7.27.1': {} - - '@babel/helpers@7.29.2': - dependencies: - '@babel/template': 7.28.6 - '@babel/types': 7.29.0 - - '@babel/parser@7.29.3': - dependencies: - '@babel/types': 7.29.0 - - '@babel/template@7.28.6': - dependencies: - '@babel/code-frame': 7.29.0 - '@babel/parser': 7.29.3 - '@babel/types': 7.29.0 - - '@babel/traverse@7.29.0': - dependencies: - '@babel/code-frame': 7.29.0 - '@babel/generator': 7.29.1 - '@babel/helper-globals': 7.28.0 - '@babel/parser': 7.29.3 - '@babel/template': 7.28.6 - '@babel/types': 7.29.0 - debug: 4.4.3 - transitivePeerDependencies: - - supports-color - - '@babel/types@7.29.0': - dependencies: - '@babel/helper-string-parser': 7.27.1 - '@babel/helper-validator-identifier': 7.28.5 - - '@emnapi/core@1.10.0': - dependencies: - '@emnapi/wasi-threads': 1.2.1 - tslib: 2.8.1 - optional: true - - '@emnapi/runtime@1.10.0': - dependencies: - tslib: 2.8.1 - optional: true - - '@emnapi/wasi-threads@1.2.1': - dependencies: - tslib: 2.8.1 - optional: true - - '@eslint-community/eslint-utils@4.9.1(eslint@9.39.4)': - dependencies: - eslint: 9.39.4 - eslint-visitor-keys: 3.4.3 - - '@eslint-community/regexpp@4.12.2': {} - - '@eslint/config-array@0.21.2': - dependencies: - '@eslint/object-schema': 2.1.7 - debug: 4.4.3 - minimatch: 3.1.5 - transitivePeerDependencies: - - supports-color - - '@eslint/config-helpers@0.4.2': - dependencies: - '@eslint/core': 0.17.0 - - '@eslint/core@0.17.0': - dependencies: - '@types/json-schema': 7.0.15 - - '@eslint/eslintrc@3.3.5': - dependencies: - ajv: 6.15.0 - debug: 4.4.3 - espree: 10.4.0 - globals: 14.0.0 - ignore: 5.3.2 - import-fresh: 3.3.1 - js-yaml: 4.1.1 - minimatch: 3.1.5 - strip-json-comments: 3.1.1 - transitivePeerDependencies: - - supports-color - - '@eslint/js@9.39.4': {} - - '@eslint/object-schema@2.1.7': {} - - '@eslint/plugin-kit@0.4.1': - dependencies: - '@eslint/core': 0.17.0 - levn: 0.4.1 - - '@humanfs/core@0.19.2': - dependencies: - '@humanfs/types': 0.15.0 - - '@humanfs/node@0.16.8': - dependencies: - '@humanfs/core': 0.19.2 - '@humanfs/types': 0.15.0 - '@humanwhocodes/retry': 0.4.3 - - '@humanfs/types@0.15.0': {} - - '@humanwhocodes/module-importer@1.0.1': {} - - '@humanwhocodes/retry@0.4.3': {} - - '@jridgewell/gen-mapping@0.3.13': - dependencies: - '@jridgewell/sourcemap-codec': 1.5.5 - '@jridgewell/trace-mapping': 0.3.31 - - '@jridgewell/remapping@2.3.5': - dependencies: - '@jridgewell/gen-mapping': 0.3.13 - '@jridgewell/trace-mapping': 0.3.31 - - '@jridgewell/resolve-uri@3.1.2': {} - - '@jridgewell/sourcemap-codec@1.5.5': {} - - '@jridgewell/trace-mapping@0.3.31': - dependencies: - '@jridgewell/resolve-uri': 3.1.2 - '@jridgewell/sourcemap-codec': 1.5.5 - - '@napi-rs/wasm-runtime@1.1.6(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': - dependencies: - '@emnapi/core': 1.10.0 - '@emnapi/runtime': 1.10.0 - '@tybys/wasm-util': 0.10.3 - optional: true - - '@oxc-project/types@0.133.0': {} - - '@rolldown/binding-android-arm64@1.0.3': - optional: true - - '@rolldown/binding-darwin-arm64@1.0.3': - optional: true - - '@rolldown/binding-darwin-x64@1.0.3': - optional: true - - '@rolldown/binding-freebsd-x64@1.0.3': - optional: true - - '@rolldown/binding-linux-arm-gnueabihf@1.0.3': - optional: true - - '@rolldown/binding-linux-arm64-gnu@1.0.3': - optional: true - - '@rolldown/binding-linux-arm64-musl@1.0.3': - optional: true - - '@rolldown/binding-linux-ppc64-gnu@1.0.3': - optional: true - - '@rolldown/binding-linux-s390x-gnu@1.0.3': - optional: true - - '@rolldown/binding-linux-x64-gnu@1.0.3': - optional: true - - '@rolldown/binding-linux-x64-musl@1.0.3': - optional: true - - '@rolldown/binding-openharmony-arm64@1.0.3': - optional: true - - '@rolldown/binding-wasm32-wasi@1.0.3': - dependencies: - '@emnapi/core': 1.10.0 - '@emnapi/runtime': 1.10.0 - '@napi-rs/wasm-runtime': 1.1.6(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) - optional: true - - '@rolldown/binding-win32-arm64-msvc@1.0.3': - optional: true - - '@rolldown/binding-win32-x64-msvc@1.0.3': - optional: true - - '@rolldown/pluginutils@1.0.0-rc.7': {} - - '@rolldown/pluginutils@1.0.1': {} - - '@tybys/wasm-util@0.10.3': - dependencies: - tslib: 2.8.1 - optional: true - - '@types/estree@1.0.9': {} - - '@types/json-schema@7.0.15': {} - - '@types/react-dom@19.2.3(@types/react@19.2.14)': - dependencies: - '@types/react': 19.2.14 - - '@types/react@19.2.14': - dependencies: - csstype: 3.2.3 - - '@vitejs/plugin-react@6.0.1(vite@8.0.16)': - dependencies: - '@rolldown/pluginutils': 1.0.0-rc.7 - vite: 8.0.16 - - acorn-jsx@5.3.2(acorn@8.16.0): - dependencies: - acorn: 8.16.0 - - acorn@8.16.0: {} - - ajv@6.15.0: - dependencies: - fast-deep-equal: 3.1.3 - fast-json-stable-stringify: 2.1.0 - json-schema-traverse: 0.4.1 - uri-js: 4.4.1 - - ansi-styles@4.3.0: - dependencies: - color-convert: 2.0.1 - - argparse@2.0.1: {} - - balanced-match@1.0.2: {} - - baseline-browser-mapping@2.10.29: {} - - brace-expansion@1.1.14: - dependencies: - balanced-match: 1.0.2 - concat-map: 0.0.1 - - browserslist@4.28.2: - dependencies: - baseline-browser-mapping: 2.10.29 - caniuse-lite: 1.0.30001792 - electron-to-chromium: 1.5.353 - node-releases: 2.0.38 - update-browserslist-db: 1.2.3(browserslist@4.28.2) - - callsites@3.1.0: {} - - caniuse-lite@1.0.30001792: {} - - chalk@4.1.2: - dependencies: - ansi-styles: 4.3.0 - supports-color: 7.2.0 - - color-convert@2.0.1: - dependencies: - color-name: 1.1.4 - - color-name@1.1.4: {} - - concat-map@0.0.1: {} - - convert-source-map@2.0.0: {} - - cross-spawn@7.0.6: - dependencies: - path-key: 3.1.1 - shebang-command: 2.0.0 - which: 2.0.2 - - csstype@3.2.3: {} - - debug@4.4.3: - dependencies: - ms: 2.1.3 - - deep-is@0.1.4: {} - - detect-libc@2.1.2: {} - - electron-to-chromium@1.5.353: {} - - escalade@3.2.0: {} - - escape-string-regexp@4.0.0: {} - - eslint-plugin-react-hooks@7.1.1(eslint@9.39.4): - dependencies: - '@babel/core': 7.29.0 - '@babel/parser': 7.29.3 - eslint: 9.39.4 - hermes-parser: 0.25.1 - zod: 4.4.3 - zod-validation-error: 4.0.2(zod@4.4.3) - transitivePeerDependencies: - - supports-color - - eslint-plugin-react-refresh@0.5.2(eslint@9.39.4): - dependencies: - eslint: 9.39.4 - - eslint-scope@8.4.0: - dependencies: - esrecurse: 4.3.0 - estraverse: 5.3.0 - - eslint-visitor-keys@3.4.3: {} - - eslint-visitor-keys@4.2.1: {} - - eslint@9.39.4: - dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4) - '@eslint-community/regexpp': 4.12.2 - '@eslint/config-array': 0.21.2 - '@eslint/config-helpers': 0.4.2 - '@eslint/core': 0.17.0 - '@eslint/eslintrc': 3.3.5 - '@eslint/js': 9.39.4 - '@eslint/plugin-kit': 0.4.1 - '@humanfs/node': 0.16.8 - '@humanwhocodes/module-importer': 1.0.1 - '@humanwhocodes/retry': 0.4.3 - '@types/estree': 1.0.9 - ajv: 6.15.0 - chalk: 4.1.2 - cross-spawn: 7.0.6 - debug: 4.4.3 - escape-string-regexp: 4.0.0 - eslint-scope: 8.4.0 - eslint-visitor-keys: 4.2.1 - espree: 10.4.0 - esquery: 1.7.0 - esutils: 2.0.3 - fast-deep-equal: 3.1.3 - file-entry-cache: 8.0.0 - find-up: 5.0.0 - glob-parent: 6.0.2 - ignore: 5.3.2 - imurmurhash: 0.1.4 - is-glob: 4.0.3 - json-stable-stringify-without-jsonify: 1.0.1 - lodash.merge: 4.6.2 - minimatch: 3.1.5 - natural-compare: 1.4.0 - optionator: 0.9.4 - transitivePeerDependencies: - - supports-color - - espree@10.4.0: - dependencies: - acorn: 8.16.0 - acorn-jsx: 5.3.2(acorn@8.16.0) - eslint-visitor-keys: 4.2.1 - - esquery@1.7.0: - dependencies: - estraverse: 5.3.0 - - esrecurse@4.3.0: - dependencies: - estraverse: 5.3.0 - - estraverse@5.3.0: {} - - esutils@2.0.3: {} - - fast-deep-equal@3.1.3: {} - - fast-json-stable-stringify@2.1.0: {} - - fast-levenshtein@2.0.6: {} - - fdir@6.5.0(picomatch@4.0.5): - optionalDependencies: - picomatch: 4.0.5 - - file-entry-cache@8.0.0: - dependencies: - flat-cache: 4.0.1 - - find-up@5.0.0: - dependencies: - locate-path: 6.0.0 - path-exists: 4.0.0 - - flat-cache@4.0.1: - dependencies: - flatted: 3.4.2 - keyv: 4.5.4 - - flatted@3.4.2: {} - - fsevents@2.3.3: - optional: true - - gensync@1.0.0-beta.2: {} - - glob-parent@6.0.2: - dependencies: - is-glob: 4.0.3 - - globals@14.0.0: {} - - globals@17.6.0: {} - - has-flag@4.0.0: {} - - hermes-estree@0.25.1: {} - - hermes-parser@0.25.1: - dependencies: - hermes-estree: 0.25.1 - - ignore@5.3.2: {} - - import-fresh@3.3.1: - dependencies: - parent-module: 1.0.1 - resolve-from: 4.0.0 - - imurmurhash@0.1.4: {} - - is-extglob@2.1.1: {} - - is-glob@4.0.3: - dependencies: - is-extglob: 2.1.1 - - isexe@2.0.0: {} - - js-tokens@4.0.0: {} - - js-yaml@4.1.1: - dependencies: - argparse: 2.0.1 - - jsesc@3.1.0: {} - - json-buffer@3.0.1: {} - - json-schema-traverse@0.4.1: {} - - json-stable-stringify-without-jsonify@1.0.1: {} - - json5@2.2.3: {} - - keyv@4.5.4: - dependencies: - json-buffer: 3.0.1 - - levn@0.4.1: - dependencies: - prelude-ls: 1.2.1 - type-check: 0.4.0 - - lightningcss-android-arm64@1.33.0: - optional: true - - lightningcss-darwin-arm64@1.33.0: - optional: true - - lightningcss-darwin-x64@1.33.0: - optional: true - - lightningcss-freebsd-x64@1.33.0: - optional: true - - lightningcss-linux-arm-gnueabihf@1.33.0: - optional: true - - lightningcss-linux-arm64-gnu@1.33.0: - optional: true - - lightningcss-linux-arm64-musl@1.33.0: - optional: true - - lightningcss-linux-x64-gnu@1.33.0: - optional: true - - lightningcss-linux-x64-musl@1.33.0: - optional: true - - lightningcss-win32-arm64-msvc@1.33.0: - optional: true - - lightningcss-win32-x64-msvc@1.33.0: - optional: true - - lightningcss@1.33.0: - dependencies: - detect-libc: 2.1.2 - optionalDependencies: - lightningcss-android-arm64: 1.33.0 - lightningcss-darwin-arm64: 1.33.0 - lightningcss-darwin-x64: 1.33.0 - lightningcss-freebsd-x64: 1.33.0 - lightningcss-linux-arm-gnueabihf: 1.33.0 - lightningcss-linux-arm64-gnu: 1.33.0 - lightningcss-linux-arm64-musl: 1.33.0 - lightningcss-linux-x64-gnu: 1.33.0 - lightningcss-linux-x64-musl: 1.33.0 - lightningcss-win32-arm64-msvc: 1.33.0 - lightningcss-win32-x64-msvc: 1.33.0 - - locate-path@6.0.0: - dependencies: - p-locate: 5.0.0 - - lodash.merge@4.6.2: {} - - lru-cache@5.1.1: - dependencies: - yallist: 3.1.1 - - minimatch@3.1.5: - dependencies: - brace-expansion: 1.1.14 - - ms@2.1.3: {} - - nanoid@3.3.16: {} - - natural-compare@1.4.0: {} - - node-releases@2.0.38: {} - - optionator@0.9.4: - dependencies: - deep-is: 0.1.4 - fast-levenshtein: 2.0.6 - levn: 0.4.1 - prelude-ls: 1.2.1 - type-check: 0.4.0 - word-wrap: 1.2.5 - - p-limit@3.1.0: - dependencies: - yocto-queue: 0.1.0 - - p-locate@5.0.0: - dependencies: - p-limit: 3.1.0 - - parent-module@1.0.1: - dependencies: - callsites: 3.1.0 - - path-exists@4.0.0: {} - - path-key@3.1.1: {} - - picocolors@1.1.1: {} - - picomatch@4.0.5: {} - - postcss@8.5.23: - dependencies: - nanoid: 3.3.16 - picocolors: 1.1.1 - source-map-js: 1.2.1 - - prelude-ls@1.2.1: {} - - punycode@2.3.1: {} - - react-dom@19.2.6(react@19.2.6): - dependencies: - react: 19.2.6 - scheduler: 0.27.0 - - react@19.2.6: {} - - resolve-from@4.0.0: {} - - rolldown@1.0.3: - dependencies: - '@oxc-project/types': 0.133.0 - '@rolldown/pluginutils': 1.0.1 - optionalDependencies: - '@rolldown/binding-android-arm64': 1.0.3 - '@rolldown/binding-darwin-arm64': 1.0.3 - '@rolldown/binding-darwin-x64': 1.0.3 - '@rolldown/binding-freebsd-x64': 1.0.3 - '@rolldown/binding-linux-arm-gnueabihf': 1.0.3 - '@rolldown/binding-linux-arm64-gnu': 1.0.3 - '@rolldown/binding-linux-arm64-musl': 1.0.3 - '@rolldown/binding-linux-ppc64-gnu': 1.0.3 - '@rolldown/binding-linux-s390x-gnu': 1.0.3 - '@rolldown/binding-linux-x64-gnu': 1.0.3 - '@rolldown/binding-linux-x64-musl': 1.0.3 - '@rolldown/binding-openharmony-arm64': 1.0.3 - '@rolldown/binding-wasm32-wasi': 1.0.3 - '@rolldown/binding-win32-arm64-msvc': 1.0.3 - '@rolldown/binding-win32-x64-msvc': 1.0.3 - - scheduler@0.27.0: {} - - semver@6.3.1: {} - - shebang-command@2.0.0: - dependencies: - shebang-regex: 3.0.0 - - shebang-regex@3.0.0: {} - - source-map-js@1.2.1: {} - - sql.js@1.14.1: {} - - strip-json-comments@3.1.1: {} - - supports-color@7.2.0: - dependencies: - has-flag: 4.0.0 - - tinyglobby@0.2.17: - dependencies: - fdir: 6.5.0(picomatch@4.0.5) - picomatch: 4.0.5 - - tslib@2.8.1: - optional: true - - type-check@0.4.0: - dependencies: - prelude-ls: 1.2.1 - - update-browserslist-db@1.2.3(browserslist@4.28.2): - dependencies: - browserslist: 4.28.2 - escalade: 3.2.0 - picocolors: 1.1.1 - - uri-js@4.4.1: - dependencies: - punycode: 2.3.1 - - vite@8.0.16: - dependencies: - lightningcss: 1.33.0 - picomatch: 4.0.5 - postcss: 8.5.23 - rolldown: 1.0.3 - tinyglobby: 0.2.17 - optionalDependencies: - fsevents: 2.3.3 - - which@2.0.2: - dependencies: - isexe: 2.0.0 - - word-wrap@1.2.5: {} - - yallist@3.1.1: {} - - yocto-queue@0.1.0: {} - - zod-validation-error@4.0.2(zod@4.4.3): - dependencies: - zod: 4.4.3 - - zod@4.4.3: {} diff --git a/2016/pnpm-workspace.yaml b/2016/pnpm-workspace.yaml deleted file mode 100644 index 65783dc..0000000 --- a/2016/pnpm-workspace.yaml +++ /dev/null @@ -1,2 +0,0 @@ -allowBuilds: - better-sqlite3: true diff --git a/2016/src/App.css b/2016/src/App.css deleted file mode 100644 index 9410c19..0000000 --- a/2016/src/App.css +++ /dev/null @@ -1,474 +0,0 @@ -.app { - max-width: 1200px; - margin: 0 auto; - padding: 2rem 1rem 3rem; -} - -/* Header */ -header { - text-align: center; - margin-bottom: 2rem; -} - -header h1 { - font-size: clamp(1.4rem, 2.4vw, 2rem); - color: var(--text); - margin: 0 0 0.4rem; - letter-spacing: -0.01em; - font-weight: 700; -} - -.subtitle { - color: var(--text-muted); - font-size: 0.95rem; - margin: 0; -} - -.subtitle .pill { - display: inline-block; - padding: 0.1rem 0.55rem; - background: var(--accent-soft); - color: var(--accent); - border-radius: 999px; - font-weight: 600; - font-size: 0.8rem; - margin-right: 0.35rem; -} - -/* Tabs */ -.tabs { - display: flex; - gap: 0; - max-width: 600px; - margin: 0 auto 1.5rem; - border-bottom: 1px solid var(--border); -} - -.tab { - flex: 1; - padding: 0.75rem 1rem; - font-size: 0.95rem; - background: none; - border: none; - border-bottom: 2px solid transparent; - margin-bottom: -1px; - cursor: pointer; - color: var(--text-muted); - font-weight: 500; - transition: color 0.15s, border-color 0.15s; -} - -.tab:hover { - color: var(--accent); -} - -.tab[aria-selected="true"] { - color: var(--accent); - border-bottom-color: var(--accent); - font-weight: 600; -} - -/* Search form */ -.search-form { - display: flex; - gap: 0.5rem; - max-width: 600px; - margin: 0 auto 1.5rem; - position: relative; -} - -.search-field { - flex: 1; - position: relative; -} - -.search-form input { - width: 100%; - padding: 0.75rem 2.25rem 0.75rem 1rem; - font-size: 1rem; - border: 1px solid var(--border-strong); - border-radius: var(--radius); - background: var(--surface); - color: var(--text); - outline: none; - transition: border-color 0.15s, box-shadow 0.15s; -} - -.search-form input:focus { - border-color: var(--accent); - box-shadow: 0 0 0 3px var(--accent-soft); -} - -.search-clear { - position: absolute; - right: 0.4rem; - top: 50%; - transform: translateY(-50%); - width: 1.75rem; - height: 1.75rem; - display: grid; - place-items: center; - border: none; - background: transparent; - color: var(--text-faint); - font-size: 1.1rem; - border-radius: 50%; - cursor: pointer; -} - -.search-clear:hover { - background: var(--surface-alt); - color: var(--text); -} - -.search-form button[type="submit"] { - padding: 0.75rem 1.5rem; - font-size: 0.95rem; - font-weight: 600; - background: var(--accent); - color: #fff; - border: none; - border-radius: var(--radius); - cursor: pointer; - transition: background 0.15s, transform 0.05s; -} - -.search-form button[type="submit"]:hover:not(:disabled) { - background: var(--accent-hover); -} - -.search-form button[type="submit"]:active:not(:disabled) { - transform: translateY(1px); -} - -.search-form button:disabled { - opacity: 0.5; - cursor: not-allowed; -} - -/* Custom query */ -.custom-query { - max-width: 960px; - margin: 0 auto; -} - -.preset-list { - display: flex; - flex-wrap: wrap; - gap: 0.4rem; - margin-bottom: 0.85rem; - align-items: center; -} - -.preset-label { - font-size: 0.8rem; - color: var(--text-muted); - white-space: nowrap; - margin-right: 0.25rem; - font-weight: 500; -} - -.preset-btn { - padding: 0.35rem 0.75rem; - font-size: 0.8rem; - background: var(--surface); - border: 1px solid var(--border); - color: var(--text); - border-radius: 999px; - cursor: pointer; - transition: background 0.15s, border-color 0.15s, color 0.15s; -} - -.preset-btn:hover:not(:disabled) { - background: var(--accent-soft); - border-color: var(--accent); - color: var(--accent); -} - -.preset-btn:disabled { - opacity: 0.5; - cursor: not-allowed; -} - -.query-form { - margin-bottom: 1rem; -} - -.query-form textarea { - width: 100%; - padding: 0.85rem 1rem; - font-size: 0.9rem; - font-family: - "JetBrains Mono", "Cascadia Code", "Fira Code", ui-monospace, monospace; - background: var(--surface); - color: var(--text); - border: 1px solid var(--border-strong); - border-radius: var(--radius); - outline: none; - resize: vertical; - min-height: 130px; - transition: border-color 0.15s, box-shadow 0.15s; - tab-size: 2; - line-height: 1.5; -} - -.query-form textarea:focus { - border-color: var(--accent); - box-shadow: 0 0 0 3px var(--accent-soft); -} - -.query-actions { - display: flex; - align-items: center; - gap: 1rem; - margin-top: 0.6rem; - flex-wrap: wrap; -} - -.query-actions button { - padding: 0.6rem 1.4rem; - font-size: 0.9rem; - font-weight: 600; - background: var(--accent); - color: #fff; - border: none; - border-radius: var(--radius); - cursor: pointer; - transition: background 0.15s; -} - -.query-actions button:hover:not(:disabled) { - background: var(--accent-hover); -} - -.query-actions button:disabled { - opacity: 0.5; - cursor: not-allowed; -} - -.exec-time { - font-size: 0.85rem; - color: var(--text-muted); - font-variant-numeric: tabular-nums; -} - -.exec-time.running { - color: var(--accent); -} - -.kbd { - display: inline-block; - padding: 0.05rem 0.4rem; - font-family: ui-monospace, monospace; - font-size: 0.75rem; - background: var(--surface-alt); - border: 1px solid var(--border); - border-bottom-width: 2px; - border-radius: 4px; - color: var(--text-muted); -} - -/* Loading */ -.loading { - max-width: 600px; - margin: 0.5rem auto 1.5rem; - padding: 0.85rem 1rem; - text-align: center; - color: var(--text-muted); - background: var(--surface); - border: 1px solid var(--border); - border-radius: var(--radius); - box-shadow: var(--shadow-sm); -} - -.loading p { - margin: 0 0 0.5rem; - font-size: 0.9rem; -} - -.progress-bar { - width: 100%; - max-width: 300px; - height: 6px; - background: var(--surface-alt); - border-radius: 4px; - margin: 0 auto; - overflow: hidden; -} - -.progress-fill { - height: 100%; - background: linear-gradient(90deg, var(--accent), var(--accent-hover)); - border-radius: 4px; - transition: width 0.3s; -} - -/* Results table */ -.table-wrapper { - overflow-x: auto; - margin-top: 1rem; - background: var(--surface); - border: 1px solid var(--border); - border-radius: var(--radius); - box-shadow: var(--shadow-sm); -} - -.result-count { - color: var(--text-muted); - font-size: 0.9rem; - margin: 0.5rem 0; - padding: 0 0.25rem; -} - -table { - width: 100%; - border-collapse: collapse; - font-size: 0.9rem; -} - -thead th { - background: var(--surface-alt); - padding: 0.65rem 0.6rem; - text-align: left; - white-space: nowrap; - position: sticky; - top: 0; - border-bottom: 1px solid var(--border-strong); - color: var(--text-muted); - font-weight: 600; - font-size: 0.8rem; - text-transform: uppercase; - letter-spacing: 0.03em; -} - -tbody td { - padding: 0.55rem 0.6rem; - border-bottom: 1px solid var(--border); -} - -tbody tr:last-child td { - border-bottom: none; -} - -tbody tr:nth-child(even) td { - background: color-mix(in srgb, var(--surface-alt) 45%, transparent); -} - -tbody tr:hover td { - background: var(--accent-soft); -} - -.sbd-cell { - font-family: ui-monospace, monospace; - font-size: 0.85rem; - color: var(--text-muted); - white-space: nowrap; -} - -.name-cell { - white-space: nowrap; - font-weight: 500; -} - -.cumthi-cell { - font-size: 0.82rem; - color: var(--text-muted); - max-width: 220px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.score-cell { - text-align: center; - font-variant-numeric: tabular-nums; -} - -.score-cell.score-high { - color: var(--ok); - font-weight: 600; -} - -.score-cell.score-low { - color: var(--danger); -} - -.score-cell.score-empty { - color: var(--text-faint); -} - -/* Messages */ -.no-results, -.error, -.warning { - text-align: center; - padding: 0.9rem 1rem; - border-radius: var(--radius); - margin: 1rem auto; - max-width: 600px; - font-size: 0.9rem; -} - -.no-results { - color: var(--text-muted); - background: var(--surface); - border: 1px dashed var(--border-strong); -} - -.error { - color: var(--danger); - background: var(--danger-bg); - border: 1px solid color-mix(in srgb, var(--danger) 25%, transparent); -} - -.warning { - color: var(--warn); - background: var(--warn-bg); -} - -/* Footer */ -footer { - text-align: center; - margin-top: 3rem; - padding-top: 1rem; - border-top: 1px solid var(--border); - color: var(--text-faint); - font-size: 0.85rem; -} - -footer a { - color: var(--text-muted); -} - -/* Responsive */ -@media (max-width: 640px) { - .app { - padding: 1.25rem 0.75rem 2rem; - } - - .search-form { - flex-direction: column; - } - - .search-form button[type="submit"] { - width: 100%; - } - - table { - font-size: 0.8rem; - } - - thead th, - tbody td { - padding: 0.45rem 0.4rem; - } - - .preset-list { - flex-direction: column; - align-items: stretch; - } - - .preset-btn { - text-align: left; - } -} diff --git a/2016/src/App.jsx b/2016/src/App.jsx deleted file mode 100644 index 494519c..0000000 --- a/2016/src/App.jsx +++ /dev/null @@ -1,174 +0,0 @@ -import { useState, useCallback, useEffect } from "react"; -import { useSqlite } from "./hooks/use-sqlite"; -import { SearchForm } from "./components/search-form"; -import { ScoreTable } from "./components/score-table"; -import { CustomQuery } from "./components/custom-query"; -import "./App.css"; - -const DB_URL = import.meta.env.BASE_URL + "thptqg2016.db.gz"; -const MAX_RESULTS = 100; - -// Strip Vietnamese diacritics for search: "nguyễn bữu lộc" → "nguyen buu loc" -function toAscii(str) { - return str - .normalize("NFD") - .replace(/[\u0300-\u036f]/g, "") - .replace(/đ/gi, "d") - .toLowerCase(); -} - -// Check if string contains only ASCII (no Vietnamese diacritics) -function isAsciiOnly(str) { - return /^[\x00-\x7F]*$/.test(str); -} - -function App() { - const { db, loading, error, progress } = useSqlite(DB_URL); - const [results, setResults] = useState(null); - const [searchError, setSearchError] = useState(null); - const [activeTab, setActiveTab] = useState("search"); - - const handleSearch = useCallback( - (query) => { - if (!db) return; - setSearchError(null); - - try { - const isExamId = /^[A-Z]{2,4}\d+$/i.test(query) || /^\d+$/.test(query); - let stmt; - - if (isExamId) { - stmt = db.prepare( - "SELECT * FROM student WHERE so_bao_danh = $q LIMIT $limit", - ); - stmt.bind({ $q: query.toUpperCase(), $limit: MAX_RESULTS }); - } else if (isAsciiOnly(query)) { - // ASCII input: search against normalized column (diacritics-insensitive) - stmt = db.prepare( - "SELECT * FROM student WHERE ho_ten_ascii LIKE $q LIMIT $limit", - ); - stmt.bind({ $q: `%${toAscii(query)}%`, $limit: MAX_RESULTS }); - } else { - // Vietnamese input: search both original and normalized - const normalized = toAscii(query); - stmt = db.prepare( - `SELECT * FROM student - WHERE ho_ten LIKE $q OR ho_ten_ascii LIKE $qn - LIMIT $limit`, - ); - stmt.bind({ - $q: `%${query}%`, - $qn: `%${normalized}%`, - $limit: MAX_RESULTS, - }); - } - - const rows = []; - while (stmt.step()) { - rows.push(stmt.getAsObject()); - } - stmt.free(); - setResults(rows); - } catch (err) { - setSearchError(err.message); - } - }, - [db], - ); - - // Ctrl+Enter shortcut to execute query in SQL tab - useEffect(() => { - function handleKeyDown(e) { - if (e.ctrlKey && e.key === "Enter" && activeTab === "sql") { - const form = document.querySelector(".query-form"); - if (form) form.requestSubmit(); - } - } - window.addEventListener("keydown", handleKeyDown); - return () => window.removeEventListener("keydown", handleKeyDown); - }, [activeTab]); - - return ( -
-
-

Tra cứu điểm thi THPT Quốc gia 2016

-

- 877.461 thí sinh - Dữ liệu toàn quốc · Hỗ trợ truy vấn SQL tùy chỉnh -

-
- -
- {loading && ( -
-

- Đang tải cơ sở dữ liệu{progress > 0 ? ` · ${progress}%` : "..."} -

-
-
-
-
- )} - - {error &&

Lỗi: {error}

} - -
- - -
- - {activeTab === "search" && ( - <> - - - {searchError && ( -

Lỗi truy vấn: {searchError}

- )} - - - - {results && results.length >= MAX_RESULTS && ( -

- Hiển thị tối đa {MAX_RESULTS} kết quả. Vui lòng tìm kiếm cụ - thể hơn. -

- )} - - )} - - {activeTab === "sql" && ( - - )} -
- -
-

- Nguồn: Sưu tầm từ trang báo thời đó · Dữ liệu chỉ mang tính tham - khảo -

-
-
- ); -} - -export default App; diff --git a/2016/src/components/score-table.jsx b/2016/src/components/score-table.jsx deleted file mode 100644 index e5258d6..0000000 --- a/2016/src/components/score-table.jsx +++ /dev/null @@ -1,74 +0,0 @@ -const SUBJECT_COLUMNS = [ - { key: "toan", label: "Toán" }, - { key: "ngu_van", label: "Ngữ văn" }, - { key: "vat_ly", label: "Vật lí" }, - { key: "hoa_hoc", label: "Hóa học" }, - { key: "sinh_hoc", label: "Sinh học" }, - { key: "lich_su", label: "Lịch sử" }, - { key: "dia_ly", label: "Địa lí" }, - { key: "tieng_anh", label: "T.Anh" }, - { key: "tieng_phap", label: "T.Pháp" }, - { key: "tieng_duc", label: "T.Đức" }, - { key: "tieng_nhat", label: "T.Nhật" }, - { key: "tieng_trung", label: "T.Trung" }, -]; - -function formatScore(val) { - if (val === null || val === undefined) return "—"; - return Number(val).toFixed(2); -} - -function scoreClass(val) { - if (val === null || val === undefined) return "score-cell score-empty"; - const num = Number(val); - if (num >= 8) return "score-cell score-high"; - if (num < 4) return "score-cell score-low"; - return "score-cell"; -} - -export function ScoreTable({ results }) { - if (!results) return null; - if (results.length === 0) { - return

Không tìm thấy kết quả.

; - } - - return ( - <> -

Tìm thấy {results.length} kết quả

-
- - - - - - - - - {SUBJECT_COLUMNS.map((col) => ( - - ))} - - - - {results.map((row) => ( - - - - - - - {SUBJECT_COLUMNS.map((col) => ( - - ))} - - ))} - -
SBDHọ tênNgày sinhCụm thiGT{col.label}
{row.so_bao_danh}{row.ho_ten}{row.ngay_sinh || "—"} - {row.ten_cum_thi || "—"} - {row.gioi_tinh || "—"} - {formatScore(row[col.key])} -
-
- - ); -} diff --git a/2016/src/components/search-form.jsx b/2016/src/components/search-form.jsx deleted file mode 100644 index bec23f7..0000000 --- a/2016/src/components/search-form.jsx +++ /dev/null @@ -1,46 +0,0 @@ -import { useState } from "react"; - -export function SearchForm({ onSearch, disabled }) { - const [query, setQuery] = useState(""); - - function handleSubmit(e) { - e.preventDefault(); - const trimmed = query.trim(); - if (trimmed) onSearch(trimmed); - } - - return ( -
-
- - setQuery(e.target.value)} - placeholder="Nhập SBD hoặc họ tên (VD: nguyen van a)..." - disabled={disabled} - autoFocus - autoComplete="off" - spellCheck={false} - /> - {query && ( - - )} -
- -
- ); -} diff --git a/2016/src/hooks/use-sqlite.js b/2016/src/hooks/use-sqlite.js deleted file mode 100644 index 71c6e3a..0000000 --- a/2016/src/hooks/use-sqlite.js +++ /dev/null @@ -1,83 +0,0 @@ -import { useState, useEffect, useRef } from "react"; -import initSqlJs from "sql.js"; - -const SQL_WASM_URL = "https://sql.js.org/dist/sql-wasm.wasm"; - -/** - * Hook to load a SQLite database from a gzipped URL into sql.js. - * Returns { db, loading, error, progress }. - */ -export function useSqlite(dbUrl) { - const [db, setDb] = useState(null); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - const [progress, setProgress] = useState(0); - const dbRef = useRef(null); - - useEffect(() => { - let cancelled = false; - - async function load() { - try { - const SQL = await initSqlJs({ locateFile: () => SQL_WASM_URL }); - - const response = await fetch(dbUrl); - if (!response.ok) - throw new Error(`Failed to fetch database: ${response.status}`); - - const contentLength = +response.headers.get("Content-Length") || 0; - const reader = response.body.getReader(); - const chunks = []; - let received = 0; - - while (true) { - const { done, value } = await reader.read(); - if (done) break; - chunks.push(value); - received += value.length; - if (contentLength > 0) { - setProgress(Math.round((received / contentLength) * 100)); - } - } - - if (cancelled) return; - - const blob = new Blob(chunks); - let arrayBuffer; - - if (dbUrl.endsWith(".gz")) { - const ds = new DecompressionStream("gzip"); - const decompressed = blob.stream().pipeThrough(ds); - const decompressedBlob = await new Response(decompressed).blob(); - arrayBuffer = await decompressedBlob.arrayBuffer(); - } else { - arrayBuffer = await blob.arrayBuffer(); - } - - if (cancelled) return; - - const database = new SQL.Database(new Uint8Array(arrayBuffer)); - dbRef.current = database; - setDb(database); - setLoading(false); - } catch (err) { - if (!cancelled) { - setError(err.message); - setLoading(false); - } - } - } - - load(); - - return () => { - cancelled = true; - if (dbRef.current) { - dbRef.current.close(); - dbRef.current = null; - } - }; - }, [dbUrl]); - - return { db, loading, error, progress }; -} diff --git a/2016/src/index.css b/2016/src/index.css deleted file mode 100644 index 52bc39d..0000000 --- a/2016/src/index.css +++ /dev/null @@ -1,74 +0,0 @@ -:root { - /* Light theme tokens */ - --bg: #f7f8fb; - --surface: #ffffff; - --surface-alt: #f2f4f9; - --border: #e3e6ee; - --border-strong: #cfd4e1; - --text: #1a1a2e; - --text-muted: #5c6275; - --text-faint: #8a8fa3; - --accent: #4361ee; - --accent-hover: #3a56d4; - --accent-soft: #e9edff; - --danger: #d32f2f; - --danger-bg: #fdecea; - --warn: #b45309; - --warn-bg: #fff4d6; - --ok: #0f8a4f; - --ok-bg: #e4f7ec; - --shadow-sm: 0 1px 2px rgba(20, 22, 38, 0.06); - --shadow-md: 0 4px 14px rgba(20, 22, 38, 0.08); - --radius: 10px; - --radius-sm: 6px; -} - -@media (prefers-color-scheme: dark) { - :root { - --bg: #0f1117; - --surface: #181b24; - --surface-alt: #21252f; - --border: #2a2f3d; - --border-strong: #3a4052; - --text: #eef0f6; - --text-muted: #a7adbe; - --text-faint: #6e7486; - --accent: #8aa1ff; - --accent-hover: #a6b8ff; - --accent-soft: #1e2647; - --danger: #ff7a7a; - --danger-bg: #3a1d1d; - --warn: #ffc875; - --warn-bg: #3a2a12; - --ok: #6ee2a4; - --ok-bg: #133524; - --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.5); - --shadow-md: 0 4px 14px rgba(0, 0, 0, 0.4); - } -} - -*, -*::before, -*::after { - box-sizing: border-box; -} - -html { - color-scheme: light dark; -} - -body { - margin: 0; - font-family: - "Inter", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; - -webkit-font-smoothing: antialiased; - background: var(--bg); - color: var(--text); - line-height: 1.5; -} - -:focus-visible { - outline: 2px solid var(--accent); - outline-offset: 2px; - border-radius: 4px; -} diff --git a/2016/src/main.jsx b/2016/src/main.jsx deleted file mode 100644 index fdce256..0000000 --- a/2016/src/main.jsx +++ /dev/null @@ -1,10 +0,0 @@ -import { StrictMode } from "react"; -import { createRoot } from "react-dom/client"; -import "./index.css"; -import App from "./App.jsx"; - -createRoot(document.getElementById("root")).render( - - - , -); diff --git a/2016/tools/sync-from-thptqg2017.sh b/2016/tools/sync-from-thptqg2017.sh deleted file mode 100755 index fd86408..0000000 --- a/2016/tools/sync-from-thptqg2017.sh +++ /dev/null @@ -1,44 +0,0 @@ -#!/usr/bin/env bash -# Re-sync xlsxread Rust source from thptqg2017. -# -# Source SHA: 8b4a755c115595bf1b937d749eb1133efb3a6e22 (chore/xlsxread-rust) -# -# Re-run this when xlsxread is updated upstream. The configs/ directory is -# NOT synced — it contains thptqg2016-specific configs and test stubs that -# must be maintained here independently. -# -# Usage: -# ./tools/sync-from-thptqg2017.sh /path/to/thptqg2017 -# -set -euo pipefail - -SRC="${1:?Usage: $0 /path/to/thptqg2017}" - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -DEST="$SCRIPT_DIR/xlsxread" - -if [ ! -d "$SRC/tools/xlsxread" ]; then - echo "Error: $SRC/tools/xlsxread not found" >&2 - exit 1 -fi - -echo "Syncing from: $SRC/tools/xlsxread" -echo "Syncing to: $DEST" -echo "" - -# Sync source, tests, and Cargo manifests — exclude build artifacts and dataset configs -for item in src tests Cargo.toml Cargo.lock; do - if [ -e "$SRC/tools/xlsxread/$item" ]; then - cp -r "$SRC/tools/xlsxread/$item" "$DEST/" - echo " synced: $item" - fi -done - -echo "" -echo "Sync complete." -echo "Next steps:" -echo " 1. Review src/ for breaking changes to config.rs / writer.rs that" -echo " may affect format_detect_2016.rs or the thptqg2016-data.toml config." -echo " 2. Rebuild: cargo build --release --manifest-path $DEST/Cargo.toml" -echo " 3. Test: cargo test --manifest-path $DEST/Cargo.toml" -echo " 4. Commit on a chore/xlsxread-sync-... branch." diff --git a/2016/tools/xlsxread/configs/thptqg2016-data.toml b/2016/tools/xlsxread/configs/thptqg2016-data.toml deleted file mode 100644 index 646ad68..0000000 --- a/2016/tools/xlsxread/configs/thptqg2016-data.toml +++ /dev/null @@ -1,86 +0,0 @@ -# Config for thptqg2016 data/ — 4 .xls + 115 .xlsx mixed files. -# -# Three column layouts exist across the 119 files; the binary selects the -# right one per-file at runtime via format_detection = "thptqg2016": -# -# separate-scores header SBD(0)/HOTEN(1)/TOAN(2)... — dhhanghai files -# mapped header SOBAODANH|SBD + DIEM_THI — most provinces -# default no header; positional 6-col layout — remaining files -# -# Schema differences from thptqg2017: -# + ten_cum_thi TEXT (exam-cluster name, TEN_CUMTHI column) -# + gioi_tinh TEXT (gender: "Nam"/"Nữ", GIOI_TINH column) -# + tieng_duc REAL (German) -# + tieng_nhat REAL (Japanese) -# - khtn, khxh, tieng_nga (not in 2016 dataset) -# -# sheet_mode = "all": several provinces overflow into Sheet2 (65k Excel row cap). -# strip_blank_rows = false: no blank-row anomaly observed in this dataset. - -format_detection = "thptqg2016" - -[reader] -sheet_mode = "all" -strip_blank_rows = false - -[validation] -require_numeric_sbd = false -require_nonempty_name = true -require_nonempty_sbd = true - -[header] -# Tokens that identify a header row by first-cell content (uppercased). -# Covers both SOBAODANH-style and SBD-style headers. -tokens = ["SOBAODANH", "SBD", "HO_TEN", "HOTEN", "HỌ TÊN", "STT"] - -[schema] -ddl = """ -CREATE TABLE student ( - so_bao_danh TEXT PRIMARY KEY, - ho_ten TEXT NOT NULL, - ho_ten_ascii TEXT NOT NULL, - ngay_sinh TEXT, - ten_cum_thi TEXT, - gioi_tinh TEXT, - toan REAL, - ngu_van REAL, - vat_ly REAL, - hoa_hoc REAL, - sinh_hoc REAL, - lich_su REAL, - dia_ly REAL, - tieng_anh REAL, - tieng_phap REAL, - tieng_duc REAL, - tieng_nhat REAL, - tieng_trung REAL -); -CREATE INDEX idx_ho_ten ON student(ho_ten); -CREATE INDEX idx_ho_ten_ascii ON student(ho_ten_ascii); -CREATE INDEX idx_ten_cum_thi ON student(ten_cum_thi); -""" - -[scores] -toan = 'Toán:\s*(\d+(?:\.\d+)?)' -ngu_van = 'Ngữ văn:\s*(\d+(?:\.\d+)?)' -vat_ly = 'Vật lí:\s*(\d+(?:\.\d+)?)' -hoa_hoc = 'Hóa học:\s*(\d+(?:\.\d+)?)' -sinh_hoc = 'Sinh học:\s*(\d+(?:\.\d+)?)' -lich_su = 'Lịch sử:\s*(\d+(?:\.\d+)?)' -dia_ly = 'Địa lí:\s*(\d+(?:\.\d+)?)' -tieng_anh = 'Tiếng Anh:\s*(\d+(?:\.\d+)?)' -tieng_phap = 'Tiếng Pháp:\s*(\d+(?:\.\d+)?)' -tieng_duc = 'Tiếng Đức:\s*(\d+(?:\.\d+)?)' -tieng_nhat = 'Tiếng Nhật:\s*(\d+(?:\.\d+)?)' -tieng_trung = 'Tiếng Trung:\s*(\d+(?:\.\d+)?)' - -[insert] -sql = """ -INSERT OR REPLACE INTO student - (so_bao_danh, ho_ten, ho_ten_ascii, ngay_sinh, ten_cum_thi, gioi_tinh, - toan, ngu_van, vat_ly, hoa_hoc, sinh_hoc, - lich_su, dia_ly, - tieng_anh, tieng_phap, tieng_duc, tieng_nhat, tieng_trung) -VALUES - (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) -""" diff --git a/2016/tools/xlsxread/configs/thptqg2017-data-old.toml b/2016/tools/xlsxread/configs/thptqg2017-data-old.toml deleted file mode 100644 index 34d59c0..0000000 --- a/2016/tools/xlsxread/configs/thptqg2017-data-old.toml +++ /dev/null @@ -1,76 +0,0 @@ -# Test-only config used by golden integration tests (data-old variant). -# Same column layout as thptqg2017-data.toml but with: -# - sheet_mode = "first" (reads only first sheet) -# - require_numeric_sbd = true (rejects non-digit SBDs) -# Schema and INSERT match SCORE_FIELDS_2017 (14 score cols). - -[reader] -sheet_mode = "first" -strip_blank_rows = false - -[columns] -ho_ten = 0 -ngay_sinh = 1 -so_bao_danh = 2 -diem_thi = 3 - -[validation] -require_numeric_sbd = true -require_nonempty_name = true -require_nonempty_sbd = true - -[header] -tokens = ["HO_TEN", "HỌ TÊN", "STT"] - -[schema] -ddl = """ -CREATE TABLE student ( - so_bao_danh TEXT PRIMARY KEY, - ho_ten TEXT NOT NULL, - ho_ten_ascii TEXT NOT NULL, - ngay_sinh TEXT, - toan REAL, - ngu_van REAL, - vat_ly REAL, - hoa_hoc REAL, - sinh_hoc REAL, - khtn REAL, - lich_su REAL, - dia_ly REAL, - gdcd REAL, - khxh REAL, - tieng_anh REAL, - tieng_phap REAL, - tieng_nga REAL, - tieng_trung REAL -); -CREATE INDEX idx_ho_ten ON student(ho_ten); -CREATE INDEX idx_ho_ten_ascii ON student(ho_ten_ascii); -""" - -[scores] -toan = 'Toán:\s*(\d+(?:\.\d+)?)' -ngu_van = 'Ngữ văn:\s*(\d+(?:\.\d+)?)' -vat_ly = 'Vật lí:\s*(\d+(?:\.\d+)?)' -hoa_hoc = 'Hóa học:\s*(\d+(?:\.\d+)?)' -sinh_hoc = 'Sinh học:\s*(\d+(?:\.\d+)?)' -khtn = 'KHTN:\s*(\d+(?:\.\d+)?)' -lich_su = 'Lịch sử:\s*(\d+(?:\.\d+)?)' -dia_ly = 'Địa lí:\s*(\d+(?:\.\d+)?)' -gdcd = 'GDCD:\s*(\d+(?:\.\d+)?)' -khxh = 'KHXH:\s*(\d+(?:\.\d+)?)' -tieng_anh = 'Tiếng Anh:\s*(\d+(?:\.\d+)?)' -tieng_phap = 'Tiếng Pháp:\s*(\d+(?:\.\d+)?)' -tieng_nga = 'Tiếng Nga:\s*(\d+(?:\.\d+)?)' -tieng_trung = 'Tiếng Trung:\s*(\d+(?:\.\d+)?)' - -[insert] -sql = """ -INSERT OR REPLACE INTO student - (so_bao_danh, ho_ten, ho_ten_ascii, ngay_sinh, - toan, ngu_van, vat_ly, hoa_hoc, sinh_hoc, khtn, - lich_su, dia_ly, gdcd, khxh, - tieng_anh, tieng_phap, tieng_nga, tieng_trung) -VALUES - (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) -""" diff --git a/2016/tools/xlsxread/configs/thptqg2017-data.toml b/2016/tools/xlsxread/configs/thptqg2017-data.toml deleted file mode 100644 index 892fc6f..0000000 --- a/2016/tools/xlsxread/configs/thptqg2017-data.toml +++ /dev/null @@ -1,76 +0,0 @@ -# Test-only config used by golden integration tests. -# Matches the fixture row layout produced by tests/golden.rs: write_xlsx() -# header: HO_TEN(0) NGAY_SINH(1) SO_BAO_DANH(2) DIEM_THI(3) -# Schema and INSERT match SCORE_FIELDS_2017 (14 score cols) so run_build_cmd -# in golden.rs can call insert_row(..., SCORE_FIELDS) without modification. - -[reader] -sheet_mode = "all" -strip_blank_rows = false - -[columns] -ho_ten = 0 -ngay_sinh = 1 -so_bao_danh = 2 -diem_thi = 3 - -[validation] -require_numeric_sbd = false -require_nonempty_name = true -require_nonempty_sbd = true - -[header] -tokens = ["HO_TEN", "HỌ TÊN", "STT"] - -[schema] -ddl = """ -CREATE TABLE student ( - so_bao_danh TEXT PRIMARY KEY, - ho_ten TEXT NOT NULL, - ho_ten_ascii TEXT NOT NULL, - ngay_sinh TEXT, - toan REAL, - ngu_van REAL, - vat_ly REAL, - hoa_hoc REAL, - sinh_hoc REAL, - khtn REAL, - lich_su REAL, - dia_ly REAL, - gdcd REAL, - khxh REAL, - tieng_anh REAL, - tieng_phap REAL, - tieng_nga REAL, - tieng_trung REAL -); -CREATE INDEX idx_ho_ten ON student(ho_ten); -CREATE INDEX idx_ho_ten_ascii ON student(ho_ten_ascii); -""" - -[scores] -toan = 'Toán:\s*(\d+(?:\.\d+)?)' -ngu_van = 'Ngữ văn:\s*(\d+(?:\.\d+)?)' -vat_ly = 'Vật lí:\s*(\d+(?:\.\d+)?)' -hoa_hoc = 'Hóa học:\s*(\d+(?:\.\d+)?)' -sinh_hoc = 'Sinh học:\s*(\d+(?:\.\d+)?)' -khtn = 'KHTN:\s*(\d+(?:\.\d+)?)' -lich_su = 'Lịch sử:\s*(\d+(?:\.\d+)?)' -dia_ly = 'Địa lí:\s*(\d+(?:\.\d+)?)' -gdcd = 'GDCD:\s*(\d+(?:\.\d+)?)' -khxh = 'KHXH:\s*(\d+(?:\.\d+)?)' -tieng_anh = 'Tiếng Anh:\s*(\d+(?:\.\d+)?)' -tieng_phap = 'Tiếng Pháp:\s*(\d+(?:\.\d+)?)' -tieng_nga = 'Tiếng Nga:\s*(\d+(?:\.\d+)?)' -tieng_trung = 'Tiếng Trung:\s*(\d+(?:\.\d+)?)' - -[insert] -sql = """ -INSERT OR REPLACE INTO student - (so_bao_danh, ho_ten, ho_ten_ascii, ngay_sinh, - toan, ngu_van, vat_ly, hoa_hoc, sinh_hoc, khtn, - lich_su, dia_ly, gdcd, khxh, - tieng_anh, tieng_phap, tieng_nga, tieng_trung) -VALUES - (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) -""" diff --git a/2016/tools/xlsxread/src/writer.rs b/2016/tools/xlsxread/src/writer.rs deleted file mode 100644 index 07a83f0..0000000 --- a/2016/tools/xlsxread/src/writer.rs +++ /dev/null @@ -1,201 +0,0 @@ -/// SQLite writer: DDL setup, batched INSERT OR REPLACE, VACUUM, stats output. -/// -/// Mirrors build-lib.js createDb + the transaction loop in each build-database*.js. -/// Stats output lines match the JS stdout exactly so existing CI log-greps still work. -/// -/// thptqg2016 differences vs thptqg2017: -/// - INSERT includes ten_cum_thi and gioi_tinh after ngay_sinh -/// - Score columns are toan/ngu_van/vat_ly/hoa_hoc/sinh_hoc/lich_su/dia_ly/ -/// tieng_anh/tieng_phap/tieng_duc/tieng_nhat/tieng_trung -/// (no khtn, khxh, tieng_nga) -use std::fs; -use std::path::Path; - -use rusqlite::{params_from_iter, Connection, ToSql}; - -use crate::config::DatasetConfig; -use crate::error::BuildError; -use crate::transform::ParsedRow; - -// --------------------------------------------------------------------------- -// DB initialisation — mirrors build-lib.js createDb (delete + recreate) -// --------------------------------------------------------------------------- - -/// Open (or recreate) the output SQLite database, execute the DDL from config, -/// and return the open connection ready for inserts. -pub fn open_db(db_path: &Path, cfg: &DatasetConfig) -> Result { - // Mirror Node behaviour: delete existing file before creating (build-lib.js:54) - if db_path.exists() { - fs::remove_file(db_path).map_err(|e| BuildError::Io { - path: db_path.display().to_string(), - source: e, - })?; - } - - // Ensure parent directory exists - if let Some(parent) = db_path.parent() { - if !parent.as_os_str().is_empty() { - fs::create_dir_all(parent).map_err(|e| BuildError::Io { - path: parent.display().to_string(), - source: e, - })?; - } - } - - let conn = Connection::open(db_path)?; - conn.execute_batch(&cfg.schema.ddl)?; - Ok(conn) -} - -// --------------------------------------------------------------------------- -// Score field lists — one per dataset schema -// --------------------------------------------------------------------------- - -/// thptqg2017 score columns (14 fields including khtn/khxh/tieng_nga). -pub const SCORE_FIELDS_2017: &[&str] = &[ - "toan", - "ngu_van", - "vat_ly", - "hoa_hoc", - "sinh_hoc", - "khtn", - "lich_su", - "dia_ly", - "gdcd", - "khxh", - "tieng_anh", - "tieng_phap", - "tieng_nga", - "tieng_trung", -]; - -/// thptqg2016 score columns (12 fields: tieng_duc/tieng_nhat present; no khtn/khxh/tieng_nga). -pub const SCORE_FIELDS_2016: &[&str] = &[ - "toan", - "ngu_van", - "vat_ly", - "hoa_hoc", - "sinh_hoc", - "lich_su", - "dia_ly", - "tieng_anh", - "tieng_phap", - "tieng_duc", - "tieng_nhat", - "tieng_trung", -]; - -/// Alias kept so thptqg2017 callers that import SCORE_FIELDS continue to compile. -pub const SCORE_FIELDS: &[&str] = SCORE_FIELDS_2017; - -// --------------------------------------------------------------------------- -// Insert a single parsed row (thptqg2017 schema — no ten_cum_thi / gioi_tinh) -// --------------------------------------------------------------------------- - -/// Bind all fields from `row` into the prepared statement and execute it. -/// Positional params: so_bao_danh, ho_ten, ho_ten_ascii, ngay_sinh, -pub fn insert_row( - conn: &Connection, - sql: &str, - row: &ParsedRow, - score_fields: &[&str], -) -> Result<(), BuildError> { - let mut params: Vec> = Vec::with_capacity(4 + score_fields.len()); - params.push(Box::new(row.so_bao_danh.clone())); - params.push(Box::new(row.ho_ten.clone())); - params.push(Box::new(row.ho_ten_ascii.clone())); - params.push(Box::new(row.ngay_sinh.clone())); - - for field in score_fields { - let val: Option = row.scores.get(*field).copied(); - params.push(Box::new(val)); - } - - conn.execute(sql, params_from_iter(params.iter().map(|p| p.as_ref())))?; - Ok(()) -} - -// --------------------------------------------------------------------------- -// Insert a single parsed row (thptqg2016 schema — includes ten_cum_thi + gioi_tinh) -// --------------------------------------------------------------------------- - -/// Bind all fields from `row` into the prepared statement for the thptqg2016 schema. -/// Positional params: so_bao_danh, ho_ten, ho_ten_ascii, ngay_sinh, ten_cum_thi, -/// gioi_tinh, -pub fn insert_row_2016( - conn: &Connection, - sql: &str, - row: &ParsedRow, - score_fields: &[&str], -) -> Result<(), BuildError> { - let mut params: Vec> = Vec::with_capacity(6 + score_fields.len()); - params.push(Box::new(row.so_bao_danh.clone())); - params.push(Box::new(row.ho_ten.clone())); - params.push(Box::new(row.ho_ten_ascii.clone())); - params.push(Box::new(row.ngay_sinh.clone())); - params.push(Box::new(row.ten_cum_thi.clone())); - params.push(Box::new(row.gioi_tinh.clone())); - - for field in score_fields { - let val: Option = row.scores.get(*field).copied(); - params.push(Box::new(val)); - } - - conn.execute(sql, params_from_iter(params.iter().map(|p| p.as_ref())))?; - Ok(()) -} - -// --------------------------------------------------------------------------- -// Post-build: VACUUM + stats output -// --------------------------------------------------------------------------- - -/// Run VACUUM and print statistics lines that mirror the Node scripts' stdout. -/// The exact prefix tokens ("Source data rows", "DB rows", "Size:") are preserved -/// so any log-grep in the deploy pipeline keeps working. -#[allow(clippy::too_many_arguments)] -pub fn finish_db( - conn: &Connection, - db_path: &Path, - source_rows: u64, - skipped: u64, - errors: u64, - dataset_label: &str, - _file_count: usize, - is_old2: bool, -) -> Result<(), BuildError> { - conn.execute_batch("VACUUM")?; - - let db_count: i64 = conn.query_row("SELECT COUNT(*) FROM student", [], |row| row.get(0))?; - - let insertable = source_rows - skipped; - - println!(); - if is_old2 { - println!("Source non-blank data rows: {source_rows}"); - println!(" skipped (empty/non-numeric SBD): {skipped}"); - } else { - println!("Source data rows (post-header): {source_rows}"); - if dataset_label.contains("old") { - println!(" skipped (empty/non-numeric SBD): {skipped}"); - } else { - println!(" skipped (empty/invalid): {skipped}"); - } - } - println!(" insertable: {insertable}"); - println!(" insert errors: {errors}"); - println!("DB rows (distinct SBD): {db_count}"); - - if !dataset_label.contains("old") && errors == 0 { - let gap = insertable as i64 - db_count; - if gap == 0 { - println!("Audit: OK — every source row made it in."); - } else { - println!("Audit: {gap} row(s) collapsed (duplicate SBDs overwriting)."); - } - } - - let sz = fs::metadata(db_path).map(|m| m.len()).unwrap_or(0); - println!("Size: {:.1} MB", sz as f64 / 1024.0 / 1024.0); - - Ok(()) -} diff --git a/2016/vite.config.js b/2016/vite.config.js deleted file mode 100644 index e6ac920..0000000 --- a/2016/vite.config.js +++ /dev/null @@ -1,7 +0,0 @@ -import { defineConfig } from "vite"; -import react from "@vitejs/plugin-react"; - -export default defineConfig({ - plugins: [react()], - base: "/thptqg/2016/", -}); diff --git a/2017/.gitignore b/2017/.gitignore deleted file mode 100644 index 6f4752f..0000000 --- a/2017/.gitignore +++ /dev/null @@ -1,27 +0,0 @@ -.idea -*.iws -*.iml -*.ipr - -### VS Code ### -.vscode/ - -### Mac OS ### -.DS_Store - -### Node ### -node_modules/ - -### Generated databases (all 3 variants) ### -public/thptqg2017.db -public/thptqg2017.db.gz -public-old/thptqg2017.db -public-old/thptqg2017.db.gz -public-old2/thptqg2017.db -public-old2/thptqg2017.db.gz - -### Build output ### -dist/ - -### Rust build artefacts ### -tools/xlsxread/target/ diff --git a/2017/LICENSE b/2017/LICENSE deleted file mode 100644 index 261eeb9..0000000 --- a/2017/LICENSE +++ /dev/null @@ -1,201 +0,0 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. diff --git a/2017/README.md b/2017/README.md deleted file mode 100644 index 6f9ec13..0000000 --- a/2017/README.md +++ /dev/null @@ -1,113 +0,0 @@ -# Vietnam THPT 2017 Score Lookup - -Static React + SQLite site for looking up Vietnamese high school graduation exam scores (2017). The full database (~861k students, 63 provinces) ships to the browser as a compressed SQLite file and queries run client-side via `sql.js` — no backend. - -## Live sites - -Three deployments, one per dataset: - -| URL | Data source | Rows | -|---|---|---| -| `/thptqg2017/` | `data/` — [baotintuc.vn](https://baotintuc.vn/tuyen-sinh/tra-cuu-diem-thi-thpt-2017-cua-63-tinh-thanh-pho-tren-baotintucvn-20170706073512672.htm) CDN, `.xls` | **861,068** | -| `/thptqg2017/old/` | `data-old/` — earlier `.xlsx` collection from Vietnamese news sites at the time (exact origin no longer known) | 847,348 | -| `/thptqg2017/old2/` | `data-old2/` — partial re-export (54 provinces) also from contemporary news sites, exact origin unrecorded | 679,764 | - -## Features - -- Diacritics-insensitive name search (`"nguyen"` matches `"Nguyễn"`) -- Live debounced search with URL deep-link (`?q=49008235`) -- Single-result detail card with per-subject TFT rarity-tiered scores (6 tiers, ≤1 → 9-10) and all 49 admission blocks (A00 – D15) -- Share button: copies a formatted summary plus deep-link URL -- SQL query tab with grouped presets (rankings, Long An filters, statistics, schema) -- Light + dark mode (follows OS preference) -- Keyboard shortcut `/` to focus search - -## Requirements - -- Node.js 24+ -- pnpm -- Rust (stable) — installed via [rustup](https://rustup.rs/); required for `build:db*` scripts - -## Quickstart - -```bash -pnpm install -pnpm build:db # compile xlsxread + parse data/ → public/thptqg2017.db (~2 min, 159 MB) -gzip -kf -9 public/thptqg2017.db -pnpm dev # http://localhost:5173 -``` - -The database build pipeline uses the `xlsxread` Rust CLI located at -`tools/xlsxread/`. It reads `.xls`/`.xlsx` source files, strips -diacritics, parses score text, and writes a SQLite database — replacing -the former Node.js + `xlsx` (SheetJS) pipeline. See -`tools/xlsxread/README.md` for CLI invocation details and config schema. - -## Scripts - -| Command | Action | -|---|---| -| `pnpm dev` | Vite dev server | -| `pnpm build` | Production build (main variant → `dist/`) | -| `pnpm build:old` / `build:old2` | Build variant sites to `dist/old/`, `dist/old2/` | -| `pnpm build:all` | All 3 web variants | -| `pnpm build:rust` | Compile `tools/xlsxread` release binary (run once; auto-called by `build:db*`) | -| `pnpm build:db` | Build main DB from `data/` via xlsxread | -| `pnpm build:db:old` / `build:db:old2` | Build old / old2 variant DBs via xlsxread | -| `pnpm build:db:all` | All 3 DBs via xlsxread | -| `pnpm lint` | ESLint | -| `node scripts/crawl-baotintuc.js` | Re-download all 63 province files from baotintuc.vn | -| `node scripts/check-duplicates.js` | MD5 + row-content duplicate audit | -| `node scripts/diff-datasets.js` | Compare `public/` vs `backup/` DB (when backup present) | - -## Project layout - -``` -. -├── data/ # 63 .xls files (source) -├── data-old/ # 63 .xlsx (previous export) -├── data-old2/ # 54 .xlsx (update/ overrides) -├── public/ # main variant assets + thptqg2017.db.gz -├── public-old/ # old variant assets -├── public-old2/ # old2 variant assets -├── scripts/ -│ ├── crawl-baotintuc.js # downloader -│ ├── check-duplicates.js # md5 dup detector -│ └── diff-datasets.js # DB-to-DB comparator -├── src/ -│ ├── App.jsx -│ ├── App.css -│ ├── components/{search-form, score-table, student-detail, custom-query}.jsx -│ ├── hooks/use-sqlite.js -│ └── lib/admission-blocks.js # 49 admission-block definitions + score-tier helper -├── tools/ -│ └── xlsxread/ # Rust CLI — reads .xls/.xlsx, writes SQLite -│ ├── configs/ -│ │ ├── thptqg2017-data.toml # config for data/ (63 .xls, all sheets) -│ │ ├── thptqg2017-data-old.toml # config for data-old/ (63 .xlsx, sheet 0) -│ │ └── thptqg2017-data-old2.toml # config for data-old2/ (54 .xlsx, all sheets) -│ ├── src/ # Rust source -│ └── README.md # CLI reference + config schema -├── docs/ # see docs/README.md -├── index.html -├── vite.config.js -└── package.json -``` - -See `docs/` for architecture + deployment details. - -## License - -See `LICENSE`. - -## Source - -- **`data/`** — scraped in full from the baotintuc.vn 2017 announcement article: - `https://baotintuc.vn/tuyen-sinh/tra-cuu-diem-thi-thpt-2017-cua-63-tinh-thanh-pho-tren-baotintucvn-20170706073512672.htm` - Reproducible via `node scripts/crawl-baotintuc.js`. -- **`data-old/`** and **`data-old2/`** — collected from Vietnamese news sites at the - time of the 2017 exam. The specific publisher URLs were not recorded and cannot - be recovered now. These datasets are preserved for comparison and historical - reference only; `data/` is the canonical source for the main deployment. - -Intended for reference only. diff --git a/2017/docs/README.md b/2017/docs/README.md deleted file mode 100644 index a16c8b6..0000000 --- a/2017/docs/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# Docs - -- [`system-architecture.md`](./system-architecture.md) — data flow, schema, 3-variant deploy, score-tier model, admission-block catalog -- [`data-pipeline.md`](./data-pipeline.md) — Excel parse quirks, overflow-sheet gotcha, audit scripts, refresh flow -- [`deployment-guide.md`](./deployment-guide.md) — GitHub Pages workflow, adding a new variant, rollback diff --git a/2017/docs/data-pipeline.md b/2017/docs/data-pipeline.md deleted file mode 100644 index 589f7d4..0000000 --- a/2017/docs/data-pipeline.md +++ /dev/null @@ -1,110 +0,0 @@ -# Data Pipeline - -From raw Excel files to a compressed SQLite file the browser can load. - -## Canonical source (data/) - -63 Excel files from baotintuc.vn CDN. Source article: -`https://baotintuc.vn/tuyen-sinh/tra-cuu-diem-thi-thpt-2017-cua-63-tinh-thanh-pho-tren-baotintucvn-20170706073512672.htm` - -Re-download anytime: - -```bash -node scripts/crawl-baotintuc.js -``` - -Idempotent — skips files already present. Saves to `data/.xls`. - -## Reference sources (data-old/, data-old2/) - -Both were collected from Vietnamese news sites around the time of the 2017 exam. -Specific publisher URLs were not recorded and cannot be recovered, so these -datasets are **not reproducible** — the files committed in git are the only -copy. They are kept for historical comparison and to expose the deploy matrix; -the main deployment always uses the reproducible `data/` source. - -## Source Excel shape - -Every file is a single sheet (for `data-old/`) or up to 3 sheets (for `data/` .xls) with columns: - -| Col | Name | Content | -|---|---|---| -| 0 | HO_TEN | full name in Vietnamese | -| 1 | NGAY_SINH | `dd/mm/yyyy` | -| 2 | SOBAODANH | 8-digit string, first 2 digits = province | -| 3 | DIEM_THI | concatenated per-subject score string in the source language, e.g. `"Toan: 6.80 Van: 5.25 ... English: 5.80"` | - -Some files lack a header row. `isHeaderRow()` in `build-lib.js` detects and skips. - -## Score text parsing - -`SCORE_PATTERNS` in `build-lib.js` defines one regex per subject. The patterns must match the exact subject labels the source files use (Vietnamese). - -Subjects supported (DB column / label in source text): - -| Column | Source label | English | -|---|---|---| -| toan | Toán | Math | -| ngu_van | Ngữ văn | Literature | -| vat_ly | Vật lí | Physics | -| hoa_hoc | Hóa học | Chemistry | -| sinh_hoc | Sinh học | Biology | -| khtn | KHTN | Natural Sciences combined | -| lich_su | Lịch sử | History | -| dia_ly | Địa lí | Geography | -| gdcd | GDCD | Civic Education | -| khxh | KHXH | Social Sciences combined | -| tieng_anh | Tiếng Anh | English | -| tieng_phap | Tiếng Pháp | French | -| tieng_nga | Tiếng Nga | Russian | -| tieng_trung | Tiếng Trung | Chinese | - -Missing-in-string → `NULL` in DB (student didn't take that subject). - -## Why three parsers - -One shared lib + three thin parsers, not one parser with flags. Each dataset's quirks stay visible in its own file: - -| File | Source dir | Sheet strategy | Extra guard | -|---|---|---|---| -| `build-database.js` | `data/` | iterate ALL sheets (.xls row overflow) | — | -| `build-database-old.js` | `data-old/` | sheet 0 only | reject rows where `so_bao_danh` isn't digits-only (rejects an `HO_TEN` → `SOBAODANH` header leak present in this export) | -| `build-database-old2.js` | `data-old2/` | iterate ALL sheets (HCM overflow) | skip fully blank rows before counting | - -## Overflow-sheet gotcha - -The old `.xls` binary format caps at 65,536 rows per sheet. Hanoi and Ho Chi Minh City exceed that in the baotintuc export, so Excel auto-splits the overflow into `Sheet2`. **Only reading `Sheet1` silently drops 13,720 students** (Hanoi +7,275, HCM +6,445). - -The audit `node scripts/audit-row-counts.js` catches this by comparing source-row-count (across all sheets) to DB-row-count. - -## Audits - -Run any time after a rebuild: - -```bash -node scripts/audit-row-counts.js # source rows vs DB rows (zero-loss check) -node scripts/check-duplicates.js # md5 + row-content duplicates in data/ -node scripts/diff-datasets.js # compare two DBs (needs backup/ dir) -``` - -Expected baseline: - -| DB | Source rows | Skipped | DB rows | -|---|---|---|---| -| main | 861,131 | 63 empty rows | **861,068** | -| old | 847,349 | 1 header-leak | **847,348** | -| old2 | 679,764 | 0 | **679,764** | - -## Refreshing the data - -```bash -rm data/*.xls # clear current snapshot -node scripts/crawl-baotintuc.js # re-download from CDN -npm run build:db # rebuild -node scripts/audit-row-counts.js # verify -gzip -kf -9 public/thptqg2017.db # compress for shipping -``` - -## Adding a new source dataset - -See `docs/deployment-guide.md` → "Adding a new variant". diff --git a/2017/docs/deployment-guide.md b/2017/docs/deployment-guide.md deleted file mode 100644 index 4410294..0000000 --- a/2017/docs/deployment-guide.md +++ /dev/null @@ -1,52 +0,0 @@ -# Deployment Guide - -Deploys to GitHub Pages via `.github/workflows/deploy.yml`. Every push to `main` rebuilds and redeploys all 3 variants. - -## What the workflow does - -1. Checkout, setup Node 20, `npm ci` -2. `npm run build:db:all` — builds 3 SQLite DBs (main, old, old2) -3. Gzip each DB at level 9 -4. `npm run build:all` — builds 3 Vite bundles into `dist/`, `dist/old/`, `dist/old2/` -5. Remove uncompressed `.db` files from `dist/` (only `.gz` ships) -6. `actions/upload-pages-artifact` + `actions/deploy-pages` - -Total CI time ≈ 4–6 min (DB build dominates). - -## Resulting URLs - -- `https://.github.io/thptqg2017/` -- `https://.github.io/thptqg2017/old/` -- `https://.github.io/thptqg2017/old2/` - -## Local reproduction - -```bash -npm ci -npm run build:db:all -gzip -kf -9 public/thptqg2017.db public-old/thptqg2017.db public-old2/thptqg2017.db -npm run build:all -npx serve dist # or any static server -``` - -## Adding a new variant - -1. Drop source Excel files into a new `data-vN/` folder -2. Add `public-vN/` to `.gitignore` patterns for the `.db` + `.db.gz` -3. Copy `scripts/build-database-old.js` → `scripts/build-database-vN.js`; update `SRC_DIR` / `DB_PATH`; adjust parse logic for the new quirks (multi-sheet? blank rows? numeric guard?) -4. Add variant to `VARIANT_CONFIG` in `vite.config.js`: - ```js - vN: { base: "/thptqg2017/vN/", publicDir: "public-vN", outDir: "dist/vN" } - ``` -5. Add `build:db:vN` and `build:vN` npm scripts; wire them into `build:db:all` and `build:all` -6. Update `.github/workflows/deploy.yml` — add gzip step + rm step for the new DB - -## Notes - -- **DB gzip is non-cacheable across deploys** — every rebuild produces a new `.db.gz` (not byte-identical due to SQLite page shuffling). First-visit users pay the 47 MB download; subsequent visits hit browser cache until the next deploy. -- **GitHub Pages file size limit**: individual files ≤ 100 MB. Main gzipped DB is ~47 MB — safe. Uncompressed 159 MB DB would not fit; that's why we ship `.gz` and the frontend decompresses via `DecompressionStream`. -- **No server-side compression assumption.** The app reads the `.gz` file directly (not `Content-Encoding: gzip`). GH Pages does not reliably gzip on-the-fly for arbitrary paths; shipping pre-gzipped bytes is deterministic. - -## Rollback - -Deploys are stateless snapshots. To rollback, revert the commit on `main` and push — the next workflow rebuilds the older state. There's no state to migrate. diff --git a/2017/docs/system-architecture.md b/2017/docs/system-architecture.md deleted file mode 100644 index aa290cb..0000000 --- a/2017/docs/system-architecture.md +++ /dev/null @@ -1,97 +0,0 @@ -# System Architecture - -Static site. No backend. Browser downloads a compressed SQLite file at boot, then every query runs locally via `sql.js` (WASM). - -## Data flow - -``` -Excel files (.xls / .xlsx) - │ - ▼ build-database*.js (Node) -SQLite DB (public*/thptqg2017.db) - │ - ▼ gzip -9 -thptqg2017.db.gz (~47 MB for main variant) - │ - ▼ Vite build copies public*/ into dist/ -Static site on GitHub Pages - │ - ▼ browser loads -sql.js (WASM) opens the .db → queries run client-side -``` - -## Three deployment variants - -One repo → three independent sites, same frontend, different dataset. - -| Variant | Route | Source dir | Public dir | Build cmd | -|---|---|---|---|---| -| main | `/thptqg2017/` | `data/` | `public/` | `npm run build` | -| old | `/thptqg2017/old/` | `data-old/` | `public-old/` | `npm run build:old` | -| old2 | `/thptqg2017/old2/` | `data-old2/` | `public-old2/` | `npm run build:old2` | - -`vite.config.js` reads `process.env.VARIANT` and switches `base`, `publicDir`, `outDir` accordingly. `emptyOutDir` is on only for the main build so the variant builds merge into `dist/old/`, `dist/old2/` cleanly. - -## Schema (all 3 DBs share this) - -```sql -CREATE TABLE student ( - so_bao_danh TEXT PRIMARY KEY, -- exam ID (8 digits, first 2 = province) - ho_ten TEXT NOT NULL, - ho_ten_ascii TEXT NOT NULL, -- NFD-stripped lowercase for accent-insensitive search - ngay_sinh TEXT, -- dd/mm/yyyy - toan, ngu_van, vat_ly, hoa_hoc, sinh_hoc, khtn, - lich_su, dia_ly, gdcd, khxh, - tieng_anh, tieng_phap, tieng_nga, tieng_trung REAL -); -CREATE INDEX idx_ho_ten ON student(ho_ten); -CREATE INDEX idx_ho_ten_ascii ON student(ho_ten_ascii); -``` - -No German / Japanese language columns — neither appears in any source file. - -Each score column is `NULL` when the student didn't take that subject. A student's "admission block" total is only computed when all 3 required subjects are non-null. - -## Parse quirks (why 3 builder scripts) - -Each dataset had a different quirk. Rather than one mega-parser with mode flags, each builder script is ~70 lines and isolates its own workarounds. - -| Dataset | Quirk | Mitigation | -|---|---|---| -| data/ (baotintuc .xls) | Hanoi + HCM overflow past the 65,536 row-per-sheet .xls limit | Iterate all `wb.SheetNames` | -| data-old/ (xlsx) | Single-sheet always; one bogus header row (`SOBAODANH`/`HO_TEN`) leaked into an earlier DB | Strict numeric-SBD guard rejects the leak | -| data-old2/ (xlsx) | HCM overflow + many blank trailing rows | Multi-sheet walk + blank-row pre-filter | - -Shared concerns (regex, schema, `toAscii`, header detection) live in `scripts/build-lib.js`. - -## Admission blocks - -Vietnamese universities admit students based on 3-subject combinations called "admission blocks" (khối thi). `src/lib/admission-blocks.js` lists all 49 official 2017 blocks computable from our schema (A00–A11, B00–B08, C00–C20, D01–D15). Each entry is `{ code, subjects: [3 keys], label }`. - -`computeBlocks(student)` returns only blocks where the student has all 3 subject scores, sorted by total desc. Used by the student detail card. - -The SQL preset "Top 10 best-block in Long An" materialises all 49 blocks as a `UNION ALL` CTE, picks each student's best block via `ROW_NUMBER() OVER (PARTITION BY so_bao_danh ORDER BY s DESC, k)`, then ranks across students. - -## Score tiers (UI coding) - -6-level TFT rarity ladder (white → green → blue → purple → gold → prismatic). Defined in `scoreTier()` in `src/lib/admission-blocks.js`. - -| Tier | Range | UI label (Vietnamese) | English | -|---|---|---|---| -| common | ≤ 1 | Điểm liệt | Disqualifying | -| uncommon | < 5 | Chưa đạt | Below passing | -| rare | 5–6.5 | Trung bình | Average | -| epic | 6.5–8 | Khá | Good | -| legendary | 8–9 | Giỏi | Very good | -| prismatic | 9–10 | Xuất sắc | Excellent | - -Color + icon + text label — never color-only. - -## Frontend key behaviors - -- Query state lives in `App.jsx` and is synced to URL `?q=...` via `history.replaceState` — bookmarkable, shareable. -- `SearchForm` is a controlled component that 300ms-debounces input changes and auto-triggers search once the query is long enough (3+ digits for SBD, 2+ chars for name). -- Detection: all-digits → exact `so_bao_danh` match; ASCII-only → folded `ho_ten_ascii LIKE`; has Vietnamese diacritics → search both `ho_ten` and folded column. -- Exactly 1 result → `StudentDetail` card; 0 or >1 → `ScoreTable`. -- Global `/` key focuses the search input when not already typing. -- Share button on the detail card uses Web Share API when present, else clipboard with a pre-formatted summary + deep-link URL. diff --git a/2017/index.html b/2017/index.html deleted file mode 100644 index 6961ca3..0000000 --- a/2017/index.html +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - Tra cứu điểm thi THPT QG 2017 - - -
- - - diff --git a/2017/package.json b/2017/package.json deleted file mode 100644 index 280936d..0000000 --- a/2017/package.json +++ /dev/null @@ -1,43 +0,0 @@ -{ - "name": "thptqg2017", - "private": true, - "packageManager": "pnpm@11.1.1", - "version": "1.0.0", - "type": "module", - "description": "Tra cứu điểm thi THPT QG 2017", - "scripts": { - "build:rust": "cargo build --release --manifest-path tools/xlsxread/Cargo.toml", - "build:db": "pnpm build:rust && ./tools/xlsxread/target/release/xlsxread build --schema tools/xlsxread/configs/thptqg2017-data.toml --input data --output public/thptqg2017.db", - "build:db:old": "pnpm build:rust && ./tools/xlsxread/target/release/xlsxread build --schema tools/xlsxread/configs/thptqg2017-data-old.toml --input data-old --output public-old/thptqg2017.db", - "build:db:old2": "pnpm build:rust && ./tools/xlsxread/target/release/xlsxread build --schema tools/xlsxread/configs/thptqg2017-data-old2.toml --input data-old2 --output public-old2/thptqg2017.db", - "build:db:all": "pnpm build:rust && ./tools/xlsxread/target/release/xlsxread build --schema tools/xlsxread/configs/thptqg2017-data.toml --input data --output public/thptqg2017.db && ./tools/xlsxread/target/release/xlsxread build --schema tools/xlsxread/configs/thptqg2017-data-old.toml --input data-old --output public-old/thptqg2017.db && ./tools/xlsxread/target/release/xlsxread build --schema tools/xlsxread/configs/thptqg2017-data-old2.toml --input data-old2 --output public-old2/thptqg2017.db", - "dev": "vite", - "build": "vite build", - "build:old": "node -e \"process.env.VARIANT='old';require('child_process').spawnSync('npx',['vite','build'],{stdio:'inherit',shell:true,env:process.env})\"", - "build:old2": "node -e \"process.env.VARIANT='old2';require('child_process').spawnSync('npx',['vite','build'],{stdio:'inherit',shell:true,env:process.env})\"", - "build:all": "pnpm build && pnpm build:old && pnpm build:old2", - "preview": "vite preview", - "lint": "eslint ." - }, - "repository": { - "type": "git", - "url": "git+https://github.com/tiennm99/thptqg2017.git" - }, - "license": "ISC", - "dependencies": { - "react": "^19.2.4", - "react-dom": "^19.2.4", - "sql.js": "^1.14.1" - }, - "devDependencies": { - "@eslint/js": "^9.39.4", - "@types/react": "^19.2.14", - "@types/react-dom": "^19.2.3", - "@vitejs/plugin-react": "^6.0.1", - "eslint": "^9.39.4", - "eslint-plugin-react-hooks": "^7.0.1", - "eslint-plugin-react-refresh": "^0.5.2", - "globals": "^17.4.0", - "vite": "^8.0.16" - } -} diff --git a/2017/pnpm-lock.yaml b/2017/pnpm-lock.yaml deleted file mode 100644 index ce02b86..0000000 --- a/2017/pnpm-lock.yaml +++ /dev/null @@ -1,1624 +0,0 @@ -lockfileVersion: '9.0' - -settings: - autoInstallPeers: true - excludeLinksFromLockfile: false - -importers: - - .: - dependencies: - react: - specifier: ^19.2.4 - version: 19.2.6 - react-dom: - specifier: ^19.2.4 - version: 19.2.6(react@19.2.6) - sql.js: - specifier: ^1.14.1 - version: 1.14.1 - devDependencies: - '@eslint/js': - specifier: ^9.39.4 - version: 9.39.4 - '@types/react': - specifier: ^19.2.14 - version: 19.2.14 - '@types/react-dom': - specifier: ^19.2.3 - version: 19.2.3(@types/react@19.2.14) - '@vitejs/plugin-react': - specifier: ^6.0.1 - version: 6.0.1(vite@8.0.16) - eslint: - specifier: ^9.39.4 - version: 9.39.4 - eslint-plugin-react-hooks: - specifier: ^7.0.1 - version: 7.1.1(eslint@9.39.4) - eslint-plugin-react-refresh: - specifier: ^0.5.2 - version: 0.5.2(eslint@9.39.4) - globals: - specifier: ^17.4.0 - version: 17.6.0 - vite: - specifier: ^8.0.16 - version: 8.0.16 - -packages: - - '@babel/code-frame@7.29.0': - resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} - engines: {node: '>=6.9.0'} - - '@babel/compat-data@7.29.3': - resolution: {integrity: sha512-LIVqM46zQWZhj17qA8wb4nW/ixr2y1Nw+r1etiAWgRM6U1IqP+LNhL1yg440jYZR72jCWcWbLWzIosH+uP1fqg==} - engines: {node: '>=6.9.0'} - - '@babel/core@7.29.0': - resolution: {integrity: sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==} - engines: {node: '>=6.9.0'} - - '@babel/generator@7.29.1': - resolution: {integrity: sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==} - engines: {node: '>=6.9.0'} - - '@babel/helper-compilation-targets@7.28.6': - resolution: {integrity: sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==} - engines: {node: '>=6.9.0'} - - '@babel/helper-globals@7.28.0': - resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} - engines: {node: '>=6.9.0'} - - '@babel/helper-module-imports@7.28.6': - resolution: {integrity: sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==} - engines: {node: '>=6.9.0'} - - '@babel/helper-module-transforms@7.28.6': - resolution: {integrity: sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0 - - '@babel/helper-string-parser@7.27.1': - resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} - engines: {node: '>=6.9.0'} - - '@babel/helper-validator-identifier@7.28.5': - resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} - engines: {node: '>=6.9.0'} - - '@babel/helper-validator-option@7.27.1': - resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} - engines: {node: '>=6.9.0'} - - '@babel/helpers@7.29.2': - resolution: {integrity: sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==} - engines: {node: '>=6.9.0'} - - '@babel/parser@7.29.3': - resolution: {integrity: sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==} - engines: {node: '>=6.0.0'} - hasBin: true - - '@babel/template@7.28.6': - resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==} - engines: {node: '>=6.9.0'} - - '@babel/traverse@7.29.0': - resolution: {integrity: sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==} - engines: {node: '>=6.9.0'} - - '@babel/types@7.29.0': - resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} - engines: {node: '>=6.9.0'} - - '@emnapi/core@1.10.0': - resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==} - - '@emnapi/runtime@1.10.0': - resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==} - - '@emnapi/wasi-threads@1.2.1': - resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} - - '@eslint-community/eslint-utils@4.9.1': - resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 - - '@eslint-community/regexpp@4.12.2': - resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} - engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} - - '@eslint/config-array@0.21.2': - resolution: {integrity: sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@eslint/config-helpers@0.4.2': - resolution: {integrity: sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@eslint/core@0.17.0': - resolution: {integrity: sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@eslint/eslintrc@3.3.5': - resolution: {integrity: sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@eslint/js@9.39.4': - resolution: {integrity: sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@eslint/object-schema@2.1.7': - resolution: {integrity: sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@eslint/plugin-kit@0.4.1': - resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@humanfs/core@0.19.2': - resolution: {integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==} - engines: {node: '>=18.18.0'} - - '@humanfs/node@0.16.8': - resolution: {integrity: sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==} - engines: {node: '>=18.18.0'} - - '@humanfs/types@0.15.0': - resolution: {integrity: sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==} - engines: {node: '>=18.18.0'} - - '@humanwhocodes/module-importer@1.0.1': - resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} - engines: {node: '>=12.22'} - - '@humanwhocodes/retry@0.4.3': - resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} - engines: {node: '>=18.18'} - - '@jridgewell/gen-mapping@0.3.13': - resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} - - '@jridgewell/remapping@2.3.5': - resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} - - '@jridgewell/resolve-uri@3.1.2': - resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} - engines: {node: '>=6.0.0'} - - '@jridgewell/sourcemap-codec@1.5.5': - resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} - - '@jridgewell/trace-mapping@0.3.31': - resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} - - '@napi-rs/wasm-runtime@1.1.6': - resolution: {integrity: sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==} - peerDependencies: - '@emnapi/core': ^1.7.1 - '@emnapi/runtime': ^1.7.1 - - '@oxc-project/types@0.133.0': - resolution: {integrity: sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==} - - '@rolldown/binding-android-arm64@1.0.3': - resolution: {integrity: sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [android] - - '@rolldown/binding-darwin-arm64@1.0.3': - resolution: {integrity: sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [darwin] - - '@rolldown/binding-darwin-x64@1.0.3': - resolution: {integrity: sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [darwin] - - '@rolldown/binding-freebsd-x64@1.0.3': - resolution: {integrity: sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [freebsd] - - '@rolldown/binding-linux-arm-gnueabihf@1.0.3': - resolution: {integrity: sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm] - os: [linux] - - '@rolldown/binding-linux-arm64-gnu@1.0.3': - resolution: {integrity: sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [linux] - libc: [glibc] - - '@rolldown/binding-linux-arm64-musl@1.0.3': - resolution: {integrity: sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [linux] - libc: [musl] - - '@rolldown/binding-linux-ppc64-gnu@1.0.3': - resolution: {integrity: sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [ppc64] - os: [linux] - libc: [glibc] - - '@rolldown/binding-linux-s390x-gnu@1.0.3': - resolution: {integrity: sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [s390x] - os: [linux] - libc: [glibc] - - '@rolldown/binding-linux-x64-gnu@1.0.3': - resolution: {integrity: sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [linux] - libc: [glibc] - - '@rolldown/binding-linux-x64-musl@1.0.3': - resolution: {integrity: sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [linux] - libc: [musl] - - '@rolldown/binding-openharmony-arm64@1.0.3': - resolution: {integrity: sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [openharmony] - - '@rolldown/binding-wasm32-wasi@1.0.3': - resolution: {integrity: sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [wasm32] - - '@rolldown/binding-win32-arm64-msvc@1.0.3': - resolution: {integrity: sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [win32] - - '@rolldown/binding-win32-x64-msvc@1.0.3': - resolution: {integrity: sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [win32] - - '@rolldown/pluginutils@1.0.0-rc.7': - resolution: {integrity: sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA==} - - '@rolldown/pluginutils@1.0.1': - resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} - - '@tybys/wasm-util@0.10.3': - resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==} - - '@types/estree@1.0.9': - resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} - - '@types/json-schema@7.0.15': - resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} - - '@types/react-dom@19.2.3': - resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} - peerDependencies: - '@types/react': ^19.2.0 - - '@types/react@19.2.14': - resolution: {integrity: sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==} - - '@vitejs/plugin-react@6.0.1': - resolution: {integrity: sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ==} - engines: {node: ^20.19.0 || >=22.12.0} - peerDependencies: - '@rolldown/plugin-babel': ^0.1.7 || ^0.2.0 - babel-plugin-react-compiler: ^1.0.0 - vite: ^8.0.0 - peerDependenciesMeta: - '@rolldown/plugin-babel': - optional: true - babel-plugin-react-compiler: - optional: true - - acorn-jsx@5.3.2: - resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} - peerDependencies: - acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 - - acorn@8.16.0: - resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==} - engines: {node: '>=0.4.0'} - hasBin: true - - ajv@6.15.0: - resolution: {integrity: sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==} - - ansi-styles@4.3.0: - resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} - engines: {node: '>=8'} - - argparse@2.0.1: - resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} - - balanced-match@1.0.2: - resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} - - baseline-browser-mapping@2.10.29: - resolution: {integrity: sha512-Asa2krT+XTPZINCS+2QcyS8WTkObE77RwkydwF7h6DmnKqbvlalz93m/dnphUyCa6SWSP51VgtEUf2FN+gelFQ==} - engines: {node: '>=6.0.0'} - hasBin: true - - brace-expansion@1.1.14: - resolution: {integrity: sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==} - - browserslist@4.28.2: - resolution: {integrity: sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==} - engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} - hasBin: true - - callsites@3.1.0: - resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} - engines: {node: '>=6'} - - caniuse-lite@1.0.30001792: - resolution: {integrity: sha512-hVLMUZFgR4JJ6ACt1uEESvQN1/dBVqPAKY0hgrV70eN3391K6juAfTjKZLKvOMsx8PxA7gsY1/tLMMTcfFLLpw==} - - chalk@4.1.2: - resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} - engines: {node: '>=10'} - - color-convert@2.0.1: - resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} - engines: {node: '>=7.0.0'} - - color-name@1.1.4: - resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} - - concat-map@0.0.1: - resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} - - convert-source-map@2.0.0: - resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} - - cross-spawn@7.0.6: - resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} - engines: {node: '>= 8'} - - csstype@3.2.3: - resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} - - debug@4.4.3: - resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} - engines: {node: '>=6.0'} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - - deep-is@0.1.4: - resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} - - detect-libc@2.1.2: - resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} - engines: {node: '>=8'} - - electron-to-chromium@1.5.353: - resolution: {integrity: sha512-kOrWphBi8TOZyiJZqsgqIle0lw+tzmnQK83pV9dZUd01Nm2POECSyFQMAuarzZdYqQW7FH9RaYOuaRo3h+bQ3w==} - - escalade@3.2.0: - resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} - engines: {node: '>=6'} - - escape-string-regexp@4.0.0: - resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} - engines: {node: '>=10'} - - eslint-plugin-react-hooks@7.1.1: - resolution: {integrity: sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==} - engines: {node: '>=18'} - peerDependencies: - eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0 - - eslint-plugin-react-refresh@0.5.2: - resolution: {integrity: sha512-hmgTH57GfzoTFjVN0yBwTggnsVUF2tcqi7RJZHqi9lIezSs4eFyAMktA68YD4r5kNw1mxyY4dmkyoFDb3FIqrA==} - peerDependencies: - eslint: ^9 || ^10 - - eslint-scope@8.4.0: - resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - eslint-visitor-keys@3.4.3: - resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - - eslint-visitor-keys@4.2.1: - resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - eslint@9.39.4: - resolution: {integrity: sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - hasBin: true - peerDependencies: - jiti: '*' - peerDependenciesMeta: - jiti: - optional: true - - espree@10.4.0: - resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - esquery@1.7.0: - resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} - engines: {node: '>=0.10'} - - esrecurse@4.3.0: - resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} - engines: {node: '>=4.0'} - - estraverse@5.3.0: - resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} - engines: {node: '>=4.0'} - - esutils@2.0.3: - resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} - engines: {node: '>=0.10.0'} - - fast-deep-equal@3.1.3: - resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} - - fast-json-stable-stringify@2.1.0: - resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} - - fast-levenshtein@2.0.6: - resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} - - fdir@6.5.0: - resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} - engines: {node: '>=12.0.0'} - peerDependencies: - picomatch: ^3 || ^4 - peerDependenciesMeta: - picomatch: - optional: true - - file-entry-cache@8.0.0: - resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} - engines: {node: '>=16.0.0'} - - find-up@5.0.0: - resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} - engines: {node: '>=10'} - - flat-cache@4.0.1: - resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} - engines: {node: '>=16'} - - flatted@3.4.2: - resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==} - - fsevents@2.3.3: - resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} - engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} - os: [darwin] - - gensync@1.0.0-beta.2: - resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} - engines: {node: '>=6.9.0'} - - glob-parent@6.0.2: - resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} - engines: {node: '>=10.13.0'} - - globals@14.0.0: - resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} - engines: {node: '>=18'} - - globals@17.6.0: - resolution: {integrity: sha512-sepffkT8stwnIYbsMBpoCHJuJM5l98FUF2AnE07hfvE0m/qp3R586hw4jF4uadbhvg1ooIdzuu7CsfD2jzCaNA==} - engines: {node: '>=18'} - - has-flag@4.0.0: - resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} - engines: {node: '>=8'} - - hermes-estree@0.25.1: - resolution: {integrity: sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==} - - hermes-parser@0.25.1: - resolution: {integrity: sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==} - - ignore@5.3.2: - resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} - engines: {node: '>= 4'} - - import-fresh@3.3.1: - resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} - engines: {node: '>=6'} - - imurmurhash@0.1.4: - resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} - engines: {node: '>=0.8.19'} - - is-extglob@2.1.1: - resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} - engines: {node: '>=0.10.0'} - - is-glob@4.0.3: - resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} - engines: {node: '>=0.10.0'} - - isexe@2.0.0: - resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} - - js-tokens@4.0.0: - resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} - - js-yaml@4.1.1: - resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} - hasBin: true - - jsesc@3.1.0: - resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} - engines: {node: '>=6'} - hasBin: true - - json-buffer@3.0.1: - resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} - - json-schema-traverse@0.4.1: - resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} - - json-stable-stringify-without-jsonify@1.0.1: - resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} - - json5@2.2.3: - resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} - engines: {node: '>=6'} - hasBin: true - - keyv@4.5.4: - resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} - - levn@0.4.1: - resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} - engines: {node: '>= 0.8.0'} - - lightningcss-android-arm64@1.33.0: - resolution: {integrity: sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==} - engines: {node: '>= 12.0.0'} - cpu: [arm64] - os: [android] - - lightningcss-darwin-arm64@1.33.0: - resolution: {integrity: sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==} - engines: {node: '>= 12.0.0'} - cpu: [arm64] - os: [darwin] - - lightningcss-darwin-x64@1.33.0: - resolution: {integrity: sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==} - engines: {node: '>= 12.0.0'} - cpu: [x64] - os: [darwin] - - lightningcss-freebsd-x64@1.33.0: - resolution: {integrity: sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==} - engines: {node: '>= 12.0.0'} - cpu: [x64] - os: [freebsd] - - lightningcss-linux-arm-gnueabihf@1.33.0: - resolution: {integrity: sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==} - engines: {node: '>= 12.0.0'} - cpu: [arm] - os: [linux] - - lightningcss-linux-arm64-gnu@1.33.0: - resolution: {integrity: sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==} - engines: {node: '>= 12.0.0'} - cpu: [arm64] - os: [linux] - libc: [glibc] - - lightningcss-linux-arm64-musl@1.33.0: - resolution: {integrity: sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==} - engines: {node: '>= 12.0.0'} - cpu: [arm64] - os: [linux] - libc: [musl] - - lightningcss-linux-x64-gnu@1.33.0: - resolution: {integrity: sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==} - engines: {node: '>= 12.0.0'} - cpu: [x64] - os: [linux] - libc: [glibc] - - lightningcss-linux-x64-musl@1.33.0: - resolution: {integrity: sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==} - engines: {node: '>= 12.0.0'} - cpu: [x64] - os: [linux] - libc: [musl] - - lightningcss-win32-arm64-msvc@1.33.0: - resolution: {integrity: sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==} - engines: {node: '>= 12.0.0'} - cpu: [arm64] - os: [win32] - - lightningcss-win32-x64-msvc@1.33.0: - resolution: {integrity: sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==} - engines: {node: '>= 12.0.0'} - cpu: [x64] - os: [win32] - - lightningcss@1.33.0: - resolution: {integrity: sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==} - engines: {node: '>= 12.0.0'} - - locate-path@6.0.0: - resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} - engines: {node: '>=10'} - - lodash.merge@4.6.2: - resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} - - lru-cache@5.1.1: - resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} - - minimatch@3.1.5: - resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==} - - ms@2.1.3: - resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - - nanoid@3.3.16: - resolution: {integrity: sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==} - engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} - hasBin: true - - natural-compare@1.4.0: - resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} - - node-releases@2.0.38: - resolution: {integrity: sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw==} - - optionator@0.9.4: - resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} - engines: {node: '>= 0.8.0'} - - p-limit@3.1.0: - resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} - engines: {node: '>=10'} - - p-locate@5.0.0: - resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} - engines: {node: '>=10'} - - parent-module@1.0.1: - resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} - engines: {node: '>=6'} - - path-exists@4.0.0: - resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} - engines: {node: '>=8'} - - path-key@3.1.1: - resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} - engines: {node: '>=8'} - - picocolors@1.1.1: - resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} - - picomatch@4.0.5: - resolution: {integrity: sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==} - engines: {node: '>=12'} - - postcss@8.5.23: - resolution: {integrity: sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg==} - engines: {node: ^10 || ^12 || >=14} - - prelude-ls@1.2.1: - resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} - engines: {node: '>= 0.8.0'} - - punycode@2.3.1: - resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} - engines: {node: '>=6'} - - react-dom@19.2.6: - resolution: {integrity: sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g==} - peerDependencies: - react: ^19.2.6 - - react@19.2.6: - resolution: {integrity: sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q==} - engines: {node: '>=0.10.0'} - - resolve-from@4.0.0: - resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} - engines: {node: '>=4'} - - rolldown@1.0.3: - resolution: {integrity: sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==} - engines: {node: ^20.19.0 || >=22.12.0} - hasBin: true - - scheduler@0.27.0: - resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} - - semver@6.3.1: - resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} - hasBin: true - - shebang-command@2.0.0: - resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} - engines: {node: '>=8'} - - shebang-regex@3.0.0: - resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} - engines: {node: '>=8'} - - source-map-js@1.2.1: - resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} - engines: {node: '>=0.10.0'} - - sql.js@1.14.1: - resolution: {integrity: sha512-gcj8zBWU5cFsi9WUP+4bFNXAyF1iRpA3LLyS/DP5xlrNzGmPIizUeBggKa8DbDwdqaKwUcTEnChtd2grWo/x/A==} - - strip-json-comments@3.1.1: - resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} - engines: {node: '>=8'} - - supports-color@7.2.0: - resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} - engines: {node: '>=8'} - - tinyglobby@0.2.17: - resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} - engines: {node: '>=12.0.0'} - - tslib@2.8.1: - resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} - - type-check@0.4.0: - resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} - engines: {node: '>= 0.8.0'} - - update-browserslist-db@1.2.3: - resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} - hasBin: true - peerDependencies: - browserslist: '>= 4.21.0' - - uri-js@4.4.1: - resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} - - vite@8.0.16: - resolution: {integrity: sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==} - engines: {node: ^20.19.0 || >=22.12.0} - hasBin: true - peerDependencies: - '@types/node': ^20.19.0 || >=22.12.0 - '@vitejs/devtools': ^0.1.18 - esbuild: ^0.27.0 || ^0.28.0 - jiti: '>=1.21.0' - less: ^4.0.0 - sass: ^1.70.0 - sass-embedded: ^1.70.0 - stylus: '>=0.54.8' - sugarss: ^5.0.0 - terser: ^5.16.0 - tsx: ^4.8.1 - yaml: ^2.4.2 - peerDependenciesMeta: - '@types/node': - optional: true - '@vitejs/devtools': - optional: true - esbuild: - optional: true - jiti: - optional: true - less: - optional: true - sass: - optional: true - sass-embedded: - optional: true - stylus: - optional: true - sugarss: - optional: true - terser: - optional: true - tsx: - optional: true - yaml: - optional: true - - which@2.0.2: - resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} - engines: {node: '>= 8'} - hasBin: true - - word-wrap@1.2.5: - resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} - engines: {node: '>=0.10.0'} - - yallist@3.1.1: - resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} - - yocto-queue@0.1.0: - resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} - engines: {node: '>=10'} - - zod-validation-error@4.0.2: - resolution: {integrity: sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==} - engines: {node: '>=18.0.0'} - peerDependencies: - zod: ^3.25.0 || ^4.0.0 - - zod@4.4.3: - resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==} - -snapshots: - - '@babel/code-frame@7.29.0': - dependencies: - '@babel/helper-validator-identifier': 7.28.5 - js-tokens: 4.0.0 - picocolors: 1.1.1 - - '@babel/compat-data@7.29.3': {} - - '@babel/core@7.29.0': - dependencies: - '@babel/code-frame': 7.29.0 - '@babel/generator': 7.29.1 - '@babel/helper-compilation-targets': 7.28.6 - '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) - '@babel/helpers': 7.29.2 - '@babel/parser': 7.29.3 - '@babel/template': 7.28.6 - '@babel/traverse': 7.29.0 - '@babel/types': 7.29.0 - '@jridgewell/remapping': 2.3.5 - convert-source-map: 2.0.0 - debug: 4.4.3 - gensync: 1.0.0-beta.2 - json5: 2.2.3 - semver: 6.3.1 - transitivePeerDependencies: - - supports-color - - '@babel/generator@7.29.1': - dependencies: - '@babel/parser': 7.29.3 - '@babel/types': 7.29.0 - '@jridgewell/gen-mapping': 0.3.13 - '@jridgewell/trace-mapping': 0.3.31 - jsesc: 3.1.0 - - '@babel/helper-compilation-targets@7.28.6': - dependencies: - '@babel/compat-data': 7.29.3 - '@babel/helper-validator-option': 7.27.1 - browserslist: 4.28.2 - lru-cache: 5.1.1 - semver: 6.3.1 - - '@babel/helper-globals@7.28.0': {} - - '@babel/helper-module-imports@7.28.6': - dependencies: - '@babel/traverse': 7.29.0 - '@babel/types': 7.29.0 - transitivePeerDependencies: - - supports-color - - '@babel/helper-module-transforms@7.28.6(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-module-imports': 7.28.6 - '@babel/helper-validator-identifier': 7.28.5 - '@babel/traverse': 7.29.0 - transitivePeerDependencies: - - supports-color - - '@babel/helper-string-parser@7.27.1': {} - - '@babel/helper-validator-identifier@7.28.5': {} - - '@babel/helper-validator-option@7.27.1': {} - - '@babel/helpers@7.29.2': - dependencies: - '@babel/template': 7.28.6 - '@babel/types': 7.29.0 - - '@babel/parser@7.29.3': - dependencies: - '@babel/types': 7.29.0 - - '@babel/template@7.28.6': - dependencies: - '@babel/code-frame': 7.29.0 - '@babel/parser': 7.29.3 - '@babel/types': 7.29.0 - - '@babel/traverse@7.29.0': - dependencies: - '@babel/code-frame': 7.29.0 - '@babel/generator': 7.29.1 - '@babel/helper-globals': 7.28.0 - '@babel/parser': 7.29.3 - '@babel/template': 7.28.6 - '@babel/types': 7.29.0 - debug: 4.4.3 - transitivePeerDependencies: - - supports-color - - '@babel/types@7.29.0': - dependencies: - '@babel/helper-string-parser': 7.27.1 - '@babel/helper-validator-identifier': 7.28.5 - - '@emnapi/core@1.10.0': - dependencies: - '@emnapi/wasi-threads': 1.2.1 - tslib: 2.8.1 - optional: true - - '@emnapi/runtime@1.10.0': - dependencies: - tslib: 2.8.1 - optional: true - - '@emnapi/wasi-threads@1.2.1': - dependencies: - tslib: 2.8.1 - optional: true - - '@eslint-community/eslint-utils@4.9.1(eslint@9.39.4)': - dependencies: - eslint: 9.39.4 - eslint-visitor-keys: 3.4.3 - - '@eslint-community/regexpp@4.12.2': {} - - '@eslint/config-array@0.21.2': - dependencies: - '@eslint/object-schema': 2.1.7 - debug: 4.4.3 - minimatch: 3.1.5 - transitivePeerDependencies: - - supports-color - - '@eslint/config-helpers@0.4.2': - dependencies: - '@eslint/core': 0.17.0 - - '@eslint/core@0.17.0': - dependencies: - '@types/json-schema': 7.0.15 - - '@eslint/eslintrc@3.3.5': - dependencies: - ajv: 6.15.0 - debug: 4.4.3 - espree: 10.4.0 - globals: 14.0.0 - ignore: 5.3.2 - import-fresh: 3.3.1 - js-yaml: 4.1.1 - minimatch: 3.1.5 - strip-json-comments: 3.1.1 - transitivePeerDependencies: - - supports-color - - '@eslint/js@9.39.4': {} - - '@eslint/object-schema@2.1.7': {} - - '@eslint/plugin-kit@0.4.1': - dependencies: - '@eslint/core': 0.17.0 - levn: 0.4.1 - - '@humanfs/core@0.19.2': - dependencies: - '@humanfs/types': 0.15.0 - - '@humanfs/node@0.16.8': - dependencies: - '@humanfs/core': 0.19.2 - '@humanfs/types': 0.15.0 - '@humanwhocodes/retry': 0.4.3 - - '@humanfs/types@0.15.0': {} - - '@humanwhocodes/module-importer@1.0.1': {} - - '@humanwhocodes/retry@0.4.3': {} - - '@jridgewell/gen-mapping@0.3.13': - dependencies: - '@jridgewell/sourcemap-codec': 1.5.5 - '@jridgewell/trace-mapping': 0.3.31 - - '@jridgewell/remapping@2.3.5': - dependencies: - '@jridgewell/gen-mapping': 0.3.13 - '@jridgewell/trace-mapping': 0.3.31 - - '@jridgewell/resolve-uri@3.1.2': {} - - '@jridgewell/sourcemap-codec@1.5.5': {} - - '@jridgewell/trace-mapping@0.3.31': - dependencies: - '@jridgewell/resolve-uri': 3.1.2 - '@jridgewell/sourcemap-codec': 1.5.5 - - '@napi-rs/wasm-runtime@1.1.6(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': - dependencies: - '@emnapi/core': 1.10.0 - '@emnapi/runtime': 1.10.0 - '@tybys/wasm-util': 0.10.3 - optional: true - - '@oxc-project/types@0.133.0': {} - - '@rolldown/binding-android-arm64@1.0.3': - optional: true - - '@rolldown/binding-darwin-arm64@1.0.3': - optional: true - - '@rolldown/binding-darwin-x64@1.0.3': - optional: true - - '@rolldown/binding-freebsd-x64@1.0.3': - optional: true - - '@rolldown/binding-linux-arm-gnueabihf@1.0.3': - optional: true - - '@rolldown/binding-linux-arm64-gnu@1.0.3': - optional: true - - '@rolldown/binding-linux-arm64-musl@1.0.3': - optional: true - - '@rolldown/binding-linux-ppc64-gnu@1.0.3': - optional: true - - '@rolldown/binding-linux-s390x-gnu@1.0.3': - optional: true - - '@rolldown/binding-linux-x64-gnu@1.0.3': - optional: true - - '@rolldown/binding-linux-x64-musl@1.0.3': - optional: true - - '@rolldown/binding-openharmony-arm64@1.0.3': - optional: true - - '@rolldown/binding-wasm32-wasi@1.0.3': - dependencies: - '@emnapi/core': 1.10.0 - '@emnapi/runtime': 1.10.0 - '@napi-rs/wasm-runtime': 1.1.6(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) - optional: true - - '@rolldown/binding-win32-arm64-msvc@1.0.3': - optional: true - - '@rolldown/binding-win32-x64-msvc@1.0.3': - optional: true - - '@rolldown/pluginutils@1.0.0-rc.7': {} - - '@rolldown/pluginutils@1.0.1': {} - - '@tybys/wasm-util@0.10.3': - dependencies: - tslib: 2.8.1 - optional: true - - '@types/estree@1.0.9': {} - - '@types/json-schema@7.0.15': {} - - '@types/react-dom@19.2.3(@types/react@19.2.14)': - dependencies: - '@types/react': 19.2.14 - - '@types/react@19.2.14': - dependencies: - csstype: 3.2.3 - - '@vitejs/plugin-react@6.0.1(vite@8.0.16)': - dependencies: - '@rolldown/pluginutils': 1.0.0-rc.7 - vite: 8.0.16 - - acorn-jsx@5.3.2(acorn@8.16.0): - dependencies: - acorn: 8.16.0 - - acorn@8.16.0: {} - - ajv@6.15.0: - dependencies: - fast-deep-equal: 3.1.3 - fast-json-stable-stringify: 2.1.0 - json-schema-traverse: 0.4.1 - uri-js: 4.4.1 - - ansi-styles@4.3.0: - dependencies: - color-convert: 2.0.1 - - argparse@2.0.1: {} - - balanced-match@1.0.2: {} - - baseline-browser-mapping@2.10.29: {} - - brace-expansion@1.1.14: - dependencies: - balanced-match: 1.0.2 - concat-map: 0.0.1 - - browserslist@4.28.2: - dependencies: - baseline-browser-mapping: 2.10.29 - caniuse-lite: 1.0.30001792 - electron-to-chromium: 1.5.353 - node-releases: 2.0.38 - update-browserslist-db: 1.2.3(browserslist@4.28.2) - - callsites@3.1.0: {} - - caniuse-lite@1.0.30001792: {} - - chalk@4.1.2: - dependencies: - ansi-styles: 4.3.0 - supports-color: 7.2.0 - - color-convert@2.0.1: - dependencies: - color-name: 1.1.4 - - color-name@1.1.4: {} - - concat-map@0.0.1: {} - - convert-source-map@2.0.0: {} - - cross-spawn@7.0.6: - dependencies: - path-key: 3.1.1 - shebang-command: 2.0.0 - which: 2.0.2 - - csstype@3.2.3: {} - - debug@4.4.3: - dependencies: - ms: 2.1.3 - - deep-is@0.1.4: {} - - detect-libc@2.1.2: {} - - electron-to-chromium@1.5.353: {} - - escalade@3.2.0: {} - - escape-string-regexp@4.0.0: {} - - eslint-plugin-react-hooks@7.1.1(eslint@9.39.4): - dependencies: - '@babel/core': 7.29.0 - '@babel/parser': 7.29.3 - eslint: 9.39.4 - hermes-parser: 0.25.1 - zod: 4.4.3 - zod-validation-error: 4.0.2(zod@4.4.3) - transitivePeerDependencies: - - supports-color - - eslint-plugin-react-refresh@0.5.2(eslint@9.39.4): - dependencies: - eslint: 9.39.4 - - eslint-scope@8.4.0: - dependencies: - esrecurse: 4.3.0 - estraverse: 5.3.0 - - eslint-visitor-keys@3.4.3: {} - - eslint-visitor-keys@4.2.1: {} - - eslint@9.39.4: - dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4) - '@eslint-community/regexpp': 4.12.2 - '@eslint/config-array': 0.21.2 - '@eslint/config-helpers': 0.4.2 - '@eslint/core': 0.17.0 - '@eslint/eslintrc': 3.3.5 - '@eslint/js': 9.39.4 - '@eslint/plugin-kit': 0.4.1 - '@humanfs/node': 0.16.8 - '@humanwhocodes/module-importer': 1.0.1 - '@humanwhocodes/retry': 0.4.3 - '@types/estree': 1.0.9 - ajv: 6.15.0 - chalk: 4.1.2 - cross-spawn: 7.0.6 - debug: 4.4.3 - escape-string-regexp: 4.0.0 - eslint-scope: 8.4.0 - eslint-visitor-keys: 4.2.1 - espree: 10.4.0 - esquery: 1.7.0 - esutils: 2.0.3 - fast-deep-equal: 3.1.3 - file-entry-cache: 8.0.0 - find-up: 5.0.0 - glob-parent: 6.0.2 - ignore: 5.3.2 - imurmurhash: 0.1.4 - is-glob: 4.0.3 - json-stable-stringify-without-jsonify: 1.0.1 - lodash.merge: 4.6.2 - minimatch: 3.1.5 - natural-compare: 1.4.0 - optionator: 0.9.4 - transitivePeerDependencies: - - supports-color - - espree@10.4.0: - dependencies: - acorn: 8.16.0 - acorn-jsx: 5.3.2(acorn@8.16.0) - eslint-visitor-keys: 4.2.1 - - esquery@1.7.0: - dependencies: - estraverse: 5.3.0 - - esrecurse@4.3.0: - dependencies: - estraverse: 5.3.0 - - estraverse@5.3.0: {} - - esutils@2.0.3: {} - - fast-deep-equal@3.1.3: {} - - fast-json-stable-stringify@2.1.0: {} - - fast-levenshtein@2.0.6: {} - - fdir@6.5.0(picomatch@4.0.5): - optionalDependencies: - picomatch: 4.0.5 - - file-entry-cache@8.0.0: - dependencies: - flat-cache: 4.0.1 - - find-up@5.0.0: - dependencies: - locate-path: 6.0.0 - path-exists: 4.0.0 - - flat-cache@4.0.1: - dependencies: - flatted: 3.4.2 - keyv: 4.5.4 - - flatted@3.4.2: {} - - fsevents@2.3.3: - optional: true - - gensync@1.0.0-beta.2: {} - - glob-parent@6.0.2: - dependencies: - is-glob: 4.0.3 - - globals@14.0.0: {} - - globals@17.6.0: {} - - has-flag@4.0.0: {} - - hermes-estree@0.25.1: {} - - hermes-parser@0.25.1: - dependencies: - hermes-estree: 0.25.1 - - ignore@5.3.2: {} - - import-fresh@3.3.1: - dependencies: - parent-module: 1.0.1 - resolve-from: 4.0.0 - - imurmurhash@0.1.4: {} - - is-extglob@2.1.1: {} - - is-glob@4.0.3: - dependencies: - is-extglob: 2.1.1 - - isexe@2.0.0: {} - - js-tokens@4.0.0: {} - - js-yaml@4.1.1: - dependencies: - argparse: 2.0.1 - - jsesc@3.1.0: {} - - json-buffer@3.0.1: {} - - json-schema-traverse@0.4.1: {} - - json-stable-stringify-without-jsonify@1.0.1: {} - - json5@2.2.3: {} - - keyv@4.5.4: - dependencies: - json-buffer: 3.0.1 - - levn@0.4.1: - dependencies: - prelude-ls: 1.2.1 - type-check: 0.4.0 - - lightningcss-android-arm64@1.33.0: - optional: true - - lightningcss-darwin-arm64@1.33.0: - optional: true - - lightningcss-darwin-x64@1.33.0: - optional: true - - lightningcss-freebsd-x64@1.33.0: - optional: true - - lightningcss-linux-arm-gnueabihf@1.33.0: - optional: true - - lightningcss-linux-arm64-gnu@1.33.0: - optional: true - - lightningcss-linux-arm64-musl@1.33.0: - optional: true - - lightningcss-linux-x64-gnu@1.33.0: - optional: true - - lightningcss-linux-x64-musl@1.33.0: - optional: true - - lightningcss-win32-arm64-msvc@1.33.0: - optional: true - - lightningcss-win32-x64-msvc@1.33.0: - optional: true - - lightningcss@1.33.0: - dependencies: - detect-libc: 2.1.2 - optionalDependencies: - lightningcss-android-arm64: 1.33.0 - lightningcss-darwin-arm64: 1.33.0 - lightningcss-darwin-x64: 1.33.0 - lightningcss-freebsd-x64: 1.33.0 - lightningcss-linux-arm-gnueabihf: 1.33.0 - lightningcss-linux-arm64-gnu: 1.33.0 - lightningcss-linux-arm64-musl: 1.33.0 - lightningcss-linux-x64-gnu: 1.33.0 - lightningcss-linux-x64-musl: 1.33.0 - lightningcss-win32-arm64-msvc: 1.33.0 - lightningcss-win32-x64-msvc: 1.33.0 - - locate-path@6.0.0: - dependencies: - p-locate: 5.0.0 - - lodash.merge@4.6.2: {} - - lru-cache@5.1.1: - dependencies: - yallist: 3.1.1 - - minimatch@3.1.5: - dependencies: - brace-expansion: 1.1.14 - - ms@2.1.3: {} - - nanoid@3.3.16: {} - - natural-compare@1.4.0: {} - - node-releases@2.0.38: {} - - optionator@0.9.4: - dependencies: - deep-is: 0.1.4 - fast-levenshtein: 2.0.6 - levn: 0.4.1 - prelude-ls: 1.2.1 - type-check: 0.4.0 - word-wrap: 1.2.5 - - p-limit@3.1.0: - dependencies: - yocto-queue: 0.1.0 - - p-locate@5.0.0: - dependencies: - p-limit: 3.1.0 - - parent-module@1.0.1: - dependencies: - callsites: 3.1.0 - - path-exists@4.0.0: {} - - path-key@3.1.1: {} - - picocolors@1.1.1: {} - - picomatch@4.0.5: {} - - postcss@8.5.23: - dependencies: - nanoid: 3.3.16 - picocolors: 1.1.1 - source-map-js: 1.2.1 - - prelude-ls@1.2.1: {} - - punycode@2.3.1: {} - - react-dom@19.2.6(react@19.2.6): - dependencies: - react: 19.2.6 - scheduler: 0.27.0 - - react@19.2.6: {} - - resolve-from@4.0.0: {} - - rolldown@1.0.3: - dependencies: - '@oxc-project/types': 0.133.0 - '@rolldown/pluginutils': 1.0.1 - optionalDependencies: - '@rolldown/binding-android-arm64': 1.0.3 - '@rolldown/binding-darwin-arm64': 1.0.3 - '@rolldown/binding-darwin-x64': 1.0.3 - '@rolldown/binding-freebsd-x64': 1.0.3 - '@rolldown/binding-linux-arm-gnueabihf': 1.0.3 - '@rolldown/binding-linux-arm64-gnu': 1.0.3 - '@rolldown/binding-linux-arm64-musl': 1.0.3 - '@rolldown/binding-linux-ppc64-gnu': 1.0.3 - '@rolldown/binding-linux-s390x-gnu': 1.0.3 - '@rolldown/binding-linux-x64-gnu': 1.0.3 - '@rolldown/binding-linux-x64-musl': 1.0.3 - '@rolldown/binding-openharmony-arm64': 1.0.3 - '@rolldown/binding-wasm32-wasi': 1.0.3 - '@rolldown/binding-win32-arm64-msvc': 1.0.3 - '@rolldown/binding-win32-x64-msvc': 1.0.3 - - scheduler@0.27.0: {} - - semver@6.3.1: {} - - shebang-command@2.0.0: - dependencies: - shebang-regex: 3.0.0 - - shebang-regex@3.0.0: {} - - source-map-js@1.2.1: {} - - sql.js@1.14.1: {} - - strip-json-comments@3.1.1: {} - - supports-color@7.2.0: - dependencies: - has-flag: 4.0.0 - - tinyglobby@0.2.17: - dependencies: - fdir: 6.5.0(picomatch@4.0.5) - picomatch: 4.0.5 - - tslib@2.8.1: - optional: true - - type-check@0.4.0: - dependencies: - prelude-ls: 1.2.1 - - update-browserslist-db@1.2.3(browserslist@4.28.2): - dependencies: - browserslist: 4.28.2 - escalade: 3.2.0 - picocolors: 1.1.1 - - uri-js@4.4.1: - dependencies: - punycode: 2.3.1 - - vite@8.0.16: - dependencies: - lightningcss: 1.33.0 - picomatch: 4.0.5 - postcss: 8.5.23 - rolldown: 1.0.3 - tinyglobby: 0.2.17 - optionalDependencies: - fsevents: 2.3.3 - - which@2.0.2: - dependencies: - isexe: 2.0.0 - - word-wrap@1.2.5: {} - - yallist@3.1.1: {} - - yocto-queue@0.1.0: {} - - zod-validation-error@4.0.2(zod@4.4.3): - dependencies: - zod: 4.4.3 - - zod@4.4.3: {} diff --git a/2017/pnpm-workspace.yaml b/2017/pnpm-workspace.yaml deleted file mode 100644 index 65783dc..0000000 --- a/2017/pnpm-workspace.yaml +++ /dev/null @@ -1,2 +0,0 @@ -allowBuilds: - better-sqlite3: true diff --git a/2017/tools/xlsxread/Cargo.lock b/2017/tools/xlsxread/Cargo.lock deleted file mode 100644 index e57f452..0000000 --- a/2017/tools/xlsxread/Cargo.lock +++ /dev/null @@ -1,1159 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 4 - -[[package]] -name = "adler2" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" - -[[package]] -name = "aes" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" -dependencies = [ - "cfg-if", - "cipher", - "cpufeatures", -] - -[[package]] -name = "ahash" -version = "0.8.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" -dependencies = [ - "cfg-if", - "once_cell", - "version_check", - "zerocopy", -] - -[[package]] -name = "aho-corasick" -version = "1.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" -dependencies = [ - "memchr", -] - -[[package]] -name = "anstream" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" -dependencies = [ - "anstyle", - "anstyle-parse", - "anstyle-query", - "anstyle-wincon", - "colorchoice", - "is_terminal_polyfill", - "utf8parse", -] - -[[package]] -name = "anstyle" -version = "1.0.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" - -[[package]] -name = "anstyle-parse" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" -dependencies = [ - "utf8parse", -] - -[[package]] -name = "anstyle-query" -version = "1.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" -dependencies = [ - "windows-sys", -] - -[[package]] -name = "anstyle-wincon" -version = "3.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" -dependencies = [ - "anstyle", - "once_cell_polyfill", - "windows-sys", -] - -[[package]] -name = "anyhow" -version = "1.0.102" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" - -[[package]] -name = "arbitrary" -version = "1.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" -dependencies = [ - "derive_arbitrary", -] - -[[package]] -name = "bitflags" -version = "2.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" - -[[package]] -name = "block-buffer" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" -dependencies = [ - "generic-array", -] - -[[package]] -name = "bumpalo" -version = "3.20.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" - -[[package]] -name = "byteorder" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" - -[[package]] -name = "bzip2" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49ecfb22d906f800d4fe833b6282cf4dc1c298f5057ca0b5445e5c209735ca47" -dependencies = [ - "bzip2-sys", -] - -[[package]] -name = "bzip2-sys" -version = "0.1.13+1.0.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "225bff33b2141874fe80d71e07d6eec4f85c5c216453dd96388240f96e1acc14" -dependencies = [ - "cc", - "pkg-config", -] - -[[package]] -name = "calamine" -version = "0.26.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "138646b9af2c5d7f1804ea4bf93afc597737d2bd4f7341d67c48b03316976eb1" -dependencies = [ - "byteorder", - "codepage", - "encoding_rs", - "log", - "quick-xml", - "serde", - "zip", -] - -[[package]] -name = "cc" -version = "1.2.62" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1dce859f0832a7d088c4f1119888ab94ef4b5d6795d1ce05afb7fe159d79f98" -dependencies = [ - "find-msvc-tools", - "jobserver", - "libc", - "shlex", -] - -[[package]] -name = "cfg-if" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" - -[[package]] -name = "cipher" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" -dependencies = [ - "crypto-common", - "inout", -] - -[[package]] -name = "clap" -version = "4.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" -dependencies = [ - "clap_builder", - "clap_derive", -] - -[[package]] -name = "clap_builder" -version = "4.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" -dependencies = [ - "anstream", - "anstyle", - "clap_lex", - "strsim", -] - -[[package]] -name = "clap_derive" -version = "4.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "clap_lex" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" - -[[package]] -name = "codepage" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48f68d061bc2828ae826206326e61251aca94c1e4a5305cf52d9138639c918b4" -dependencies = [ - "encoding_rs", -] - -[[package]] -name = "colorchoice" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" - -[[package]] -name = "constant_time_eq" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" - -[[package]] -name = "cpufeatures" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" -dependencies = [ - "libc", -] - -[[package]] -name = "crc" -version = "3.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" -dependencies = [ - "crc-catalog", -] - -[[package]] -name = "crc-catalog" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "217698eaf96b4a3f0bc4f3662aaa55bdf913cd54d7204591faa790070c6d0853" - -[[package]] -name = "crc32fast" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "crossbeam-utils" -version = "0.8.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" - -[[package]] -name = "crypto-common" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" -dependencies = [ - "generic-array", - "typenum", -] - -[[package]] -name = "deflate64" -version = "0.1.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac6b926516df9c60bfa16e107b21086399f8285a44ca9711344b9e553c5146e2" - -[[package]] -name = "deranged" -version = "0.5.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" -dependencies = [ - "powerfmt", -] - -[[package]] -name = "derive_arbitrary" -version = "1.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "digest" -version = "0.10.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" -dependencies = [ - "block-buffer", - "crypto-common", - "subtle", -] - -[[package]] -name = "displaydoc" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "encoding_rs" -version = "0.8.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "equivalent" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" - -[[package]] -name = "fallible-iterator" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" - -[[package]] -name = "fallible-streaming-iterator" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" - -[[package]] -name = "find-msvc-tools" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" - -[[package]] -name = "flate2" -version = "1.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" -dependencies = [ - "crc32fast", - "miniz_oxide", -] - -[[package]] -name = "generic-array" -version = "0.14.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" -dependencies = [ - "typenum", - "version_check", -] - -[[package]] -name = "getrandom" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" -dependencies = [ - "cfg-if", - "js-sys", - "libc", - "r-efi", - "wasip2", - "wasm-bindgen", -] - -[[package]] -name = "glob" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" - -[[package]] -name = "hashbrown" -version = "0.14.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" -dependencies = [ - "ahash", -] - -[[package]] -name = "hashbrown" -version = "0.17.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" - -[[package]] -name = "hashlink" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af" -dependencies = [ - "hashbrown 0.14.5", -] - -[[package]] -name = "heck" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" - -[[package]] -name = "hmac" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" -dependencies = [ - "digest", -] - -[[package]] -name = "indexmap" -version = "2.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" -dependencies = [ - "equivalent", - "hashbrown 0.17.1", -] - -[[package]] -name = "inout" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" -dependencies = [ - "generic-array", -] - -[[package]] -name = "is_terminal_polyfill" -version = "1.70.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" - -[[package]] -name = "jobserver" -version = "0.1.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" -dependencies = [ - "getrandom", - "libc", -] - -[[package]] -name = "js-sys" -version = "0.3.98" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67df7112613f8bfd9150013a0314e196f4800d3201ae742489d999db2f979f08" -dependencies = [ - "cfg-if", - "once_cell", - "wasm-bindgen", -] - -[[package]] -name = "libc" -version = "0.2.186" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" - -[[package]] -name = "libsqlite3-sys" -version = "0.30.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" -dependencies = [ - "cc", - "pkg-config", - "vcpkg", -] - -[[package]] -name = "log" -version = "0.4.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" - -[[package]] -name = "lzma-rs" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "297e814c836ae64db86b36cf2a557ba54368d03f6afcd7d947c266692f71115e" -dependencies = [ - "byteorder", - "crc", -] - -[[package]] -name = "lzma-sys" -version = "0.1.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fda04ab3764e6cde78b9974eec4f779acaba7c4e84b36eca3cf77c581b85d27" -dependencies = [ - "cc", - "libc", - "pkg-config", -] - -[[package]] -name = "memchr" -version = "2.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" - -[[package]] -name = "miniz_oxide" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" -dependencies = [ - "adler2", - "simd-adler32", -] - -[[package]] -name = "num-conv" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" - -[[package]] -name = "once_cell" -version = "1.21.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" - -[[package]] -name = "once_cell_polyfill" -version = "1.70.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" - -[[package]] -name = "pbkdf2" -version = "0.12.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" -dependencies = [ - "digest", - "hmac", -] - -[[package]] -name = "pkg-config" -version = "0.3.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" - -[[package]] -name = "powerfmt" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" - -[[package]] -name = "proc-macro2" -version = "1.0.106" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "quick-xml" -version = "0.31.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1004a344b30a54e2ee58d66a71b32d2db2feb0a31f9a2d302bf0536f15de2a33" -dependencies = [ - "encoding_rs", - "memchr", -] - -[[package]] -name = "quote" -version = "1.0.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "r-efi" -version = "5.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" - -[[package]] -name = "regex" -version = "1.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" -dependencies = [ - "aho-corasick", - "memchr", - "regex-automata", - "regex-syntax", -] - -[[package]] -name = "regex-automata" -version = "0.4.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", -] - -[[package]] -name = "regex-syntax" -version = "0.8.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" - -[[package]] -name = "rusqlite" -version = "0.32.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7753b721174eb8ff87a9a0e799e2d7bc3749323e773db92e0984debb00019d6e" -dependencies = [ - "bitflags", - "fallible-iterator", - "fallible-streaming-iterator", - "hashlink", - "libsqlite3-sys", - "smallvec", -] - -[[package]] -name = "rustversion" -version = "1.0.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" - -[[package]] -name = "serde" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" -dependencies = [ - "serde_core", - "serde_derive", -] - -[[package]] -name = "serde_core" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "serde_spanned" -version = "0.6.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" -dependencies = [ - "serde", -] - -[[package]] -name = "sha1" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - -[[package]] -name = "shlex" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" - -[[package]] -name = "simd-adler32" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" - -[[package]] -name = "smallvec" -version = "1.15.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" - -[[package]] -name = "strsim" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" - -[[package]] -name = "subtle" -version = "2.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" - -[[package]] -name = "syn" -version = "2.0.117" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "thiserror" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" -dependencies = [ - "thiserror-impl 1.0.69", -] - -[[package]] -name = "thiserror" -version = "2.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" -dependencies = [ - "thiserror-impl 2.0.18", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "thiserror-impl" -version = "2.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "time" -version = "0.3.47" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" -dependencies = [ - "deranged", - "num-conv", - "powerfmt", - "serde_core", - "time-core", -] - -[[package]] -name = "time-core" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" - -[[package]] -name = "tinyvec" -version = "1.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" -dependencies = [ - "tinyvec_macros", -] - -[[package]] -name = "tinyvec_macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" - -[[package]] -name = "toml" -version = "0.8.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" -dependencies = [ - "serde", - "serde_spanned", - "toml_datetime", - "toml_edit", -] - -[[package]] -name = "toml_datetime" -version = "0.6.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" -dependencies = [ - "serde", -] - -[[package]] -name = "toml_edit" -version = "0.22.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" -dependencies = [ - "indexmap", - "serde", - "serde_spanned", - "toml_datetime", - "toml_write", - "winnow", -] - -[[package]] -name = "toml_write" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" - -[[package]] -name = "typenum" -version = "1.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" - -[[package]] -name = "unicode-ident" -version = "1.0.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" - -[[package]] -name = "unicode-normalization" -version = "0.1.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" -dependencies = [ - "tinyvec", -] - -[[package]] -name = "utf8parse" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" - -[[package]] -name = "vcpkg" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" - -[[package]] -name = "version_check" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" - -[[package]] -name = "wasip2" -version = "1.0.3+wasi-0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" -dependencies = [ - "wit-bindgen", -] - -[[package]] -name = "wasm-bindgen" -version = "0.2.121" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49ace1d07c165b0864824eee619580c4689389afa9dc9ed3a4c75040d82e6790" -dependencies = [ - "cfg-if", - "once_cell", - "rustversion", - "wasm-bindgen-macro", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.121" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e68e6f4afd367a562002c05637acb8578ff2dea1943df76afb9e83d177c8578" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.121" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d95a9ec35c64b2a7cb35d3fead40c4238d0940c86d107136999567a4703259f2" -dependencies = [ - "bumpalo", - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.121" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4e0100b01e9f0d03189a92b96772a1fb998639d981193d7dbab487302513441" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "windows-link" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" - -[[package]] -name = "windows-sys" -version = "0.61.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" -dependencies = [ - "windows-link", -] - -[[package]] -name = "winnow" -version = "0.7.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" -dependencies = [ - "memchr", -] - -[[package]] -name = "wit-bindgen" -version = "0.57.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" - -[[package]] -name = "xlsxread" -version = "0.1.0" -dependencies = [ - "anyhow", - "calamine", - "clap", - "glob", - "regex", - "rusqlite", - "serde", - "thiserror 1.0.69", - "toml", - "unicode-normalization", - "zip", -] - -[[package]] -name = "xz2" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "388c44dc09d76f1536602ead6d325eb532f5c122f17782bd57fb47baeeb767e2" -dependencies = [ - "lzma-sys", -] - -[[package]] -name = "zerocopy" -version = "0.8.48" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" -dependencies = [ - "zerocopy-derive", -] - -[[package]] -name = "zerocopy-derive" -version = "0.8.48" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "zeroize" -version = "1.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" -dependencies = [ - "zeroize_derive", -] - -[[package]] -name = "zeroize_derive" -version = "1.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "zip" -version = "2.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fabe6324e908f85a1c52063ce7aa26b68dcb7eb6dbc83a2d148403c9bc3eba50" -dependencies = [ - "aes", - "arbitrary", - "bzip2", - "constant_time_eq", - "crc32fast", - "crossbeam-utils", - "deflate64", - "displaydoc", - "flate2", - "getrandom", - "hmac", - "indexmap", - "lzma-rs", - "memchr", - "pbkdf2", - "sha1", - "thiserror 2.0.18", - "time", - "xz2", - "zeroize", - "zopfli", - "zstd", -] - -[[package]] -name = "zopfli" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f05cd8797d63865425ff89b5c4a48804f35ba0ce8d125800027ad6017d2b5249" -dependencies = [ - "bumpalo", - "crc32fast", - "log", - "simd-adler32", -] - -[[package]] -name = "zstd" -version = "0.13.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" -dependencies = [ - "zstd-safe", -] - -[[package]] -name = "zstd-safe" -version = "7.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" -dependencies = [ - "zstd-sys", -] - -[[package]] -name = "zstd-sys" -version = "2.0.16+zstd.1.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" -dependencies = [ - "cc", - "pkg-config", -] diff --git a/2017/tools/xlsxread/Cargo.toml b/2017/tools/xlsxread/Cargo.toml deleted file mode 100644 index d23dcca..0000000 --- a/2017/tools/xlsxread/Cargo.toml +++ /dev/null @@ -1,30 +0,0 @@ -[package] -name = "xlsxread" -version = "0.1.0" -edition = "2021" -description = "Rust CLI replacing SheetJS xlsx build scripts for thptqg2017/thptqg2016" - -[dependencies] -calamine = "0.26" -rusqlite = { version = "0.32", features = ["bundled"] } -clap = { version = "4", features = ["derive"] } -serde = { version = "1", features = ["derive"] } -toml = "0.8" -regex = "1" -unicode-normalization = "0.1" -thiserror = "1" -anyhow = "1" -glob = "0.3" - -# zip is already a transitive dep of calamine; pin explicitly so tests can use it -[dev-dependencies] -zip = "2" -rusqlite = { version = "0.32", features = ["bundled"] } - -[[bin]] -name = "xlsxread" -path = "src/main.rs" - -[[test]] -name = "golden" -path = "tests/golden.rs" diff --git a/2017/tools/xlsxread/configs/thptqg2017-data-old.toml b/2017/tools/xlsxread/configs/thptqg2017-data-old.toml deleted file mode 100644 index 03d51be..0000000 --- a/2017/tools/xlsxread/configs/thptqg2017-data-old.toml +++ /dev/null @@ -1,75 +0,0 @@ -# Config for data-old/ — 63 .xlsx files (pre-baotintuc refresh) -# Sheet mode: "first" — single-sheet workbooks, never hit 65k row cap -# SBD validation: require ^\d+$ (build-database-old.js:55 guard) -# Blank row strip: off (no explicit blank-skip in build-database-old.js) - -[reader] -sheet_mode = "first" -strip_blank_rows = false - -[columns] -ho_ten = 0 -ngay_sinh = 1 -so_bao_danh = 2 -diem_thi = 3 - -[validation] -require_numeric_sbd = true -require_nonempty_name = true -require_nonempty_sbd = true - -[header] -tokens = ["HO_TEN", "HỌ TÊN", "STT"] - -[schema] -ddl = """ -CREATE TABLE student ( - so_bao_danh TEXT PRIMARY KEY, - ho_ten TEXT NOT NULL, - ho_ten_ascii TEXT NOT NULL, - ngay_sinh TEXT, - toan REAL, - ngu_van REAL, - vat_ly REAL, - hoa_hoc REAL, - sinh_hoc REAL, - khtn REAL, - lich_su REAL, - dia_ly REAL, - gdcd REAL, - khxh REAL, - tieng_anh REAL, - tieng_phap REAL, - tieng_nga REAL, - tieng_trung REAL -); -CREATE INDEX idx_ho_ten ON student(ho_ten); -CREATE INDEX idx_ho_ten_ascii ON student(ho_ten_ascii); -""" - -[scores] -toan = 'Toán:\s*(\d+(?:\.\d+)?)' -ngu_van = 'Ngữ văn:\s*(\d+(?:\.\d+)?)' -vat_ly = 'Vật lí:\s*(\d+(?:\.\d+)?)' -hoa_hoc = 'Hóa học:\s*(\d+(?:\.\d+)?)' -sinh_hoc = 'Sinh học:\s*(\d+(?:\.\d+)?)' -khtn = 'KHTN:\s*(\d+(?:\.\d+)?)' -lich_su = 'Lịch sử:\s*(\d+(?:\.\d+)?)' -dia_ly = 'Địa lí:\s*(\d+(?:\.\d+)?)' -gdcd = 'GDCD:\s*(\d+(?:\.\d+)?)' -khxh = 'KHXH:\s*(\d+(?:\.\d+)?)' -tieng_anh = 'Tiếng Anh:\s*(\d+(?:\.\d+)?)' -tieng_phap = 'Tiếng Pháp:\s*(\d+(?:\.\d+)?)' -tieng_nga = 'Tiếng Nga:\s*(\d+(?:\.\d+)?)' -tieng_trung = 'Tiếng Trung:\s*(\d+(?:\.\d+)?)' - -[insert] -sql = """ -INSERT OR REPLACE INTO student - (so_bao_danh, ho_ten, ho_ten_ascii, ngay_sinh, - toan, ngu_van, vat_ly, hoa_hoc, sinh_hoc, khtn, - lich_su, dia_ly, gdcd, khxh, - tieng_anh, tieng_phap, tieng_nga, tieng_trung) -VALUES - (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) -""" diff --git a/2017/tools/xlsxread/configs/thptqg2017-data-old2.toml b/2017/tools/xlsxread/configs/thptqg2017-data-old2.toml deleted file mode 100644 index 2c3f52c..0000000 --- a/2017/tools/xlsxread/configs/thptqg2017-data-old2.toml +++ /dev/null @@ -1,76 +0,0 @@ -# Config for data-old2/ — 54 .xlsx files (corrected-export set) -# Sheet mode: "all" — HCM (24.HCM_UTLQ.xlsx) overflows into Sheet2 (+6,446 rows) -# SBD validation: require ^\d+$ (build-database-old2.js:57 guard) -# Blank row strip: true — skip fully blank rows BEFORE counting sourceRows -# (build-database-old2.js:50-51: blank row check before sourceRows++) - -[reader] -sheet_mode = "all" -strip_blank_rows = true - -[columns] -ho_ten = 0 -ngay_sinh = 1 -so_bao_danh = 2 -diem_thi = 3 - -[validation] -require_numeric_sbd = true -require_nonempty_name = true -require_nonempty_sbd = true - -[header] -tokens = ["HO_TEN", "HỌ TÊN", "STT"] - -[schema] -ddl = """ -CREATE TABLE student ( - so_bao_danh TEXT PRIMARY KEY, - ho_ten TEXT NOT NULL, - ho_ten_ascii TEXT NOT NULL, - ngay_sinh TEXT, - toan REAL, - ngu_van REAL, - vat_ly REAL, - hoa_hoc REAL, - sinh_hoc REAL, - khtn REAL, - lich_su REAL, - dia_ly REAL, - gdcd REAL, - khxh REAL, - tieng_anh REAL, - tieng_phap REAL, - tieng_nga REAL, - tieng_trung REAL -); -CREATE INDEX idx_ho_ten ON student(ho_ten); -CREATE INDEX idx_ho_ten_ascii ON student(ho_ten_ascii); -""" - -[scores] -toan = 'Toán:\s*(\d+(?:\.\d+)?)' -ngu_van = 'Ngữ văn:\s*(\d+(?:\.\d+)?)' -vat_ly = 'Vật lí:\s*(\d+(?:\.\d+)?)' -hoa_hoc = 'Hóa học:\s*(\d+(?:\.\d+)?)' -sinh_hoc = 'Sinh học:\s*(\d+(?:\.\d+)?)' -khtn = 'KHTN:\s*(\d+(?:\.\d+)?)' -lich_su = 'Lịch sử:\s*(\d+(?:\.\d+)?)' -dia_ly = 'Địa lí:\s*(\d+(?:\.\d+)?)' -gdcd = 'GDCD:\s*(\d+(?:\.\d+)?)' -khxh = 'KHXH:\s*(\d+(?:\.\d+)?)' -tieng_anh = 'Tiếng Anh:\s*(\d+(?:\.\d+)?)' -tieng_phap = 'Tiếng Pháp:\s*(\d+(?:\.\d+)?)' -tieng_nga = 'Tiếng Nga:\s*(\d+(?:\.\d+)?)' -tieng_trung = 'Tiếng Trung:\s*(\d+(?:\.\d+)?)' - -[insert] -sql = """ -INSERT OR REPLACE INTO student - (so_bao_danh, ho_ten, ho_ten_ascii, ngay_sinh, - toan, ngu_van, vat_ly, hoa_hoc, sinh_hoc, khtn, - lich_su, dia_ly, gdcd, khxh, - tieng_anh, tieng_phap, tieng_nga, tieng_trung) -VALUES - (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) -""" diff --git a/2017/tools/xlsxread/configs/thptqg2017-data.toml b/2017/tools/xlsxread/configs/thptqg2017-data.toml deleted file mode 100644 index c879f7b..0000000 --- a/2017/tools/xlsxread/configs/thptqg2017-data.toml +++ /dev/null @@ -1,75 +0,0 @@ -# Config for data/ — 63 .xls files from baotintuc.vn -# Sheet mode: "all" because Hà Nội and HCM overflow into Sheet2 (65k row cap) -# SBD validation: no numeric guard (build-database.js does not apply ^\d+$) -# Blank row strip: off - -[reader] -sheet_mode = "all" -strip_blank_rows = false - -[columns] -ho_ten = 0 -ngay_sinh = 1 -so_bao_danh = 2 -diem_thi = 3 - -[validation] -require_numeric_sbd = false -require_nonempty_name = true -require_nonempty_sbd = true - -[header] -tokens = ["HO_TEN", "HỌ TÊN", "STT"] - -[schema] -ddl = """ -CREATE TABLE student ( - so_bao_danh TEXT PRIMARY KEY, - ho_ten TEXT NOT NULL, - ho_ten_ascii TEXT NOT NULL, - ngay_sinh TEXT, - toan REAL, - ngu_van REAL, - vat_ly REAL, - hoa_hoc REAL, - sinh_hoc REAL, - khtn REAL, - lich_su REAL, - dia_ly REAL, - gdcd REAL, - khxh REAL, - tieng_anh REAL, - tieng_phap REAL, - tieng_nga REAL, - tieng_trung REAL -); -CREATE INDEX idx_ho_ten ON student(ho_ten); -CREATE INDEX idx_ho_ten_ascii ON student(ho_ten_ascii); -""" - -[scores] -toan = 'Toán:\s*(\d+(?:\.\d+)?)' -ngu_van = 'Ngữ văn:\s*(\d+(?:\.\d+)?)' -vat_ly = 'Vật lí:\s*(\d+(?:\.\d+)?)' -hoa_hoc = 'Hóa học:\s*(\d+(?:\.\d+)?)' -sinh_hoc = 'Sinh học:\s*(\d+(?:\.\d+)?)' -khtn = 'KHTN:\s*(\d+(?:\.\d+)?)' -lich_su = 'Lịch sử:\s*(\d+(?:\.\d+)?)' -dia_ly = 'Địa lí:\s*(\d+(?:\.\d+)?)' -gdcd = 'GDCD:\s*(\d+(?:\.\d+)?)' -khxh = 'KHXH:\s*(\d+(?:\.\d+)?)' -tieng_anh = 'Tiếng Anh:\s*(\d+(?:\.\d+)?)' -tieng_phap = 'Tiếng Pháp:\s*(\d+(?:\.\d+)?)' -tieng_nga = 'Tiếng Nga:\s*(\d+(?:\.\d+)?)' -tieng_trung = 'Tiếng Trung:\s*(\d+(?:\.\d+)?)' - -[insert] -sql = """ -INSERT OR REPLACE INTO student - (so_bao_danh, ho_ten, ho_ten_ascii, ngay_sinh, - toan, ngu_van, vat_ly, hoa_hoc, sinh_hoc, khtn, - lich_su, dia_ly, gdcd, khxh, - tieng_anh, tieng_phap, tieng_nga, tieng_trung) -VALUES - (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) -""" diff --git a/2017/tools/xlsxread/src/audit.rs b/2017/tools/xlsxread/src/audit.rs deleted file mode 100644 index 30ec8f4..0000000 --- a/2017/tools/xlsxread/src/audit.rs +++ /dev/null @@ -1,174 +0,0 @@ -/// Audit subcommand: replicates audit-row-counts.js exactly. -/// -/// Reads all .xlsx files from the input directory (sheet 0 only, matching the -/// JS script's behaviour at audit-row-counts.js:33), collects distinct SBDs -/// into a HashSet, then queries `SELECT COUNT(*) FROM student` from the DB. -/// Prints the same lines as audit-row-counts.js:54-62 and exits 0 on match, -/// 1 on mismatch. -use std::collections::HashSet; -use std::path::Path; - -use calamine::{open_workbook_auto, Data, Reader}; - -use crate::config::DatasetConfig; -use crate::error::BuildError; -use crate::reader::is_header_row; - -// --------------------------------------------------------------------------- -// Audit result -// --------------------------------------------------------------------------- - -pub struct AuditResult { - pub total_data_rows: u64, - pub both_empty: u64, - pub empty_name: u64, - pub empty_sbd: u64, - pub distinct_sbds: usize, - pub db_count: i64, - pub matched: bool, -} - -// --------------------------------------------------------------------------- -// Main audit logic -// --------------------------------------------------------------------------- - -/// Collect distinct SBDs from all xlsx files in `input_dir`, query `db_path`, -/// print the audit report and return the result. -/// -/// The JS script reads only sheet 0 for every file (audit-row-counts.js:33). -/// Unlike build-database.js, the audit script does NOT iterate all sheets. -pub fn run_audit( - input_dir: &Path, - db_path: &Path, - cfg: &DatasetConfig, -) -> Result { - // Collect .xlsx files (audit-row-counts.js only checks .xlsx — line 15) - let mut files: Vec = std::fs::read_dir(input_dir) - .map_err(|e| BuildError::Io { - path: input_dir.display().to_string(), - source: e, - })? - .filter_map(|e| e.ok()) - .map(|e| e.path()) - .filter(|p| { - p.is_file() - && p.extension() - .and_then(|e| e.to_str()) - .map(|e| e.eq_ignore_ascii_case("xlsx")) - .unwrap_or(false) - }) - .collect(); - files.sort(); - - let mut all_sbd: HashSet = HashSet::new(); - let mut total_data_rows: u64 = 0; - let mut empty_name: u64 = 0; - let mut empty_sbd: u64 = 0; - let mut both_empty: u64 = 0; - - for file in &files { - let path_str = file.display().to_string(); - let mut workbook = open_workbook_auto(file).map_err(|e| BuildError::Calamine { - path: path_str.clone(), - source: e, - })?; - - let sheet_names = workbook.sheet_names().to_vec(); - if sheet_names.is_empty() { - continue; - } - - // audit-row-counts.js reads only sheet 0 (line 33: wb.SheetNames[0]) - let range = - workbook - .worksheet_range(&sheet_names[0]) - .map_err(|e| BuildError::Calamine { - path: path_str.clone(), - source: e, - })?; - - let mut first_row = true; - for raw in range.rows() { - let row: Vec = raw.to_vec(); - - // Skip header row on first row only - if first_row { - first_row = false; - if is_header_row(&row, &cfg.header) { - continue; - } - } - - total_data_rows += 1; - - let ho_ten = row - .get(cfg.columns.ho_ten) - .map(|c| c.to_string().trim().to_owned()) - .unwrap_or_default(); - let sbd = row - .get(cfg.columns.so_bao_danh) - .map(|c| c.to_string().trim().to_owned()) - .unwrap_or_default(); - - if ho_ten.is_empty() && sbd.is_empty() { - both_empty += 1; - continue; - } - if ho_ten.is_empty() { - empty_name += 1; - } - if sbd.is_empty() { - empty_sbd += 1; - } - if !sbd.is_empty() { - all_sbd.insert(sbd); - } - } - } - - // Query DB count - let conn = - rusqlite::Connection::open_with_flags(db_path, rusqlite::OpenFlags::SQLITE_OPEN_READ_ONLY)?; - let db_count: i64 = conn.query_row("SELECT COUNT(*) FROM student", [], |row| row.get(0))?; - - let distinct_sbds = all_sbd.len(); - let matched = distinct_sbds as i64 == db_count; - - Ok(AuditResult { - total_data_rows, - both_empty, - empty_name, - empty_sbd, - distinct_sbds, - db_count, - matched, - }) -} - -// --------------------------------------------------------------------------- -// Print audit report — mirrors audit-row-counts.js:54-62 exactly -// --------------------------------------------------------------------------- - -pub fn print_audit_report(r: &AuditResult) { - println!("=== Source vs DB ==="); - println!( - "Source: total data rows across all files: {}", - r.total_data_rows - ); - println!( - "Source: rows with empty name AND sbd (skipped): {}", - r.both_empty - ); - println!("Source: rows with missing name only: {}", r.empty_name); - println!("Source: rows with missing sbd only: {}", r.empty_sbd); - println!("Source: distinct SBDs: {}", r.distinct_sbds); - println!("DB: row count: {}", r.db_count); - println!( - "Match: {}", - if r.matched { - "YES — all unique SBDs accounted for".to_string() - } else { - format!("NO — gap of {}", r.distinct_sbds as i64 - r.db_count) - } - ); -} diff --git a/2017/tools/xlsxread/src/cli.rs b/2017/tools/xlsxread/src/cli.rs deleted file mode 100644 index bce8285..0000000 --- a/2017/tools/xlsxread/src/cli.rs +++ /dev/null @@ -1,48 +0,0 @@ -/// CLI argument structs via clap derive. -use std::path::PathBuf; - -use clap::{Parser, Subcommand}; - -#[derive(Parser)] -#[command( - name = "xlsxread", - version, - about = "Read .xls/.xlsx files and build SQLite databases for thptqg datasets" -)] -pub struct Cli { - #[command(subcommand)] - pub cmd: Cmd, -} - -#[derive(Subcommand)] -pub enum Cmd { - /// Read input spreadsheets and write a SQLite database - Build { - /// Path to the dataset TOML config file - #[arg(long)] - schema: PathBuf, - - /// Directory containing the .xls / .xlsx source files - #[arg(long)] - input: PathBuf, - - /// Output SQLite database path - #[arg(long)] - output: PathBuf, - }, - - /// Audit: compare distinct SBD count from xlsx files vs DB row count - Audit { - /// Path to the dataset TOML config file - #[arg(long)] - schema: PathBuf, - - /// Directory containing the .xlsx source files - #[arg(long)] - input: PathBuf, - - /// SQLite database to compare against - #[arg(long)] - db: PathBuf, - }, -} diff --git a/2017/tools/xlsxread/src/config.rs b/2017/tools/xlsxread/src/config.rs deleted file mode 100644 index 8a2cd4c..0000000 --- a/2017/tools/xlsxread/src/config.rs +++ /dev/null @@ -1,146 +0,0 @@ -use std::collections::HashMap; -use std::fs; -use std::path::Path; - -use serde::Deserialize; - -use crate::error::BuildError; - -// --------------------------------------------------------------------------- -// Top-level dataset configuration loaded from a .toml file -// --------------------------------------------------------------------------- - -#[derive(Debug, Deserialize, Clone)] -pub struct DatasetConfig { - pub reader: ReaderCfg, - pub columns: ColumnMap, - pub validation: ValidationCfg, - pub header: HeaderCfg, - pub schema: SchemaCfg, - /// field name → regex source string (one entry per scoreable subject) - pub scores: HashMap, - pub insert: InsertCfg, -} - -#[derive(Debug, Deserialize, Clone)] -pub struct ReaderCfg { - /// "all" → iterate every sheet (handles HCM/HN overflow); "first" → sheet 0 only - pub sheet_mode: SheetMode, - /// If true, skip rows where every cell is empty/null before counting (data-old2 quirk) - pub strip_blank_rows: bool, -} - -#[derive(Debug, Deserialize, Clone, PartialEq, Eq)] -#[serde(rename_all = "lowercase")] -pub enum SheetMode { - All, - First, -} - -/// Zero-indexed column positions in the source spreadsheet row. -#[derive(Debug, Deserialize, Clone)] -pub struct ColumnMap { - pub ho_ten: usize, - pub ngay_sinh: usize, - pub so_bao_danh: usize, - pub diem_thi: usize, -} - -#[derive(Debug, Deserialize, Clone)] -pub struct ValidationCfg { - /// build-database-old.js / -old2.js require soBaoDanh to match ^\d+$ - pub require_numeric_sbd: bool, - pub require_nonempty_name: bool, - pub require_nonempty_sbd: bool, -} - -#[derive(Debug, Deserialize, Clone)] -pub struct HeaderCfg { - /// Tokens to match against row[0].to_uppercase() to detect a header row - pub tokens: Vec, -} - -#[derive(Debug, Deserialize, Clone)] -pub struct SchemaCfg { - /// DDL executed verbatim before inserts (CREATE TABLE + CREATE INDEX) - pub ddl: String, -} - -#[derive(Debug, Deserialize, Clone)] -pub struct InsertCfg { - /// Parameterised INSERT OR REPLACE SQL using :named_param style - pub sql: String, -} - -// --------------------------------------------------------------------------- -// Loader -// --------------------------------------------------------------------------- - -pub fn load_config(path: &Path) -> Result { - let text = fs::read_to_string(path).map_err(|e| BuildError::Io { - path: path.display().to_string(), - source: e, - })?; - let cfg: DatasetConfig = toml::from_str(&text)?; - Ok(cfg) -} - -// --------------------------------------------------------------------------- -// Unit tests -// --------------------------------------------------------------------------- - -#[cfg(test)] -mod tests { - use super::*; - - const SAMPLE_TOML: &str = r#" -[reader] -sheet_mode = "all" -strip_blank_rows = false - -[columns] -ho_ten = 0 -ngay_sinh = 1 -so_bao_danh = 2 -diem_thi = 3 - -[validation] -require_numeric_sbd = false -require_nonempty_name = true -require_nonempty_sbd = true - -[header] -tokens = ["HO_TEN", "HỌ TÊN", "STT"] - -[schema] -ddl = "CREATE TABLE student (so_bao_danh TEXT PRIMARY KEY);" - -[scores] -toan = 'Toán:\s*(\d+(?:\.\d+)?)' -ngu_van = 'Ngữ văn:\s*(\d+(?:\.\d+)?)' - -[insert] -sql = "INSERT OR REPLACE INTO student (so_bao_danh) VALUES (:so_bao_danh)" -"#; - - #[test] - fn config_round_trip() { - let cfg: DatasetConfig = toml::from_str(SAMPLE_TOML).expect("parse failed"); - assert_eq!(cfg.reader.sheet_mode, SheetMode::All); - assert!(!cfg.reader.strip_blank_rows); - assert_eq!(cfg.columns.ho_ten, 0); - assert_eq!(cfg.columns.diem_thi, 3); - assert!(!cfg.validation.require_numeric_sbd); - assert!(cfg.validation.require_nonempty_name); - assert_eq!(cfg.header.tokens.len(), 3); - assert!(cfg.scores.contains_key("toan")); - assert!(cfg.scores.contains_key("ngu_van")); - } - - #[test] - fn config_first_sheet_mode() { - let toml_str = SAMPLE_TOML.replace(r#"sheet_mode = "all""#, r#"sheet_mode = "first""#); - let cfg: DatasetConfig = toml::from_str(&toml_str).expect("parse failed"); - assert_eq!(cfg.reader.sheet_mode, SheetMode::First); - } -} diff --git a/2017/tools/xlsxread/src/error.rs b/2017/tools/xlsxread/src/error.rs deleted file mode 100644 index e15edc9..0000000 --- a/2017/tools/xlsxread/src/error.rs +++ /dev/null @@ -1,34 +0,0 @@ -use thiserror::Error; - -#[derive(Debug, Error)] -pub enum BuildError { - #[error("I/O error for {path}: {source}")] - Io { - path: String, - #[source] - source: std::io::Error, - }, - - #[error("Calamine error for {path}: {source}")] - Calamine { - path: String, - #[source] - source: calamine::Error, - }, - - #[error("SQLite error: {0}")] - Sqlite(#[from] rusqlite::Error), - - #[error("Config parse error: {0}")] - Config(#[from] toml::de::Error), - - #[error("Regex compile error for pattern '{pattern}': {source}")] - Regex { - pattern: String, - #[source] - source: regex::Error, - }, - - #[error("Schema has no sheets in file: {0}")] - NoSheets(String), -} diff --git a/2017/tools/xlsxread/src/lib.rs b/2017/tools/xlsxread/src/lib.rs deleted file mode 100644 index 948fa08..0000000 --- a/2017/tools/xlsxread/src/lib.rs +++ /dev/null @@ -1,9 +0,0 @@ -/// Public library interface for integration tests. -/// The binary entry point is src/main.rs; this file re-exports the internal -/// modules so tests/golden.rs can call them without going through the CLI. -pub mod audit; -pub mod config; -pub mod error; -pub mod reader; -pub mod transform; -pub mod writer; diff --git a/2017/tools/xlsxread/src/main.rs b/2017/tools/xlsxread/src/main.rs deleted file mode 100644 index 963f8f3..0000000 --- a/2017/tools/xlsxread/src/main.rs +++ /dev/null @@ -1,193 +0,0 @@ -/// xlsxread — Rust CLI replacing the SheetJS xlsx build scripts. -/// -/// Subcommands: -/// build — read .xls/.xlsx files → write SQLite DB -/// audit — compare distinct SBD count from xlsx vs DB row count -/// -/// Library modules are declared in lib.rs; main.rs only adds the CLI layer. -mod cli; - -use std::path::Path; - -use anyhow::{Context, Result}; -use clap::Parser; - -use cli::{Cli, Cmd}; -use xlsxread::audit; -use xlsxread::config::load_config; -use xlsxread::reader::{is_all_blank, process_file}; -use xlsxread::transform::{validate_row, CompiledPatterns, SkipReason}; -use xlsxread::writer::{finish_db, insert_row, open_db, SCORE_FIELDS}; - -fn main() -> Result<()> { - let cli = Cli::parse(); - - match cli.cmd { - Cmd::Build { - schema, - input, - output, - } => { - run_build(&schema, &input, &output)?; - } - Cmd::Audit { schema, input, db } => { - let cfg = load_config(&schema) - .with_context(|| format!("Failed to load config: {}", schema.display()))?; - let result = audit::run_audit(&input, &db, &cfg).with_context(|| "Audit failed")?; - audit::print_audit_report(&result); - if !result.matched { - std::process::exit(1); - } - } - } - - Ok(()) -} - -// --------------------------------------------------------------------------- -// Build subcommand -// --------------------------------------------------------------------------- - -fn run_build(schema_path: &Path, input_dir: &Path, output_path: &Path) -> Result<()> { - let cfg = load_config(schema_path) - .with_context(|| format!("Failed to load config: {}", schema_path.display()))?; - - // Compile score regexes once at startup - let patterns = - CompiledPatterns::new(&cfg.scores).with_context(|| "Failed to compile score regexes")?; - - // Collect input files (.xls and .xlsx), sorted for deterministic order - let mut files: Vec = std::fs::read_dir(input_dir) - .with_context(|| format!("Cannot read input dir: {}", input_dir.display()))? - .filter_map(|e| e.ok()) - .map(|e| e.path()) - .filter(|p| { - p.is_file() - && p.extension() - .and_then(|e| e.to_str()) - .map(|e| { - let lower = e.to_lowercase(); - lower == "xls" || lower == "xlsx" - }) - .unwrap_or(false) - }) - .collect(); - files.sort(); - - let dataset_label = input_dir - .file_name() - .and_then(|n| n.to_str()) - .unwrap_or("data"); - - println!( - "[build] {dataset_label}/ → {} ({} files)", - output_path.display(), - files.len() - ); - - // Open (or recreate) DB and apply DDL - let conn = open_db(output_path, &cfg) - .with_context(|| format!("Failed to open DB: {}", output_path.display()))?; - - let mut total_source_rows: u64 = 0; - let mut total_skipped: u64 = 0; - let mut total_errors: u64 = 0; - - let is_old2 = dataset_label.contains("old2"); - let strip_blank = cfg.reader.strip_blank_rows; - - // Single transaction over all files — mirrors the Node `db.transaction(() => { ... })()` - conn.execute_batch("BEGIN")?; - - for file in &files { - let base = file - .file_name() - .and_then(|n| n.to_str()) - .unwrap_or("?") - .to_owned(); - let mut file_rows: u64 = 0; - let mut file_skipped: u64 = 0; - let mut file_errors: u64 = 0; - - let process_result = process_file(file, &cfg, |_sheet_idx, raw| { - // data-old2: skip fully blank rows BEFORE counting sourceRows - let all_blank = is_all_blank(raw); - if strip_blank && all_blank { - return; - } - - total_source_rows += 1; - - let ho_ten = raw - .get(cfg.columns.ho_ten) - .map(|c| c.to_string().trim().to_owned()) - .unwrap_or_default(); - let so_bao_danh = raw - .get(cfg.columns.so_bao_danh) - .map(|c| c.to_string().trim().to_owned()) - .unwrap_or_default(); - - match validate_row( - &ho_ten, - &so_bao_danh, - &cfg.validation, - strip_blank, - all_blank, - ) { - Err(SkipReason::BlankRow) => { - // Already guarded above; won't reach here - } - Err(_) => { - file_skipped += 1; - return; - } - Ok(()) => {} - } - - let parsed = xlsxread::transform::transform_row(raw, &cfg, &patterns); - - match insert_row(&conn, &cfg.insert.sql, &parsed, SCORE_FIELDS) { - Ok(()) => { - file_rows += 1; - } - Err(e) => { - file_errors += 1; - if total_errors + file_errors <= 5 { - eprintln!(" [warn] {base}: {e}"); - } - } - } - }); - - match process_result { - Ok(_) => {} - Err(e) => { - eprintln!(" [error] {base}: {e}"); - file_errors += 1; - } - } - - total_skipped += file_skipped; - total_errors += file_errors; - - // Per-file row count line — mirrors `console.log(` ${base}: ${fileRows} rows`)` - println!(" {base}: {file_rows} rows"); - } - - conn.execute_batch("COMMIT")?; - - // VACUUM + stats output - finish_db( - &conn, - output_path, - total_source_rows, - total_skipped, - total_errors, - dataset_label, - files.len(), - is_old2, - ) - .with_context(|| "Failed to finalise DB")?; - - Ok(()) -} diff --git a/2017/tools/xlsxread/src/reader.rs b/2017/tools/xlsxread/src/reader.rs deleted file mode 100644 index ac71da1..0000000 --- a/2017/tools/xlsxread/src/reader.rs +++ /dev/null @@ -1,197 +0,0 @@ -/// Spreadsheet reader: wraps calamine to iterate rows across sheets. -/// -/// Sheet selection mirrors the JS scripts: -/// - sheet_mode = "all" → iterate every sheet (handles HCM/HN 65k overflow in data/) -/// - sheet_mode = "first" → sheet 0 only (data-old/) -/// -/// Header detection mirrors build-lib.js isHeaderRow: -/// row[0].toUpperCase() in {"HO_TEN", "HỌ TÊN", "STT"} -use std::path::Path; - -use calamine::{open_workbook_auto, Data, Reader, Sheets}; - -use crate::config::{DatasetConfig, HeaderCfg, SheetMode}; -use crate::error::BuildError; - -// --------------------------------------------------------------------------- -// Public row representation from calamine -// --------------------------------------------------------------------------- - -pub type RawRow = Vec; - -// --------------------------------------------------------------------------- -// Header detection — mirrors build-lib.js isHeaderRow -// --------------------------------------------------------------------------- - -/// Returns true when the first cell (uppercased) matches one of the configured -/// header tokens. Used to skip the header row on the first row of each sheet. -pub fn is_header_row(row: &[Data], header_cfg: &HeaderCfg) -> bool { - if row.len() < 3 { - return false; - } - let first = row[0].to_string().trim().to_uppercase(); - header_cfg.tokens.iter().any(|t| t.to_uppercase() == first) -} - -// --------------------------------------------------------------------------- -// All-blank row check (data-old2: strip_blank_rows) -// --------------------------------------------------------------------------- - -pub fn is_all_blank(row: &[Data]) -> bool { - row.iter() - .all(|c| matches!(c, Data::Empty) || c.to_string().trim().is_empty()) -} - -// --------------------------------------------------------------------------- -// File processor — yields all data rows from the file -// --------------------------------------------------------------------------- - -/// Process one spreadsheet file, calling `on_row` for each data row. -/// -/// `on_row` receives `(sheet_index, row_index_in_sheet, raw_row)` where -/// `row_index_in_sheet` is 0-based AFTER the header has been consumed. -/// Returns `(sheets_seen, total_rows_yielded)`. -pub fn process_file( - path: &Path, - cfg: &DatasetConfig, - mut on_row: F, -) -> Result<(usize, usize), BuildError> -where - F: FnMut(usize, &RawRow), -{ - let path_str = path.display().to_string(); - - // calamine::open_workbook_auto dispatches on file extension - let mut workbook: Sheets<_> = open_workbook_auto(path).map_err(|e| BuildError::Calamine { - path: path_str.clone(), - source: e, - })?; - - let sheet_names: Vec = workbook.sheet_names().to_vec(); - if sheet_names.is_empty() { - return Err(BuildError::NoSheets(path_str.clone())); - } - - // Sheet selection per config - let sheets_to_read: Vec = match cfg.reader.sheet_mode { - SheetMode::All => sheet_names.clone(), - SheetMode::First => vec![sheet_names[0].clone()], - }; - - let mut total_rows = 0usize; - - for (sheet_idx, sheet_name) in sheets_to_read.iter().enumerate() { - let range = workbook - .worksheet_range(sheet_name) - .map_err(|e| BuildError::Calamine { - path: path_str.clone(), - source: e, - })?; - - let mut first_row = true; - - for raw in range.rows() { - let row: RawRow = raw.to_vec(); - - // Skip header row on first row of each sheet (matches JS: `if (i === 0 && isHeaderRow(...))`) - if first_row { - first_row = false; - if is_header_row(&row, &cfg.header) { - continue; - } - } - - on_row(sheet_idx, &row); - total_rows += 1; - } - } - - Ok((sheets_to_read.len(), total_rows)) -} - -// --------------------------------------------------------------------------- -// Unit tests -// --------------------------------------------------------------------------- - -#[cfg(test)] -mod tests { - use super::*; - use crate::config::HeaderCfg; - - fn hdr(tokens: &[&str]) -> HeaderCfg { - HeaderCfg { - tokens: tokens.iter().map(|s| s.to_string()).collect(), - } - } - - #[test] - fn header_detects_ho_ten() { - let row = vec![ - Data::String("HO_TEN".into()), - Data::String("NGAY_SINH".into()), - Data::String("SBD".into()), - ]; - let cfg = hdr(&["HO_TEN", "HỌ TÊN", "STT"]); - assert!(is_header_row(&row, &cfg)); - } - - #[test] - fn header_detects_stt() { - let row = vec![ - Data::String("STT".into()), - Data::String("B".into()), - Data::String("C".into()), - ]; - let cfg = hdr(&["HO_TEN", "HỌ TÊN", "STT"]); - assert!(is_header_row(&row, &cfg)); - } - - #[test] - fn header_detects_ho_ten_unicode() { - let row = vec![ - Data::String("HỌ TÊN".into()), - Data::String("B".into()), - Data::String("C".into()), - ]; - let cfg = hdr(&["HO_TEN", "HỌ TÊN", "STT"]); - assert!(is_header_row(&row, &cfg)); - } - - #[test] - fn header_rejects_data_row() { - let row = vec![ - Data::String("Nguyen Van A".into()), - Data::String("01/01/2000".into()), - Data::String("12345678".into()), - ]; - let cfg = hdr(&["HO_TEN", "HỌ TÊN", "STT"]); - assert!(!is_header_row(&row, &cfg)); - } - - #[test] - fn header_rejects_short_row() { - let row = vec![Data::String("HO_TEN".into()), Data::Empty]; - let cfg = hdr(&["HO_TEN", "HỌ TÊN", "STT"]); - assert!(!is_header_row(&row, &cfg)); - } - - #[test] - fn header_case_insensitive() { - let row = vec![ - Data::String("ho_ten".into()), - Data::String("B".into()), - Data::String("C".into()), - ]; - let cfg = hdr(&["HO_TEN", "HỌ TÊN", "STT"]); - assert!(is_header_row(&row, &cfg)); - } - - #[test] - fn blank_row_detection() { - let row = vec![Data::Empty, Data::Empty, Data::String("".into())]; - assert!(is_all_blank(&row)); - - let row2 = vec![Data::String("Nguyen".into()), Data::Empty, Data::Empty]; - assert!(!is_all_blank(&row2)); - } -} diff --git a/2017/tools/xlsxread/src/transform.rs b/2017/tools/xlsxread/src/transform.rs deleted file mode 100644 index bff5c21..0000000 --- a/2017/tools/xlsxread/src/transform.rs +++ /dev/null @@ -1,398 +0,0 @@ -/// Row transformation: ascii normalisation, score regex parsing, validation. -/// -/// `to_ascii` replicates build-lib.js `toAscii` exactly: -/// str.normalize("NFD").replace(/[̀-ͯ]/g,"").replace(/đ/gi,"d").toLowerCase() -use std::collections::HashMap; - -use regex::Regex; -use unicode_normalization::UnicodeNormalization; - -use crate::config::{DatasetConfig, ValidationCfg}; -use crate::error::BuildError; - -// --------------------------------------------------------------------------- -// Compiled score patterns (built once at startup from config) -// --------------------------------------------------------------------------- - -pub struct CompiledPatterns { - /// Ordered list so INSERT column order is deterministic - pub patterns: Vec<(String, Regex)>, -} - -impl CompiledPatterns { - pub fn new(scores: &HashMap) -> Result { - let mut patterns = Vec::with_capacity(scores.len()); - for (field, src) in scores { - let re = Regex::new(src).map_err(|e| BuildError::Regex { - pattern: src.clone(), - source: e, - })?; - patterns.push((field.clone(), re)); - } - // Sort for deterministic order across HashMap iteration - patterns.sort_by(|a, b| a.0.cmp(&b.0)); - Ok(Self { patterns }) - } -} - -// --------------------------------------------------------------------------- -// to_ascii — must be byte-for-byte equivalent to build-lib.js toAscii -// --------------------------------------------------------------------------- - -/// Normalise a Vietnamese name to an ASCII slug. -/// -/// Algorithm mirrors the JavaScript `toAscii` in build-lib.js: -/// 1. NFD decompose (splits base + combining diacritics) -/// 2. Drop all Unicode combining marks (U+0300–U+036F) -/// 3. Replace đ/Đ with d (NFD does not decompose đ) -/// 4. Lowercase -pub fn to_ascii(s: &str) -> String { - // Step 1 + 2: NFD then filter out combining marks (Unicode category M) - let decomposed: String = s - .nfd() - .filter(|c| !('\u{0300}'..='\u{036f}').contains(c)) - .collect(); - - // Step 3: đ/Đ are not decomposed by NFD — replace explicitly - let replaced = decomposed.replace(['đ', 'Đ'], "d"); - - // Step 4: lowercase - replaced.to_lowercase() -} - -// --------------------------------------------------------------------------- -// Parsed row ready for DB insert -// --------------------------------------------------------------------------- - -pub struct ParsedRow { - pub so_bao_danh: String, - pub ho_ten: String, - pub ho_ten_ascii: String, - pub ngay_sinh: Option, - /// Subject field → float value; absent subjects not in map → NULL - pub scores: HashMap, -} - -// --------------------------------------------------------------------------- -// Row validation — mirrors the per-script skip logic -// --------------------------------------------------------------------------- - -/// Returns `None` when the row should be skipped entirely (before sourceRows counter). -/// Returns `Some(reason)` when the row should be counted as sourceRows but skipped. -#[derive(Debug, PartialEq, Eq)] -pub enum SkipReason { - /// Row is fully blank (data-old2 only, before sourceRows counter) - BlankRow, - /// soBaoDanh or hoTen empty/missing - EmptyField, - /// soBaoDanh contains non-digit characters (data-old / data-old2 guard) - NonNumericSbd, -} - -/// Validates a raw cell slice against the dataset's `ValidationCfg`. -/// Returns `Ok(())` on pass, `Err(SkipReason)` on fail. -pub fn validate_row( - ho_ten: &str, - so_bao_danh: &str, - cfg: &ValidationCfg, - strip_blank_rows: bool, - all_blank: bool, -) -> Result<(), SkipReason> { - // data-old2: skip fully blank rows BEFORE counting sourceRows - if strip_blank_rows && all_blank { - return Err(SkipReason::BlankRow); - } - - if cfg.require_nonempty_sbd && so_bao_danh.is_empty() { - return Err(SkipReason::EmptyField); - } - if cfg.require_nonempty_name && ho_ten.is_empty() { - return Err(SkipReason::EmptyField); - } - if cfg.require_numeric_sbd && !so_bao_danh.chars().all(|c| c.is_ascii_digit()) { - return Err(SkipReason::NonNumericSbd); - } - Ok(()) -} - -// --------------------------------------------------------------------------- -// Score parsing — mirrors build-lib.js parseScores -// --------------------------------------------------------------------------- - -/// Parse a DIEM_THI cell string and extract matching subject scores. -pub fn parse_scores(diem_thi: &str, patterns: &CompiledPatterns) -> HashMap { - let mut out = HashMap::new(); - for (field, re) in &patterns.patterns { - if let Some(caps) = re.captures(diem_thi) { - if let Some(m) = caps.get(1) { - if let Ok(v) = m.as_str().parse::() { - if v.is_finite() { - out.insert(field.clone(), v); - } - } - } - } - } - out -} - -// --------------------------------------------------------------------------- -// Full row transform -// --------------------------------------------------------------------------- - -/// Extract and transform one spreadsheet row into a `ParsedRow`. -/// `raw` is the full cell slice; column indices come from `cfg.columns`. -pub fn transform_row( - raw: &[calamine::Data], - cfg: &DatasetConfig, - patterns: &CompiledPatterns, -) -> ParsedRow { - let get = |idx: usize| -> String { - raw.get(idx) - .map(|cell| cell.to_string().trim().to_owned()) - .unwrap_or_default() - }; - - let ho_ten = get(cfg.columns.ho_ten); - let ngay_sinh = get(cfg.columns.ngay_sinh); - let so_bao_danh = get(cfg.columns.so_bao_danh); - let diem_thi = raw - .get(cfg.columns.diem_thi) - .map(|c| c.to_string()) - .unwrap_or_default(); - - let ho_ten_ascii = to_ascii(&ho_ten); - let scores = parse_scores(&diem_thi, patterns); - let ngay_sinh_opt = if ngay_sinh.is_empty() { - None - } else { - Some(ngay_sinh) - }; - - ParsedRow { - so_bao_danh, - ho_ten, - ho_ten_ascii, - ngay_sinh: ngay_sinh_opt, - scores, - } -} - -// --------------------------------------------------------------------------- -// Unit tests — 20 cases for to_ascii (real Vietnamese names) -// --------------------------------------------------------------------------- - -#[cfg(test)] -mod tests { - use super::*; - - // Helper: assert to_ascii(input) == expected - fn check(input: &str, expected: &str) { - assert_eq!( - to_ascii(input), - expected, - "to_ascii({input:?}) expected {expected:?}" - ); - } - - #[test] - fn ascii_plain_latin() { - check("Nguyen Van A", "nguyen van a"); - } - - #[test] - fn ascii_nguyen_thi_hoa() { - check("Nguyễn Thị Hoa", "nguyen thi hoa"); - } - - #[test] - fn ascii_tran_van_duc() { - // đ/Đ replacement - check("Trần Văn Đức", "tran van duc"); - } - - #[test] - fn ascii_le_thi_my_duyen() { - check("Lê Thị Mỹ Duyên", "le thi my duyen"); - } - - #[test] - fn ascii_pham_thi_lan() { - check("Phạm Thị Lan", "pham thi lan"); - } - - #[test] - fn ascii_bui_thi_thu() { - check("Bùi Thị Thu", "bui thi thu"); - } - - #[test] - fn ascii_hoang_van_truong() { - check("Hoàng Văn Trường", "hoang van truong"); - } - - #[test] - fn ascii_do_thi_ngan() { - // Đ uppercase at start - check("Đỗ Thị Ngân", "do thi ngan"); - } - - #[test] - fn ascii_nguyen_van_khanh() { - check("Nguyễn Văn Khánh", "nguyen van khanh"); - } - - #[test] - fn ascii_trinh_thi_bich_ngoc() { - check("Trịnh Thị Bích Ngọc", "trinh thi bich ngoc"); - } - - #[test] - fn ascii_vu_thi_dieu() { - // ề = e + combining grave + combining circumflex (after NFD) - check("Vũ Thị Diệu", "vu thi dieu"); - } - - #[test] - fn ascii_nguyen_thi_tuong_vi() { - check("Nguyễn Thị Tường Vi", "nguyen thi tuong vi"); - } - - #[test] - fn ascii_lowercase_d_stroke() { - // Lowercase đ → d - check("đặng thị hằng", "dang thi hang"); - } - - #[test] - fn ascii_uppercase_d_stroke() { - check("ĐẶNG THỊ HẰNG", "dang thi hang"); - } - - #[test] - fn ascii_mixed_case() { - check("NGUYỄN VĂN AN", "nguyen van an"); - } - - #[test] - fn ascii_tran_thi_kim_anh() { - check("Trần Thị Kim Anh", "tran thi kim anh"); - } - - #[test] - fn ascii_nguyen_thi_phuong_thao() { - check("Nguyễn Thị Phương Thảo", "nguyen thi phuong thao"); - } - - #[test] - fn ascii_le_van_long() { - check("Lê Văn Long", "le van long"); - } - - #[test] - fn ascii_vo_thi_xuan_mai() { - check("Võ Thị Xuân Mai", "vo thi xuan mai"); - } - - #[test] - fn ascii_empty_string() { - check("", ""); - } - - // --- Score parsing tests --- - - fn make_patterns() -> CompiledPatterns { - let mut map = HashMap::new(); - map.insert("toan".into(), r"Toán:\s*(\d+(?:\.\d+)?)".into()); - map.insert("ngu_van".into(), r"Ngữ văn:\s*(\d+(?:\.\d+)?)".into()); - map.insert("vat_ly".into(), r"Vật lí:\s*(\d+(?:\.\d+)?)".into()); - CompiledPatterns::new(&map).unwrap() - } - - #[test] - fn parse_scores_single() { - let p = make_patterns(); - let s = "Toán: 8.5"; - let scores = parse_scores(s, &p); - assert_eq!(scores.get("toan"), Some(&8.5)); - assert!(scores.get("ngu_van").is_none()); - } - - #[test] - fn parse_scores_multiple() { - let p = make_patterns(); - let s = "Toán: 7.25 Ngữ văn: 6.0 Vật lí: 9"; - let scores = parse_scores(s, &p); - assert_eq!(scores.get("toan"), Some(&7.25)); - assert_eq!(scores.get("ngu_van"), Some(&6.0)); - assert_eq!(scores.get("vat_ly"), Some(&9.0)); - } - - #[test] - fn parse_scores_empty_cell() { - let p = make_patterns(); - let scores = parse_scores("", &p); - assert!(scores.is_empty()); - } - - // --- Validation tests --- - - fn default_validation() -> ValidationCfg { - ValidationCfg { - require_numeric_sbd: false, - require_nonempty_name: true, - require_nonempty_sbd: true, - } - } - - #[test] - fn validate_ok() { - let v = default_validation(); - assert!(validate_row("Nguyen Van A", "12345678", &v, false, false).is_ok()); - } - - #[test] - fn validate_empty_sbd() { - let v = default_validation(); - assert_eq!( - validate_row("Nguyen Van A", "", &v, false, false), - Err(SkipReason::EmptyField) - ); - } - - #[test] - fn validate_empty_name() { - let v = default_validation(); - assert_eq!( - validate_row("", "12345678", &v, false, false), - Err(SkipReason::EmptyField) - ); - } - - #[test] - fn validate_non_numeric_sbd_rejected() { - let mut v = default_validation(); - v.require_numeric_sbd = true; - assert_eq!( - validate_row("Nguyen Van A", "12AB5678", &v, false, false), - Err(SkipReason::NonNumericSbd) - ); - } - - #[test] - fn validate_numeric_sbd_accepted() { - let mut v = default_validation(); - v.require_numeric_sbd = true; - assert!(validate_row("Nguyen Van A", "12345678", &v, false, false).is_ok()); - } - - #[test] - fn validate_blank_row_skipped() { - let v = default_validation(); - // strip_blank_rows=true AND all_blank=true → BlankRow - assert_eq!( - validate_row("", "", &v, true, true), - Err(SkipReason::BlankRow) - ); - } -} diff --git a/2017/tools/xlsxread/tests/fixtures/README.md b/2017/tools/xlsxread/tests/fixtures/README.md deleted file mode 100644 index c0885f3..0000000 --- a/2017/tools/xlsxread/tests/fixtures/README.md +++ /dev/null @@ -1,18 +0,0 @@ -# Test Fixtures - -Anonymised `.xlsx` files for integration testing. All student PII has been replaced: - -- `ho_ten` replaced with `Nguyen Van Test NNN` / `Tran Thi Test NNN` patterns -- `so_bao_danh` replaced with sequential synthetic numbers (e.g. `10000001`) -- `ngay_sinh` replaced with fixed synthetic dates -- Scores are realistic random values in the 0–10 range - -Files: - -- `province-100.xlsx` — 100-row single-sheet file (simulates a normal province) -- `hcm-overflow.xlsx` — 2-sheet file (200 rows Sheet1 + 200 rows Sheet2, simulating HCM overflow) -- `province-numeric-sbd.xlsx` — 100 rows with strictly numeric SBDs (for data-old variant) - -These files are generated by `tests/golden.rs` `generate_fixtures()` if they do not already exist -on disk. The generator is pure Rust (uses the `zip` crate already pulled in via calamine). -No external Python or Node tooling required for unit/integration tests. diff --git a/2017/tools/xlsxread/tests/fixtures/hcm-overflow.xlsx b/2017/tools/xlsxread/tests/fixtures/hcm-overflow.xlsx deleted file mode 100644 index c92e498..0000000 Binary files a/2017/tools/xlsxread/tests/fixtures/hcm-overflow.xlsx and /dev/null differ diff --git a/2017/tools/xlsxread/tests/fixtures/province-100.xlsx b/2017/tools/xlsxread/tests/fixtures/province-100.xlsx deleted file mode 100644 index 5d1a109..0000000 Binary files a/2017/tools/xlsxread/tests/fixtures/province-100.xlsx and /dev/null differ diff --git a/2017/tools/xlsxread/tests/fixtures/province-numeric-sbd.xlsx b/2017/tools/xlsxread/tests/fixtures/province-numeric-sbd.xlsx deleted file mode 100644 index 5eb518c..0000000 Binary files a/2017/tools/xlsxread/tests/fixtures/province-numeric-sbd.xlsx and /dev/null differ diff --git a/2017/tools/xlsxread/tests/golden.rs b/2017/tools/xlsxread/tests/golden.rs deleted file mode 100644 index fc2128a..0000000 --- a/2017/tools/xlsxread/tests/golden.rs +++ /dev/null @@ -1,690 +0,0 @@ -/// Stage 5 golden tests — integration tests using anonymised fixture files. -/// -/// Fixture files are generated in-process via raw OOXML + zip if they do not -/// already exist on disk. No external Python or Node tooling required for the -/// Rust-side tests. The Node golden comparison is marked #[ignore] when pnpm -/// is not in PATH. -use std::io::Write as IoWrite; -use std::path::{Path, PathBuf}; - -// --------------------------------------------------------------------------- -// Minimal OOXML xlsx generator -// -// Produces a valid .xlsx that calamine can read. Only uses the `zip` crate -// which is already pulled in as a transitive dependency of calamine. -// --------------------------------------------------------------------------- - -/// One row of cell data for a fixture sheet. -struct XlsxRow { - values: Vec, -} - -/// Write a minimal .xlsx to `path` with the given sheets. -/// `sheets`: Vec<(sheet_name, rows)> where rows[0] is the header. -fn write_xlsx(path: &Path, sheets: &[(String, Vec)]) { - use zip::{write::SimpleFileOptions, ZipWriter}; - - let file = std::fs::File::create(path).expect("create fixture xlsx"); - let mut zip = ZipWriter::new(file); - let opts = SimpleFileOptions::default(); - - // [Content_Types].xml - let mut content_types = String::from( - r#" - - - - -"#, - ); - for (i, _) in sheets.iter().enumerate() { - content_types.push_str(&format!( - r#" -"#, - i + 1 - )); - } - content_types.push_str(""); - zip.start_file("[Content_Types].xml", opts).unwrap(); - zip.write_all(content_types.as_bytes()).unwrap(); - - // _rels/.rels - zip.start_file("_rels/.rels", opts).unwrap(); - zip.write_all( - br#" - - -"#, - ) - .unwrap(); - - // xl/_rels/workbook.xml.rels - let mut wb_rels = String::from( - r#" - -"#, - ); - for (i, _) in sheets.iter().enumerate() { - wb_rels.push_str(&format!( - r#" -"#, - i + 1, - i + 1 - )); - } - wb_rels.push_str(""); - zip.start_file("xl/_rels/workbook.xml.rels", opts).unwrap(); - zip.write_all(wb_rels.as_bytes()).unwrap(); - - // xl/workbook.xml - let mut wb = String::from( - r#" - - -"#, - ); - for (i, (name, _)) in sheets.iter().enumerate() { - let escaped = xml_escape(name); - wb.push_str(&format!( - r#" -"#, - escaped, - i + 1, - i + 1 - )); - } - wb.push_str(" \n"); - zip.start_file("xl/workbook.xml", opts).unwrap(); - zip.write_all(wb.as_bytes()).unwrap(); - - // xl/worksheets/sheetN.xml - for (i, (_, rows)) in sheets.iter().enumerate() { - let mut ws = String::from( - r#" - - -"#, - ); - for (row_idx, row) in rows.iter().enumerate() { - ws.push_str(&format!( - r#" -"#, - row_idx + 1 - )); - for (col_idx, val) in row.values.iter().enumerate() { - let col_letter = col_letter(col_idx); - let cell_ref = format!("{}{}", col_letter, row_idx + 1); - let escaped = xml_escape(val); - ws.push_str(&format!( - r#" {} -"#, - cell_ref, escaped - )); - } - ws.push_str(" \n"); - } - ws.push_str(" \n"); - zip.start_file(&format!("xl/worksheets/sheet{}.xml", i + 1), opts) - .unwrap(); - zip.write_all(ws.as_bytes()).unwrap(); - } - - zip.finish().unwrap(); -} - -fn col_letter(idx: usize) -> &'static str { - const LETTERS: &[&str] = &[ - "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", - "S", "T", "U", "V", "W", "X", "Y", "Z", - ]; - LETTERS[idx % 26] -} - -fn xml_escape(s: &str) -> String { - s.replace('&', "&") - .replace('<', "<") - .replace('>', ">") - .replace('"', """) -} - -// --------------------------------------------------------------------------- -// Fixture data builders -// --------------------------------------------------------------------------- - -fn header_row() -> XlsxRow { - XlsxRow { - values: vec![ - "HO_TEN".into(), - "NGAY_SINH".into(), - "SO_BAO_DANH".into(), - "DIEM_THI".into(), - ], - } -} - -fn data_row(idx: usize, scores: &str) -> XlsxRow { - // Anonymised: name uses sequential pattern, SBD is purely synthetic - let name = if idx % 2 == 0 { - format!("Nguyen Van Test {:03}", idx) - } else { - format!("Tran Thi Test {:03}", idx) - }; - XlsxRow { - values: vec![ - name, - format!("15/0{}/{}", (idx % 9) + 1, 1999 + (idx % 5)), - format!("1000{:04}", idx), - scores.to_owned(), - ], - } -} - -fn sample_scores(idx: usize) -> String { - // Realistic scores in 0–10 range, varies by idx - let toan = 4.0 + (idx % 60) as f64 / 10.0; - let van = 3.5 + (idx % 65) as f64 / 10.0; - format!("Toán: {toan:.1} Ngữ văn: {van:.1} Tiếng Anh: 7.5") -} - -// --------------------------------------------------------------------------- -// Fixture file paths -// --------------------------------------------------------------------------- - -fn fixtures_dir() -> PathBuf { - // tests/fixtures/ relative to the crate root - let mut p = PathBuf::from(env!("CARGO_MANIFEST_DIR")); - p.push("tests"); - p.push("fixtures"); - p -} - -fn province_fixture_path() -> PathBuf { - fixtures_dir().join("province-100.xlsx") -} - -fn hcm_overflow_fixture_path() -> PathBuf { - fixtures_dir().join("hcm-overflow.xlsx") -} - -fn numeric_sbd_fixture_path() -> PathBuf { - fixtures_dir().join("province-numeric-sbd.xlsx") -} - -// --------------------------------------------------------------------------- -// Fixture generation — called once per test run if files missing -// --------------------------------------------------------------------------- - -fn ensure_fixtures() { - let dir = fixtures_dir(); - std::fs::create_dir_all(&dir).expect("create fixtures dir"); - - // province-100.xlsx — 100 data rows, single sheet, with header - if !province_fixture_path().exists() { - let mut rows = vec![header_row()]; - for i in 0..100 { - rows.push(data_row(i, &sample_scores(i))); - } - write_xlsx(&province_fixture_path(), &[("Sheet1".to_owned(), rows)]); - } - - // hcm-overflow.xlsx — 2 sheets × 200 rows each (no header on sheet 2) - if !hcm_overflow_fixture_path().exists() { - let mut sheet1 = vec![header_row()]; - for i in 0..200 { - sheet1.push(data_row(i, &sample_scores(i))); - } - // Sheet2: continuation rows, no header row (as in real HCM overflow) - let mut sheet2 = Vec::new(); - for i in 200..400 { - sheet2.push(data_row(i, &sample_scores(i))); - } - write_xlsx( - &hcm_overflow_fixture_path(), - &[("Sheet1".to_owned(), sheet1), ("Sheet2".to_owned(), sheet2)], - ); - } - - // province-numeric-sbd.xlsx — strictly numeric SBDs for data-old config - if !numeric_sbd_fixture_path().exists() { - let mut rows = vec![header_row()]; - for i in 0..100 { - rows.push(XlsxRow { - values: vec![ - format!("Nguyen Van Test {:03}", i), - "01/01/2000".to_owned(), - format!("{:08}", 20000000 + i), // pure digits - sample_scores(i), - ], - }); - } - write_xlsx(&numeric_sbd_fixture_path(), &[("Sheet1".to_owned(), rows)]); - } -} - -// --------------------------------------------------------------------------- -// Config helpers -// --------------------------------------------------------------------------- - -fn make_data_config() -> xlsxread::config::DatasetConfig { - let cfg_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")) - .join("configs") - .join("thptqg2017-data.toml"); - xlsxread::config::load_config(&cfg_path).expect("load data config") -} - -// --------------------------------------------------------------------------- -// Integration tests — pure Rust, no Node dependency -// --------------------------------------------------------------------------- - -#[test] -fn province_100_builds_100_rows() { - ensure_fixtures(); - let dir = tempdir(); - let db_path = dir.join("test.db"); - let fixture_dir = dir.join("input"); - std::fs::create_dir_all(&fixture_dir).unwrap(); - std::fs::copy(province_fixture_path(), fixture_dir.join("province.xlsx")).unwrap(); - - run_build_cmd(&fixture_dir, &db_path, "thptqg2017-data.toml"); - - let count = query_count(&db_path); - assert_eq!(count, 100, "expected 100 rows from province-100 fixture"); -} - -#[test] -fn hcm_overflow_builds_400_rows() { - ensure_fixtures(); - let dir = tempdir(); - let db_path = dir.join("test.db"); - let fixture_dir = dir.join("input"); - std::fs::create_dir_all(&fixture_dir).unwrap(); - std::fs::copy(hcm_overflow_fixture_path(), fixture_dir.join("hcm.xlsx")).unwrap(); - - run_build_cmd(&fixture_dir, &db_path, "thptqg2017-data.toml"); - - let count = query_count(&db_path); - assert_eq!( - count, 400, - "expected 400 rows (200 × 2 sheets) from hcm-overflow fixture" - ); -} - -#[test] -fn data_old_first_sheet_only_100_rows() { - ensure_fixtures(); - let dir = tempdir(); - let db_path = dir.join("test.db"); - let fixture_dir = dir.join("input"); - std::fs::create_dir_all(&fixture_dir).unwrap(); - // Use the overflow file but with data-old config (first sheet only → 200 rows) - std::fs::copy(hcm_overflow_fixture_path(), fixture_dir.join("hcm.xlsx")).unwrap(); - - run_build_cmd(&fixture_dir, &db_path, "thptqg2017-data-old.toml"); - - // data-old: sheet_mode=first → only 200 rows from sheet1; but SBDs "1000NNNN" are - // all digits so all pass the numeric guard - let count = query_count(&db_path); - assert_eq!( - count, 200, - "data-old config should read only first sheet (200 rows)" - ); -} - -#[test] -fn numeric_sbd_guard_rejects_non_numeric() { - ensure_fixtures(); - let dir = tempdir(); - let db_path = dir.join("test.db"); - let fixture_dir = dir.join("input"); - std::fs::create_dir_all(&fixture_dir).unwrap(); - - // Write a fixture with one non-numeric SBD mixed in - let mut rows = vec![header_row()]; - for i in 0..10 { - rows.push(XlsxRow { - values: vec![ - format!("Test {:03}", i), - "01/01/2000".to_owned(), - if i == 5 { - "ABC123".to_owned() - } else { - format!("{:08}", 20000000 + i) - }, - sample_scores(i), - ], - }); - } - let mixed_path = fixture_dir.join("mixed.xlsx"); - write_xlsx(&mixed_path, &[("Sheet1".to_owned(), rows)]); - - run_build_cmd(&fixture_dir, &db_path, "thptqg2017-data-old.toml"); - - // Row i=5 has non-numeric SBD → rejected by data-old config - let count = query_count(&db_path); - assert_eq!( - count, 9, - "non-numeric SBD row should be skipped by data-old config" - ); -} - -#[test] -fn scores_parsed_correctly_into_db() { - ensure_fixtures(); - let dir = tempdir(); - let db_path = dir.join("test.db"); - let fixture_dir = dir.join("input"); - std::fs::create_dir_all(&fixture_dir).unwrap(); - - let rows = vec![ - header_row(), - XlsxRow { - values: vec![ - "Nguyen Van Test 001".to_owned(), - "01/01/2000".to_owned(), - "10000001".to_owned(), - "Toán: 8.5 Ngữ văn: 7.0 Tiếng Anh: 9.25".to_owned(), - ], - }, - ]; - write_xlsx( - &fixture_dir.join("one.xlsx"), - &[("Sheet1".to_owned(), rows)], - ); - - run_build_cmd(&fixture_dir, &db_path, "thptqg2017-data.toml"); - - let conn = rusqlite::Connection::open(&db_path).unwrap(); - let (toan, van, anh): (f64, f64, f64) = conn - .query_row( - "SELECT toan, ngu_van, tieng_anh FROM student WHERE so_bao_danh = '10000001'", - [], - |r| Ok((r.get(0)?, r.get(1)?, r.get(2)?)), - ) - .expect("row not found"); - assert!((toan - 8.5).abs() < 1e-9); - assert!((van - 7.0).abs() < 1e-9); - assert!((anh - 9.25).abs() < 1e-9); -} - -#[test] -fn to_ascii_stored_correctly() { - ensure_fixtures(); - let dir = tempdir(); - let db_path = dir.join("test.db"); - let fixture_dir = dir.join("input"); - std::fs::create_dir_all(&fixture_dir).unwrap(); - - let rows = vec![ - header_row(), - XlsxRow { - values: vec![ - "Nguyễn Văn Đức".to_owned(), - "".to_owned(), - "20000001".to_owned(), - "".to_owned(), - ], - }, - ]; - write_xlsx( - &fixture_dir.join("one.xlsx"), - &[("Sheet1".to_owned(), rows)], - ); - - run_build_cmd(&fixture_dir, &db_path, "thptqg2017-data.toml"); - - let conn = rusqlite::Connection::open(&db_path).unwrap(); - let ascii: String = conn - .query_row( - "SELECT ho_ten_ascii FROM student WHERE so_bao_danh = '20000001'", - [], - |r| r.get(0), - ) - .expect("row not found"); - assert_eq!(ascii, "nguyen van duc"); -} - -#[test] -fn audit_subcommand_matches_after_build() { - ensure_fixtures(); - let dir = tempdir(); - let db_path = dir.join("test.db"); - let fixture_dir = dir.join("input"); - std::fs::create_dir_all(&fixture_dir).unwrap(); - std::fs::copy(province_fixture_path(), fixture_dir.join("province.xlsx")).unwrap(); - - run_build_cmd(&fixture_dir, &db_path, "thptqg2017-data.toml"); - - // audit should match (100 distinct SBDs in xlsx == 100 rows in DB) - let cfg = make_data_config(); - let result = xlsxread::audit::run_audit(&fixture_dir, &db_path, &cfg).expect("audit failed"); - assert!(result.matched, "audit should match after build"); - assert_eq!(result.distinct_sbds, 100); - assert_eq!(result.db_count, 100); -} - -#[test] -fn audit_subcommand_mismatch_detected() { - ensure_fixtures(); - let dir = tempdir(); - let db_path = dir.join("test.db"); - let fixture_dir = dir.join("input"); - std::fs::create_dir_all(&fixture_dir).unwrap(); - - // Write 10 rows to xlsx but build DB from only 5 rows - let mut all_rows = vec![header_row()]; - for i in 0..10 { - all_rows.push(data_row(i, &sample_scores(i))); - } - write_xlsx( - &fixture_dir.join("all.xlsx"), - &[("Sheet1".to_owned(), all_rows)], - ); - - // Build DB with only first 5 rows in a different file - let build_dir = dir.join("build_input"); - std::fs::create_dir_all(&build_dir).unwrap(); - let mut five_rows = vec![header_row()]; - for i in 0..5 { - five_rows.push(data_row(i, &sample_scores(i))); - } - write_xlsx( - &build_dir.join("five.xlsx"), - &[("Sheet1".to_owned(), five_rows)], - ); - - run_build_cmd(&build_dir, &db_path, "thptqg2017-data.toml"); - - // audit against fixture_dir (10 xlsx rows) but DB has 5 rows → mismatch - let cfg = make_data_config(); - let result = xlsxread::audit::run_audit(&fixture_dir, &db_path, &cfg).expect("audit failed"); - assert!(!result.matched, "audit should not match (10 xlsx vs 5 db)"); - assert_eq!(result.distinct_sbds, 10); - assert_eq!(result.db_count, 5); -} - -// --------------------------------------------------------------------------- -// Golden test: compare Rust DB vs Node DB on identical fixture -// Marked #[ignore] when pnpm / node is not in PATH — CI installs them first. -// --------------------------------------------------------------------------- - -#[test] -#[ignore] -fn golden_rust_matches_node_db() { - // This test requires: pnpm, node, and the thptqg2017 package to be installed - // Run with: cargo test -- --ignored golden_rust_matches_node_db - let which_pnpm = std::process::Command::new("which") - .arg("pnpm") - .output() - .map(|o| o.status.success()) - .unwrap_or(false); - if !which_pnpm { - eprintln!("pnpm not in PATH — skipping golden test"); - return; - } - - let dir = tempdir(); - let fixture_dir = dir.join("input"); - std::fs::create_dir_all(&fixture_dir).unwrap(); - std::fs::copy(province_fixture_path(), fixture_dir.join("province.xlsx")).unwrap(); - - // Build with Rust - let rust_db = dir.join("rust.db"); - run_build_cmd(&fixture_dir, &rust_db, "thptqg2017-data.toml"); - - // Build with Node (run build-database.js with DATA_DIR / DB_PATH overrides) - // Node script reads env-vars via a thin wrapper — see scripts/build-database.js - // For now: diff via SELECT * ORDER BY so_bao_danh - let node_db = dir.join("node.db"); - let status = std::process::Command::new("pnpm") - .args(["exec", "node", "scripts/build-database.js"]) - .env("OVERRIDE_SRC_DIR", fixture_dir.to_str().unwrap()) - .env("OVERRIDE_DB_PATH", node_db.to_str().unwrap()) - .current_dir( - PathBuf::from(env!("CARGO_MANIFEST_DIR")) - .parent() - .unwrap() - .parent() - .unwrap(), - ) - .status() - .expect("failed to run node build script"); - - if !status.success() { - panic!("Node build script failed with: {status}"); - } - - // Row-by-row comparison - diff_dbs(&rust_db, &node_db); -} - -// --------------------------------------------------------------------------- -// Helpers -// --------------------------------------------------------------------------- - -fn tempdir() -> PathBuf { - let base = std::env::temp_dir().join(format!( - "xlsxread-test-{}", - std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap() - .subsec_nanos() - )); - std::fs::create_dir_all(&base).unwrap(); - base -} - -fn run_build_cmd(input_dir: &Path, db_path: &Path, config_name: &str) { - let cfg_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")) - .join("configs") - .join(config_name); - - let cfg = xlsxread::config::load_config(&cfg_path) - .unwrap_or_else(|e| panic!("load config {config_name}: {e}")); - let patterns = - xlsxread::transform::CompiledPatterns::new(&cfg.scores).expect("compile patterns"); - - // Collect files - let mut files: Vec = std::fs::read_dir(input_dir) - .unwrap() - .filter_map(|e| e.ok()) - .map(|e| e.path()) - .filter(|p| { - p.is_file() - && p.extension() - .and_then(|e| e.to_str()) - .map(|e| { - let l = e.to_lowercase(); - l == "xls" || l == "xlsx" - }) - .unwrap_or(false) - }) - .collect(); - files.sort(); - - let conn = xlsxread::writer::open_db(db_path, &cfg).expect("open db"); - conn.execute_batch("BEGIN").unwrap(); - - for file in &files { - xlsxread::reader::process_file(file, &cfg, |_, raw| { - let all_blank = xlsxread::reader::is_all_blank(raw); - if cfg.reader.strip_blank_rows && all_blank { - return; - } - let ho_ten = raw - .get(cfg.columns.ho_ten) - .map(|c| c.to_string().trim().to_owned()) - .unwrap_or_default(); - let so_bao_danh = raw - .get(cfg.columns.so_bao_danh) - .map(|c| c.to_string().trim().to_owned()) - .unwrap_or_default(); - if xlsxread::transform::validate_row( - &ho_ten, - &so_bao_danh, - &cfg.validation, - cfg.reader.strip_blank_rows, - all_blank, - ) - .is_err() - { - return; - } - let row = xlsxread::transform::transform_row(raw, &cfg, &patterns); - let _ = xlsxread::writer::insert_row( - &conn, - &cfg.insert.sql, - &row, - xlsxread::writer::SCORE_FIELDS, - ); - }) - .expect("process file"); - } - - conn.execute_batch("COMMIT").unwrap(); - conn.execute_batch("VACUUM").unwrap(); -} - -fn query_count(db_path: &Path) -> i64 { - let conn = rusqlite::Connection::open(db_path).expect("open db for count"); - conn.query_row("SELECT COUNT(*) FROM student", [], |r| r.get(0)) - .expect("count query") -} - -fn diff_dbs(a: &Path, b: &Path) { - let conn_a = rusqlite::Connection::open(a).unwrap(); - - // Attach b as "other" - conn_a - .execute_batch(&format!("ATTACH DATABASE '{}' AS other", b.display())) - .unwrap(); - - // Rows in a not in b - let missing_in_b: i64 = conn_a - .query_row( - "SELECT COUNT(*) FROM main.student s - WHERE NOT EXISTS (SELECT 1 FROM other.student o WHERE o.so_bao_danh = s.so_bao_danh)", - [], - |r| r.get(0), - ) - .unwrap(); - - // Rows in b not in a - let missing_in_a: i64 = conn_a - .query_row( - "SELECT COUNT(*) FROM other.student o - WHERE NOT EXISTS (SELECT 1 FROM main.student s WHERE s.so_bao_danh = o.so_bao_danh)", - [], - |r| r.get(0), - ) - .unwrap(); - - assert_eq!( - missing_in_b, 0, - "{missing_in_b} rows in Rust DB missing from Node DB" - ); - assert_eq!( - missing_in_a, 0, - "{missing_in_a} rows in Node DB missing from Rust DB" - ); -} diff --git a/2017/vite.config.js b/2017/vite.config.js deleted file mode 100644 index 87e0811..0000000 --- a/2017/vite.config.js +++ /dev/null @@ -1,25 +0,0 @@ -import { defineConfig } from "vite"; -import react from "@vitejs/plugin-react"; - -// VARIANT selects which dataset to ship: -// (unset) → main site at /thptqg2017/ using public/ -// old → at /thptqg2017/old/ using public-old/ -// old2 → at /thptqg2017/old2/ using public-old2/ -const VARIANT = process.env.VARIANT || ""; -const VARIANT_CONFIG = { - "": { base: "/thptqg/2017/", publicDir: "public", outDir: "dist" }, - old: { base: "/thptqg/2017/old/", publicDir: "public-old", outDir: "dist/old" }, - old2: { base: "/thptqg/2017/old2/", publicDir: "public-old2", outDir: "dist/old2" }, -}; -const cfg = VARIANT_CONFIG[VARIANT]; -if (!cfg) throw new Error(`Unknown VARIANT: ${VARIANT}`); - -export default defineConfig({ - plugins: [react()], - base: cfg.base, - publicDir: cfg.publicDir, - build: { - outDir: cfg.outDir, - emptyOutDir: VARIANT === "", // only main build wipes dist root - }, -}); diff --git a/2016/LICENSE b/LICENSE similarity index 100% rename from 2016/LICENSE rename to LICENSE diff --git a/README.md b/README.md index a50ca4a..da6a3df 100644 --- a/README.md +++ b/README.md @@ -2,18 +2,67 @@ Tra cứu điểm thi THPT Quốc gia — exam-score lookup for Vietnam's national high school graduation exam. Client-side SQL (sql.js) over a SQLite database built -from the ministry's raw `.xls` score files by the Rust `xlsxread` CLI. +from the ministry's raw `.xls` score files by the Rust `xlsxread` parser. -Served as one site at **[tiennm99.github.io/thptqg](https://tiennm99.github.io/thptqg/)**. +Live at **[tiennm99.github.io/thptqg](https://tiennm99.github.io/thptqg/)**. -| Directory | Year | Size | Site | +| Dataset | Exam | Candidates | Site | | --- | --- | --- | --- | -| [`2016/`](./2016/) | THPT QG 2016 — 877,461 candidates | ~50 MB raw data | [/2016/](https://tiennm99.github.io/thptqg/2016/) | -| [`2017/`](./2017/) | THPT QG 2017 — ~861,000 candidates | ~117 MB raw data | [/2017/](https://tiennm99.github.io/thptqg/2017/) (+ [old](https://tiennm99.github.io/thptqg/2017/old/), [old2](https://tiennm99.github.io/thptqg/2017/old2/) generations) | +| `2016` | 2016 | 877,461 | [/2016/](https://tiennm99.github.io/thptqg/2016/) | +| `2017` | 2017 | 861,068 | [/2017/](https://tiennm99.github.io/thptqg/2017/) | +| `2017-old` | 2017 | 847,348 | [/2017-old/](https://tiennm99.github.io/thptqg/2017-old/) | +| `2017-old2` | 2017 | 679,764 | [/2017-old2/](https://tiennm99.github.io/thptqg/2017-old2/) | -Each year was a standalone repository (`thptqg2016`, `thptqg2017`), merged here -with full history. Each keeps its own `tools/xlsxread` copy because the raw -data formats differ per year. +The three 2017 datasets are successive publications of the same exam and they +disagree; all three are kept so the differences stay inspectable. -The `Deploy to GitHub Pages` workflow builds both years' databases and site -bundles and publishes them behind a root index. +## Layout + +``` +index.html + src/ the frontend — one app serving all four datasets and the hub + datasets.js the four dataset ids and their per-dataset content + router.js pathname → dataset +data// raw Excel files, one directory per dataset +parser/ the Rust parser + src/schema.rs canonical 22-column table: DDL, INSERT, subject regexes + configs/.toml per-dataset parse rules only, no SQL + scripts/ database build, crawler, parity verification +scripts/ site assembly +docs/ architecture, data pipeline, deployment +``` + +The dataset id is one identifier end to end: + +``` +data/2017-old/ → parser/configs/2017-old.toml → db/2017-old.db.gz → /thptqg/2017-old/ +``` + +## Build + +```bash +npm ci +npm run build:rust # compile the parser +npm run build:db # build + gzip all four databases (add an id for just one) +npm run build:site # one Vite build, then assemble into _site/ +npx serve _site +``` + +Pushing to `main` runs the same steps in +`.github/workflows/deploy-pages.yml` and publishes to GitHub Pages. + +## Adding a dataset + +1. Put the Excel files in `data//` +2. Add `parser/configs/.toml` — sheet mode, column indices, validation + guards. No SQL; the schema is canonical. +3. Add an entry to `DATASETS` in `src/datasets.js` + +Everything else follows: the build script, the site assembly and the router all +read that one list, and the UI adapts to whichever columns the dataset fills. + +## Docs + +See [`docs/`](./docs/) — [overview](./docs/project-overview.md), +[architecture](./docs/system-architecture.md), +[data pipeline](./docs/data-pipeline.md), +[deployment](./docs/deployment-guide.md). diff --git a/2016/data/023718c7d3cf7ace3a7116fabb12bd9cdhyduoccantho-1468920829104.xlsx b/data/2016/023718c7d3cf7ace3a7116fabb12bd9cdhyduoccantho-1468920829104.xlsx similarity index 100% rename from 2016/data/023718c7d3cf7ace3a7116fabb12bd9cdhyduoccantho-1468920829104.xlsx rename to data/2016/023718c7d3cf7ace3a7116fabb12bd9cdhyduoccantho-1468920829104.xlsx diff --git a/2016/data/08cdeb3636dcb2adaef829d62968274atravinh-1468902443859.xlsx b/data/2016/08cdeb3636dcb2adaef829d62968274atravinh-1468902443859.xlsx similarity index 100% rename from 2016/data/08cdeb3636dcb2adaef829d62968274atravinh-1468902443859.xlsx rename to data/2016/08cdeb3636dcb2adaef829d62968274atravinh-1468902443859.xlsx diff --git a/2016/data/0b583c9875443e65ffa449d5ca76fae8ninhbinh-1468899778557.xlsx b/data/2016/0b583c9875443e65ffa449d5ca76fae8ninhbinh-1468899778557.xlsx similarity index 100% rename from 2016/data/0b583c9875443e65ffa449d5ca76fae8ninhbinh-1468899778557.xlsx rename to data/2016/0b583c9875443e65ffa449d5ca76fae8ninhbinh-1468899778557.xlsx diff --git a/2016/data/15c85d901a73dd49f2bae71aadcdbb5cninhthuan-1468920071763.xlsx b/data/2016/15c85d901a73dd49f2bae71aadcdbb5cninhthuan-1468920071763.xlsx similarity index 100% rename from 2016/data/15c85d901a73dd49f2bae71aadcdbb5cninhthuan-1468920071763.xlsx rename to data/2016/15c85d901a73dd49f2bae71aadcdbb5cninhthuan-1468920071763.xlsx diff --git a/2016/data/167116af6c8a096c2a9307edfa115f77dhcnthucpham-tphcm-1468910578187.xls b/data/2016/167116af6c8a096c2a9307edfa115f77dhcnthucpham-tphcm-1468910578187.xls similarity index 100% rename from 2016/data/167116af6c8a096c2a9307edfa115f77dhcnthucpham-tphcm-1468910578187.xls rename to data/2016/167116af6c8a096c2a9307edfa115f77dhcnthucpham-tphcm-1468910578187.xls diff --git a/2016/data/17843bccde10f362c2b9600bc49cd3f8backan-1468933016076.xlsx b/data/2016/17843bccde10f362c2b9600bc49cd3f8backan-1468933016076.xlsx similarity index 100% rename from 2016/data/17843bccde10f362c2b9600bc49cd3f8backan-1468933016076.xlsx rename to data/2016/17843bccde10f362c2b9600bc49cd3f8backan-1468933016076.xlsx diff --git a/2016/data/178814c33c8b85a77d0fc295fa6d2be1hagiang-1468932847230.xlsx b/data/2016/178814c33c8b85a77d0fc295fa6d2be1hagiang-1468932847230.xlsx similarity index 100% rename from 2016/data/178814c33c8b85a77d0fc295fa6d2be1hagiang-1468932847230.xlsx rename to data/2016/178814c33c8b85a77d0fc295fa6d2be1hagiang-1468932847230.xlsx diff --git a/2016/data/18055d17e46854ecc6c8be585bcb6e5cbaclieu-1468934144150.xlsx b/data/2016/18055d17e46854ecc6c8be585bcb6e5cbaclieu-1468934144150.xlsx similarity index 100% rename from 2016/data/18055d17e46854ecc6c8be585bcb6e5cbaclieu-1468934144150.xlsx rename to data/2016/18055d17e46854ecc6c8be585bcb6e5cbaclieu-1468934144150.xlsx diff --git a/2016/data/1c66db0265bf45df237b37a7bcaeaef3hanoi-1468932790746.xlsx b/data/2016/1c66db0265bf45df237b37a7bcaeaef3hanoi-1468932790746.xlsx similarity index 100% rename from 2016/data/1c66db0265bf45df237b37a7bcaeaef3hanoi-1468932790746.xlsx rename to data/2016/1c66db0265bf45df237b37a7bcaeaef3hanoi-1468932790746.xlsx diff --git a/2016/data/2004a8d225fd87524e324d2d519e87a4daclac-1468899960505.xlsx b/data/2016/2004a8d225fd87524e324d2d519e87a4daclac-1468899960505.xlsx similarity index 100% rename from 2016/data/2004a8d225fd87524e324d2d519e87a4daclac-1468899960505.xlsx rename to data/2016/2004a8d225fd87524e324d2d519e87a4daclac-1468899960505.xlsx diff --git a/2016/data/20db5eaace927596f1806f151649ee6edhtantrao-1468901296040.xlsx b/data/2016/20db5eaace927596f1806f151649ee6edhtantrao-1468901296040.xlsx similarity index 100% rename from 2016/data/20db5eaace927596f1806f151649ee6edhtantrao-1468901296040.xlsx rename to data/2016/20db5eaace927596f1806f151649ee6edhtantrao-1468901296040.xlsx diff --git a/2016/data/216ee4e385dbc236767be899b068bf98dhtaynguyen-1468940490240.xlsx b/data/2016/216ee4e385dbc236767be899b068bf98dhtaynguyen-1468940490240.xlsx similarity index 100% rename from 2016/data/216ee4e385dbc236767be899b068bf98dhtaynguyen-1468940490240.xlsx rename to data/2016/216ee4e385dbc236767be899b068bf98dhtaynguyen-1468940490240.xlsx diff --git a/2016/data/2208903e1fa12220d100f93e40d5ff3bquangtri-1468933440097.xlsx b/data/2016/2208903e1fa12220d100f93e40d5ff3bquangtri-1468933440097.xlsx similarity index 100% rename from 2016/data/2208903e1fa12220d100f93e40d5ff3bquangtri-1468933440097.xlsx rename to data/2016/2208903e1fa12220d100f93e40d5ff3bquangtri-1468933440097.xlsx diff --git a/2016/data/22e8617886412abd90ae3d33cc6d7db4dhyduocthaibinh-1468941020145.xlsx b/data/2016/22e8617886412abd90ae3d33cc6d7db4dhyduocthaibinh-1468941020145.xlsx similarity index 100% rename from 2016/data/22e8617886412abd90ae3d33cc6d7db4dhyduocthaibinh-1468941020145.xlsx rename to data/2016/22e8617886412abd90ae3d33cc6d7db4dhyduocthaibinh-1468941020145.xlsx diff --git a/2016/data/238adc19e80c0daef089a307af23edcethanhhoa-1468933304999.xlsx b/data/2016/238adc19e80c0daef089a307af23edcethanhhoa-1468933304999.xlsx similarity index 100% rename from 2016/data/238adc19e80c0daef089a307af23edcethanhhoa-1468933304999.xlsx rename to data/2016/238adc19e80c0daef089a307af23edcethanhhoa-1468933304999.xlsx diff --git a/2016/data/2b33640b0dd154490f2809d96be81797dhdalat-1468902130616.xlsx b/data/2016/2b33640b0dd154490f2809d96be81797dhdalat-1468902130616.xlsx similarity index 100% rename from 2016/data/2b33640b0dd154490f2809d96be81797dhdalat-1468902130616.xlsx rename to data/2016/2b33640b0dd154490f2809d96be81797dhdalat-1468902130616.xlsx diff --git a/2016/data/2fa6d9a308bd11bc09f02d787eef2c15lamdong-1468933821082.xlsx b/data/2016/2fa6d9a308bd11bc09f02d787eef2c15lamdong-1468933821082.xlsx similarity index 100% rename from 2016/data/2fa6d9a308bd11bc09f02d787eef2c15lamdong-1468933821082.xlsx rename to data/2016/2fa6d9a308bd11bc09f02d787eef2c15lamdong-1468933821082.xlsx diff --git a/2016/data/357c6aaf59aeaa6c11c3e83c595d38cfdhbachkhoa-tphcm-1468939678829.xlsx b/data/2016/357c6aaf59aeaa6c11c3e83c595d38cfdhbachkhoa-tphcm-1468939678829.xlsx similarity index 100% rename from 2016/data/357c6aaf59aeaa6c11c3e83c595d38cfdhbachkhoa-tphcm-1468939678829.xlsx rename to data/2016/357c6aaf59aeaa6c11c3e83c595d38cfdhbachkhoa-tphcm-1468939678829.xlsx diff --git a/2016/data/388fcdfdac18ef6eee6125a2a015714ddhdongthap-1468934545359.xlsx b/data/2016/388fcdfdac18ef6eee6125a2a015714ddhdongthap-1468934545359.xlsx similarity index 100% rename from 2016/data/388fcdfdac18ef6eee6125a2a015714ddhdongthap-1468934545359.xlsx rename to data/2016/388fcdfdac18ef6eee6125a2a015714ddhdongthap-1468934545359.xlsx diff --git a/2016/data/3c0e56abd8c5334f4e1ff7df3ac23a15dienbien-1468920113820.xlsx b/data/2016/3c0e56abd8c5334f4e1ff7df3ac23a15dienbien-1468920113820.xlsx similarity index 100% rename from 2016/data/3c0e56abd8c5334f4e1ff7df3ac23a15dienbien-1468920113820.xlsx rename to data/2016/3c0e56abd8c5334f4e1ff7df3ac23a15dienbien-1468920113820.xlsx diff --git a/2016/data/3daa6ee33fdafadc3c112c18bff18c2ddhthainguyen-1468940268065.xlsx b/data/2016/3daa6ee33fdafadc3c112c18bff18c2ddhthainguyen-1468940268065.xlsx similarity index 100% rename from 2016/data/3daa6ee33fdafadc3c112c18bff18c2ddhthainguyen-1468940268065.xlsx rename to data/2016/3daa6ee33fdafadc3c112c18bff18c2ddhthainguyen-1468940268065.xlsx diff --git a/2016/data/412e62265f35ca0078a0d94af5d1ed8abinhdinh-1468899884192.xlsx b/data/2016/412e62265f35ca0078a0d94af5d1ed8abinhdinh-1468899884192.xlsx similarity index 100% rename from 2016/data/412e62265f35ca0078a0d94af5d1ed8abinhdinh-1468899884192.xlsx rename to data/2016/412e62265f35ca0078a0d94af5d1ed8abinhdinh-1468899884192.xlsx diff --git a/2016/data/43213f61074068d9177685eb6c3f2d3bdhbachkhoa-dhdanang-1468938494437.xlsx b/data/2016/43213f61074068d9177685eb6c3f2d3bdhbachkhoa-dhdanang-1468938494437.xlsx similarity index 100% rename from 2016/data/43213f61074068d9177685eb6c3f2d3bdhbachkhoa-dhdanang-1468938494437.xlsx rename to data/2016/43213f61074068d9177685eb6c3f2d3bdhbachkhoa-dhdanang-1468938494437.xlsx diff --git a/2016/data/4334ea476a2b8196d54ba40a341831echvkythuatquansu-1468920342994.xlsx b/data/2016/4334ea476a2b8196d54ba40a341831echvkythuatquansu-1468920342994.xlsx similarity index 100% rename from 2016/data/4334ea476a2b8196d54ba40a341831echvkythuatquansu-1468920342994.xlsx rename to data/2016/4334ea476a2b8196d54ba40a341831echvkythuatquansu-1468920342994.xlsx diff --git a/2016/data/487c7287c1c9735722b31d877942becddhcongnghieptphcm-1468983373303.xlsx b/data/2016/487c7287c1c9735722b31d877942becddhcongnghieptphcm-1468983373303.xlsx similarity index 100% rename from 2016/data/487c7287c1c9735722b31d877942becddhcongnghieptphcm-1468983373303.xlsx rename to data/2016/487c7287c1c9735722b31d877942becddhcongnghieptphcm-1468983373303.xlsx diff --git a/2016/data/4a3fa95a83fe9b3b2fafab309323fd22dhthuyloi-1468901840919.xlsx b/data/2016/4a3fa95a83fe9b3b2fafab309323fd22dhthuyloi-1468901840919.xlsx similarity index 100% rename from 2016/data/4a3fa95a83fe9b3b2fafab309323fd22dhthuyloi-1468901840919.xlsx rename to data/2016/4a3fa95a83fe9b3b2fafab309323fd22dhthuyloi-1468901840919.xlsx diff --git a/2016/data/4ad416ccf014bd5d01f415238566b210khanhhoa-1468933789968.xlsx b/data/2016/4ad416ccf014bd5d01f415238566b210khanhhoa-1468933789968.xlsx similarity index 100% rename from 2016/data/4ad416ccf014bd5d01f415238566b210khanhhoa-1468933789968.xlsx rename to data/2016/4ad416ccf014bd5d01f415238566b210khanhhoa-1468933789968.xlsx diff --git a/2016/data/4b98fdd7300b86a94636e4d15394fc19sonla-1468899319124.xlsx b/data/2016/4b98fdd7300b86a94636e4d15394fc19sonla-1468899319124.xlsx similarity index 100% rename from 2016/data/4b98fdd7300b86a94636e4d15394fc19sonla-1468899319124.xlsx rename to data/2016/4b98fdd7300b86a94636e4d15394fc19sonla-1468899319124.xlsx diff --git a/2016/data/4c2fd846ed2a0670ba6d1200d1d68e1dspkythuatvinhlong-1468902640438.xlsx b/data/2016/4c2fd846ed2a0670ba6d1200d1d68e1dspkythuatvinhlong-1468902640438.xlsx similarity index 100% rename from 2016/data/4c2fd846ed2a0670ba6d1200d1d68e1dspkythuatvinhlong-1468902640438.xlsx rename to data/2016/4c2fd846ed2a0670ba6d1200d1d68e1dspkythuatvinhlong-1468902640438.xlsx diff --git a/2016/data/4e0ccee19eb64513d23a6dff96ff7f9cvinhphuc-1468919874814.xlsx b/data/2016/4e0ccee19eb64513d23a6dff96ff7f9cvinhphuc-1468919874814.xlsx similarity index 100% rename from 2016/data/4e0ccee19eb64513d23a6dff96ff7f9cvinhphuc-1468919874814.xlsx rename to data/2016/4e0ccee19eb64513d23a6dff96ff7f9cvinhphuc-1468919874814.xlsx diff --git a/2016/data/4e9bb192d56c9fa92b3e0ec13d8236c8dhxaydungmientrung-1468940978346.xlsx b/data/2016/4e9bb192d56c9fa92b3e0ec13d8236c8dhxaydungmientrung-1468940978346.xlsx similarity index 100% rename from 2016/data/4e9bb192d56c9fa92b3e0ec13d8236c8dhxaydungmientrung-1468940978346.xlsx rename to data/2016/4e9bb192d56c9fa92b3e0ec13d8236c8dhxaydungmientrung-1468940978346.xlsx diff --git a/2016/data/505a876dfd1ea321291e29192aebe95ehoabinh-1468983220125.xlsx b/data/2016/505a876dfd1ea321291e29192aebe95ehoabinh-1468983220125.xlsx similarity index 100% rename from 2016/data/505a876dfd1ea321291e29192aebe95ehoabinh-1468983220125.xlsx rename to data/2016/505a876dfd1ea321291e29192aebe95ehoabinh-1468983220125.xlsx diff --git a/2016/data/514877c1cfebdc9c2251f651b5f02c7edhspkythuattphcm-1468906568655.xlsx b/data/2016/514877c1cfebdc9c2251f651b5f02c7edhspkythuattphcm-1468906568655.xlsx similarity index 100% rename from 2016/data/514877c1cfebdc9c2251f651b5f02c7edhspkythuattphcm-1468906568655.xlsx rename to data/2016/514877c1cfebdc9c2251f651b5f02c7edhspkythuattphcm-1468906568655.xlsx diff --git a/2016/data/51bb2f9b6e9f565c2d705606fd56958bdhgiaothongvantai-1468939107463.xlsx b/data/2016/51bb2f9b6e9f565c2d705606fd56958bdhgiaothongvantai-1468939107463.xlsx similarity index 100% rename from 2016/data/51bb2f9b6e9f565c2d705606fd56958bdhgiaothongvantai-1468939107463.xlsx rename to data/2016/51bb2f9b6e9f565c2d705606fd56958bdhgiaothongvantai-1468939107463.xlsx diff --git a/2016/data/538c9676b70f2509573245d58fba93bcdongnai-1468938241935.xlsx b/data/2016/538c9676b70f2509573245d58fba93bcdongnai-1468938241935.xlsx similarity index 100% rename from 2016/data/538c9676b70f2509573245d58fba93bcdongnai-1468938241935.xlsx rename to data/2016/538c9676b70f2509573245d58fba93bcdongnai-1468938241935.xlsx diff --git a/2016/data/53e5e20ebd74d84741513b703fbd13bfdhnonglam-dhthainguyen-1468939023637.xlsx b/data/2016/53e5e20ebd74d84741513b703fbd13bfdhnonglam-dhthainguyen-1468939023637.xlsx similarity index 100% rename from 2016/data/53e5e20ebd74d84741513b703fbd13bfdhnonglam-dhthainguyen-1468939023637.xlsx rename to data/2016/53e5e20ebd74d84741513b703fbd13bfdhnonglam-dhthainguyen-1468939023637.xlsx diff --git a/2016/data/5465cca59495dcd9a48f689e2d38b85ephutho-1468902409682.xlsx b/data/2016/5465cca59495dcd9a48f689e2d38b85ephutho-1468902409682.xlsx similarity index 100% rename from 2016/data/5465cca59495dcd9a48f689e2d38b85ephutho-1468902409682.xlsx rename to data/2016/5465cca59495dcd9a48f689e2d38b85ephutho-1468902409682.xlsx diff --git a/2016/data/54bbcf865c137d58cf62af15733bd376dhsupham-dhdanang-1468938526558.xlsx b/data/2016/54bbcf865c137d58cf62af15733bd376dhsupham-dhdanang-1468938526558.xlsx similarity index 100% rename from 2016/data/54bbcf865c137d58cf62af15733bd376dhsupham-dhdanang-1468938526558.xlsx rename to data/2016/54bbcf865c137d58cf62af15733bd376dhsupham-dhdanang-1468938526558.xlsx diff --git a/2016/data/575df8a4df74e87abb8f55d807324374dongthap-1468933902699.xlsx b/data/2016/575df8a4df74e87abb8f55d807324374dongthap-1468933902699.xlsx similarity index 100% rename from 2016/data/575df8a4df74e87abb8f55d807324374dongthap-1468933902699.xlsx rename to data/2016/575df8a4df74e87abb8f55d807324374dongthap-1468933902699.xlsx diff --git a/2016/data/5aede4951e7af8c1e1056c747a553386hvnongnghiepvn-1468900610577.xlsx b/data/2016/5aede4951e7af8c1e1056c747a553386hvnongnghiepvn-1468900610577.xlsx similarity index 100% rename from 2016/data/5aede4951e7af8c1e1056c747a553386hvnongnghiepvn-1468900610577.xlsx rename to data/2016/5aede4951e7af8c1e1056c747a553386hvnongnghiepvn-1468900610577.xlsx diff --git a/2016/data/5b16d7156a03bb7ed0618796bae87bc3cantho-1468933954714.xlsx b/data/2016/5b16d7156a03bb7ed0618796bae87bc3cantho-1468933954714.xlsx similarity index 100% rename from 2016/data/5b16d7156a03bb7ed0618796bae87bc3cantho-1468933954714.xlsx rename to data/2016/5b16d7156a03bb7ed0618796bae87bc3cantho-1468933954714.xlsx diff --git a/2016/data/5ca241aa1de8d25a3e6ff6db75234025dhcantho-baclieu-1468907071460.xls b/data/2016/5ca241aa1de8d25a3e6ff6db75234025dhcantho-baclieu-1468907071460.xls similarity index 100% rename from 2016/data/5ca241aa1de8d25a3e6ff6db75234025dhcantho-baclieu-1468907071460.xls rename to data/2016/5ca241aa1de8d25a3e6ff6db75234025dhcantho-baclieu-1468907071460.xls diff --git a/2016/data/5fb3a1b66aff077ee3da482be08cd391thainguyen-1468902797020.xlsx b/data/2016/5fb3a1b66aff077ee3da482be08cd391thainguyen-1468902797020.xlsx similarity index 100% rename from 2016/data/5fb3a1b66aff077ee3da482be08cd391thainguyen-1468902797020.xlsx rename to data/2016/5fb3a1b66aff077ee3da482be08cd391thainguyen-1468902797020.xlsx diff --git a/2016/data/5fd13ae5287cbf0149bd733b507e0608dhkhxhnv-dhqgtphcm-1468920630193.xlsx b/data/2016/5fd13ae5287cbf0149bd733b507e0608dhkhxhnv-dhqgtphcm-1468920630193.xlsx similarity index 100% rename from 2016/data/5fd13ae5287cbf0149bd733b507e0608dhkhxhnv-dhqgtphcm-1468920630193.xlsx rename to data/2016/5fd13ae5287cbf0149bd733b507e0608dhkhxhnv-dhqgtphcm-1468920630193.xlsx diff --git a/2016/data/6375fd13557bee08d20f1f3e46dac130dhnhatrang-coso1-1468901350425.xlsx b/data/2016/6375fd13557bee08d20f1f3e46dac130dhnhatrang-coso1-1468901350425.xlsx similarity index 100% rename from 2016/data/6375fd13557bee08d20f1f3e46dac130dhnhatrang-coso1-1468901350425.xlsx rename to data/2016/6375fd13557bee08d20f1f3e46dac130dhnhatrang-coso1-1468901350425.xlsx diff --git a/2016/data/64d2635efd30a0d0db4f7c30b9735261dhvinh-1468940230027.xlsx b/data/2016/64d2635efd30a0d0db4f7c30b9735261dhvinh-1468940230027.xlsx similarity index 100% rename from 2016/data/64d2635efd30a0d0db4f7c30b9735261dhvinh-1468940230027.xlsx rename to data/2016/64d2635efd30a0d0db4f7c30b9735261dhvinh-1468940230027.xlsx diff --git a/2016/data/64e44466ffeff9a587260795644805d0dhkhtunhien-dhqgtphcm-1468921015146.xlsx b/data/2016/64e44466ffeff9a587260795644805d0dhkhtunhien-dhqgtphcm-1468921015146.xlsx similarity index 100% rename from 2016/data/64e44466ffeff9a587260795644805d0dhkhtunhien-dhqgtphcm-1468921015146.xlsx rename to data/2016/64e44466ffeff9a587260795644805d0dhkhtunhien-dhqgtphcm-1468921015146.xlsx diff --git a/2016/data/6960b6fa493e1365a183af461e945389thaibinh-1468920019273.xlsx b/data/2016/6960b6fa493e1365a183af461e945389thaibinh-1468920019273.xlsx similarity index 100% rename from 2016/data/6960b6fa493e1365a183af461e945389thaibinh-1468920019273.xlsx rename to data/2016/6960b6fa493e1365a183af461e945389thaibinh-1468920019273.xlsx diff --git a/2016/data/698b930ab7697a0672bbc39168024c9cgialai-1468933753098.xlsx b/data/2016/698b930ab7697a0672bbc39168024c9cgialai-1468933753098.xlsx similarity index 100% rename from 2016/data/698b930ab7697a0672bbc39168024c9cgialai-1468933753098.xlsx rename to data/2016/698b930ab7697a0672bbc39168024c9cgialai-1468933753098.xlsx diff --git a/2016/data/6badc7cfa258b59ad67e2b64f713b95fthuathienhue-1468983258438.xlsx b/data/2016/6badc7cfa258b59ad67e2b64f713b95fthuathienhue-1468983258438.xlsx similarity index 100% rename from 2016/data/6badc7cfa258b59ad67e2b64f713b95fthuathienhue-1468983258438.xlsx rename to data/2016/6badc7cfa258b59ad67e2b64f713b95fthuathienhue-1468983258438.xlsx diff --git a/2016/data/70bf8cd2b6e87523219e55149a8524c5dhquynhon-1468938878328.xlsx b/data/2016/70bf8cd2b6e87523219e55149a8524c5dhquynhon-1468938878328.xlsx similarity index 100% rename from 2016/data/70bf8cd2b6e87523219e55149a8524c5dhquynhon-1468938878328.xlsx rename to data/2016/70bf8cd2b6e87523219e55149a8524c5dhquynhon-1468938878328.xlsx diff --git a/2016/data/74145979ee30186bf4e40ee3e6a3ac74bentre-1468934010040.xlsx b/data/2016/74145979ee30186bf4e40ee3e6a3ac74bentre-1468934010040.xlsx similarity index 100% rename from 2016/data/74145979ee30186bf4e40ee3e6a3ac74bentre-1468934010040.xlsx rename to data/2016/74145979ee30186bf4e40ee3e6a3ac74bentre-1468934010040.xlsx diff --git a/2016/data/7418e83ff9d07b146216a3beafa927bbdhtravinh-1468902716174.xlsx b/data/2016/7418e83ff9d07b146216a3beafa927bbdhtravinh-1468902716174.xlsx similarity index 100% rename from 2016/data/7418e83ff9d07b146216a3beafa927bbdhtravinh-1468902716174.xlsx rename to data/2016/7418e83ff9d07b146216a3beafa927bbdhtravinh-1468902716174.xlsx diff --git a/2016/data/7435bb054e8e4ec34c173af1b6ad8402kiengiang-1468900147395.xlsx b/data/2016/7435bb054e8e4ec34c173af1b6ad8402kiengiang-1468900147395.xlsx similarity index 100% rename from 2016/data/7435bb054e8e4ec34c173af1b6ad8402kiengiang-1468900147395.xlsx rename to data/2016/7435bb054e8e4ec34c173af1b6ad8402kiengiang-1468900147395.xlsx diff --git a/2016/data/74e6cd0dc78a3fb7dcf56ae57283b9b9dhtaybac-1468940302052.xlsx b/data/2016/74e6cd0dc78a3fb7dcf56ae57283b9b9dhtaybac-1468940302052.xlsx similarity index 100% rename from 2016/data/74e6cd0dc78a3fb7dcf56ae57283b9b9dhtaybac-1468940302052.xlsx rename to data/2016/74e6cd0dc78a3fb7dcf56ae57283b9b9dhtaybac-1468940302052.xlsx diff --git a/2016/data/76f19098881e9c79f66ac4f1b42a8a55dhsuphamhanoi-1468901144203.xlsx b/data/2016/76f19098881e9c79f66ac4f1b42a8a55dhsuphamhanoi-1468901144203.xlsx similarity index 100% rename from 2016/data/76f19098881e9c79f66ac4f1b42a8a55dhsuphamhanoi-1468901144203.xlsx rename to data/2016/76f19098881e9c79f66ac4f1b42a8a55dhsuphamhanoi-1468901144203.xlsx diff --git a/2016/data/778ab074ed6a95d209fa901fd5d68843dhhungvuong-1468901964127.xlsx b/data/2016/778ab074ed6a95d209fa901fd5d68843dhhungvuong-1468901964127.xlsx similarity index 100% rename from 2016/data/778ab074ed6a95d209fa901fd5d68843dhhungvuong-1468901964127.xlsx rename to data/2016/778ab074ed6a95d209fa901fd5d68843dhhungvuong-1468901964127.xlsx diff --git a/2016/data/7afd5d7fdff7c170a6342cd8f9474b22langson-1468932984587.xlsx b/data/2016/7afd5d7fdff7c170a6342cd8f9474b22langson-1468932984587.xlsx similarity index 100% rename from 2016/data/7afd5d7fdff7c170a6342cd8f9474b22langson-1468932984587.xlsx rename to data/2016/7afd5d7fdff7c170a6342cd8f9474b22langson-1468932984587.xlsx diff --git a/2016/data/7b5d74564ab9f6e0e137f6d161fab6f0dhxaydung-1468940561682.xlsx b/data/2016/7b5d74564ab9f6e0e137f6d161fab6f0dhxaydung-1468940561682.xlsx similarity index 100% rename from 2016/data/7b5d74564ab9f6e0e137f6d161fab6f0dhxaydung-1468940561682.xlsx rename to data/2016/7b5d74564ab9f6e0e137f6d161fab6f0dhxaydung-1468940561682.xlsx diff --git a/2016/data/7cfc91bd6a1f5ae9d7917e920f469b90haiduong-1468899640007.xlsx b/data/2016/7cfc91bd6a1f5ae9d7917e920f469b90haiduong-1468899640007.xlsx similarity index 100% rename from 2016/data/7cfc91bd6a1f5ae9d7917e920f469b90haiduong-1468899640007.xlsx rename to data/2016/7cfc91bd6a1f5ae9d7917e920f469b90haiduong-1468899640007.xlsx diff --git a/2016/data/7e0d0b6fb981d407cffdb79d724e8aecbacninh-1468919984998.xlsx b/data/2016/7e0d0b6fb981d407cffdb79d724e8aecbacninh-1468919984998.xlsx similarity index 100% rename from 2016/data/7e0d0b6fb981d407cffdb79d724e8aecbacninh-1468919984998.xlsx rename to data/2016/7e0d0b6fb981d407cffdb79d724e8aecbacninh-1468919984998.xlsx diff --git a/2016/data/7f12b90225e91f9d00aac3bcdf9974e1nghean-1468920043121.xlsx b/data/2016/7f12b90225e91f9d00aac3bcdf9974e1nghean-1468920043121.xlsx similarity index 100% rename from 2016/data/7f12b90225e91f9d00aac3bcdf9974e1nghean-1468920043121.xlsx rename to data/2016/7f12b90225e91f9d00aac3bcdf9974e1nghean-1468920043121.xlsx diff --git a/2016/data/7f1cbe7f35d1f539ca817a0ab8bd00c2dhcantho-haugiang-1468907167921.xls b/data/2016/7f1cbe7f35d1f539ca817a0ab8bd00c2dhcantho-haugiang-1468907167921.xls similarity index 100% rename from 2016/data/7f1cbe7f35d1f539ca817a0ab8bd00c2dhcantho-haugiang-1468907167921.xls rename to data/2016/7f1cbe7f35d1f539ca817a0ab8bd00c2dhcantho-haugiang-1468907167921.xls diff --git a/2016/data/7f4362ef567cdf74495368e0fc11072avinhlong-1468934067981.xlsx b/data/2016/7f4362ef567cdf74495368e0fc11072avinhlong-1468934067981.xlsx similarity index 100% rename from 2016/data/7f4362ef567cdf74495368e0fc11072avinhlong-1468934067981.xlsx rename to data/2016/7f4362ef567cdf74495368e0fc11072avinhlong-1468934067981.xlsx diff --git a/2016/data/82338a4a93ea1e0cde26e0061066ca8adhcongnghiephanoi-1468920154757.xlsx b/data/2016/82338a4a93ea1e0cde26e0061066ca8adhcongnghiephanoi-1468920154757.xlsx similarity index 100% rename from 2016/data/82338a4a93ea1e0cde26e0061066ca8adhcongnghiephanoi-1468920154757.xlsx rename to data/2016/82338a4a93ea1e0cde26e0061066ca8adhcongnghiephanoi-1468920154757.xlsx diff --git a/2016/data/86a692bebf9b0b7c58adb9876b9befd5dhnganhangtphcm-1468902244566.xlsx b/data/2016/86a692bebf9b0b7c58adb9876b9befd5dhnganhangtphcm-1468902244566.xlsx similarity index 100% rename from 2016/data/86a692bebf9b0b7c58adb9876b9befd5dhnganhangtphcm-1468902244566.xlsx rename to data/2016/86a692bebf9b0b7c58adb9876b9befd5dhnganhangtphcm-1468902244566.xlsx diff --git a/2016/data/87ffaf3197ba20f0919f98a0b3f33914dhkhoahoc-dhhue-1468938729472.xlsx b/data/2016/87ffaf3197ba20f0919f98a0b3f33914dhkhoahoc-dhhue-1468938729472.xlsx similarity index 100% rename from 2016/data/87ffaf3197ba20f0919f98a0b3f33914dhkhoahoc-dhhue-1468938729472.xlsx rename to data/2016/87ffaf3197ba20f0919f98a0b3f33914dhkhoahoc-dhhue-1468938729472.xlsx diff --git a/2016/data/8916da7b3c4d58cffc1dd29dc7a8aabfdhkinhte-dhhue-1468938560205.xlsx b/data/2016/8916da7b3c4d58cffc1dd29dc7a8aabfdhkinhte-dhhue-1468938560205.xlsx similarity index 100% rename from 2016/data/8916da7b3c4d58cffc1dd29dc7a8aabfdhkinhte-dhhue-1468938560205.xlsx rename to data/2016/8916da7b3c4d58cffc1dd29dc7a8aabfdhkinhte-dhhue-1468938560205.xlsx diff --git a/2016/data/8af8f1b29b6a40e26ccb4a2e19e8eededhkythuatcongnghiep-dhthainguyen-1468901919568.xlsx b/data/2016/8af8f1b29b6a40e26ccb4a2e19e8eededhkythuatcongnghiep-dhthainguyen-1468901919568.xlsx similarity index 100% rename from 2016/data/8af8f1b29b6a40e26ccb4a2e19e8eededhkythuatcongnghiep-dhthainguyen-1468901919568.xlsx rename to data/2016/8af8f1b29b6a40e26ccb4a2e19e8eededhkythuatcongnghiep-dhthainguyen-1468901919568.xlsx diff --git a/2016/data/8f948c2a5ab49a3c5fd03b6ea04887a7dhkinhtequocdanfix-1468902083782.xlsx b/data/2016/8f948c2a5ab49a3c5fd03b6ea04887a7dhkinhtequocdanfix-1468902083782.xlsx similarity index 100% rename from 2016/data/8f948c2a5ab49a3c5fd03b6ea04887a7dhkinhtequocdanfix-1468902083782.xlsx rename to data/2016/8f948c2a5ab49a3c5fd03b6ea04887a7dhkinhtequocdanfix-1468902083782.xlsx diff --git a/2016/data/919276a0348634459f8937dfcd6c7129dhhue-1468938792115.xlsx b/data/2016/919276a0348634459f8937dfcd6c7129dhhue-1468938792115.xlsx similarity index 100% rename from 2016/data/919276a0348634459f8937dfcd6c7129dhhue-1468938792115.xlsx rename to data/2016/919276a0348634459f8937dfcd6c7129dhhue-1468938792115.xlsx diff --git a/2016/data/92045214159cb0335b2836aa1c4bb25ahungyen-1468899732002.xlsx b/data/2016/92045214159cb0335b2836aa1c4bb25ahungyen-1468899732002.xlsx similarity index 100% rename from 2016/data/92045214159cb0335b2836aa1c4bb25ahungyen-1468899732002.xlsx rename to data/2016/92045214159cb0335b2836aa1c4bb25ahungyen-1468899732002.xlsx diff --git a/2016/data/964c4368131f6be6058fabb1caefba1chaugiang-1468934247324.xlsx b/data/2016/964c4368131f6be6058fabb1caefba1chaugiang-1468934247324.xlsx similarity index 100% rename from 2016/data/964c4368131f6be6058fabb1caefba1chaugiang-1468934247324.xlsx rename to data/2016/964c4368131f6be6058fabb1caefba1chaugiang-1468934247324.xlsx diff --git a/2016/data/96c95c85c5549a06f2e92ffcacf8ff21hvtaichinh-1468934470924.xlsx b/data/2016/96c95c85c5549a06f2e92ffcacf8ff21hvtaichinh-1468934470924.xlsx similarity index 100% rename from 2016/data/96c95c85c5549a06f2e92ffcacf8ff21hvtaichinh-1468934470924.xlsx rename to data/2016/96c95c85c5549a06f2e92ffcacf8ff21hvtaichinh-1468934470924.xlsx diff --git a/2016/data/97aa741a4c2b1276559eb021779daa32yenbai-1468933116055.xlsx b/data/2016/97aa741a4c2b1276559eb021779daa32yenbai-1468933116055.xlsx similarity index 100% rename from 2016/data/97aa741a4c2b1276559eb021779daa32yenbai-1468933116055.xlsx rename to data/2016/97aa741a4c2b1276559eb021779daa32yenbai-1468933116055.xlsx diff --git a/2016/data/9ba098a7913ad837a66e173b3fc41fd7dhngoaingu-dhdanang-1468902750424.xlsx b/data/2016/9ba098a7913ad837a66e173b3fc41fd7dhngoaingu-dhdanang-1468902750424.xlsx similarity index 100% rename from 2016/data/9ba098a7913ad837a66e173b3fc41fd7dhngoaingu-dhdanang-1468902750424.xlsx rename to data/2016/9ba098a7913ad837a66e173b3fc41fd7dhngoaingu-dhdanang-1468902750424.xlsx diff --git a/2016/data/9c730c43bf4309171528c3853ed53b19quangbinh-1468933376032.xlsx b/data/2016/9c730c43bf4309171528c3853ed53b19quangbinh-1468933376032.xlsx similarity index 100% rename from 2016/data/9c730c43bf4309171528c3853ed53b19quangbinh-1468933376032.xlsx rename to data/2016/9c730c43bf4309171528c3853ed53b19quangbinh-1468933376032.xlsx diff --git a/2016/data/9d78f6dce7965fb4b7a337279920d4b1dhtonducthang-1468902168623.xlsx b/data/2016/9d78f6dce7965fb4b7a337279920d4b1dhtonducthang-1468902168623.xlsx similarity index 100% rename from 2016/data/9d78f6dce7965fb4b7a337279920d4b1dhtonducthang-1468902168623.xlsx rename to data/2016/9d78f6dce7965fb4b7a337279920d4b1dhtonducthang-1468902168623.xlsx diff --git a/2016/data/9e0e564ae801e347bd7649da8b89ad90dhluathn-1468920377978.xlsx b/data/2016/9e0e564ae801e347bd7649da8b89ad90dhluathn-1468920377978.xlsx similarity index 100% rename from 2016/data/9e0e564ae801e347bd7649da8b89ad90dhluathn-1468920377978.xlsx rename to data/2016/9e0e564ae801e347bd7649da8b89ad90dhluathn-1468920377978.xlsx diff --git a/2016/data/a11e707e6ed5fe4d29a8e32380d7558cdhsphanoi-2-1468902006284.xlsx b/data/2016/a11e707e6ed5fe4d29a8e32380d7558cdhsphanoi-2-1468902006284.xlsx similarity index 100% rename from 2016/data/a11e707e6ed5fe4d29a8e32380d7558cdhsphanoi-2-1468902006284.xlsx rename to data/2016/a11e707e6ed5fe4d29a8e32380d7558cdhsphanoi-2-1468902006284.xlsx diff --git a/2016/data/a31b3446bae5a17c517348d8c38c5b7equangninh-1468919920197.xlsx b/data/2016/a31b3446bae5a17c517348d8c38c5b7equangninh-1468919920197.xlsx similarity index 100% rename from 2016/data/a31b3446bae5a17c517348d8c38c5b7equangninh-1468919920197.xlsx rename to data/2016/a31b3446bae5a17c517348d8c38c5b7equangninh-1468919920197.xlsx diff --git a/2016/data/a3aa106104f114c6426deb2d5e5da5fdlaichau-1468932954079.xlsx b/data/2016/a3aa106104f114c6426deb2d5e5da5fdlaichau-1468932954079.xlsx similarity index 100% rename from 2016/data/a3aa106104f114c6426deb2d5e5da5fdlaichau-1468932954079.xlsx rename to data/2016/a3aa106104f114c6426deb2d5e5da5fdlaichau-1468932954079.xlsx diff --git a/2016/data/a8ff80d9478747e36d21ec95cc9ecb8fsoctrang-1468902686409.xlsx b/data/2016/a8ff80d9478747e36d21ec95cc9ecb8fsoctrang-1468902686409.xlsx similarity index 100% rename from 2016/data/a8ff80d9478747e36d21ec95cc9ecb8fsoctrang-1468902686409.xlsx rename to data/2016/a8ff80d9478747e36d21ec95cc9ecb8fsoctrang-1468902686409.xlsx diff --git a/2016/data/a995de3f7a7055cf10b2f002f0e194f7dhsptphcm-1468920792291.xlsx b/data/2016/a995de3f7a7055cf10b2f002f0e194f7dhsptphcm-1468920792291.xlsx similarity index 100% rename from 2016/data/a995de3f7a7055cf10b2f002f0e194f7dhsptphcm-1468920792291.xlsx rename to data/2016/a995de3f7a7055cf10b2f002f0e194f7dhsptphcm-1468920792291.xlsx diff --git a/2016/data/ab2f259ef55cb08b3a436d3f15d42cc7dhsupham-dhhue-1468934431828.xlsx b/data/2016/ab2f259ef55cb08b3a436d3f15d42cc7dhsupham-dhhue-1468934431828.xlsx similarity index 100% rename from 2016/data/ab2f259ef55cb08b3a436d3f15d42cc7dhsupham-dhhue-1468934431828.xlsx rename to data/2016/ab2f259ef55cb08b3a436d3f15d42cc7dhsupham-dhhue-1468934431828.xlsx diff --git a/2016/data/af6828f837a80e03d77a65f3d310ec60dh-tai-chinh-marketing-1468924711596.xlsx b/data/2016/af6828f837a80e03d77a65f3d310ec60dh-tai-chinh-marketing-1468924711596.xlsx similarity index 100% rename from 2016/data/af6828f837a80e03d77a65f3d310ec60dh-tai-chinh-marketing-1468924711596.xlsx rename to data/2016/af6828f837a80e03d77a65f3d310ec60dh-tai-chinh-marketing-1468924711596.xlsx diff --git a/2016/data/b2552e20c9ff4b237244dfbf2e278a47dhthuongmai-1468902876264.xlsx b/data/2016/b2552e20c9ff4b237244dfbf2e278a47dhthuongmai-1468902876264.xlsx similarity index 100% rename from 2016/data/b2552e20c9ff4b237244dfbf2e278a47dhthuongmai-1468902876264.xlsx rename to data/2016/b2552e20c9ff4b237244dfbf2e278a47dhthuongmai-1468902876264.xlsx diff --git a/2016/data/b2a739584a8861502573d1c6372fd6e9dhnonglamtphcm-1468939555326.xlsx b/data/2016/b2a739584a8861502573d1c6372fd6e9dhnonglamtphcm-1468939555326.xlsx similarity index 100% rename from 2016/data/b2a739584a8861502573d1c6372fd6e9dhnonglamtphcm-1468939555326.xlsx rename to data/2016/b2a739584a8861502573d1c6372fd6e9dhnonglamtphcm-1468939555326.xlsx diff --git a/2016/data/b34c777942ca8a4de91f23a35cce1c6bdhbachkhoahn-1468901491486.xlsx b/data/2016/b34c777942ca8a4de91f23a35cce1c6bdhbachkhoahn-1468901491486.xlsx similarity index 100% rename from 2016/data/b34c777942ca8a4de91f23a35cce1c6bdhbachkhoahn-1468901491486.xlsx rename to data/2016/b34c777942ca8a4de91f23a35cce1c6bdhbachkhoahn-1468901491486.xlsx diff --git a/2016/data/b35a3f26a162bec3740e2da5639327afdhcantho-1468907131146.xls b/data/2016/b35a3f26a162bec3740e2da5639327afdhcantho-1468907131146.xls similarity index 100% rename from 2016/data/b35a3f26a162bec3740e2da5639327afdhcantho-1468907131146.xls rename to data/2016/b35a3f26a162bec3740e2da5639327afdhcantho-1468907131146.xls diff --git a/2016/data/b42fd58ead4c54279d7df5f53160a851dhdanang-1468920187203.xlsx b/data/2016/b42fd58ead4c54279d7df5f53160a851dhdanang-1468920187203.xlsx similarity index 100% rename from 2016/data/b42fd58ead4c54279d7df5f53160a851dhdanang-1468920187203.xlsx rename to data/2016/b42fd58ead4c54279d7df5f53160a851dhdanang-1468920187203.xlsx diff --git a/2016/data/b7a0c1ecce7c8445b3a1021091a5e654bacgiang-1468983153932.xlsx b/data/2016/b7a0c1ecce7c8445b3a1021091a5e654bacgiang-1468983153932.xlsx similarity index 100% rename from 2016/data/b7a0c1ecce7c8445b3a1021091a5e654bacgiang-1468983153932.xlsx rename to data/2016/b7a0c1ecce7c8445b3a1021091a5e654bacgiang-1468983153932.xlsx diff --git a/2016/data/bacd1b2133b37d0ea9fbf46445265036dhtiengiang-1468940391597.xlsx b/data/2016/bacd1b2133b37d0ea9fbf46445265036dhtiengiang-1468940391597.xlsx similarity index 100% rename from 2016/data/bacd1b2133b37d0ea9fbf46445265036dhtiengiang-1468940391597.xlsx rename to data/2016/bacd1b2133b37d0ea9fbf46445265036dhtiengiang-1468940391597.xlsx diff --git a/2016/data/bd857add0cfe92c1c247469ff2104527dh-hong-duc-1468900427119.xlsx b/data/2016/bd857add0cfe92c1c247469ff2104527dh-hong-duc-1468900427119.xlsx similarity index 100% rename from 2016/data/bd857add0cfe92c1c247469ff2104527dh-hong-duc-1468900427119.xlsx rename to data/2016/bd857add0cfe92c1c247469ff2104527dh-hong-duc-1468900427119.xlsx diff --git a/2016/data/bf259a908ff8a8dcc067a761204459f4daknong-1468934173803.xlsx b/data/2016/bf259a908ff8a8dcc067a761204459f4daknong-1468934173803.xlsx similarity index 100% rename from 2016/data/bf259a908ff8a8dcc067a761204459f4daknong-1468934173803.xlsx rename to data/2016/bf259a908ff8a8dcc067a761204459f4daknong-1468934173803.xlsx diff --git a/2016/data/c174631d90303276ac63ff38b31157bbdhmodiachat-1468900697349.xlsx b/data/2016/c174631d90303276ac63ff38b31157bbdhmodiachat-1468900697349.xlsx similarity index 100% rename from 2016/data/c174631d90303276ac63ff38b31157bbdhmodiachat-1468900697349.xlsx rename to data/2016/c174631d90303276ac63ff38b31157bbdhmodiachat-1468900697349.xlsx diff --git a/2016/data/c2cfaaf287fb1e7120894e545771b027dhkhoahoc-dhthainguyen-1468900365545.xlsx b/data/2016/c2cfaaf287fb1e7120894e545771b027dhkhoahoc-dhthainguyen-1468900365545.xlsx similarity index 100% rename from 2016/data/c2cfaaf287fb1e7120894e545771b027dhkhoahoc-dhthainguyen-1468900365545.xlsx rename to data/2016/c2cfaaf287fb1e7120894e545771b027dhkhoahoc-dhthainguyen-1468900365545.xlsx diff --git a/2016/data/c5c28959fcd0b4e39dd9fd538db5448adhgtvttphcm-1468939150821.xlsx b/data/2016/c5c28959fcd0b4e39dd9fd538db5448adhgtvttphcm-1468939150821.xlsx similarity index 100% rename from 2016/data/c5c28959fcd0b4e39dd9fd538db5448adhgtvttphcm-1468939150821.xlsx rename to data/2016/c5c28959fcd0b4e39dd9fd538db5448adhgtvttphcm-1468939150821.xlsx diff --git a/2016/data/c5ecc58a731fd1a54b7e6cc3bc99449ddhlamnghiep-1468939300790.xlsx b/data/2016/c5ecc58a731fd1a54b7e6cc3bc99449ddhlamnghiep-1468939300790.xlsx similarity index 100% rename from 2016/data/c5ecc58a731fd1a54b7e6cc3bc99449ddhlamnghiep-1468939300790.xlsx rename to data/2016/c5ecc58a731fd1a54b7e6cc3bc99449ddhlamnghiep-1468939300790.xlsx diff --git a/2016/data/c6ef16bc9f75ce0fd16c1229afad4e71dhhanghai-1468906457479.xlsx b/data/2016/c6ef16bc9f75ce0fd16c1229afad4e71dhhanghai-1468906457479.xlsx similarity index 100% rename from 2016/data/c6ef16bc9f75ce0fd16c1229afad4e71dhhanghai-1468906457479.xlsx rename to data/2016/c6ef16bc9f75ce0fd16c1229afad4e71dhhanghai-1468906457479.xlsx diff --git a/2016/data/cc57dc986acb525086b86e85df54949dkontum-1468933632113.xlsx b/data/2016/cc57dc986acb525086b86e85df54949dkontum-1468933632113.xlsx similarity index 100% rename from 2016/data/cc57dc986acb525086b86e85df54949dkontum-1468933632113.xlsx rename to data/2016/cc57dc986acb525086b86e85df54949dkontum-1468933632113.xlsx diff --git a/2016/data/cdc912664d7217c75eb9b67c6ef3797adhkiengiang-1468901222446.xlsx b/data/2016/cdc912664d7217c75eb9b67c6ef3797adhkiengiang-1468901222446.xlsx similarity index 100% rename from 2016/data/cdc912664d7217c75eb9b67c6ef3797adhkiengiang-1468901222446.xlsx rename to data/2016/cdc912664d7217c75eb9b67c6ef3797adhkiengiang-1468901222446.xlsx diff --git a/2016/data/d035f470c13adbf5785797c6ccdcb204laocai-1468919805337.xlsx b/data/2016/d035f470c13adbf5785797c6ccdcb204laocai-1468919805337.xlsx similarity index 100% rename from 2016/data/d035f470c13adbf5785797c6ccdcb204laocai-1468919805337.xlsx rename to data/2016/d035f470c13adbf5785797c6ccdcb204laocai-1468919805337.xlsx diff --git a/2016/data/d5ecd2574b5ab36c62dffad435262cb8tuyenquang-1468902296807.xlsx b/data/2016/d5ecd2574b5ab36c62dffad435262cb8tuyenquang-1468902296807.xlsx similarity index 100% rename from 2016/data/d5ecd2574b5ab36c62dffad435262cb8tuyenquang-1468902296807.xlsx rename to data/2016/d5ecd2574b5ab36c62dffad435262cb8tuyenquang-1468902296807.xlsx diff --git a/2016/data/d652d24e8bd4fe6a48d634b116f37c27dhluattphcm-1468939398928.xlsx b/data/2016/d652d24e8bd4fe6a48d634b116f37c27dhluattphcm-1468939398928.xlsx similarity index 100% rename from 2016/data/d652d24e8bd4fe6a48d634b116f37c27dhluattphcm-1468939398928.xlsx rename to data/2016/d652d24e8bd4fe6a48d634b116f37c27dhluattphcm-1468939398928.xlsx diff --git a/2016/data/d75282bff0a4d8cd1421c3fb74f789c3dhsaigon-1468900867561.xlsx b/data/2016/d75282bff0a4d8cd1421c3fb74f789c3dhsaigon-1468900867561.xlsx similarity index 100% rename from 2016/data/d75282bff0a4d8cd1421c3fb74f789c3dhsaigon-1468900867561.xlsx rename to data/2016/d75282bff0a4d8cd1421c3fb74f789c3dhsaigon-1468900867561.xlsx diff --git a/2016/data/ddb631f833af894200ef10cd408d2ef0caobang-1468932877769.xlsx b/data/2016/ddb631f833af894200ef10cd408d2ef0caobang-1468932877769.xlsx similarity index 100% rename from 2016/data/ddb631f833af894200ef10cd408d2ef0caobang-1468932877769.xlsx rename to data/2016/ddb631f833af894200ef10cd408d2ef0caobang-1468932877769.xlsx diff --git a/2016/data/deca197a916a8f633f6894b4e13c3ca4dhhaiphong-1468901178368.xlsx b/data/2016/deca197a916a8f633f6894b4e13c3ca4dhhaiphong-1468901178368.xlsx similarity index 100% rename from 2016/data/deca197a916a8f633f6894b4e13c3ca4dhhaiphong-1468901178368.xlsx rename to data/2016/deca197a916a8f633f6894b4e13c3ca4dhhaiphong-1468901178368.xlsx diff --git a/2016/data/dfa63c5b68a8f638379ca0e9c5a1c556namdinh-1468933234061.xlsx b/data/2016/dfa63c5b68a8f638379ca0e9c5a1c556namdinh-1468933234061.xlsx similarity index 100% rename from 2016/data/dfa63c5b68a8f638379ca0e9c5a1c556namdinh-1468933234061.xlsx rename to data/2016/dfa63c5b68a8f638379ca0e9c5a1c556namdinh-1468933234061.xlsx diff --git a/2016/data/e693bc9de67acc916032c6a3b2fc9eedhanam-1468933146526.xlsx b/data/2016/e693bc9de67acc916032c6a3b2fc9eedhanam-1468933146526.xlsx similarity index 100% rename from 2016/data/e693bc9de67acc916032c6a3b2fc9eedhanam-1468933146526.xlsx rename to data/2016/e693bc9de67acc916032c6a3b2fc9eedhanam-1468933146526.xlsx diff --git a/2016/data/e80dc028d53fa49ee247391a0653dee5quangngai-1468933592400.xlsx b/data/2016/e80dc028d53fa49ee247391a0653dee5quangngai-1468933592400.xlsx similarity index 100% rename from 2016/data/e80dc028d53fa49ee247391a0653dee5quangngai-1468933592400.xlsx rename to data/2016/e80dc028d53fa49ee247391a0653dee5quangngai-1468933592400.xlsx diff --git a/2016/data/ec765c32190773f5a18cd8202a5eaf0dquangnam-1468933501467.xlsx b/data/2016/ec765c32190773f5a18cd8202a5eaf0dquangnam-1468933501467.xlsx similarity index 100% rename from 2016/data/ec765c32190773f5a18cd8202a5eaf0dquangnam-1468933501467.xlsx rename to data/2016/ec765c32190773f5a18cd8202a5eaf0dquangnam-1468933501467.xlsx diff --git a/2016/data/ec84067268f47d523b1e80b493248377dhngoaithuong-1468920480753.xlsx b/data/2016/ec84067268f47d523b1e80b493248377dhngoaithuong-1468920480753.xlsx similarity index 100% rename from 2016/data/ec84067268f47d523b1e80b493248377dhngoaithuong-1468920480753.xlsx rename to data/2016/ec84067268f47d523b1e80b493248377dhngoaithuong-1468920480753.xlsx diff --git a/2016/data/ee09c723da4e86cdbe26213413450af2dhsupham-dhthainguyen-1468939061872.xlsx b/data/2016/ee09c723da4e86cdbe26213413450af2dhsupham-dhthainguyen-1468939061872.xlsx similarity index 100% rename from 2016/data/ee09c723da4e86cdbe26213413450af2dhsupham-dhthainguyen-1468939061872.xlsx rename to data/2016/ee09c723da4e86cdbe26213413450af2dhsupham-dhthainguyen-1468939061872.xlsx diff --git a/2016/data/f0aa5a19c2c8c1fde8211341f0e98e26dhkinhtetphcm-1468939229043.xlsx b/data/2016/f0aa5a19c2c8c1fde8211341f0e98e26dhkinhtetphcm-1468939229043.xlsx similarity index 100% rename from 2016/data/f0aa5a19c2c8c1fde8211341f0e98e26dhkinhtetphcm-1468939229043.xlsx rename to data/2016/f0aa5a19c2c8c1fde8211341f0e98e26dhkinhtetphcm-1468939229043.xlsx diff --git a/2016/data/f2846894bd5533e02b122a1e3b4333f6hvnganhang-1468939472850.xlsx b/data/2016/f2846894bd5533e02b122a1e3b4333f6hvnganhang-1468939472850.xlsx similarity index 100% rename from 2016/data/f2846894bd5533e02b122a1e3b4333f6hvnganhang-1468939472850.xlsx rename to data/2016/f2846894bd5533e02b122a1e3b4333f6hvnganhang-1468939472850.xlsx diff --git a/2016/data/f85f04bb2bbd84457b83ac846728a2a6dhspkythuathungyen-1468920667091.xlsx b/data/2016/f85f04bb2bbd84457b83ac846728a2a6dhspkythuathungyen-1468920667091.xlsx similarity index 100% rename from 2016/data/f85f04bb2bbd84457b83ac846728a2a6dhspkythuathungyen-1468920667091.xlsx rename to data/2016/f85f04bb2bbd84457b83ac846728a2a6dhspkythuathungyen-1468920667091.xlsx diff --git a/2016/data/fc511852659150f6305f6f624445300ddhangiang-1468940189321.xlsx b/data/2016/fc511852659150f6305f6f624445300ddhangiang-1468940189321.xlsx similarity index 100% rename from 2016/data/fc511852659150f6305f6f624445300ddhangiang-1468940189321.xlsx rename to data/2016/fc511852659150f6305f6f624445300ddhangiang-1468940189321.xlsx diff --git a/2016/data/ff877788e43bd84f0119dae026996892dhkinhteluat-dhqgtphcm-1468939860894.xlsx b/data/2016/ff877788e43bd84f0119dae026996892dhkinhteluat-dhqgtphcm-1468939860894.xlsx similarity index 100% rename from 2016/data/ff877788e43bd84f0119dae026996892dhkinhteluat-dhqgtphcm-1468939860894.xlsx rename to data/2016/ff877788e43bd84f0119dae026996892dhkinhteluat-dhqgtphcm-1468939860894.xlsx diff --git a/2017/data-old/10_BinhThuan_RIIW.xls.xlsx b/data/2017-old/10_BinhThuan_RIIW.xls.xlsx similarity index 100% rename from 2017/data-old/10_BinhThuan_RIIW.xls.xlsx rename to data/2017-old/10_BinhThuan_RIIW.xls.xlsx diff --git a/2017/data-old/10_Ca_Mau_BKXT.xls.xlsx b/data/2017-old/10_Ca_Mau_BKXT.xls.xlsx similarity index 100% rename from 2017/data-old/10_Ca_Mau_BKXT.xls.xlsx rename to data/2017-old/10_Ca_Mau_BKXT.xls.xlsx diff --git a/2017/data-old/10_LamDong_GNFT.xls.xlsx b/data/2017-old/10_LamDong_GNFT.xls.xlsx similarity index 100% rename from 2017/data-old/10_LamDong_GNFT.xls.xlsx rename to data/2017-old/10_LamDong_GNFT.xls.xlsx diff --git a/2017/data-old/10_Soc_Trang_XCGJ.xls.xlsx b/data/2017-old/10_Soc_Trang_XCGJ.xls.xlsx similarity index 100% rename from 2017/data-old/10_Soc_Trang_XCGJ.xls.xlsx rename to data/2017-old/10_Soc_Trang_XCGJ.xls.xlsx diff --git a/2017/data-old/11_BinhDuong_RYQL.xls.xlsx b/data/2017-old/11_BinhDuong_RYQL.xls.xlsx similarity index 100% rename from 2017/data-old/11_BinhDuong_RYQL.xls.xlsx rename to data/2017-old/11_BinhDuong_RYQL.xls.xlsx diff --git a/2017/data-old/11_LaoCai_GMSU.xls.xlsx b/data/2017-old/11_LaoCai_GMSU.xls.xlsx similarity index 100% rename from 2017/data-old/11_LaoCai_GMSU.xls.xlsx rename to data/2017-old/11_LaoCai_GMSU.xls.xlsx diff --git a/2017/data-old/12_BenTre_DKWF.xls.xlsx b/data/2017-old/12_BenTre_DKWF.xls.xlsx similarity index 100% rename from 2017/data-old/12_BenTre_DKWF.xls.xlsx rename to data/2017-old/12_BenTre_DKWF.xls.xlsx diff --git a/2017/data-old/12_LongAn_ZZUK.xls.xlsx b/data/2017-old/12_LongAn_ZZUK.xls.xlsx similarity index 100% rename from 2017/data-old/12_LongAn_ZZUK.xls.xlsx rename to data/2017-old/12_LongAn_ZZUK.xls.xlsx diff --git a/2017/data-old/13_NamDinh_ESEL.xls.xlsx b/data/2017-old/13_NamDinh_ESEL.xls.xlsx similarity index 100% rename from 2017/data-old/13_NamDinh_ESEL.xls.xlsx rename to data/2017-old/13_NamDinh_ESEL.xls.xlsx diff --git a/2017/data-old/13_TraVinh_LKUJ.xls.xlsx b/data/2017-old/13_TraVinh_LKUJ.xls.xlsx similarity index 100% rename from 2017/data-old/13_TraVinh_LKUJ.xls.xlsx rename to data/2017-old/13_TraVinh_LKUJ.xls.xlsx diff --git a/2017/data-old/14_NgheAn_BSLY.xls.xlsx b/data/2017-old/14_NgheAn_BSLY.xls.xlsx similarity index 100% rename from 2017/data-old/14_NgheAn_BSLY.xls.xlsx rename to data/2017-old/14_NgheAn_BSLY.xls.xlsx diff --git a/2017/data-old/15_PhuTho_ABWQ.xls.xlsx b/data/2017-old/15_PhuTho_ABWQ.xls.xlsx similarity index 100% rename from 2017/data-old/15_PhuTho_ABWQ.xls.xlsx rename to data/2017-old/15_PhuTho_ABWQ.xls.xlsx diff --git a/2017/data-old/16_QuangBinh_KGEU.xls.xlsx b/data/2017-old/16_QuangBinh_KGEU.xls.xlsx similarity index 100% rename from 2017/data-old/16_QuangBinh_KGEU.xls.xlsx rename to data/2017-old/16_QuangBinh_KGEU.xls.xlsx diff --git a/2017/data-old/17_QuangNam_AMTK.xls.xlsx b/data/2017-old/17_QuangNam_AMTK.xls.xlsx similarity index 100% rename from 2017/data-old/17_QuangNam_AMTK.xls.xlsx rename to data/2017-old/17_QuangNam_AMTK.xls.xlsx diff --git a/2017/data-old/18_QuangNgai_KOFP.xls.xlsx b/data/2017-old/18_QuangNgai_KOFP.xls.xlsx similarity index 100% rename from 2017/data-old/18_QuangNgai_KOFP.xls.xlsx rename to data/2017-old/18_QuangNgai_KOFP.xls.xlsx diff --git a/2017/data-old/19_QuangTri_OMZF.xls.xlsx b/data/2017-old/19_QuangTri_OMZF.xls.xlsx similarity index 100% rename from 2017/data-old/19_QuangTri_OMZF.xls.xlsx rename to data/2017-old/19_QuangTri_OMZF.xls.xlsx diff --git a/2017/data-old/1_BaRia_VungTau_HJKG.xls.xlsx b/data/2017-old/1_BaRia_VungTau_HJKG.xls.xlsx similarity index 100% rename from 2017/data-old/1_BaRia_VungTau_HJKG.xls.xlsx rename to data/2017-old/1_BaRia_VungTau_HJKG.xls.xlsx diff --git a/2017/data-old/1_Da_Nang_AHWJ.xls.xlsx b/data/2017-old/1_Da_Nang_AHWJ.xls.xlsx similarity index 100% rename from 2017/data-old/1_Da_Nang_AHWJ.xls.xlsx rename to data/2017-old/1_Da_Nang_AHWJ.xls.xlsx diff --git a/2017/data-old/1_Ha_Noi_CVXG.xls.xlsx b/data/2017-old/1_Ha_Noi_CVXG.xls.xlsx similarity index 100% rename from 2017/data-old/1_Ha_Noi_CVXG.xls.xlsx rename to data/2017-old/1_Ha_Noi_CVXG.xls.xlsx diff --git a/2017/data-old/1_Son_La_JIDP.xls.xlsx b/data/2017-old/1_Son_La_JIDP.xls.xlsx similarity index 100% rename from 2017/data-old/1_Son_La_JIDP.xls.xlsx rename to data/2017-old/1_Son_La_JIDP.xls.xlsx diff --git a/2017/data-old/1_TuyenQuang_JBYF.xls.xlsx b/data/2017-old/1_TuyenQuang_JBYF.xls.xlsx similarity index 100% rename from 2017/data-old/1_TuyenQuang_JBYF.xls.xlsx rename to data/2017-old/1_TuyenQuang_JBYF.xls.xlsx diff --git a/2017/data-old/20_TayNinh_ILFA.xls.xlsx b/data/2017-old/20_TayNinh_ILFA.xls.xlsx similarity index 100% rename from 2017/data-old/20_TayNinh_ILFA.xls.xlsx rename to data/2017-old/20_TayNinh_ILFA.xls.xlsx diff --git a/2017/data-old/21_ThaiBinh_FTVG.xls.xlsx b/data/2017-old/21_ThaiBinh_FTVG.xls.xlsx similarity index 100% rename from 2017/data-old/21_ThaiBinh_FTVG.xls.xlsx rename to data/2017-old/21_ThaiBinh_FTVG.xls.xlsx diff --git a/2017/data-old/22_ThaiNguyen_TLTW.xls.xlsx b/data/2017-old/22_ThaiNguyen_TLTW.xls.xlsx similarity index 100% rename from 2017/data-old/22_ThaiNguyen_TLTW.xls.xlsx rename to data/2017-old/22_ThaiNguyen_TLTW.xls.xlsx diff --git a/2017/data-old/23_HaiPhong_HXBV.xls.xlsx b/data/2017-old/23_HaiPhong_HXBV.xls.xlsx similarity index 100% rename from 2017/data-old/23_HaiPhong_HXBV.xls.xlsx rename to data/2017-old/23_HaiPhong_HXBV.xls.xlsx diff --git a/2017/data-old/24_HCM_XULN.xls.xlsx b/data/2017-old/24_HCM_XULN.xls.xlsx similarity index 100% rename from 2017/data-old/24_HCM_XULN.xls.xlsx rename to data/2017-old/24_HCM_XULN.xls.xlsx diff --git a/2017/data-old/2_BacKan_GFVQ.xls.xlsx b/data/2017-old/2_BacKan_GFVQ.xls.xlsx similarity index 100% rename from 2017/data-old/2_BacKan_GFVQ.xls.xlsx rename to data/2017-old/2_BacKan_GFVQ.xls.xlsx diff --git a/2017/data-old/2_Ha_Giang_QNCM.xls.xlsx b/data/2017-old/2_Ha_Giang_QNCM.xls.xlsx similarity index 100% rename from 2017/data-old/2_Ha_Giang_QNCM.xls.xlsx rename to data/2017-old/2_Ha_Giang_QNCM.xls.xlsx diff --git a/2017/data-old/2_Ninh_Thuan_VHLY.xls.xlsx b/data/2017-old/2_Ninh_Thuan_VHLY.xls.xlsx similarity index 100% rename from 2017/data-old/2_Ninh_Thuan_VHLY.xls.xlsx rename to data/2017-old/2_Ninh_Thuan_VHLY.xls.xlsx diff --git a/2017/data-old/2_Thanh_Hoa_AUFV.xls.xlsx b/data/2017-old/2_Thanh_Hoa_AUFV.xls.xlsx similarity index 100% rename from 2017/data-old/2_Thanh_Hoa_AUFV.xls.xlsx rename to data/2017-old/2_Thanh_Hoa_AUFV.xls.xlsx diff --git a/2017/data-old/2_VinhPhuc_GUDK.xls.xlsx b/data/2017-old/2_VinhPhuc_GUDK.xls.xlsx similarity index 100% rename from 2017/data-old/2_VinhPhuc_GUDK.xls.xlsx rename to data/2017-old/2_VinhPhuc_GUDK.xls.xlsx diff --git a/2017/data-old/3_BacGiang_TOIF.xls.xlsx b/data/2017-old/3_BacGiang_TOIF.xls.xlsx similarity index 100% rename from 2017/data-old/3_BacGiang_TOIF.xls.xlsx rename to data/2017-old/3_BacGiang_TOIF.xls.xlsx diff --git a/2017/data-old/3_BinhPhuoc_YFMU.xls.xlsx b/data/2017-old/3_BinhPhuoc_YFMU.xls.xlsx similarity index 100% rename from 2017/data-old/3_BinhPhuoc_YFMU.xls.xlsx rename to data/2017-old/3_BinhPhuoc_YFMU.xls.xlsx diff --git a/2017/data-old/3_Cao_Bang_CIEY.xls.xlsx b/data/2017-old/3_Cao_Bang_CIEY.xls.xlsx similarity index 100% rename from 2017/data-old/3_Cao_Bang_CIEY.xls.xlsx rename to data/2017-old/3_Cao_Bang_CIEY.xls.xlsx diff --git a/2017/data-old/3_Dong_Thap_GSXQ.xls.xlsx b/data/2017-old/3_Dong_Thap_GSXQ.xls.xlsx similarity index 100% rename from 2017/data-old/3_Dong_Thap_GSXQ.xls.xlsx rename to data/2017-old/3_Dong_Thap_GSXQ.xls.xlsx diff --git a/2017/data-old/3_Thua_Thien_Hue_HMDB.xls.xlsx b/data/2017-old/3_Thua_Thien_Hue_HMDB.xls.xlsx similarity index 100% rename from 2017/data-old/3_Thua_Thien_Hue_HMDB.xls.xlsx rename to data/2017-old/3_Thua_Thien_Hue_HMDB.xls.xlsx diff --git a/2017/data-old/4_An_Giang_JNOS.xls.xlsx b/data/2017-old/4_An_Giang_JNOS.xls.xlsx similarity index 100% rename from 2017/data-old/4_An_Giang_JNOS.xls.xlsx rename to data/2017-old/4_An_Giang_JNOS.xls.xlsx diff --git a/2017/data-old/4_BacNinh_STLR.xls.xlsx b/data/2017-old/4_BacNinh_STLR.xls.xlsx similarity index 100% rename from 2017/data-old/4_BacNinh_STLR.xls.xlsx rename to data/2017-old/4_BacNinh_STLR.xls.xlsx diff --git a/2017/data-old/4_Binh_Dinh_WWZW.xls.xlsx b/data/2017-old/4_Binh_Dinh_WWZW.xls.xlsx similarity index 100% rename from 2017/data-old/4_Binh_Dinh_WWZW.xls.xlsx rename to data/2017-old/4_Binh_Dinh_WWZW.xls.xlsx diff --git a/2017/data-old/4_DienBien_SOJG.xls.xlsx b/data/2017-old/4_DienBien_SOJG.xls.xlsx similarity index 100% rename from 2017/data-old/4_DienBien_SOJG.xls.xlsx rename to data/2017-old/4_DienBien_SOJG.xls.xlsx diff --git a/2017/data-old/4_Lang_Son_NYQL.xls.xlsx b/data/2017-old/4_Lang_Son_NYQL.xls.xlsx similarity index 100% rename from 2017/data-old/4_Lang_Son_NYQL.xls.xlsx rename to data/2017-old/4_Lang_Son_NYQL.xls.xlsx diff --git a/2017/data-old/5_Bac_Lieu_XEKH.xls.xlsx b/data/2017-old/5_Bac_Lieu_XEKH.xls.xlsx similarity index 100% rename from 2017/data-old/5_Bac_Lieu_XEKH.xls.xlsx rename to data/2017-old/5_Bac_Lieu_XEKH.xls.xlsx diff --git a/2017/data-old/5_Gia_Lai_ABZI.xls.xlsx b/data/2017-old/5_Gia_Lai_ABZI.xls.xlsx similarity index 100% rename from 2017/data-old/5_Gia_Lai_ABZI.xls.xlsx rename to data/2017-old/5_Gia_Lai_ABZI.xls.xlsx diff --git a/2017/data-old/5_HaiDuong_WWWG.xls.xlsx b/data/2017-old/5_HaiDuong_WWWG.xls.xlsx similarity index 100% rename from 2017/data-old/5_HaiDuong_WWWG.xls.xlsx rename to data/2017-old/5_HaiDuong_WWWG.xls.xlsx diff --git a/2017/data-old/5_Hanam_QGJS.xls.xlsx b/data/2017-old/5_Hanam_QGJS.xls.xlsx similarity index 100% rename from 2017/data-old/5_Hanam_QGJS.xls.xlsx rename to data/2017-old/5_Hanam_QGJS.xls.xlsx diff --git a/2017/data-old/5_Yen_Bai_FAQR.xls.xlsx b/data/2017-old/5_Yen_Bai_FAQR.xls.xlsx similarity index 100% rename from 2017/data-old/5_Yen_Bai_FAQR.xls.xlsx rename to data/2017-old/5_Yen_Bai_FAQR.xls.xlsx diff --git a/2017/data-old/6_Dong_Nai_WOTM.xls.xlsx b/data/2017-old/6_Dong_Nai_WOTM.xls.xlsx similarity index 100% rename from 2017/data-old/6_Dong_Nai_WOTM.xls.xlsx rename to data/2017-old/6_Dong_Nai_WOTM.xls.xlsx diff --git a/2017/data-old/6_Hau_Giang_KWDM.xls.xlsx b/data/2017-old/6_Hau_Giang_KWDM.xls.xlsx similarity index 100% rename from 2017/data-old/6_Hau_Giang_KWDM.xls.xlsx rename to data/2017-old/6_Hau_Giang_KWDM.xls.xlsx diff --git a/2017/data-old/6_HoaBinh_TPZY.xls.xlsx b/data/2017-old/6_HoaBinh_TPZY.xls.xlsx similarity index 100% rename from 2017/data-old/6_HoaBinh_TPZY.xls.xlsx rename to data/2017-old/6_HoaBinh_TPZY.xls.xlsx diff --git a/2017/data-old/6_NinhBinh_IGFT.xls.xlsx b/data/2017-old/6_NinhBinh_IGFT.xls.xlsx similarity index 100% rename from 2017/data-old/6_NinhBinh_IGFT.xls.xlsx rename to data/2017-old/6_NinhBinh_IGFT.xls.xlsx diff --git a/2017/data-old/6_Quang_Ninh_DQCJ.xls.xlsx b/data/2017-old/6_Quang_Ninh_DQCJ.xls.xlsx similarity index 100% rename from 2017/data-old/6_Quang_Ninh_DQCJ.xls.xlsx rename to data/2017-old/6_Quang_Ninh_DQCJ.xls.xlsx diff --git a/2017/data-old/7_HaTinh_DDHD.xls.xlsx b/data/2017-old/7_HaTinh_DDHD.xls.xlsx similarity index 100% rename from 2017/data-old/7_HaTinh_DDHD.xls.xlsx rename to data/2017-old/7_HaTinh_DDHD.xls.xlsx diff --git a/2017/data-old/7_HungYen_LTIK.xls.xlsx b/data/2017-old/7_HungYen_LTIK.xls.xlsx similarity index 100% rename from 2017/data-old/7_HungYen_LTIK.xls.xlsx rename to data/2017-old/7_HungYen_LTIK.xls.xlsx diff --git a/2017/data-old/7_Kon_Tum_RSLR.xls.xlsx b/data/2017-old/7_Kon_Tum_RSLR.xls.xlsx similarity index 100% rename from 2017/data-old/7_Kon_Tum_RSLR.xls.xlsx rename to data/2017-old/7_Kon_Tum_RSLR.xls.xlsx diff --git a/2017/data-old/7_Tien_Giang_EFHX.xls.xlsx b/data/2017-old/7_Tien_Giang_EFHX.xls.xlsx similarity index 100% rename from 2017/data-old/7_Tien_Giang_EFHX.xls.xlsx rename to data/2017-old/7_Tien_Giang_EFHX.xls.xlsx diff --git a/2017/data-old/8_Can_Tho_RQZM.xls.xlsx b/data/2017-old/8_Can_Tho_RQZM.xls.xlsx similarity index 100% rename from 2017/data-old/8_Can_Tho_RQZM.xls.xlsx rename to data/2017-old/8_Can_Tho_RQZM.xls.xlsx diff --git a/2017/data-old/8_Dak_Lak_YKPR.xls.xlsx b/data/2017-old/8_Dak_Lak_YKPR.xls.xlsx similarity index 100% rename from 2017/data-old/8_Dak_Lak_YKPR.xls.xlsx rename to data/2017-old/8_Dak_Lak_YKPR.xls.xlsx diff --git a/2017/data-old/8_KienGiang_OTOB.xls.xlsx b/data/2017-old/8_KienGiang_OTOB.xls.xlsx similarity index 100% rename from 2017/data-old/8_KienGiang_OTOB.xls.xlsx rename to data/2017-old/8_KienGiang_OTOB.xls.xlsx diff --git a/2017/data-old/8_PhuYen_OGIM.xls.xlsx b/data/2017-old/8_PhuYen_OGIM.xls.xlsx similarity index 100% rename from 2017/data-old/8_PhuYen_OGIM.xls.xlsx rename to data/2017-old/8_PhuYen_OGIM.xls.xlsx diff --git a/2017/data-old/9_DakNong_FBOP.xls.xlsx b/data/2017-old/9_DakNong_FBOP.xls.xlsx similarity index 100% rename from 2017/data-old/9_DakNong_FBOP.xls.xlsx rename to data/2017-old/9_DakNong_FBOP.xls.xlsx diff --git a/2017/data-old/9_Khanh_Hoa_KPKQ.xls.xlsx b/data/2017-old/9_Khanh_Hoa_KPKQ.xls.xlsx similarity index 100% rename from 2017/data-old/9_Khanh_Hoa_KPKQ.xls.xlsx rename to data/2017-old/9_Khanh_Hoa_KPKQ.xls.xlsx diff --git a/2017/data-old/9_LaiChau_ALKN.xls.xlsx b/data/2017-old/9_LaiChau_ALKN.xls.xlsx similarity index 100% rename from 2017/data-old/9_LaiChau_ALKN.xls.xlsx rename to data/2017-old/9_LaiChau_ALKN.xls.xlsx diff --git a/2017/data-old/9_Vinh_Long_OLWA.xls.xlsx b/data/2017-old/9_Vinh_Long_OLWA.xls.xlsx similarity index 100% rename from 2017/data-old/9_Vinh_Long_OLWA.xls.xlsx rename to data/2017-old/9_Vinh_Long_OLWA.xls.xlsx diff --git a/2017/data-old2/1.BaRia-VungTau_PGZT.xlsx b/data/2017-old2/1.BaRia-VungTau_PGZT.xlsx similarity index 100% rename from 2017/data-old2/1.BaRia-VungTau_PGZT.xlsx rename to data/2017-old2/1.BaRia-VungTau_PGZT.xlsx diff --git a/2017/data-old2/1.Da Nang_ABWU.xlsx b/data/2017-old2/1.Da Nang_ABWU.xlsx similarity index 100% rename from 2017/data-old2/1.Da Nang_ABWU.xlsx rename to data/2017-old2/1.Da Nang_ABWU.xlsx diff --git a/2017/data-old2/1.Son La_XLFN.xlsx b/data/2017-old2/1.Son La_XLFN.xlsx similarity index 100% rename from 2017/data-old2/1.Son La_XLFN.xlsx rename to data/2017-old2/1.Son La_XLFN.xlsx diff --git a/2017/data-old2/1.TuyenQuang_PTMR.xlsx b/data/2017-old2/1.TuyenQuang_PTMR.xlsx similarity index 100% rename from 2017/data-old2/1.TuyenQuang_PTMR.xlsx rename to data/2017-old2/1.TuyenQuang_PTMR.xlsx diff --git a/2017/data-old2/10.BinhThuan_MVVG.xlsx b/data/2017-old2/10.BinhThuan_MVVG.xlsx similarity index 100% rename from 2017/data-old2/10.BinhThuan_MVVG.xlsx rename to data/2017-old2/10.BinhThuan_MVVG.xlsx diff --git a/2017/data-old2/10.LamDong_YUQA.xlsx b/data/2017-old2/10.LamDong_YUQA.xlsx similarity index 100% rename from 2017/data-old2/10.LamDong_YUQA.xlsx rename to data/2017-old2/10.LamDong_YUQA.xlsx diff --git a/2017/data-old2/10.Soc Trang_LQWU.xlsx b/data/2017-old2/10.Soc Trang_LQWU.xlsx similarity index 100% rename from 2017/data-old2/10.Soc Trang_LQWU.xlsx rename to data/2017-old2/10.Soc Trang_LQWU.xlsx diff --git a/2017/data-old2/11.BinhDuong_HVAH.xlsx b/data/2017-old2/11.BinhDuong_HVAH.xlsx similarity index 100% rename from 2017/data-old2/11.BinhDuong_HVAH.xlsx rename to data/2017-old2/11.BinhDuong_HVAH.xlsx diff --git a/2017/data-old2/11.LaoCai_ZTBP.xlsx b/data/2017-old2/11.LaoCai_ZTBP.xlsx similarity index 100% rename from 2017/data-old2/11.LaoCai_ZTBP.xlsx rename to data/2017-old2/11.LaoCai_ZTBP.xlsx diff --git a/2017/data-old2/12.BenTre_NQTU.xlsx b/data/2017-old2/12.BenTre_NQTU.xlsx similarity index 100% rename from 2017/data-old2/12.BenTre_NQTU.xlsx rename to data/2017-old2/12.BenTre_NQTU.xlsx diff --git a/2017/data-old2/12.LongAn_PDRH.xlsx b/data/2017-old2/12.LongAn_PDRH.xlsx similarity index 100% rename from 2017/data-old2/12.LongAn_PDRH.xlsx rename to data/2017-old2/12.LongAn_PDRH.xlsx diff --git a/2017/data-old2/13.NamDinh_NAYR.xlsx b/data/2017-old2/13.NamDinh_NAYR.xlsx similarity index 100% rename from 2017/data-old2/13.NamDinh_NAYR.xlsx rename to data/2017-old2/13.NamDinh_NAYR.xlsx diff --git a/2017/data-old2/13.TraVinh_FODZ.xlsx b/data/2017-old2/13.TraVinh_FODZ.xlsx similarity index 100% rename from 2017/data-old2/13.TraVinh_FODZ.xlsx rename to data/2017-old2/13.TraVinh_FODZ.xlsx diff --git a/2017/data-old2/14.NgheAn_HTKD.xlsx b/data/2017-old2/14.NgheAn_HTKD.xlsx similarity index 100% rename from 2017/data-old2/14.NgheAn_HTKD.xlsx rename to data/2017-old2/14.NgheAn_HTKD.xlsx diff --git a/2017/data-old2/15.PhuTho_IJZW.xlsx b/data/2017-old2/15.PhuTho_IJZW.xlsx similarity index 100% rename from 2017/data-old2/15.PhuTho_IJZW.xlsx rename to data/2017-old2/15.PhuTho_IJZW.xlsx diff --git a/2017/data-old2/17.QuangNam_NQMG.xlsx b/data/2017-old2/17.QuangNam_NQMG.xlsx similarity index 100% rename from 2017/data-old2/17.QuangNam_NQMG.xlsx rename to data/2017-old2/17.QuangNam_NQMG.xlsx diff --git a/2017/data-old2/18.QuangNgai_IUPY.xlsx b/data/2017-old2/18.QuangNgai_IUPY.xlsx similarity index 100% rename from 2017/data-old2/18.QuangNgai_IUPY.xlsx rename to data/2017-old2/18.QuangNgai_IUPY.xlsx diff --git a/2017/data-old2/19.QuangTri_MKNN.xlsx b/data/2017-old2/19.QuangTri_MKNN.xlsx similarity index 100% rename from 2017/data-old2/19.QuangTri_MKNN.xlsx rename to data/2017-old2/19.QuangTri_MKNN.xlsx diff --git a/2017/data-old2/2.BacKan_YQNX.xlsx b/data/2017-old2/2.BacKan_YQNX.xlsx similarity index 100% rename from 2017/data-old2/2.BacKan_YQNX.xlsx rename to data/2017-old2/2.BacKan_YQNX.xlsx diff --git a/2017/data-old2/2.Ha Giang_PIYK.xlsx b/data/2017-old2/2.Ha Giang_PIYK.xlsx similarity index 100% rename from 2017/data-old2/2.Ha Giang_PIYK.xlsx rename to data/2017-old2/2.Ha Giang_PIYK.xlsx diff --git a/2017/data-old2/2.Ninh Thuan_BAGG.xlsx b/data/2017-old2/2.Ninh Thuan_BAGG.xlsx similarity index 100% rename from 2017/data-old2/2.Ninh Thuan_BAGG.xlsx rename to data/2017-old2/2.Ninh Thuan_BAGG.xlsx diff --git a/2017/data-old2/2.Thanh Hoa_UOPE.xlsx b/data/2017-old2/2.Thanh Hoa_UOPE.xlsx similarity index 100% rename from 2017/data-old2/2.Thanh Hoa_UOPE.xlsx rename to data/2017-old2/2.Thanh Hoa_UOPE.xlsx diff --git a/2017/data-old2/2.VinhPhuc_QZJK.xlsx b/data/2017-old2/2.VinhPhuc_QZJK.xlsx similarity index 100% rename from 2017/data-old2/2.VinhPhuc_QZJK.xlsx rename to data/2017-old2/2.VinhPhuc_QZJK.xlsx diff --git a/2017/data-old2/20.TayNinh_KJAQ.xlsx b/data/2017-old2/20.TayNinh_KJAQ.xlsx similarity index 100% rename from 2017/data-old2/20.TayNinh_KJAQ.xlsx rename to data/2017-old2/20.TayNinh_KJAQ.xlsx diff --git a/2017/data-old2/21.ThaiBinh_KTQN.xlsx b/data/2017-old2/21.ThaiBinh_KTQN.xlsx similarity index 100% rename from 2017/data-old2/21.ThaiBinh_KTQN.xlsx rename to data/2017-old2/21.ThaiBinh_KTQN.xlsx diff --git a/2017/data-old2/22.ThaiNguyen_BKIF.xlsx b/data/2017-old2/22.ThaiNguyen_BKIF.xlsx similarity index 100% rename from 2017/data-old2/22.ThaiNguyen_BKIF.xlsx rename to data/2017-old2/22.ThaiNguyen_BKIF.xlsx diff --git a/2017/data-old2/24.HCM_UTLQ.xlsx b/data/2017-old2/24.HCM_UTLQ.xlsx similarity index 100% rename from 2017/data-old2/24.HCM_UTLQ.xlsx rename to data/2017-old2/24.HCM_UTLQ.xlsx diff --git a/2017/data-old2/3.BacGiang_SAVS.xlsx b/data/2017-old2/3.BacGiang_SAVS.xlsx similarity index 100% rename from 2017/data-old2/3.BacGiang_SAVS.xlsx rename to data/2017-old2/3.BacGiang_SAVS.xlsx diff --git a/2017/data-old2/3.BinhPhuoc_IPHL.xlsx b/data/2017-old2/3.BinhPhuoc_IPHL.xlsx similarity index 100% rename from 2017/data-old2/3.BinhPhuoc_IPHL.xlsx rename to data/2017-old2/3.BinhPhuoc_IPHL.xlsx diff --git a/2017/data-old2/3.Cao Bang_WMUU.xlsx b/data/2017-old2/3.Cao Bang_WMUU.xlsx similarity index 100% rename from 2017/data-old2/3.Cao Bang_WMUU.xlsx rename to data/2017-old2/3.Cao Bang_WMUU.xlsx diff --git a/2017/data-old2/3.Dong Thap_HKJX.xlsx b/data/2017-old2/3.Dong Thap_HKJX.xlsx similarity index 100% rename from 2017/data-old2/3.Dong Thap_HKJX.xlsx rename to data/2017-old2/3.Dong Thap_HKJX.xlsx diff --git a/2017/data-old2/3.Thua Thien -Hue_MAET.xlsx b/data/2017-old2/3.Thua Thien -Hue_MAET.xlsx similarity index 100% rename from 2017/data-old2/3.Thua Thien -Hue_MAET.xlsx rename to data/2017-old2/3.Thua Thien -Hue_MAET.xlsx diff --git a/2017/data-old2/4.An Giang_PMJD.xlsx b/data/2017-old2/4.An Giang_PMJD.xlsx similarity index 100% rename from 2017/data-old2/4.An Giang_PMJD.xlsx rename to data/2017-old2/4.An Giang_PMJD.xlsx diff --git a/2017/data-old2/4.BacNinh_NNIS.xlsx b/data/2017-old2/4.BacNinh_NNIS.xlsx similarity index 100% rename from 2017/data-old2/4.BacNinh_NNIS.xlsx rename to data/2017-old2/4.BacNinh_NNIS.xlsx diff --git a/2017/data-old2/4.Binh Dinh_VOMJ.xlsx b/data/2017-old2/4.Binh Dinh_VOMJ.xlsx similarity index 100% rename from 2017/data-old2/4.Binh Dinh_VOMJ.xlsx rename to data/2017-old2/4.Binh Dinh_VOMJ.xlsx diff --git a/2017/data-old2/4.DienBien_FYGN.xlsx b/data/2017-old2/4.DienBien_FYGN.xlsx similarity index 100% rename from 2017/data-old2/4.DienBien_FYGN.xlsx rename to data/2017-old2/4.DienBien_FYGN.xlsx diff --git a/2017/data-old2/4.Lang Son_QWOG.xlsx b/data/2017-old2/4.Lang Son_QWOG.xlsx similarity index 100% rename from 2017/data-old2/4.Lang Son_QWOG.xlsx rename to data/2017-old2/4.Lang Son_QWOG.xlsx diff --git a/2017/data-old2/5.Bac Lieu_VIVY.xlsx b/data/2017-old2/5.Bac Lieu_VIVY.xlsx similarity index 100% rename from 2017/data-old2/5.Bac Lieu_VIVY.xlsx rename to data/2017-old2/5.Bac Lieu_VIVY.xlsx diff --git a/2017/data-old2/5.Gia Lai_TAAS.xlsx b/data/2017-old2/5.Gia Lai_TAAS.xlsx similarity index 100% rename from 2017/data-old2/5.Gia Lai_TAAS.xlsx rename to data/2017-old2/5.Gia Lai_TAAS.xlsx diff --git a/2017/data-old2/5.HaiDuong_WNHD.xlsx b/data/2017-old2/5.HaiDuong_WNHD.xlsx similarity index 100% rename from 2017/data-old2/5.HaiDuong_WNHD.xlsx rename to data/2017-old2/5.HaiDuong_WNHD.xlsx diff --git a/2017/data-old2/5.Hanam_SDKN.xlsx b/data/2017-old2/5.Hanam_SDKN.xlsx similarity index 100% rename from 2017/data-old2/5.Hanam_SDKN.xlsx rename to data/2017-old2/5.Hanam_SDKN.xlsx diff --git a/2017/data-old2/5.Yen Bai_BSLV.xlsx b/data/2017-old2/5.Yen Bai_BSLV.xlsx similarity index 100% rename from 2017/data-old2/5.Yen Bai_BSLV.xlsx rename to data/2017-old2/5.Yen Bai_BSLV.xlsx diff --git a/2017/data-old2/6.Hau Giang_SIAJ.xlsx b/data/2017-old2/6.Hau Giang_SIAJ.xlsx similarity index 100% rename from 2017/data-old2/6.Hau Giang_SIAJ.xlsx rename to data/2017-old2/6.Hau Giang_SIAJ.xlsx diff --git a/2017/data-old2/6.HoaBinh_HLYQ.xlsx b/data/2017-old2/6.HoaBinh_HLYQ.xlsx similarity index 100% rename from 2017/data-old2/6.HoaBinh_HLYQ.xlsx rename to data/2017-old2/6.HoaBinh_HLYQ.xlsx diff --git a/2017/data-old2/6.NinhBinh_PKMQ.xlsx b/data/2017-old2/6.NinhBinh_PKMQ.xlsx similarity index 100% rename from 2017/data-old2/6.NinhBinh_PKMQ.xlsx rename to data/2017-old2/6.NinhBinh_PKMQ.xlsx diff --git a/2017/data-old2/6.Quang Ninh_YAKJ.xlsx b/data/2017-old2/6.Quang Ninh_YAKJ.xlsx similarity index 100% rename from 2017/data-old2/6.Quang Ninh_YAKJ.xlsx rename to data/2017-old2/6.Quang Ninh_YAKJ.xlsx diff --git a/2017/data-old2/7.HaTinh_XMFQ.xlsx b/data/2017-old2/7.HaTinh_XMFQ.xlsx similarity index 100% rename from 2017/data-old2/7.HaTinh_XMFQ.xlsx rename to data/2017-old2/7.HaTinh_XMFQ.xlsx diff --git a/2017/data-old2/7.HungYen_TCBE.xlsx b/data/2017-old2/7.HungYen_TCBE.xlsx similarity index 100% rename from 2017/data-old2/7.HungYen_TCBE.xlsx rename to data/2017-old2/7.HungYen_TCBE.xlsx diff --git a/2017/data-old2/7.Kon Tum_CAQU.xlsx b/data/2017-old2/7.Kon Tum_CAQU.xlsx similarity index 100% rename from 2017/data-old2/7.Kon Tum_CAQU.xlsx rename to data/2017-old2/7.Kon Tum_CAQU.xlsx diff --git a/2017/data-old2/7.Tien Giang_AOWZ.xlsx b/data/2017-old2/7.Tien Giang_AOWZ.xlsx similarity index 100% rename from 2017/data-old2/7.Tien Giang_AOWZ.xlsx rename to data/2017-old2/7.Tien Giang_AOWZ.xlsx diff --git a/2017/data-old2/8.PhuYen_MLTQ.xlsx b/data/2017-old2/8.PhuYen_MLTQ.xlsx similarity index 100% rename from 2017/data-old2/8.PhuYen_MLTQ.xlsx rename to data/2017-old2/8.PhuYen_MLTQ.xlsx diff --git a/2017/data-old2/9.DakNong_ZWJT.xlsx b/data/2017-old2/9.DakNong_ZWJT.xlsx similarity index 100% rename from 2017/data-old2/9.DakNong_ZWJT.xlsx rename to data/2017-old2/9.DakNong_ZWJT.xlsx diff --git a/2017/data-old2/9.Khanh Hoa_IBTX.xlsx b/data/2017-old2/9.Khanh Hoa_IBTX.xlsx similarity index 100% rename from 2017/data-old2/9.Khanh Hoa_IBTX.xlsx rename to data/2017-old2/9.Khanh Hoa_IBTX.xlsx diff --git a/2017/data-old2/9.Vinh Long_WVKI.xlsx b/data/2017-old2/9.Vinh Long_WVKI.xlsx similarity index 100% rename from 2017/data-old2/9.Vinh Long_WVKI.xlsx rename to data/2017-old2/9.Vinh Long_WVKI.xlsx diff --git a/2017/data/an-giang.xls b/data/2017/an-giang.xls similarity index 100% rename from 2017/data/an-giang.xls rename to data/2017/an-giang.xls diff --git a/2017/data/ba-ria-vung-tau.xls b/data/2017/ba-ria-vung-tau.xls similarity index 100% rename from 2017/data/ba-ria-vung-tau.xls rename to data/2017/ba-ria-vung-tau.xls diff --git a/2017/data/bac-giang.xls b/data/2017/bac-giang.xls similarity index 100% rename from 2017/data/bac-giang.xls rename to data/2017/bac-giang.xls diff --git a/2017/data/bac-kan.xls b/data/2017/bac-kan.xls similarity index 100% rename from 2017/data/bac-kan.xls rename to data/2017/bac-kan.xls diff --git a/2017/data/bac-lieu.xls b/data/2017/bac-lieu.xls similarity index 100% rename from 2017/data/bac-lieu.xls rename to data/2017/bac-lieu.xls diff --git a/2017/data/bac-ninh.xls b/data/2017/bac-ninh.xls similarity index 100% rename from 2017/data/bac-ninh.xls rename to data/2017/bac-ninh.xls diff --git a/2017/data/ben-tre.xls b/data/2017/ben-tre.xls similarity index 100% rename from 2017/data/ben-tre.xls rename to data/2017/ben-tre.xls diff --git a/2017/data/binh-dinh.xls b/data/2017/binh-dinh.xls similarity index 100% rename from 2017/data/binh-dinh.xls rename to data/2017/binh-dinh.xls diff --git a/2017/data/binh-duong.xls b/data/2017/binh-duong.xls similarity index 100% rename from 2017/data/binh-duong.xls rename to data/2017/binh-duong.xls diff --git a/2017/data/binh-phuoc.xls b/data/2017/binh-phuoc.xls similarity index 100% rename from 2017/data/binh-phuoc.xls rename to data/2017/binh-phuoc.xls diff --git a/2017/data/binh-thuan.xls b/data/2017/binh-thuan.xls similarity index 100% rename from 2017/data/binh-thuan.xls rename to data/2017/binh-thuan.xls diff --git a/2017/data/ca-mau.xls b/data/2017/ca-mau.xls similarity index 100% rename from 2017/data/ca-mau.xls rename to data/2017/ca-mau.xls diff --git a/2017/data/can-tho.xls b/data/2017/can-tho.xls similarity index 100% rename from 2017/data/can-tho.xls rename to data/2017/can-tho.xls diff --git a/2017/data/cao-bang.xls b/data/2017/cao-bang.xls similarity index 100% rename from 2017/data/cao-bang.xls rename to data/2017/cao-bang.xls diff --git a/2017/data/da-nang.xls b/data/2017/da-nang.xls similarity index 100% rename from 2017/data/da-nang.xls rename to data/2017/da-nang.xls diff --git a/2017/data/dak-lak.xls b/data/2017/dak-lak.xls similarity index 100% rename from 2017/data/dak-lak.xls rename to data/2017/dak-lak.xls diff --git a/2017/data/dak-nong.xls b/data/2017/dak-nong.xls similarity index 100% rename from 2017/data/dak-nong.xls rename to data/2017/dak-nong.xls diff --git a/2017/data/dien-bien.xls b/data/2017/dien-bien.xls similarity index 100% rename from 2017/data/dien-bien.xls rename to data/2017/dien-bien.xls diff --git a/2017/data/dong-nai.xls b/data/2017/dong-nai.xls similarity index 100% rename from 2017/data/dong-nai.xls rename to data/2017/dong-nai.xls diff --git a/2017/data/dong-thap.xls b/data/2017/dong-thap.xls similarity index 100% rename from 2017/data/dong-thap.xls rename to data/2017/dong-thap.xls diff --git a/2017/data/gia-lai.xls b/data/2017/gia-lai.xls similarity index 100% rename from 2017/data/gia-lai.xls rename to data/2017/gia-lai.xls diff --git a/2017/data/ha-giang.xls b/data/2017/ha-giang.xls similarity index 100% rename from 2017/data/ha-giang.xls rename to data/2017/ha-giang.xls diff --git a/2017/data/ha-nam.xls b/data/2017/ha-nam.xls similarity index 100% rename from 2017/data/ha-nam.xls rename to data/2017/ha-nam.xls diff --git a/2017/data/ha-noi.xls b/data/2017/ha-noi.xls similarity index 100% rename from 2017/data/ha-noi.xls rename to data/2017/ha-noi.xls diff --git a/2017/data/ha-tinh.xls b/data/2017/ha-tinh.xls similarity index 100% rename from 2017/data/ha-tinh.xls rename to data/2017/ha-tinh.xls diff --git a/2017/data/hai-duong.xls b/data/2017/hai-duong.xls similarity index 100% rename from 2017/data/hai-duong.xls rename to data/2017/hai-duong.xls diff --git a/2017/data/hai-phong.xls b/data/2017/hai-phong.xls similarity index 100% rename from 2017/data/hai-phong.xls rename to data/2017/hai-phong.xls diff --git a/2017/data/hau-giang.xls b/data/2017/hau-giang.xls similarity index 100% rename from 2017/data/hau-giang.xls rename to data/2017/hau-giang.xls diff --git a/2017/data/ho-chi-minh.xls b/data/2017/ho-chi-minh.xls similarity index 100% rename from 2017/data/ho-chi-minh.xls rename to data/2017/ho-chi-minh.xls diff --git a/2017/data/hoa-binh.xls b/data/2017/hoa-binh.xls similarity index 100% rename from 2017/data/hoa-binh.xls rename to data/2017/hoa-binh.xls diff --git a/2017/data/hung-yen.xls b/data/2017/hung-yen.xls similarity index 100% rename from 2017/data/hung-yen.xls rename to data/2017/hung-yen.xls diff --git a/2017/data/khanh-hoa.xls b/data/2017/khanh-hoa.xls similarity index 100% rename from 2017/data/khanh-hoa.xls rename to data/2017/khanh-hoa.xls diff --git a/2017/data/kien-giang.xls b/data/2017/kien-giang.xls similarity index 100% rename from 2017/data/kien-giang.xls rename to data/2017/kien-giang.xls diff --git a/2017/data/kon-tum.xls b/data/2017/kon-tum.xls similarity index 100% rename from 2017/data/kon-tum.xls rename to data/2017/kon-tum.xls diff --git a/2017/data/lai-chau.xls b/data/2017/lai-chau.xls similarity index 100% rename from 2017/data/lai-chau.xls rename to data/2017/lai-chau.xls diff --git a/2017/data/lam-dong.xls b/data/2017/lam-dong.xls similarity index 100% rename from 2017/data/lam-dong.xls rename to data/2017/lam-dong.xls diff --git a/2017/data/lang-son.xls b/data/2017/lang-son.xls similarity index 100% rename from 2017/data/lang-son.xls rename to data/2017/lang-son.xls diff --git a/2017/data/lao-cai.xls b/data/2017/lao-cai.xls similarity index 100% rename from 2017/data/lao-cai.xls rename to data/2017/lao-cai.xls diff --git a/2017/data/long-an.xls b/data/2017/long-an.xls similarity index 100% rename from 2017/data/long-an.xls rename to data/2017/long-an.xls diff --git a/2017/data/nam-dinh.xls b/data/2017/nam-dinh.xls similarity index 100% rename from 2017/data/nam-dinh.xls rename to data/2017/nam-dinh.xls diff --git a/2017/data/nghe-an.xls b/data/2017/nghe-an.xls similarity index 100% rename from 2017/data/nghe-an.xls rename to data/2017/nghe-an.xls diff --git a/2017/data/ninh-binh.xls b/data/2017/ninh-binh.xls similarity index 100% rename from 2017/data/ninh-binh.xls rename to data/2017/ninh-binh.xls diff --git a/2017/data/ninh-thuan.xls b/data/2017/ninh-thuan.xls similarity index 100% rename from 2017/data/ninh-thuan.xls rename to data/2017/ninh-thuan.xls diff --git a/2017/data/phu-tho.xls b/data/2017/phu-tho.xls similarity index 100% rename from 2017/data/phu-tho.xls rename to data/2017/phu-tho.xls diff --git a/2017/data/phu-yen.xls b/data/2017/phu-yen.xls similarity index 100% rename from 2017/data/phu-yen.xls rename to data/2017/phu-yen.xls diff --git a/2017/data/quang-binh.xls b/data/2017/quang-binh.xls similarity index 100% rename from 2017/data/quang-binh.xls rename to data/2017/quang-binh.xls diff --git a/2017/data/quang-nam.xls b/data/2017/quang-nam.xls similarity index 100% rename from 2017/data/quang-nam.xls rename to data/2017/quang-nam.xls diff --git a/2017/data/quang-ngai.xls b/data/2017/quang-ngai.xls similarity index 100% rename from 2017/data/quang-ngai.xls rename to data/2017/quang-ngai.xls diff --git a/2017/data/quang-ninh.xls b/data/2017/quang-ninh.xls similarity index 100% rename from 2017/data/quang-ninh.xls rename to data/2017/quang-ninh.xls diff --git a/2017/data/quang-tri.xls b/data/2017/quang-tri.xls similarity index 100% rename from 2017/data/quang-tri.xls rename to data/2017/quang-tri.xls diff --git a/2017/data/soc-trang.xls b/data/2017/soc-trang.xls similarity index 100% rename from 2017/data/soc-trang.xls rename to data/2017/soc-trang.xls diff --git a/2017/data/son-la.xls b/data/2017/son-la.xls similarity index 100% rename from 2017/data/son-la.xls rename to data/2017/son-la.xls diff --git a/2017/data/tay-ninh.xls b/data/2017/tay-ninh.xls similarity index 100% rename from 2017/data/tay-ninh.xls rename to data/2017/tay-ninh.xls diff --git a/2017/data/thai-binh.xls b/data/2017/thai-binh.xls similarity index 100% rename from 2017/data/thai-binh.xls rename to data/2017/thai-binh.xls diff --git a/2017/data/thai-nguyen.xls b/data/2017/thai-nguyen.xls similarity index 100% rename from 2017/data/thai-nguyen.xls rename to data/2017/thai-nguyen.xls diff --git a/2017/data/thanh-hoa.xls b/data/2017/thanh-hoa.xls similarity index 100% rename from 2017/data/thanh-hoa.xls rename to data/2017/thanh-hoa.xls diff --git a/2017/data/thua-thien-hue.xls b/data/2017/thua-thien-hue.xls similarity index 100% rename from 2017/data/thua-thien-hue.xls rename to data/2017/thua-thien-hue.xls diff --git a/2017/data/tien-giang.xls b/data/2017/tien-giang.xls similarity index 100% rename from 2017/data/tien-giang.xls rename to data/2017/tien-giang.xls diff --git a/2017/data/tra-vinh.xls b/data/2017/tra-vinh.xls similarity index 100% rename from 2017/data/tra-vinh.xls rename to data/2017/tra-vinh.xls diff --git a/2017/data/tuyen-quang.xls b/data/2017/tuyen-quang.xls similarity index 100% rename from 2017/data/tuyen-quang.xls rename to data/2017/tuyen-quang.xls diff --git a/2017/data/vinh-long.xls b/data/2017/vinh-long.xls similarity index 100% rename from 2017/data/vinh-long.xls rename to data/2017/vinh-long.xls diff --git a/2017/data/vinh-phuc.xls b/data/2017/vinh-phuc.xls similarity index 100% rename from 2017/data/vinh-phuc.xls rename to data/2017/vinh-phuc.xls diff --git a/2017/data/yen-bai.xls b/data/2017/yen-bai.xls similarity index 100% rename from 2017/data/yen-bai.xls rename to data/2017/yen-bai.xls diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..b2ad89a --- /dev/null +++ b/docs/README.md @@ -0,0 +1,6 @@ +# Docs + +- [`project-overview.md`](./project-overview.md) — goal, scope, constraints, the four datasets, history +- [`system-architecture.md`](./system-architecture.md) — data flow, canonical schema, routing, how one frontend serves both exam years +- [`data-pipeline.md`](./data-pipeline.md) — Excel parse quirks, per-dataset formats, overflow-sheet gotcha, expected row counts, verifying a rebuild +- [`deployment-guide.md`](./deployment-guide.md) — GitHub Pages workflow, adding a dataset, rollback, troubleshooting diff --git a/docs/data-pipeline.md b/docs/data-pipeline.md new file mode 100644 index 0000000..aa8d477 --- /dev/null +++ b/docs/data-pipeline.md @@ -0,0 +1,149 @@ +# Data Pipeline + +From raw Excel files to a compressed SQLite file the browser can load. + +One Rust binary (`parser/`) builds every dataset. What differs per dataset is +parse rules only — sheet strategy, column layout, validation guards — declared +in `parser/configs/.toml`. The table shape, the INSERT and the subject +regexes are canonical and live in `parser/src/schema.rs`. + +## Sources + +| id | Files | Reproducible | Origin | +| --- | --- | --- | --- | +| `2016` | 4 `.xls` + 115 `.xlsx` | no | Bộ GD&ĐT, collected 2016 | +| `2017` | 63 `.xls` | **yes** | baotintuc.vn CDN | +| `2017-old` | 63 `.xlsx` | no | pre-refresh archive | +| `2017-old2` | 54 `.xlsx` | no | corrected re-export | + +Only `2017` can be re-fetched: + +```bash +node parser/scripts/crawl-baotintuc.js +``` + +Idempotent — skips files already present, saves to `data/2017/`. Source article: +`https://baotintuc.vn/tuyen-sinh/tra-cuu-diem-thi-thpt-2017-cua-63-tinh-thanh-pho-tren-baotintucvn-20170706073512672.htm` + +The other three were collected from Vietnamese news sites at the time and the +publisher URLs were not recorded. **The files committed in git are the only +copy.** + +## Source Excel shapes + +The 2017 datasets share one layout: + +| Col | Name | Content | +| --- | --- | --- | +| 0 | HO_TEN | full name in Vietnamese | +| 1 | NGAY_SINH | `dd/mm/yyyy` | +| 2 | SOBAODANH | 8-digit string, first 2 digits = province | +| 3 | DIEM_THI | concatenated per-subject scores, e.g. `"Toán: 6.80 Ngữ văn: 5.25 …"` | + +2016 has **three** layouts across its 119 files, chosen per file at runtime via +`format_detection = "thptqg2016"` in its config: + +| Format | Detected by | Notes | +| --- | --- | --- | +| `separate-scores` | `row[0] == "SBD"` and `row[2] == "TOAN"` | one column per subject; scores read directly, no regex | +| `mapped` | header contains `SOBAODANH`/`SBD` plus `DIEM_THI` | column indices derived from the header | +| `default` | no recognised header | positional 6-column layout | + +The `mapped` and `default` layouts also carry `TEN_CUMTHI` and `GIOI_TINH`, +which is why only 2016 populates those columns. + +## Score text parsing + +`SCORE_PATTERNS` in `parser/src/schema.rs` defines one regex per subject, and +**all 16 run against every dataset**. A subject a given exam year did not offer +simply never matches and stays NULL. + +| Column | Source label | English | 2016 | 2017 | +| --- | --- | --- | --- | --- | +| toan | Toán | Math | ✓ | ✓ | +| ngu_van | Ngữ văn | Literature | ✓ | ✓ | +| vat_ly | Vật lí | Physics | ✓ | ✓ | +| hoa_hoc | Hóa học | Chemistry | ✓ | ✓ | +| sinh_hoc | Sinh học | Biology | ✓ | ✓ | +| khtn | KHTN | Natural Sciences combined | — | ✓ | +| lich_su | Lịch sử | History | ✓ | ✓ | +| dia_ly | Địa lí | Geography | ✓ | ✓ | +| gdcd | GDCD | Civic Education | — | ✓ | +| khxh | KHXH | Social Sciences combined | — | ✓ | +| tieng_anh | Tiếng Anh | English | ✓ | ✓ | +| tieng_phap | Tiếng Pháp | French | ✓ | ✓ | +| tieng_nga | Tiếng Nga | Russian | ✓ | ✓ | +| tieng_duc | Tiếng Đức | German | ✓ | ✓ | +| tieng_nhat | Tiếng Nhật | Japanese | ✓ | ✓ | +| tieng_trung | Tiếng Trung | Chinese | ✓ | ✓ | + +### The foreign-language recovery + +Before the parser was unified, the 2016 config listed 12 subject patterns and +the 2017 configs listed 14. Neither list was complete: candidates could sit +German, Japanese and Russian in both years, so 1,691 students ended up with **no +foreign-language score at all**. + +Running all 16 patterns everywhere recovered them — 182 Russian in 2016, and +German and Japanese across all three 2017 generations. See +`plans/reports/parser-parity-result.md` for the evidence that these are real +scores rather than false matches. + +## Per-dataset quirks + +| id | Sheet strategy | Guards | +| --- | --- | --- | +| `2016` | all sheets | per-file format detection; header-token rows rejected | +| `2017` | all sheets — Hà Nội and HCM overflow | none | +| `2017-old` | first sheet only | reject non-numeric SBD (rejects a header leak in this export) | +| `2017-old2` | all sheets — HCM overflows | reject non-numeric SBD; skip blank rows before counting | + +### Overflow-sheet gotcha + +The old `.xls` format caps at 65,536 rows per sheet. Hanoi and Ho Chi Minh City +exceed that, so Excel splits the overflow into `Sheet2`. **Reading only `Sheet1` +silently drops 13,720 students** (Hanoi +7,275, HCM +6,445). That is what +`sheet_mode = "all"` exists for. + +## Expected row counts + +| id | Source rows | Skipped | DB rows | +| --- | --- | --- | --- | +| `2016` | 877,464 | 3 duplicate SBDs collapsed | **877,461** | +| `2017` | 861,131 | 63 empty | **861,068** | +| `2017-old` | 847,349 | 1 header leak | **847,348** | +| `2017-old2` | 679,764 | 0 | **679,764** | + +## Verifying a rebuild + +`parser/scripts/db-stats.js` dumps row counts, per-column non-NULL counts, file +size and a deterministic student sample as JSON. +`parser/scripts/verify-parity.js` diffs two such files and exits non-zero on any +mismatch. + +```bash +node parser/scripts/db-stats.js 2016=.db … > current.json +node parser/scripts/verify-parity.js plans/reports/parser-parity-baseline.json current.json +``` + +The committed baseline was built with the pre-refactor code and cannot be +regenerated — the two old crates no longer exist. Both scripts use the built-in +`node:sqlite`, so they need no dependencies. + +## Refreshing the 2017 data + +```bash +rm data/2017/*.xls +node parser/scripts/crawl-baotintuc.js +node parser/scripts/build-db.js 2017 +``` + +Then re-run the parity check above and confirm the row count still matches. + +## Legacy scripts + +`parser/scripts/check-duplicates.js` and `diff-datasets.js` are one-off audits +that were already broken before the repo was unified — a hardcoded Windows path +in one, an undeclared `better-sqlite3` dependency and stale paths in the other. +Each carries a comment saying so. For comparing two builds, use `db-stats.js` +plus `verify-parity.js` instead. diff --git a/docs/deployment-guide.md b/docs/deployment-guide.md new file mode 100644 index 0000000..f5b29b8 --- /dev/null +++ b/docs/deployment-guide.md @@ -0,0 +1,105 @@ +# Deployment Guide + +Deploys to GitHub Pages via `.github/workflows/deploy-pages.yml`. Every push to +`main` rebuilds all four datasets and redeploys the whole site. + +One-time setup: **Settings → Pages → Source: GitHub Actions**. + +## What the workflow does + +1. Checkout, Rust toolchain, Node 24, `npm ci` +2. `npm run build:rust` — one parser binary +3. `npm run build:db` — builds and gzips all four databases into + `.build/public/db/` +4. `npm run build:site` — one Vite build, then `scripts/assemble-site.js` +5. `actions/upload-pages-artifact` + `actions/deploy-pages` + +The database build dominates the runtime: roughly 419 MB of Excel is parsed on +every deploy. + +## Resulting URLs + +``` +https://.github.io/thptqg/ +https://.github.io/thptqg/2016/ +https://.github.io/thptqg/2017/ +https://.github.io/thptqg/2017-old/ +https://.github.io/thptqg/2017-old2/ +``` + +`/thptqg/2017/old/` and `/thptqg/2017/old2/` were the pre-flattening URLs. They +are still served, and the router rewrites them to the flat form with the query +string intact. + +## Local reproduction + +```bash +npm ci +npm run build:rust +npm run build:db # all four; pass an id to build just one +npm run build:site # vite build + assemble into _site/ +npx serve _site +``` + +To rebuild a single dataset: + +```bash +node parser/scripts/build-db.js 2017-old +``` + +## Base path + +`vite.config.js` sets `base: "/thptqg/"`. If you fork under a different repo +name, update it to match — assets are referenced absolutely, so a mismatch shows +up as a blank page with 404s on `/assets/...`. + +## Adding a dataset + +1. Put the Excel files in `data//` +2. Add `parser/configs/.toml` with the parse rules — sheet mode, column + indices, SBD validation, header tokens, blank-row stripping. No SQL: the + schema is canonical and lives in `parser/src/schema.rs` +3. Add an entry to `DATASETS` in `src/datasets.js` + +Nothing else. The build script, the site assembly and the router all read that +one list, and the frontend adapts to whichever columns the dataset populates. + +## Why no uncompressed database can ship + +`build-db.js` runs `gzip -9` **without** `-k`, so the raw file does not survive +the build. `assemble-site.js` then fails the job if any `.db`, `.db-journal`, +`.db-wal` or `.db-shm` reached the output. + +Both guards exist because the previous pipeline wrote a 100+ MB uncompressed +database into the source tree and relied on an `rm` step to keep it out of the +artifact — one missing line away from publishing it. + +## Notes + +- **The gzipped database is not cacheable across deploys.** Every rebuild + produces a different `.db.gz`, because SQLite does not lay pages out + deterministically. First-visit users pay the full download; later visits hit + browser cache until the next deploy. +- **GitHub Pages caps individual files at 100 MB.** The largest gzipped database + is about 48 MB. Uncompressed they run 135–234 MB and would not fit — which is + why the browser decompresses via `DecompressionStream`. +- **No server-side compression is assumed.** The app fetches the `.gz` bytes + directly rather than relying on `Content-Encoding: gzip`; Pages does not + reliably compress arbitrary paths on the fly. +- **Total artifact is about 177 MB**, well inside the 1 GB site limit. + +## Rollback + +Deploys are stateless snapshots. Revert the commit on `main` and push; the next +run rebuilds the older state. There is no data to migrate. + +## Troubleshooting + +| Symptom | Typical cause | +| --- | --- | +| Blank page, 404 on assets | `base` in `vite.config.js` does not match the repo name | +| `Failed to fetch database: 404` | Dataset id in `src/datasets.js` does not match the file in `db/` | +| A route 404s | `assemble-site.js` did not run, or the id is missing from `DATASETS` | +| WASM fails to load | `sql.js.org` unreachable — self-host `sql-wasm.wasm` and update `SQL_WASM_URL` in `use-sqlite.js` | +| Deploy fails on assembly | An uncompressed database artefact reached the output; the error names the files | +| Missing rows after a data update | Unknown Excel header — check the per-file row counts the parser prints | diff --git a/docs/project-overview.md b/docs/project-overview.md new file mode 100644 index 0000000..af8dfdd --- /dev/null +++ b/docs/project-overview.md @@ -0,0 +1,66 @@ +# Project Overview + +## Goal + +A public lookup tool for Vietnam's National High School Graduation Exam scores, +running entirely in the browser and hosted for free on GitHub Pages. Covers the +2016 and 2017 exams — 1.7 million candidates across four datasets. + +## Scope + +- Lookup by exam ID or full name, with Vietnamese diacritics handled +- Read-only SQL queries against a single `student` table +- Admission-block (khối thi) totals computed per candidate +- Static datasets — both exams are long over and the data is frozen + +## Target users + +- Former candidates checking their scores +- Education researchers and data journalists running aggregate statistics +- Developers exploring SQL against a real-world dataset + +## Constraints + +- **Zero backend.** The full database (38–48 MB gzipped per dataset) is + downloaded to the browser and queried in-process. +- **Read-only.** `INSERT`/`UPDATE`/`DELETE` are rejected, so nobody is misled + into thinking edits persist. `sql.js` is in-memory anyway. +- **Row caps.** 100 rows for lookups, 1000 for custom SQL, to prevent browser + hangs. +- **Vietnamese-first UI.** App labels and data are Vietnamese; documentation is + English. + +## Datasets + +| id | Exam | Candidates | Notes | +| --- | --- | --- | --- | +| `2016` | 2016 | 877,461 | 119 files, three column layouts | +| `2017` | 2017 | 861,068 | current generation, reproducible from source | +| `2017-old` | 2017 | 847,348 | pre-refresh archive | +| `2017-old2` | 2017 | 679,764 | corrected re-export | + +The three 2017 datasets are kept side by side because they disagree, and the +disagreement is itself informative. Only `2017` is re-fetchable; the rest exist +solely as the copies committed here. + +Original 2016 aggregator link +() +is no longer accessible, which is why the raw files are mirrored in `data/2016/`. + +## History + +Each year began as a standalone repository (`thptqg2016`, `thptqg2017`), merged +here with full history. They initially kept separate frontends and separate +copies of the same Rust parser, synchronised by hand. That duplication was +removed: there is now one frontend, one parser, and one canonical schema, with +per-dataset differences confined to four small config files and one registry +entry each. + +The unification also fixed a latent data-loss bug — neither year's parser +config listed the complete set of subjects, so 1,691 candidates were missing +their foreign-language score. See `data-pipeline.md`. + +## Status + +Stable, data frozen. Work is limited to UX polish and keeping the pipeline +maintainable. diff --git a/docs/system-architecture.md b/docs/system-architecture.md new file mode 100644 index 0000000..7eb5cce --- /dev/null +++ b/docs/system-architecture.md @@ -0,0 +1,180 @@ +# System Architecture + +Static site, no backend. The browser downloads a compressed SQLite file at boot +and every query runs locally via `sql.js` (SQLite compiled to WebAssembly). + +One frontend, one parser, one schema, four datasets. + +## Data flow + +``` +data//*.xls(x) + │ + ▼ parser/ (Rust, one binary, one config per dataset) + .build/public/db/.db + │ + ▼ gzip -9 (no -k: the raw file does not survive) + .build/public/db/.db.gz + │ + ▼ vite build (publicDir = .build/public) + dist/ + │ + ▼ scripts/assemble-site.js + _site/ → GitHub Pages + │ + ▼ browser + sql.js (WASM) opens the .db → queries run client-side +``` + +## The dataset id + +One identifier ties the whole pipeline together: + +``` +data/2017-old/ → parser/configs/2017-old.toml → db/2017-old.db.gz → /thptqg/2017-old/ +``` + +`src/datasets.js` declares the four ids once. The frontend, the database build +(`parser/scripts/build-db.js`) and the site assembly all import that list, so +adding a dataset means adding one entry and one config file. + +| id | Exam | Rows | Source | +| --- | --- | --- | --- | +| `2016` | 2016 | 877,461 | Bộ GD&ĐT | +| `2017` | 2017 | 861,068 | baotintuc.vn | +| `2017-old` | 2017 | 847,348 | pre-refresh archive | +| `2017-old2` | 2017 | 679,764 | corrected re-export | + +## Canonical schema + +Defined once in `parser/src/schema.rs` — DDL, INSERT, column order and the 16 +subject regexes. The four TOML configs carry no SQL at all, only per-dataset +parse rules. Config parsing uses `deny_unknown_fields`, so a leftover `[schema]` +block fails loudly instead of looking effective while `schema.rs` drives the +build. + +```sql +CREATE TABLE student ( + so_bao_danh TEXT PRIMARY KEY, -- 2017: 8 digits; 2016: 9 digits or a + -- 2-4 letter cluster code then digits + ho_ten TEXT NOT NULL, + ho_ten_ascii TEXT NOT NULL, -- NFD-stripped lowercase, for accent-insensitive search + ngay_sinh TEXT, -- dd/mm/yyyy + ten_cum_thi TEXT, -- 2016 only + gioi_tinh TEXT, -- 2016 only + toan, ngu_van, vat_ly, hoa_hoc, sinh_hoc, khtn, + lich_su, dia_ly, gdcd, khxh, + tieng_anh, tieng_phap, tieng_nga, tieng_duc, tieng_nhat, tieng_trung REAL +); +CREATE INDEX idx_ho_ten ON student(ho_ten); +CREATE INDEX idx_ho_ten_ascii ON student(ho_ten_ascii); +CREATE INDEX idx_ten_cum_thi ON student(ten_cum_thi) WHERE ten_cum_thi IS NOT NULL; +``` + +Every dataset gets all 22 columns; ones it has no data for are NULL, costing +about a byte per row. `khtn`, `khxh` and `gdcd` are empty on 2016; +`ten_cum_thi` and `gioi_tinh` are empty on the 2017 datasets. + +`idx_ten_cum_thi` is partial, so it holds zero entries where the column is +always NULL. + +## Routing + +URLs are flat, one segment per dataset, and the segment is the id: + +``` +/thptqg/ hub +/thptqg/2016/ +/thptqg/2017/ +/thptqg/2017-old/ +/thptqg/2017-old2/ +``` + +`src/router.js` is an exact match on that segment. The nested form used before +(`/thptqg/2017/old/`) would have needed longest-prefix matching, since it also +starts with `/thptqg/2017/`. Those two legacy URLs still resolve: the router +rewrites them to the flat equivalent with `history.replaceState`, preserving the +query string so `?q=` deep links survive. + +A single Vite build emits one `index.html`. Because `base` is absolute +(`/thptqg/`), that file references `/thptqg/assets/...` regardless of the +directory it is served from, so the assemble step copies it to every route and +each URL is a real static file. **No SPA 404-fallback redirect is used** — the +usual hack rewrites URLs and would interfere with the deep links. + +## Serving both exam years without branching + +No component contains a per-dataset conditional. Two mechanisms do the work: + +- **All-NULL columns are hidden.** `score-table.jsx` drops any column where + every row in the result set is NULL, so 2016 rows surface Cụm thi / GT / Đức / + Nhật and 2017 rows surface KHTN / KHXH / GDCD / Nga. +- **Incomplete admission blocks are skipped.** `computeBlocks()` only returns a + block when the student has all three subjects, so one block list covers both + years: GDCD blocks self-exclude on 2016, German and Japanese blocks + self-exclude wherever those languages were not sat. + +Anything genuinely per-dataset — title, source, database size, search examples, +SQL presets — lives in `src/datasets.js`. + +## Exam ID formats + +`src/lib/query-mode.js` decides whether a query is an exam ID or a name, and is +shared by `App.jsx` and `search-form.jsx` (they previously held separate copies +and had drifted apart on exactly this rule). + +| Form | Example | Where | +| --- | --- | --- | +| 8 digits | `49008235` | 2017 — first two digits are the province | +| 9 digits with leading zero | `017006021` | 2016 | +| 2-4 letters then digits | `BAL000001` | 2016 — exam cluster code | + +The letter-prefixed form is the majority case for 2016: 616,593 of 877,461 +candidates (70.3%). Letter prefixes are upper-cased before lookup, so +`bal000001` resolves. + +## Score tiers + +Six-level ladder in `scoreTier()` (`src/lib/admission-blocks.js`), paired with a +symbol so meaning is never colour-only. + +| Tier | Range | Vietnamese | +| --- | --- | --- | +| common | ≤ 1 | Điểm liệt | +| uncommon | < 5 | Chưa đạt | +| rare | 5–6.5 | Trung bình | +| epic | 6.5–8 | Khá | +| legendary | 8–9 | Giỏi | +| prismatic | 9–10 | Xuất sắc | + +## Admission blocks + +Vietnamese universities admit on three-subject combinations (khối thi). +`src/lib/admission-blocks.js` lists the blocks computable from this schema +(A00–A11, B00–B08, C00–C20, D01–D15, plus D05/D06 for German and Japanese). +`computeBlocks(student)` returns those where all three scores exist, sorted by +total descending. + +## Design decisions + +| Concern | Choice | Rationale | +| --- | --- | --- | +| Storage | Static SQLite file | No backend; the datasets are frozen | +| Compression | gzip in CI, `DecompressionStream` in the browser | Native API, no extra library | +| WASM hosting | `sql.js.org` CDN | Smaller self-hosted artifact | +| Diacritics search | Pre-computed `ho_ten_ascii` | `LOWER(REPLACE(...))` at query time defeats the index | +| SQL safety | Leading-keyword allowlist | `sql.js` is in-memory so writes cannot persist; the allowlist prevents confusion | +| Row caps | 100 (lookup), 1000 (SQL) | Keeps DOM render sizes reasonable | +| Routing | Hand-rolled, ~20 lines | Five static routes do not justify a router dependency | + +## Risks and limitations + +- **Database size.** 38–48 MB gzipped per dataset; slow links wait, mitigated by + a progress bar. +- **Browser memory.** The full database lives in RAM; older mobile devices may + run out. +- **`sql.js.org` dependency.** If that CDN is unreachable, the WASM fails to + load. Self-hosting `sql-wasm.wasm` and updating `SQL_WASM_URL` in + `use-sqlite.js` is the fix. +- **Excel format drift.** A new source file with an unseen header layout needs a + new branch in `format_detect_2016.rs` or a new config. diff --git a/2017/eslint.config.js b/eslint.config.js similarity index 79% rename from 2017/eslint.config.js rename to eslint.config.js index 302e607..78101ed 100644 --- a/2017/eslint.config.js +++ b/eslint.config.js @@ -5,7 +5,7 @@ import reactRefresh from 'eslint-plugin-react-refresh' import { defineConfig, globalIgnores } from 'eslint/config' export default defineConfig([ - globalIgnores(['dist']), + globalIgnores(['dist', '.build', '_site']), { files: ['**/*.{js,jsx}'], extends: [ @@ -27,8 +27,8 @@ export default defineConfig([ }, }, { - // Node-executed files (Vite config, build scripts) run with Node globals. - files: ['vite.config.js', 'scripts/**/*.js'], + // Node-executed files (Vite config, parser tooling) run with Node globals. + files: ['vite.config.js', 'scripts/**/*.js', 'parser/scripts/**/*.js'], languageOptions: { globals: { ...globals.node }, }, diff --git a/index.html b/index.html index adbaed1..6961ca3 100644 --- a/index.html +++ b/index.html @@ -1,26 +1,19 @@ - - - - Tra cứu điểm thi THPT Quốc gia - - - -

Tra cứu điểm thi THPT Quốc gia

-

Tra cứu điểm thi tốt nghiệp THPT Quốc gia theo số báo danh — truy vấn SQL - chạy hoàn toàn trên trình duyệt (sql.js).

- -

Phiên bản cũ của trang 2017: old · - old2

- + + + + + + + + Tra cứu điểm thi THPT QG 2017 + + +
+ + diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..cc1c89f --- /dev/null +++ b/package-lock.json @@ -0,0 +1,2569 @@ +{ + "name": "thptqg", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "thptqg", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "react": "^19.2.4", + "react-dom": "^19.2.4", + "sql.js": "^1.14.1" + }, + "devDependencies": { + "@eslint/js": "^9.39.4", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^6.0.1", + "eslint": "^9.39.4", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.5.2", + "globals": "^17.4.0", + "vite": "^8.0.16" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.8.tgz", + "integrity": "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.8", + "@babel/types": "^7.29.8", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.8.tgz", + "integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.8" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.8.tgz", + "integrity": "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.8", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.8", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.8", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz", + "integrity": "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", + "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.5" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.6.tgz", + "integrity": "sha512-l2Ul9PrHsPCKcEY/ac7VgFj9D80C7S68sOKc618SyHDPK36s1XcFebXY0iTzUVn4Yq+YbwvSnDmCz9yxjX+QrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.14.0", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.3.0", + "minimatch": "^3.1.5", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.5", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.5.tgz", + "integrity": "sha512-QywQuszQh77pIXCsq998c8hbhSTI/azTty1Z6N53dmAudKHhy573j3yvRLsX2BSp8YpLtoCEG8E9DJe+8zUh4A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.144.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.144.0.tgz", + "integrity": "sha512-nuhZIOLuI6TFQ32I/WnUx+SCPY7SdSKwgnFHydAuoS1+Z4BRcaP+RRJmGzl9lw+0OFF7UmaESf7KQRXaNLHypg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.4.tgz", + "integrity": "sha512-jHC2cnyKz5xU2fhECtFl8OZ83cYNt13GZQD+0uMJ/X3o+ijmd56okHhTUwxVSHPx1IRVIJEZ1/1pPzeLCU6XKA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-Dc5mPD8F5F/FS8i01syd7FTF6yB2fVthH/TRkjwJkzUK6EpoxHtqvZQP5Zwq80/5z19TWYHIg1KOHboCgVx/aQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.4.tgz", + "integrity": "sha512-fpDm4oBo6SqLvWUYCmFhdde3U9KH2fRNNMeAnAPAIwxRL345xutL0EtEUcuoxsoazdJGv/MuDBQHlCDrtbvqOg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.4.tgz", + "integrity": "sha512-rSJoreDE/HoIzoaib6MTp5jQtCTdMHKIvItAKT/ImS6Y6Ww76oUaeMyp4Vc/fAgd/ehji068IxetHXAnqUwN9A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.4.tgz", + "integrity": "sha512-/jm8OGHgn7oGaJu3i/qZI9spUGcJ+y/lk43ttQ/iO1tOd9NissG6o97bighBCiL+BKRngmcDuR6ikfwYdJmVuQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.4.tgz", + "integrity": "sha512-tIP06BeD9EqvECBrPZ+sqdPlYrT+aYaAiu1wYziVx5elRK/ftm33JxVDy2bXGbr6J0CrtirCkR87/X5a2euEng==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.4.tgz", + "integrity": "sha512-Ql1Q0EQqVThvn9VAVlwNzsUvbSFtCMGjLpRRi4pk5i7NZZ4n5ISiLMjHYtus4VQ2PvkSw24zyaCVsiS+sXPj1w==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.4.tgz", + "integrity": "sha512-GjbjXD4XXfN19D0LZNbmiCBUoDiRACsYHr0yaIbbn8aFsXjHZifcYqu/W5Er5X2X990WjHXFrxarn5chzItorQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.4.tgz", + "integrity": "sha512-p5WR0NOwaRmJ/B1b6IjEFLLivwEsf3PrdBIhRbhTCQisbo2SvHHpG4ELB/+FgQNnB88LTOF86upmJmbvZdQ2lw==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.4.tgz", + "integrity": "sha512-4/GyVjmhR+Tc6HLJvwc1sOhPqAZtySiSMesOZyX6JQ5XBxoTDEMKQzvo07NIK6nTon/SivlZqvhzvuVBNQhObQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.4.tgz", + "integrity": "sha512-l9eeLsCNvPpmSXUej0etw/J1eqV0Jj1D5G/xG6YTijmE6dkv6E2QezgWbTfQk63v952DPqrjOCoiqxq7Bw0YUQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.4.tgz", + "integrity": "sha512-e0F355MSTMm3+UOqtV3L24gFUp2N5m1f8L/7d56deik6va+AXdrt9F8LbzGpeWGWRbZEDq4m8NVnJDeBtf9DZg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.4.tgz", + "integrity": "sha512-AWLi0uBRYh6QlE7OKhiz+phZC0qwtij2QZmhmOdsLdFn64m7oMpooE9ICE3lhm9xMb4SpDo2WbHcxX1iFLFtqw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.4.tgz", + "integrity": "sha512-UwSDJOg3dqCAejWdxclJjCsh3Qq4vLYMDxmyHqo1btz3stK2VqgwNd3mm5tuIwzSlGIQ/1H9Hr+Zn09mrezNqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "19.2.18", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.18.tgz", + "integrity": "sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.4.tgz", + "integrity": "sha512-Bsc+QHgp+P/F02XDzNCY9jnZNCUuLki36KT7VKrTXXLdHf+vHMNZnW1rVu5DNW/rCK+fya3DATySbLM4yhtKUw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.5.tgz", + "integrity": "sha512-BOVzne/NL162sMdResB25mUv+vWMF5NoAjNf09TeGlE7ZpszZWSD3winycicLJw72yeVsoCn/2kOhEuCvEShMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "^1.0.1" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", + "babel-plugin-react-compiler": "^1.0.0", + "vite": "^8.0.0" + }, + "peerDependenciesMeta": { + "@rolldown/plugin-babel": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + } + } + }, + "node_modules/acorn": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz", + "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.11.13", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.13.tgz", + "integrity": "sha512-k9HNuUVMlqVjQ9UHzfPjIqiDbWw7WqT1AoT7GL8VwvF3r0ZfArtgiSPAlmupyNquNgOJHTuH4CKYf8ttMTWBTQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/browserslist": { + "version": "4.28.8", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.8.tgz", + "integrity": "sha512-V2NpofLblG64mfOtSgDhOJESZEGogzDMBv/q+W6oc4LXWP/q75eOXoOaaOu1EOadB9U4Bwx/e0yzbvwKH8zalA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.11.12", + "caniuse-lite": "^1.0.30001809", + "electron-to-chromium": "^1.5.402", + "node-releases": "^2.0.53", + "update-browserslist-db": "^1.3.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001809", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001809.tgz", + "integrity": "sha512-xxWVywk6a6Arlk+hymeycyn/VgqEfLDxupvhH/xiY5SJ/18kmi9o6MiO320DCUzypORHLtvh0I4i04tUhCNHNQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.405", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.405.tgz", + "integrity": "sha512-bNglH7lPH5l+yHOes7Zr4VqxhOy4BQ9ZBUX4VdoFgxMpzJk7W1ZoO3Vgd9Pxa9PyjQ76sfm2aKH/nzEcCNRlew==", + "dev": true, + "license": "ISC" + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.39.5", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.5.tgz", + "integrity": "sha512-DgZS62aPLXKlnxILS/AYCoRvHaZeXceIzlXPkkGGzJWSow1aEk0lbTlxUSlyjC8jcaKxAdOnTDz+o1JFSBsyjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.2", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.6", + "@eslint/js": "9.39.5", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.5", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.1.1.tgz", + "integrity": "sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.24.4", + "@babel/parser": "^7.24.4", + "hermes-parser": "^0.25.1", + "zod": "^3.25.0 || ^4.0.0", + "zod-validation-error": "^3.5.0 || ^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.5.4.tgz", + "integrity": "sha512-7bqTKz7T0r+HKWFarNXByDE9/5+73wI2ru+M3zuqGbR7s/b/5/pQJXZoufWlrngqGqoZto73ZkGumCdLxk+4rw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": "^9 || ^10" + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.4.tgz", + "integrity": "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "17.11.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.11.0.tgz", + "integrity": "sha512-Z2I8hM+PbJDXQDq3Icgpzv+mPdwr68iZUU9d5WW4FuXfDUQfkZaZuvjMv42/5crNyw154+9+VWXbYrUgDXbxNw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/hermes-estree": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", + "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", + "dev": true, + "license": "MIT" + }, + "node_modules/hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hermes-estree": "0.25.1" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.1.tgz", + "integrity": "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lightningcss": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.53", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.53.tgz", + "integrity": "sha512-D9UOmYG3UH1V+ENW56t5QXBwJw1YEY18ruVeus89Rw+SyIgjPkCO84bRzO3uNIYosJbNwiabWVn48o3uJLjxFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.26", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz", + "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.17", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/react": { + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.8.tgz", + "integrity": "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.8.tgz", + "integrity": "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.8" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/rolldown": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.4.tgz", + "integrity": "sha512-rSr7irW0K7QRWzjdJXqZowkcRdDtjRduh43rBltnVKd0VFq839l1lJoDvGJb6gl7+4rTTCrPWu+YfujUL8Ug7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.144.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.2.4", + "@rolldown/binding-darwin-arm64": "1.2.4", + "@rolldown/binding-darwin-x64": "1.2.4", + "@rolldown/binding-freebsd-x64": "1.2.4", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.4", + "@rolldown/binding-linux-arm64-gnu": "1.2.4", + "@rolldown/binding-linux-arm64-musl": "1.2.4", + "@rolldown/binding-linux-ppc64-gnu": "1.2.4", + "@rolldown/binding-linux-s390x-gnu": "1.2.4", + "@rolldown/binding-linux-x64-gnu": "1.2.4", + "@rolldown/binding-linux-x64-musl": "1.2.4", + "@rolldown/binding-openharmony-arm64": "1.2.4", + "@rolldown/binding-win32-arm64-msvc": "1.2.4", + "@rolldown/binding-win32-x64-msvc": "1.2.4" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sql.js": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/sql.js/-/sql.js-1.14.1.tgz", + "integrity": "sha512-gcj8zBWU5cFsi9WUP+4bFNXAyF1iRpA3LLyS/DP5xlrNzGmPIizUeBggKa8DbDwdqaKwUcTEnChtd2grWo/x/A==", + "license": "MIT" + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.1.tgz", + "integrity": "sha512-ZZ61DsRsOnakl74HAmp3oSN4aXUmEWXf+i/yv0h7tIBfICc3VdrFErQKUUKPgu3AMsTUMbcongALEN4l6GSUrQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/vite": { + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.2.1.tgz", + "integrity": "sha512-EU/eS7BH3XROHh2YnBefjM6DBKA6ZeMZEYQbj7NLWg5wHYlhB8B/Mayd5XsgWq+NFYccDOTemRpdETWR6Ka/lw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.33.0", + "picomatch": "^4.0.5", + "postcss": "^8.5.25", + "rolldown": "~1.2.1", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.4.0", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", + "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-validation-error": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", + "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + } + } + } +} diff --git a/2016/package.json b/package.json similarity index 55% rename from 2016/package.json rename to package.json index 245c829..d5cc3c6 100644 --- a/2016/package.json +++ b/package.json @@ -1,21 +1,22 @@ { - "name": "thptqg2016", + "name": "thptqg", "private": true, - "packageManager": "pnpm@11.1.1", "version": "1.0.0", "type": "module", - "description": "Tra cứu điểm thi THPT QG 2016 - Hỗ trợ truy vấn SQL tùy chỉnh", + "description": "Tra cứu điểm thi THPT Quốc gia — 2016 và 2017", "scripts": { - "build:rust": "cargo build --release --manifest-path tools/xlsxread/Cargo.toml", - "build:db": "cargo build --release --manifest-path tools/xlsxread/Cargo.toml && ./tools/xlsxread/target/release/xlsxread build --schema tools/xlsxread/configs/thptqg2016-data.toml --input data --output public/thptqg2016.db", + "build:rust": "cargo build --release --manifest-path parser/Cargo.toml", + "build:db": "node parser/scripts/build-db.js", "dev": "vite", "build": "vite build", + "assemble": "node scripts/assemble-site.js", + "build:site": "npm run build && npm run assemble", "preview": "vite preview", "lint": "eslint ." }, "repository": { "type": "git", - "url": "git+https://github.com/tiennm99/thptqg2016.git" + "url": "git+https://github.com/tiennm99/thptqg.git" }, "license": "ISC", "dependencies": { diff --git a/2016/tools/xlsxread/Cargo.lock b/parser/Cargo.lock similarity index 100% rename from 2016/tools/xlsxread/Cargo.lock rename to parser/Cargo.lock diff --git a/2016/tools/xlsxread/Cargo.toml b/parser/Cargo.toml similarity index 100% rename from 2016/tools/xlsxread/Cargo.toml rename to parser/Cargo.toml diff --git a/parser/configs/2016.toml b/parser/configs/2016.toml new file mode 100644 index 0000000..6bde781 --- /dev/null +++ b/parser/configs/2016.toml @@ -0,0 +1,32 @@ +# thptqg2016 data/ — 4 .xls + 115 .xlsx mixed files. +# +# Three column layouts exist across the 119 files; the binary selects the +# right one per-file at runtime via format_detection = "thptqg2016": +# +# separate-scores header SBD(0)/HOTEN(1)/TOAN(2)... — dhhanghai files +# mapped header SOBAODANH|SBD + DIEM_THI — most provinces +# default no header; positional 6-col layout — remaining files +# +# This is the only dataset that populates ten_cum_thi and gioi_tinh, and the +# only one whose files carry Tiếng Đức / Tiếng Nhật scores. +# +# sheet_mode = "all": several provinces overflow into Sheet2 (65k Excel row cap). +# strip_blank_rows = false: no blank-row anomaly observed in this dataset. +# +# Table shape, INSERT and subject regexes are canonical — see src/schema.rs. + +format_detection = "thptqg2016" + +[reader] +sheet_mode = "all" +strip_blank_rows = false + +[validation] +require_numeric_sbd = false +require_nonempty_name = true +require_nonempty_sbd = true + +[header] +# Tokens that identify a header row by first-cell content (uppercased). +# Covers both SOBAODANH-style and SBD-style headers. +tokens = ["SOBAODANH", "SBD", "HO_TEN", "HOTEN", "HỌ TÊN", "STT"] diff --git a/parser/configs/2017-old.toml b/parser/configs/2017-old.toml new file mode 100644 index 0000000..f21f8aa --- /dev/null +++ b/parser/configs/2017-old.toml @@ -0,0 +1,25 @@ +# thptqg2017 data-old/ — 63 .xlsx files (pre-baotintuc refresh). +# +# sheet_mode = "first": single-sheet workbooks, never hit the 65k row cap. +# SBD validation: require ^\d+$ (build-database-old.js:55 guard). +# strip_blank_rows = false: no explicit blank-skip in build-database-old.js. +# +# Table shape, INSERT and subject regexes are canonical — see src/schema.rs. + +[reader] +sheet_mode = "first" +strip_blank_rows = false + +[columns] +ho_ten = 0 +ngay_sinh = 1 +so_bao_danh = 2 +diem_thi = 3 + +[validation] +require_numeric_sbd = true +require_nonempty_name = true +require_nonempty_sbd = true + +[header] +tokens = ["HO_TEN", "HỌ TÊN", "STT"] diff --git a/parser/configs/2017-old2.toml b/parser/configs/2017-old2.toml new file mode 100644 index 0000000..c4fd6b7 --- /dev/null +++ b/parser/configs/2017-old2.toml @@ -0,0 +1,26 @@ +# thptqg2017 data-old2/ — 54 .xlsx files (corrected-export set). +# +# sheet_mode = "all": 24.HCM_UTLQ.xlsx overflows into Sheet2 (+6,446 rows). +# SBD validation: require ^\d+$ (build-database-old2.js:57 guard). +# strip_blank_rows = true: skip fully blank rows BEFORE counting sourceRows +# (build-database-old2.js:50-51 checks blank before sourceRows++). +# +# Table shape, INSERT and subject regexes are canonical — see src/schema.rs. + +[reader] +sheet_mode = "all" +strip_blank_rows = true + +[columns] +ho_ten = 0 +ngay_sinh = 1 +so_bao_danh = 2 +diem_thi = 3 + +[validation] +require_numeric_sbd = true +require_nonempty_name = true +require_nonempty_sbd = true + +[header] +tokens = ["HO_TEN", "HỌ TÊN", "STT"] diff --git a/parser/configs/2017.toml b/parser/configs/2017.toml new file mode 100644 index 0000000..99dccbb --- /dev/null +++ b/parser/configs/2017.toml @@ -0,0 +1,25 @@ +# thptqg2017 data/ — 63 .xls files from baotintuc.vn (current generation). +# +# sheet_mode = "all": Hà Nội and HCM overflow into Sheet2 (65k Excel row cap). +# SBD validation: no numeric guard — build-database.js did not apply ^\d+$. +# strip_blank_rows = false: no blank-row anomaly in this dataset. +# +# Table shape, INSERT and subject regexes are canonical — see src/schema.rs. + +[reader] +sheet_mode = "all" +strip_blank_rows = false + +[columns] +ho_ten = 0 +ngay_sinh = 1 +so_bao_danh = 2 +diem_thi = 3 + +[validation] +require_numeric_sbd = false +require_nonempty_name = true +require_nonempty_sbd = true + +[header] +tokens = ["HO_TEN", "HỌ TÊN", "STT"] diff --git a/parser/scripts/build-db.js b/parser/scripts/build-db.js new file mode 100644 index 0000000..9558ebb --- /dev/null +++ b/parser/scripts/build-db.js @@ -0,0 +1,66 @@ +#!/usr/bin/env node +/** + * Build the SQLite database for one or all datasets, then gzip it. + * + * Replaces the six per-dataset npm scripts the two old projects carried. The + * dataset list comes from src/datasets.js so it is written in exactly one place. + * + * Output goes to .build/public/db/ — the directory Vite copies as its publicDir. + * Only the .gz survives: shipping a 100+ MB uncompressed database is made + * structurally impossible rather than left to a cleanup step. + * + * Usage: + * node parser/scripts/build-db.js # all four datasets + * node parser/scripts/build-db.js 2017-old # just one + */ + +import { execFileSync } from "node:child_process"; +import { mkdirSync, rmSync, existsSync } from "node:fs"; +import { dirname, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +import { DATASET_IDS } from "../../src/datasets.js"; + +const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "../.."); +const BIN = resolve(ROOT, "parser/target/release/xlsxread"); +const OUT_DIR = resolve(ROOT, ".build/public/db"); + +const requested = process.argv.slice(2); +const unknown = requested.filter((id) => !DATASET_IDS.includes(id)); +if (unknown.length) { + console.error(`unknown dataset(s): ${unknown.join(", ")}`); + console.error(`known: ${DATASET_IDS.join(", ")}`); + process.exit(2); +} +const targets = requested.length ? requested : DATASET_IDS; + +if (!existsSync(BIN)) { + console.error(`parser binary not found at ${BIN}`); + console.error("run: npm run build:rust"); + process.exit(1); +} + +mkdirSync(OUT_DIR, { recursive: true }); + +for (const id of targets) { + const db = resolve(OUT_DIR, `${id}.db`); + + execFileSync( + BIN, + [ + "build", + "--schema", + resolve(ROOT, `parser/configs/${id}.toml`), + "--input", + resolve(ROOT, `data/${id}`), + "--output", + db, + ], + { stdio: "inherit" }, + ); + + // -9 without -k: the raw .db must not reach the published artifact. + rmSync(`${db}.gz`, { force: true }); + execFileSync("gzip", ["-9", db], { stdio: "inherit" }); + console.log(` → db/${id}.db.gz\n`); +} diff --git a/2017/scripts/check-duplicates.js b/parser/scripts/check-duplicates.js similarity index 74% rename from 2017/scripts/check-duplicates.js rename to parser/scripts/check-duplicates.js index 7b17e1f..c0b4a53 100644 --- a/2017/scripts/check-duplicates.js +++ b/parser/scripts/check-duplicates.js @@ -1,4 +1,8 @@ -// One-off audit: detect content-identical Excel files via md5 +// One-off audit: detect content-identical Excel files via md5. +// +// BROKEN as committed — `dirs` below is a hardcoded Windows path from the +// original author's machine. Predates the repo unification; left unchanged +// rather than half-fixed. Point `dirs` at data/ to use it. import crypto from "crypto"; import fs from "fs"; import path from "path"; diff --git a/2017/scripts/crawl-baotintuc.js b/parser/scripts/crawl-baotintuc.js similarity index 97% rename from 2017/scripts/crawl-baotintuc.js rename to parser/scripts/crawl-baotintuc.js index 7fb8036..8f4c20c 100644 --- a/2017/scripts/crawl-baotintuc.js +++ b/parser/scripts/crawl-baotintuc.js @@ -1,12 +1,12 @@ // Crawl Excel score files for all 63 provinces from baotintuc.vn article: // https://baotintuc.vn/tuyen-sinh/tra-cuu-diem-thi-thpt-2017-cua-63-tinh-thanh-pho-tren-baotintucvn-20170706073512672.htm -// Saves to data/.xls (63 files). Idempotent: skips already-downloaded files. +// Saves to data/2017/.xls (63 files). Idempotent: skips already-downloaded files. import fs from "fs"; import path from "path"; import { fileURLToPath } from "url"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); -const OUT_DIR = path.join(__dirname, "..", "data"); +const OUT_DIR = path.join(__dirname, "..", "..", "data", "2017"); const LINKS = [ ["An Giang", "https://cdnmedia.baotintuc.vn/2017/07/06/17/57/Angiang.xls"], diff --git a/parser/scripts/db-stats.js b/parser/scripts/db-stats.js new file mode 100644 index 0000000..8582821 --- /dev/null +++ b/parser/scripts/db-stats.js @@ -0,0 +1,79 @@ +#!/usr/bin/env node +/** + * Dump per-dataset statistics from built SQLite databases as JSON. + * + * Used twice: once against the pre-refactor databases to capture a baseline, + * and again after the schema unification. Comparing the two outputs is what + * proves no score data was lost or invented. + * + * Schema-agnostic on purpose — columns come from PRAGMA table_info, so the same + * script runs against the old 18/20-column tables and the new 22-column one. + * + * Usage: + * node db-stats.js