Compare commits
8 Commits
map
...
4ee8757577
| Author | SHA1 | Date | |
|---|---|---|---|
| 4ee8757577 | |||
| 1cd6764078 | |||
| fc172cfa36 | |||
| 58ab00f08d | |||
| 4dd4317d66 | |||
| fe07f166cf | |||
| fba47c142c | |||
| 3ee3ffeb17 |
6
.gitignore
vendored
@@ -43,6 +43,11 @@ Temporary Items
|
|||||||
*.sln
|
*.sln
|
||||||
*.sw?
|
*.sw?
|
||||||
|
|
||||||
|
# Backup files
|
||||||
|
*.bak
|
||||||
|
*.bak.*
|
||||||
|
*.tmp
|
||||||
|
|
||||||
# Svelte related
|
# Svelte related
|
||||||
.svelte-kit/
|
.svelte-kit/
|
||||||
|
|
||||||
@@ -56,3 +61,4 @@ Temporary Items
|
|||||||
# Make favicon generation script executable
|
# Make favicon generation script executable
|
||||||
chmod +x ./scripts/generate-favicons.js
|
chmod +x ./scripts/generate-favicons.js
|
||||||
chmod +x ./scripts/update-data.js
|
chmod +x ./scripts/update-data.js
|
||||||
|
chmod +x ./scripts/*
|
||||||
|
|||||||
132
CLAUDE.md
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
|
||||||
|
This is a Svelte-based logo gallery and quiz game application that displays company/brand logos and provides interactive games like flag quizzes. The project uses Docker for development and deploys to GitHub Pages.
|
||||||
|
|
||||||
|
## Development Commands
|
||||||
|
|
||||||
|
### Docker-based Development (Recommended)
|
||||||
|
- `make dev` - Start development server with live reload at http://localhost:5006
|
||||||
|
- `make build` - Build the Docker container
|
||||||
|
- `make start` - Start application in background
|
||||||
|
- `make stop` - Stop the application
|
||||||
|
- `make restart` - Restart the application
|
||||||
|
- `make logs` - View application logs
|
||||||
|
- `make run CMD="command"` - Run any command inside the container
|
||||||
|
|
||||||
|
### Data Management
|
||||||
|
- `make update-data` - Scan logos directory and regenerate data files
|
||||||
|
- `npm run update-data` - Same as above, but run directly (inside container)
|
||||||
|
|
||||||
|
### Asset Generation
|
||||||
|
- `make favicon` - Generate favicon variants
|
||||||
|
- `npm run generate-variants` - Generate SVG color variants
|
||||||
|
- `npm run generate-favicons` - Generate favicon files
|
||||||
|
- `npm run pwa-cache-list` - Generate PWA cache manifest
|
||||||
|
|
||||||
|
### Build Commands
|
||||||
|
- `npm run build` - Build production bundle using Rollup
|
||||||
|
- `npm run dev` - Development build with live reload
|
||||||
|
- `npm start` - Start sirv server on port 5006
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
### Tech Stack
|
||||||
|
- **Frontend**: Svelte 3.59.2 with SPA routing
|
||||||
|
- **Bundler**: Rollup with plugins for Svelte, CSS, and minification
|
||||||
|
- **Routing**: svelte-spa-router for single-page navigation
|
||||||
|
- **Styling**: CSS with theme support (light/dark/system)
|
||||||
|
- **Development**: Docker with live reload
|
||||||
|
|
||||||
|
### Key Application Structure
|
||||||
|
|
||||||
|
#### Main App (`src/App.svelte`)
|
||||||
|
- Central state management through `window.appData` global object
|
||||||
|
- Handles routing, theme management, and data collection switching
|
||||||
|
- Manages search, filtering (tags, brands, variants), and view modes
|
||||||
|
- Supports multiple collections (logos, flags) via dynamic data loading
|
||||||
|
|
||||||
|
#### Pages (`src/pages/`)
|
||||||
|
- `Home.svelte` - Main logo gallery with grid/list/compact views
|
||||||
|
- `Game.svelte` - Game selection landing page
|
||||||
|
- `FlagQuiz.svelte` - Flag identification quiz with adaptive learning
|
||||||
|
- `CapitalsQuiz.svelte` - Country capitals quiz
|
||||||
|
- `GeographyQuiz.svelte` - Geography-based quiz game
|
||||||
|
- `Preview.svelte` - Individual logo preview modal
|
||||||
|
|
||||||
|
#### Components (`src/components/`)
|
||||||
|
- `CardFull.svelte` - Full logo display card with actions
|
||||||
|
- `Header.svelte` - Navigation and search interface
|
||||||
|
- `Actions.svelte` - Action buttons for copy/download
|
||||||
|
- `Achievements.svelte` - Quiz achievement system
|
||||||
|
- Various card sizes (`CardSmall`, `CardMiddle`) for different views
|
||||||
|
|
||||||
|
#### Data Flow
|
||||||
|
- Logo data loaded from JSON files in `public/data/` (logos.json, flags.json)
|
||||||
|
- Collections switchable via dropdown, stored in localStorage
|
||||||
|
- Global state shared via `window.appData` object
|
||||||
|
- Theme persistence with system preference detection
|
||||||
|
- URL-based state for search/filter sharing
|
||||||
|
|
||||||
|
### Key Features
|
||||||
|
|
||||||
|
#### Multi-Collection Support
|
||||||
|
- Supports different data collections (logos, flags)
|
||||||
|
- Collection switching triggers data reload and state reset
|
||||||
|
- Each collection has its own data file structure
|
||||||
|
|
||||||
|
#### Advanced Filtering
|
||||||
|
- Text search across name, title, brand, and metadata
|
||||||
|
- Tag-based filtering with colored tags
|
||||||
|
- Brand filtering for logo variants
|
||||||
|
- Variant filtering (different logo styles)
|
||||||
|
- Compact mode to show unique brands only
|
||||||
|
|
||||||
|
#### Theme System
|
||||||
|
- Light/dark/system theme options
|
||||||
|
- CSS custom properties for theme switching
|
||||||
|
- Persistent theme preferences
|
||||||
|
|
||||||
|
#### Quiz System
|
||||||
|
- Adaptive learning algorithms
|
||||||
|
- Achievement tracking
|
||||||
|
- Score persistence
|
||||||
|
- Multiple quiz types with shared logic in `src/quizLogic/`
|
||||||
|
|
||||||
|
## File Structure Conventions
|
||||||
|
|
||||||
|
### Static Assets
|
||||||
|
- `public/logos/` - Logo files (SVG, PNG)
|
||||||
|
- `public/data/` - JSON data files generated from asset scanning
|
||||||
|
- `public/build/` - Compiled JS/CSS output (generated)
|
||||||
|
|
||||||
|
### Scripts (`scripts/`)
|
||||||
|
- `update-data.js` - Scans asset directories and generates JSON manifests
|
||||||
|
- `generate-svg-variants.js` - Creates color variants of SVG logos
|
||||||
|
- `generate-favicons.js` - Generates favicon files from source images
|
||||||
|
- `cleanup_worldmap.py` - SVG cleanup utilities
|
||||||
|
|
||||||
|
### Development Files
|
||||||
|
- `rollup.config.js` - Bundler configuration with dev/prod modes
|
||||||
|
- `Makefile` - Docker development commands
|
||||||
|
- `compose.dev.yml` - Docker Compose configuration
|
||||||
|
- `Dockerfile.dev` - Development container setup
|
||||||
|
|
||||||
|
## Development Workflow
|
||||||
|
|
||||||
|
1. **Adding New Logos**: Place files in `public/logos/`, run `make update-data`
|
||||||
|
2. **UI Changes**: Edit Svelte files in `src/`, changes auto-reload in dev mode
|
||||||
|
3. **Asset Changes**: Regenerate variants with `npm run generate-variants`
|
||||||
|
4. **Deployment**: Push to `main` branch triggers automatic GitHub Pages deployment
|
||||||
|
|
||||||
|
## Important Notes
|
||||||
|
|
||||||
|
- All development should use Docker containers for consistency
|
||||||
|
- The app uses a global `window.appData` object for component communication
|
||||||
|
- Theme changes require CSS custom property updates
|
||||||
|
- Data files are auto-generated - don't edit JSON files directly
|
||||||
|
- Quiz logic is modular and shared between different quiz types
|
||||||
|
- SVG logos support automatic color variant generation
|
||||||
248
public/data/ISO3166-1.json
Normal file
@@ -0,0 +1,248 @@
|
|||||||
|
{
|
||||||
|
"AF": "Afghanistan",
|
||||||
|
"AX": "Aland Islands",
|
||||||
|
"AL": "Albania",
|
||||||
|
"DZ": "Algeria",
|
||||||
|
"AS": "American Samoa",
|
||||||
|
"AD": "Andorra",
|
||||||
|
"AO": "Angola",
|
||||||
|
"AI": "Anguilla",
|
||||||
|
"AQ": "Antarctica",
|
||||||
|
"AG": "Antigua And Barbuda",
|
||||||
|
"AR": "Argentina",
|
||||||
|
"AM": "Armenia",
|
||||||
|
"AW": "Aruba",
|
||||||
|
"AU": "Australia",
|
||||||
|
"AT": "Austria",
|
||||||
|
"AZ": "Azerbaijan",
|
||||||
|
"BS": "Bahamas",
|
||||||
|
"BH": "Bahrain",
|
||||||
|
"BD": "Bangladesh",
|
||||||
|
"BB": "Barbados",
|
||||||
|
"BY": "Belarus",
|
||||||
|
"BE": "Belgium",
|
||||||
|
"BZ": "Belize",
|
||||||
|
"BJ": "Benin",
|
||||||
|
"BM": "Bermuda",
|
||||||
|
"BT": "Bhutan",
|
||||||
|
"BO": "Bolivia",
|
||||||
|
"BA": "Bosnia And Herzegovina",
|
||||||
|
"BW": "Botswana",
|
||||||
|
"BV": "Bouvet Island",
|
||||||
|
"BR": "Brazil",
|
||||||
|
"IO": "British Indian Ocean Territory",
|
||||||
|
"BN": "Brunei Darussalam",
|
||||||
|
"BG": "Bulgaria",
|
||||||
|
"BF": "Burkina Faso",
|
||||||
|
"BI": "Burundi",
|
||||||
|
"KH": "Cambodia",
|
||||||
|
"CM": "Cameroon",
|
||||||
|
"CA": "Canada",
|
||||||
|
"CV": "Cape Verde",
|
||||||
|
"KY": "Cayman Islands",
|
||||||
|
"CF": "Central African Republic",
|
||||||
|
"TD": "Chad",
|
||||||
|
"CL": "Chile",
|
||||||
|
"CN": "China",
|
||||||
|
"CX": "Christmas Island",
|
||||||
|
"CC": "Cocos (Keeling) Islands",
|
||||||
|
"CO": "Colombia",
|
||||||
|
"KM": "Comoros",
|
||||||
|
"CG": "Congo",
|
||||||
|
"CD": "Congo, Democratic Republic",
|
||||||
|
"CK": "Cook Islands",
|
||||||
|
"CR": "Costa Rica",
|
||||||
|
"CI": "Cote D\"Ivoire",
|
||||||
|
"HR": "Croatia",
|
||||||
|
"CU": "Cuba",
|
||||||
|
"CY": "Cyprus",
|
||||||
|
"CZ": "Czech Republic",
|
||||||
|
"DK": "Denmark",
|
||||||
|
"DJ": "Djibouti",
|
||||||
|
"DM": "Dominica",
|
||||||
|
"DO": "Dominican Republic",
|
||||||
|
"EC": "Ecuador",
|
||||||
|
"EG": "Egypt",
|
||||||
|
"SV": "El Salvador",
|
||||||
|
"GQ": "Equatorial Guinea",
|
||||||
|
"ER": "Eritrea",
|
||||||
|
"EE": "Estonia",
|
||||||
|
"ET": "Ethiopia",
|
||||||
|
"FK": "Falkland Islands (Malvinas)",
|
||||||
|
"FO": "Faroe Islands",
|
||||||
|
"FJ": "Fiji",
|
||||||
|
"FI": "Finland",
|
||||||
|
"FR": "France",
|
||||||
|
"GF": "French Guiana",
|
||||||
|
"PF": "French Polynesia",
|
||||||
|
"TF": "French Southern Territories",
|
||||||
|
"GA": "Gabon",
|
||||||
|
"GM": "Gambia",
|
||||||
|
"GE": "Georgia",
|
||||||
|
"DE": "Germany",
|
||||||
|
"GH": "Ghana",
|
||||||
|
"GI": "Gibraltar",
|
||||||
|
"GR": "Greece",
|
||||||
|
"GL": "Greenland",
|
||||||
|
"GD": "Grenada",
|
||||||
|
"GP": "Guadeloupe",
|
||||||
|
"GU": "Guam",
|
||||||
|
"GT": "Guatemala",
|
||||||
|
"GG": "Guernsey",
|
||||||
|
"GN": "Guinea",
|
||||||
|
"GW": "Guinea-Bissau",
|
||||||
|
"GY": "Guyana",
|
||||||
|
"HT": "Haiti",
|
||||||
|
"HM": "Heard Island & Mcdonald Islands",
|
||||||
|
"VA": "Holy See (Vatican City State)",
|
||||||
|
"HN": "Honduras",
|
||||||
|
"HK": "Hong Kong",
|
||||||
|
"HU": "Hungary",
|
||||||
|
"IS": "Iceland",
|
||||||
|
"IN": "India",
|
||||||
|
"ID": "Indonesia",
|
||||||
|
"IR": "Iran, Islamic Republic Of",
|
||||||
|
"IQ": "Iraq",
|
||||||
|
"IE": "Ireland",
|
||||||
|
"IM": "Isle Of Man",
|
||||||
|
"IL": "Israel",
|
||||||
|
"IT": "Italy",
|
||||||
|
"JM": "Jamaica",
|
||||||
|
"JP": "Japan",
|
||||||
|
"JE": "Jersey",
|
||||||
|
"JO": "Jordan",
|
||||||
|
"KZ": "Kazakhstan",
|
||||||
|
"KE": "Kenya",
|
||||||
|
"KI": "Kiribati",
|
||||||
|
"KR": "Korea",
|
||||||
|
"KP": "North Korea",
|
||||||
|
"KW": "Kuwait",
|
||||||
|
"KG": "Kyrgyzstan",
|
||||||
|
"LA": "Lao People\"s Democratic Republic",
|
||||||
|
"LV": "Latvia",
|
||||||
|
"LB": "Lebanon",
|
||||||
|
"LS": "Lesotho",
|
||||||
|
"LR": "Liberia",
|
||||||
|
"LY": "Libyan Arab Jamahiriya",
|
||||||
|
"LI": "Liechtenstein",
|
||||||
|
"LT": "Lithuania",
|
||||||
|
"LU": "Luxembourg",
|
||||||
|
"MO": "Macao",
|
||||||
|
"MK": "Macedonia",
|
||||||
|
"MG": "Madagascar",
|
||||||
|
"MW": "Malawi",
|
||||||
|
"MY": "Malaysia",
|
||||||
|
"MV": "Maldives",
|
||||||
|
"ML": "Mali",
|
||||||
|
"MT": "Malta",
|
||||||
|
"MH": "Marshall Islands",
|
||||||
|
"MQ": "Martinique",
|
||||||
|
"MR": "Mauritania",
|
||||||
|
"MU": "Mauritius",
|
||||||
|
"YT": "Mayotte",
|
||||||
|
"MX": "Mexico",
|
||||||
|
"FM": "Micronesia, Federated States Of",
|
||||||
|
"MD": "Moldova",
|
||||||
|
"MC": "Monaco",
|
||||||
|
"MN": "Mongolia",
|
||||||
|
"ME": "Montenegro",
|
||||||
|
"MS": "Montserrat",
|
||||||
|
"MA": "Morocco",
|
||||||
|
"MZ": "Mozambique",
|
||||||
|
"MM": "Myanmar",
|
||||||
|
"NA": "Namibia",
|
||||||
|
"NR": "Nauru",
|
||||||
|
"NP": "Nepal",
|
||||||
|
"NL": "Netherlands",
|
||||||
|
"AN": "Netherlands Antilles",
|
||||||
|
"NC": "New Caledonia",
|
||||||
|
"NZ": "New Zealand",
|
||||||
|
"NI": "Nicaragua",
|
||||||
|
"NE": "Niger",
|
||||||
|
"NG": "Nigeria",
|
||||||
|
"NU": "Niue",
|
||||||
|
"NF": "Norfolk Island",
|
||||||
|
"MP": "Northern Mariana Islands",
|
||||||
|
"NO": "Norway",
|
||||||
|
"OM": "Oman",
|
||||||
|
"PK": "Pakistan",
|
||||||
|
"PW": "Palau",
|
||||||
|
"PS": "Palestinian Territory, Occupied",
|
||||||
|
"PA": "Panama",
|
||||||
|
"PG": "Papua New Guinea",
|
||||||
|
"PY": "Paraguay",
|
||||||
|
"PE": "Peru",
|
||||||
|
"PH": "Philippines",
|
||||||
|
"PN": "Pitcairn",
|
||||||
|
"PL": "Poland",
|
||||||
|
"PT": "Portugal",
|
||||||
|
"PR": "Puerto Rico",
|
||||||
|
"QA": "Qatar",
|
||||||
|
"RE": "Reunion",
|
||||||
|
"RO": "Romania",
|
||||||
|
"RU": "Russian Federation",
|
||||||
|
"RW": "Rwanda",
|
||||||
|
"BL": "Saint Barthelemy",
|
||||||
|
"SH": "Saint Helena",
|
||||||
|
"KN": "Saint Kitts And Nevis",
|
||||||
|
"LC": "Saint Lucia",
|
||||||
|
"MF": "Saint Martin",
|
||||||
|
"PM": "Saint Pierre And Miquelon",
|
||||||
|
"VC": "Saint Vincent And Grenadines",
|
||||||
|
"WS": "Samoa",
|
||||||
|
"SM": "San Marino",
|
||||||
|
"ST": "Sao Tome And Principe",
|
||||||
|
"SA": "Saudi Arabia",
|
||||||
|
"SN": "Senegal",
|
||||||
|
"RS": "Serbia",
|
||||||
|
"SC": "Seychelles",
|
||||||
|
"SL": "Sierra Leone",
|
||||||
|
"SG": "Singapore",
|
||||||
|
"SK": "Slovakia",
|
||||||
|
"SI": "Slovenia",
|
||||||
|
"SB": "Solomon Islands",
|
||||||
|
"SO": "Somalia",
|
||||||
|
"ZA": "South Africa",
|
||||||
|
"GS": "South Georgia And Sandwich Isl.",
|
||||||
|
"ES": "Spain",
|
||||||
|
"LK": "Sri Lanka",
|
||||||
|
"SD": "Sudan",
|
||||||
|
"SR": "Suriname",
|
||||||
|
"SJ": "Svalbard And Jan Mayen",
|
||||||
|
"SZ": "Swaziland",
|
||||||
|
"SE": "Sweden",
|
||||||
|
"CH": "Switzerland",
|
||||||
|
"SY": "Syrian Arab Republic",
|
||||||
|
"TW": "Taiwan",
|
||||||
|
"TJ": "Tajikistan",
|
||||||
|
"TZ": "Tanzania",
|
||||||
|
"TH": "Thailand",
|
||||||
|
"TL": "Timor-Leste",
|
||||||
|
"TG": "Togo",
|
||||||
|
"TK": "Tokelau",
|
||||||
|
"TO": "Tonga",
|
||||||
|
"TT": "Trinidad And Tobago",
|
||||||
|
"TN": "Tunisia",
|
||||||
|
"TR": "Turkey",
|
||||||
|
"TM": "Turkmenistan",
|
||||||
|
"TC": "Turks And Caicos Islands",
|
||||||
|
"TV": "Tuvalu",
|
||||||
|
"UG": "Uganda",
|
||||||
|
"UA": "Ukraine",
|
||||||
|
"AE": "United Arab Emirates",
|
||||||
|
"GB": "United Kingdom",
|
||||||
|
"US": "United States",
|
||||||
|
"UM": "United States Outlying Islands",
|
||||||
|
"UY": "Uruguay",
|
||||||
|
"UZ": "Uzbekistan",
|
||||||
|
"VU": "Vanuatu",
|
||||||
|
"VE": "Venezuela",
|
||||||
|
"VN": "Vietnam",
|
||||||
|
"VG": "Virgin Islands, British",
|
||||||
|
"VI": "Virgin Islands, U.S.",
|
||||||
|
"WF": "Wallis And Futuna",
|
||||||
|
"EH": "Western Sahara",
|
||||||
|
"YE": "Yemen",
|
||||||
|
"ZM": "Zambia",
|
||||||
|
"ZW": "Zimbabwe"
|
||||||
|
}
|
||||||
4443
public/data/MapChart_Map.svg
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
@@ -1354,7 +1354,23 @@
|
|||||||
],
|
],
|
||||||
"brand": "Dalnoboy Service",
|
"brand": "Dalnoboy Service",
|
||||||
"colors": {
|
"colors": {
|
||||||
"orange": "#ee7800"
|
"orange": "#ee7800",
|
||||||
|
"black": "#000000",
|
||||||
|
"white": "#ffffff"
|
||||||
|
},
|
||||||
|
"targets": {
|
||||||
|
"main": "path"
|
||||||
|
},
|
||||||
|
"sets": {
|
||||||
|
"orange": {
|
||||||
|
"main": "orange"
|
||||||
|
},
|
||||||
|
"black": {
|
||||||
|
"main": "black"
|
||||||
|
},
|
||||||
|
"white": {
|
||||||
|
"main": "white"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -2524,7 +2540,40 @@
|
|||||||
"tags": [
|
"tags": [
|
||||||
"furniture"
|
"furniture"
|
||||||
],
|
],
|
||||||
"brand": "Nikamebel"
|
"brand": "Nikamebel",
|
||||||
|
"colors": {
|
||||||
|
"orange": "#fbc02d",
|
||||||
|
"sandstone": "#CEB17D",
|
||||||
|
"black": "#000000",
|
||||||
|
"transparent": "none"
|
||||||
|
},
|
||||||
|
"targets": {
|
||||||
|
"nika": "#nika-path",
|
||||||
|
"mebli": "#ua-group",
|
||||||
|
"mebel": "#ru-group"
|
||||||
|
},
|
||||||
|
"sets": {
|
||||||
|
"orange-ru": {
|
||||||
|
"nika": "orange",
|
||||||
|
"mebli": "transparent",
|
||||||
|
"mebel": "black"
|
||||||
|
},
|
||||||
|
"sandstone-ru": {
|
||||||
|
"nika": "sandstone",
|
||||||
|
"mebli": "transparent",
|
||||||
|
"mebel": "black"
|
||||||
|
},
|
||||||
|
"orange-ua": {
|
||||||
|
"nika": "orange",
|
||||||
|
"mebli": "black",
|
||||||
|
"mebel": "transparent"
|
||||||
|
},
|
||||||
|
"sandstone-ua": {
|
||||||
|
"nika": "sandstone",
|
||||||
|
"mebli": "black",
|
||||||
|
"mebel": "transparent"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "nova_post",
|
"name": "nova_post",
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 148 KiB After Width: | Height: | Size: 223 KiB |
1473
public/data/world_prev.svg
Normal file
|
After Width: | Height: | Size: 155 KiB |
562
public/data/worldmap.svg
Normal file
|
After Width: | Height: | Size: 2.1 MiB |
24
public/data/worldmap_orig.svg
Normal file
|
After Width: | Height: | Size: 2.2 MiB |
@@ -1,13 +1,34 @@
|
|||||||
<svg width="100%" height="100%" viewBox="0 0 2786 400" xmlns="http://www.w3.org/2000/svg">
|
<svg width="100%" height="100%" viewBox="0 0 11667 1667" version="1.1" xmlns="http://www.w3.org/2000/svg">
|
||||||
<g id="g4138">
|
<path id="nika-path" fill="#fbc02d"
|
||||||
<path id="path4150" fill="#fbc02d" stroke="none"
|
d="M0,1666.6l0,-1664.03l202.677,-0c139.101,-0 207.738,13.06 218.811,41.636c41.636,107.456 732.281,1026.96 763.553,1016.57c24.329,-8.082 35.521,-176.667 35.521,-535.037l-0,-523.237l364.583,0.069l0,1664.03l-220.805,0c-192.917,0 -225.373,-8.454 -256.979,-66.94c-98.195,-181.708 -674.291,-939.421 -706.189,-928.819c-25.57,8.498 -36.588,160.279 -36.588,503.994l-0,491.834l-182.292,-0.035l-182.292,-0.034Zm2013.13,-0.004l0,-1664.03l396.287,-0l-0,1664.03l-396.287,0Zm792.573,0l-0,-1664.03l396.286,-0l0,361.062c0,198.584 8.816,361.062 19.592,361.062c28.697,0 212.951,-161.236 526.36,-460.603l276.477,-264.09l250.688,9.134l250.688,9.133l-343.638,298.269c-189.002,164.048 -344.195,321.384 -344.874,349.637c-0.679,28.253 163.451,264.905 364.733,525.895l365.967,474.526l-498.754,0l-251.328,-368.911c-138.23,-202.901 -268.508,-368.911 -289.506,-368.911c-20.998,-0 -103.029,53.685 -182.292,119.3l-144.113,119.3l0,499.222l-396.286,0Zm1948.21,-243.324c64.057,-133.829 236.832,-508.095 383.947,-831.704l267.483,-588.379l3130.66,-0.309l3130.66,-0.309l-0,1664.03l-5482.84,-0l-68.735,-158.237c-37.805,-87.03 -94.329,-171.801 -125.609,-188.38c-74.477,-39.474 -682.468,-39.047 -757.143,0.532c-31.832,16.871 -83.645,101.642 -115.14,188.38l-57.262,157.705l-422.489,-0l116.465,-243.324l-0,-0Zm1096.99,-424.818c19.132,-49.375 -190.332,-534.869 -237.483,-550.434c-35.927,-11.86 -216.393,358.022 -246.051,504.301l-17.505,86.341l242.73,0c168.036,0 247.523,-12.373 258.309,-40.208Z" />
|
||||||
d="M -6.561563 194.457001 L -6.561563 0.125549 L 42.063084 0.125549 C 75.435112 0.125549 91.90197 3.176025 94.55838 9.850372 C 104.547394 34.948578 270.241608 249.715546 277.744019 247.289246 C 283.58078 245.401611 286.265991 206.025452 286.265991 122.321838 L 286.265991 0.110474 L 329.999908 0.1185 L 373.733917 0.126526 L 373.733917 194.457993 L 373.733917 388.789398 L 320.760162 388.789398 C 274.476929 388.789398 266.690277 386.814728 259.107666 373.154388 C 235.549438 330.713257 97.337158 153.735779 89.684387 156.211914 C 83.549988 158.196823 80.906425 193.648026 80.906425 273.928772 L 80.906425 388.805389 L 37.172432 388.797333 L -6.561563 388.789307 L -6.561563 194.457886 Z M 476.413635 194.457001 L 476.413635 0.125549 L 523.950623 0.125549 L 571.487549 0.125549 L 571.487549 194.457001 L 571.487549 388.788422 L 523.950623 388.788422 L 476.413635 388.788422 L 476.413635 194.457001 Z M 666.56134 194.457001 L 666.56134 0.125549 L 714.098328 0.125549 L 761.635254 0.125549 L 761.635254 84.458069 C 761.635254 130.840942 763.750366 168.790588 766.33551 168.790588 C 773.220276 168.790588 817.425171 131.131042 892.615723 61.208527 L 958.945923 -0.474487 L 1019.08905 1.658844 L 1079.232056 3.792175 L 996.789001 73.458191 C 951.445251 111.774475 914.212463 148.523254 914.049561 155.122162 C 913.88678 161.721039 953.263428 216.995483 1001.553406 277.954285 L 1089.353394 388.788422 L 1029.524902 388.788422 L 969.696411 388.788422 L 909.399719 302.62262 C 876.236633 255.231415 844.981384 216.456787 839.943665 216.456787 C 834.906006 216.456787 815.225708 228.995895 796.209656 244.321503 L 761.635254 272.186218 L 761.635254 330.487335 L 761.635254 388.788422 L 714.098328 388.788422 L 666.56134 388.788422 L 666.56134 194.457001 Z M 1133.959961 331.955658 C 1149.328003 300.697632 1190.778931 213.280945 1226.073608 137.696411 L 1290.24585 0.269928 L 2041.329102 0.197784 L 2792.412598 0.125549 L 2792.412598 194.457001 L 2792.412598 388.788391 L 2134.712891 388.788391 L 1477.013428 388.788391 L 1460.522949 351.829437 C 1451.453247 331.502014 1437.892456 311.702209 1430.387939 307.829895 C 1412.519897 298.609924 1266.655518 298.709564 1248.740112 307.953949 C 1241.103271 311.894501 1228.672852 331.694336 1221.116821 351.953522 L 1207.378784 388.788391 L 1156.698608 388.788391 L 1106.018555 388.788391 L 1133.959961 331.955627 Z M 1397.140747 232.731873 C 1401.730713 221.199402 1351.477661 107.80365 1340.165771 104.168182 C 1331.546387 101.398102 1288.250244 187.790558 1281.135132 221.956726 L 1276.935425 242.123199 L 1335.169189 242.123199 C 1375.483154 242.123199 1394.553101 239.233276 1397.140747 232.731873 Z" />
|
<g id="ru-group" fill="none">
|
||||||
<text xml:space="preserve"
|
<path
|
||||||
style="font-style:normal;font-weight:normal;font-size:397.18811035px;line-height:125%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
d="M7038.77,1336.34c-10.536,-31.46 -24.496,-71.44 -41.88,-119.941c-17.384,-48.501 -36.086,-100.935 -56.104,-157.301c-20.018,-56.366 -41.354,-114.37 -64.006,-174.013c-22.652,-59.643 -43.988,-116.009 -64.006,-169.098c-20.018,-53.089 -38.719,-100.607 -56.104,-142.554c-17.384,-41.946 -31.344,-74.062 -41.88,-96.346c-11.59,154.679 -21.072,322.138 -28.447,502.378c-7.375,180.24 -13.697,362.119 -18.965,545.636l-150.137,-0c4.214,-117.976 8.955,-236.934 14.223,-356.876c5.268,-119.941 11.326,-237.916 18.175,-353.925c6.848,-116.009 14.223,-229.069 22.125,-339.179c7.902,-110.111 16.594,-214.322 26.077,-312.635l134.333,0c28.447,57.677 59.001,125.84 91.663,204.491c32.661,78.65 65.323,160.905 97.984,246.765c32.662,85.859 64.27,171.719 94.824,257.579c30.554,85.86 58.474,164.182 83.761,234.967c25.286,-70.785 53.206,-149.107 83.761,-234.967c30.554,-85.86 62.162,-171.72 94.823,-257.579c32.662,-85.86 65.323,-168.115 97.985,-246.765c32.661,-78.651 63.215,-146.814 91.662,-204.491l134.334,0c35.822,439.13 62.689,893.335 80.6,1362.62l-150.137,-0c-5.268,-183.517 -11.59,-365.396 -18.965,-545.636c-7.375,-180.24 -16.858,-347.699 -28.447,-502.378c-10.536,22.284 -24.496,54.4 -41.881,96.346c-17.384,41.947 -36.085,89.465 -56.104,142.554c-20.018,53.089 -41.353,109.455 -64.006,169.098c-22.652,59.643 -43.987,117.647 -64.005,174.013c-20.019,56.366 -38.72,108.8 -56.104,157.301c-17.385,48.501 -31.345,88.481 -41.881,119.941l-123.271,0Z"
|
||||||
x="246" y="309" transform="scale(0.95881214,1.0429572)">
|
style="fill-rule:nonzero;" />
|
||||||
<tspan id="tspan4168" x="246" y="309"
|
<path
|
||||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Ubuntu;-inkscape-font-specification:Ubuntu">
|
d="M7923.79,1525.11l-0,-1362.62l668.506,0l0,163.199l-515.208,0l-0,405.049l458.314,-0l0,159.266l-458.314,0l-0,471.902l554.718,-0l0,163.199l-708.016,-0Z"
|
||||||
MEBEL</tspan>
|
style="fill-rule:nonzero;" />
|
||||||
</text>
|
<path
|
||||||
</g>
|
d="M9121.73,1536.9c-22.125,0 -46.095,-0.655 -71.908,-1.966c-25.813,-1.311 -51.626,-3.277 -77.439,-5.899c-25.813,-2.621 -51.363,-5.899 -76.649,-9.831c-25.286,-3.933 -48.466,-9.176 -69.537,-15.73l-0,-1321.32c21.071,-6.555 44.251,-11.798 69.537,-15.73c25.286,-3.933 50.836,-7.21 76.649,-9.832c25.813,-2.621 51.363,-4.588 76.649,-5.898c25.286,-1.311 48.992,-1.967 71.118,-1.967c63.215,0 122.48,5.899 177.794,17.697c55.314,11.797 103.252,31.787 143.816,59.97c40.563,28.183 72.435,65.215 95.614,111.094c23.179,45.879 34.768,102.245 34.768,169.098c0,74.717 -14.223,135.999 -42.67,183.845c-28.447,47.845 -66.377,83.565 -113.789,107.16c64.27,23.595 115.369,60.954 153.298,112.077c37.93,51.123 56.895,123.219 56.895,216.288c-0,136.327 -40.3,238.9 -120.9,307.719c-80.601,68.819 -208.349,103.228 -383.246,103.228Zm-143.816,-646.898l0,479.767c11.59,1.31 25.287,2.621 41.091,3.932c13.696,1.311 29.764,2.294 48.202,2.949c18.437,0.656 39.773,0.984 64.005,0.984c45.305,-0 88.239,-3.605 128.803,-10.815c40.563,-7.209 76.122,-19.99 106.676,-38.342c30.554,-18.351 55.05,-43.913 73.488,-76.684c18.438,-32.771 27.657,-74.062 27.657,-123.874c0,-44.568 -6.848,-82.255 -20.545,-113.059c-13.697,-30.805 -33.451,-55.383 -59.265,-73.735c-25.813,-18.352 -56.63,-31.46 -92.453,-39.325c-35.822,-7.865 -75.858,-11.798 -120.109,-11.798l-197.55,0Zm0,-153.367l161.2,-0c37.93,-0 73.752,-3.278 107.467,-9.832c33.715,-6.554 62.953,-18.351 87.712,-35.392c24.76,-17.041 44.251,-39.325 58.475,-66.853c14.223,-27.528 21.335,-62.265 21.335,-104.212c-0,-39.325 -7.375,-72.423 -22.126,-99.295c-14.75,-26.873 -35.032,-48.501 -60.845,-64.887c-25.813,-16.385 -56.104,-28.51 -90.872,-36.376c-34.769,-7.865 -71.645,-11.797 -110.628,-11.797c-38.983,-0 -69.537,0.655 -91.663,1.966c-22.125,1.311 -42.144,3.277 -60.055,5.899l0,420.779Z"
|
||||||
</svg>
|
style="fill-rule:nonzero;" />
|
||||||
|
<path
|
||||||
|
d="M9842.39,1525.11l-0,-1362.62l668.506,0l0,163.199l-515.208,0l-0,405.049l458.314,-0l-0,159.266l-458.314,0l-0,471.902l554.718,-0l-0,163.199l-708.016,-0Z"
|
||||||
|
style="fill-rule:nonzero;" />
|
||||||
|
<path d="M11403.8,1359.94l0,165.166l-659.024,-0l0,-1362.62l153.298,0l0,1197.45l505.726,0Z"
|
||||||
|
style="fill-rule:nonzero;" />
|
||||||
|
</g>
|
||||||
|
<g id="ua-group" fill="black">
|
||||||
|
<path
|
||||||
|
d="M7185.19,1329.23c-11.729,-31.46 -27.271,-71.441 -46.624,-119.942c-19.354,-48.5 -40.174,-100.934 -62.46,-157.3c-22.286,-56.366 -46.038,-114.37 -71.256,-174.013c-25.219,-59.644 -48.971,-116.01 -71.257,-169.098c-22.286,-53.089 -43.106,-100.607 -62.459,-142.554c-19.354,-41.947 -34.896,-74.062 -46.625,-96.346c-12.902,154.678 -23.459,322.138 -31.67,502.378c-8.21,180.24 -15.248,362.118 -21.113,545.635l-167.145,0c4.692,-117.975 9.97,-236.933 15.835,-356.875c5.865,-119.941 12.609,-237.917 20.234,-353.926c7.624,-116.009 15.834,-229.068 24.631,-339.179c8.798,-110.11 18.474,-214.321 29.031,-312.634l149.551,-0c31.669,57.677 65.685,125.84 102.046,204.49c36.361,78.651 72.723,160.906 109.084,246.765c36.361,85.86 71.55,171.72 105.565,257.58c34.016,85.86 65.099,164.182 93.25,234.967c28.15,-70.785 59.233,-149.107 93.249,-234.967c34.015,-85.86 69.204,-171.72 105.565,-257.58c36.362,-85.859 72.723,-168.114 109.084,-246.765c36.362,-78.65 70.377,-146.813 102.047,-204.49l149.55,-0c39.881,439.13 69.791,893.335 89.731,1362.61l-167.145,0c-5.865,-183.517 -12.903,-365.395 -21.113,-545.635c-8.211,-180.24 -18.767,-347.7 -31.67,-502.378c-11.729,22.284 -27.271,54.399 -46.624,96.346c-19.354,41.947 -40.174,89.465 -62.46,142.554c-22.286,53.088 -46.038,109.454 -71.256,169.098c-25.219,59.643 -48.971,117.647 -71.257,174.013c-22.286,56.366 -43.106,108.8 -62.459,157.3c-19.354,48.501 -34.896,88.482 -46.625,119.942l-137.235,-0Z"
|
||||||
|
style="fill-rule:nonzero;" />
|
||||||
|
<path
|
||||||
|
d="M8170.47,1517.99l-0,-1362.61l744.235,-0l-0,163.199l-573.571,0l-0,405.049l510.232,-0l-0,159.266l-510.232,0l-0,471.901l617.556,0l0,163.199l-788.22,0Z"
|
||||||
|
style="fill-rule:nonzero;" />
|
||||||
|
<path
|
||||||
|
d="M9504.11,1529.79c-24.632,-0 -51.317,-0.655 -80.054,-1.966c-28.737,-1.311 -57.474,-3.277 -86.212,-5.899c-28.737,-2.622 -57.181,-5.899 -85.332,-9.831c-28.15,-3.933 -53.955,-9.176 -77.414,-15.73l-0,-1321.32c23.459,-6.554 49.264,-11.797 77.414,-15.73c28.151,-3.932 56.595,-7.209 85.332,-9.831c28.738,-2.622 57.182,-4.588 85.332,-5.899c28.151,-1.311 54.542,-1.966 79.174,-1.966c70.377,0 136.355,5.899 197.935,17.696c61.58,11.798 114.949,31.788 160.107,59.971c45.159,28.183 80.64,65.214 106.445,111.094c25.805,45.879 38.707,102.245 38.707,169.097c0,74.718 -15.834,136 -47.504,183.845c-31.669,47.846 -73.896,83.566 -126.678,107.161c71.55,23.595 128.438,60.954 170.664,112.077c42.226,51.122 63.339,123.218 63.339,216.288c-0,136.327 -44.865,238.9 -134.596,307.719c-89.73,68.819 -231.95,103.228 -426.659,103.228Zm-160.108,-646.898l0,479.766c12.903,1.311 28.151,2.622 45.745,3.933c15.249,1.311 33.136,2.294 53.663,2.949c20.526,0.656 44.278,0.983 71.256,0.983c50.437,0 98.234,-3.604 143.393,-10.814c45.158,-7.21 84.745,-19.99 118.761,-38.342c34.015,-18.352 61.286,-43.913 81.813,-76.684c20.526,-32.771 30.79,-74.062 30.79,-123.874c-0,-44.568 -7.624,-82.255 -22.873,-113.06c-15.248,-30.804 -37.241,-55.383 -65.978,-73.734c-28.737,-18.352 -63.046,-31.46 -102.926,-39.325c-39.88,-7.865 -84.452,-11.798 -133.716,-11.798l-219.928,0Zm0,-153.368l179.461,0c42.226,0 82.107,-3.277 119.641,-9.831c37.534,-6.554 70.083,-18.352 97.648,-35.393c27.564,-17.04 49.263,-39.325 65.098,-66.852c15.835,-27.528 23.752,-62.265 23.752,-104.212c0,-39.325 -8.21,-72.423 -24.631,-99.296c-16.422,-26.872 -39.001,-48.501 -67.738,-64.886c-28.737,-16.385 -62.46,-28.511 -101.167,-36.376c-38.707,-7.865 -79.76,-11.797 -123.159,-11.797c-43.399,-0 -77.415,0.655 -102.047,1.966c-24.632,1.311 -46.918,3.277 -66.858,5.899l0,420.778Z"
|
||||||
|
style="fill-rule:nonzero;" />
|
||||||
|
<path d="M11040.1,1352.83l-0,165.165l-733.679,0l0,-1362.61l170.664,-0l0,1197.45l563.015,0Z"
|
||||||
|
style="fill-rule:nonzero;" />
|
||||||
|
<rect x="11219.5" y="155.38" width="170.664" height="1362.62" style="fill-rule:nonzero;" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 9.3 KiB |
492
scripts/cleanup_worldmap.py
Normal file
@@ -0,0 +1,492 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
SVG cleanup utility for worldmap.svg
|
||||||
|
|
||||||
|
This script performs a series of conservative, text-based transforms on
|
||||||
|
an SVG file to normalize path tags, strip unwanted attributes, add
|
||||||
|
data-iso attributes when possible, and pretty-print the result.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python3 scripts/cleanup_worldmap.py [--in-place]
|
||||||
|
|
||||||
|
By default the script does a dry run and prints a small preview. Use
|
||||||
|
--in-place to overwrite the file (a timestamped backup will be created).
|
||||||
|
"""
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
import argparse
|
||||||
|
import datetime
|
||||||
|
from xml.dom import minidom
|
||||||
|
from xml.parsers.expat import ExpatError
|
||||||
|
import xml.etree.ElementTree as ET
|
||||||
|
from xml.dom import Node
|
||||||
|
import json
|
||||||
|
import unicodedata
|
||||||
|
|
||||||
|
FILE_PATH = Path("public/data/worldmap.svg")
|
||||||
|
ISO_JSON = FILE_PATH.parent / "ISO3166-1.json"
|
||||||
|
|
||||||
|
def read_text(p: Path) -> str:
|
||||||
|
"""Read a text file using UTF-8 and return its contents as a string."""
|
||||||
|
return p.read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
def write_text(p: Path, s: str) -> None:
|
||||||
|
"""Write the given string to path using UTF-8 encoding."""
|
||||||
|
p.write_text(s, encoding="utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
def backup(p: Path) -> Path:
|
||||||
|
"""Create a timestamped backup of path and return the backup Path."""
|
||||||
|
ts = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||||
|
bak = p.with_suffix(p.suffix + f".bak.{ts}")
|
||||||
|
bak.write_text(p.read_text(encoding="utf-8"), encoding="utf-8")
|
||||||
|
return bak
|
||||||
|
|
||||||
|
|
||||||
|
def validate_xml(s: str) -> (bool, str):
|
||||||
|
"""Validate that the provided string is well-formed XML using minidom.
|
||||||
|
|
||||||
|
Returns (True, "OK") on success or (False, error_message) on failure.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
minidom.parseString(s)
|
||||||
|
return True, "OK"
|
||||||
|
except ExpatError as e:
|
||||||
|
return False, str(e)
|
||||||
|
except Exception as e:
|
||||||
|
return False, str(e)
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_name(s: str) -> str:
|
||||||
|
"""Normalize a country name for loose matching.
|
||||||
|
|
||||||
|
Removes diacritics, lowercases, replaces punctuation with spaces and
|
||||||
|
collapses runs of whitespace.
|
||||||
|
"""
|
||||||
|
if not s:
|
||||||
|
return ""
|
||||||
|
s = unicodedata.normalize("NFKD", s)
|
||||||
|
s = "".join(ch for ch in s if not unicodedata.combining(ch))
|
||||||
|
s = s.lower()
|
||||||
|
s = s.replace("&", " and ")
|
||||||
|
s = re.sub(r"[^a-z0-9]+", " ", s)
|
||||||
|
return re.sub(r"\s+", " ", s).strip()
|
||||||
|
|
||||||
|
def extract_inner_svg(svg: str) -> str:
|
||||||
|
"""If the file contains a nested <svg>, extract and return the inner SVG block.
|
||||||
|
|
||||||
|
This helps when the source file wraps the actual map inside an outer svg.
|
||||||
|
"""
|
||||||
|
m = re.search(r"<svg\b", svg, flags=re.IGNORECASE)
|
||||||
|
if not m:
|
||||||
|
return svg
|
||||||
|
first = m.start()
|
||||||
|
m2 = re.search(r"<svg\b", svg[first + 1 :], flags=re.IGNORECASE)
|
||||||
|
if not m2:
|
||||||
|
return svg
|
||||||
|
inner_open = first + 1 + m2.start()
|
||||||
|
patt = re.compile(r"</?svg\b", flags=re.IGNORECASE)
|
||||||
|
depth = 0
|
||||||
|
for match in patt.finditer(svg, inner_open):
|
||||||
|
token = match.group(0)
|
||||||
|
if token.lower().startswith("<svg"):
|
||||||
|
depth += 1
|
||||||
|
else:
|
||||||
|
depth -= 1
|
||||||
|
if depth == 0:
|
||||||
|
gt = svg.find('>', match.start())
|
||||||
|
if gt == -1:
|
||||||
|
return svg
|
||||||
|
return svg[inner_open: gt + 1]
|
||||||
|
return svg
|
||||||
|
|
||||||
|
|
||||||
|
def collapse_path_tags(svg: str) -> str:
|
||||||
|
"""Replace full <path>...</path> pairs with compact self-closing <path ... /> tags."""
|
||||||
|
return re.sub(r"<path\b([^>]*)>\s*</path\s*>", r"<path\1 />", svg, flags=re.IGNORECASE | re.DOTALL)
|
||||||
|
|
||||||
|
|
||||||
|
def split_attributes_multiline(svg: str) -> str:
|
||||||
|
"""Format attributes of <path> and <g> tags so each attribute appears on its own indented line.
|
||||||
|
|
||||||
|
This is purely for editor readability; it doesn't change element names or attribute values.
|
||||||
|
"""
|
||||||
|
# attributes that can be very long and benefit from value-wrapping
|
||||||
|
LONG_ATTRS = {"d", "points", "style"}
|
||||||
|
MAX_WIDTH = 120
|
||||||
|
|
||||||
|
attr_pair_re = re.compile(r'([A-Za-z_:][-A-Za-z0-9_:.]*)\s*=\s*"([^"]*)"', flags=re.DOTALL)
|
||||||
|
|
||||||
|
def wrap_value(name: str, val: str) -> str:
|
||||||
|
"""Wrap long attribute values into newline-separated chunks inside the quotes.
|
||||||
|
|
||||||
|
We break on spaces to avoid splitting tokens. Returns the wrapped value (no surrounding quotes).
|
||||||
|
"""
|
||||||
|
if not val:
|
||||||
|
return val
|
||||||
|
if name not in LONG_ATTRS or len(val) <= MAX_WIDTH:
|
||||||
|
return val
|
||||||
|
parts = val.split()
|
||||||
|
lines = []
|
||||||
|
cur = []
|
||||||
|
for p in parts:
|
||||||
|
if cur and len(" ".join(cur + [p])) > MAX_WIDTH:
|
||||||
|
lines.append(" ".join(cur))
|
||||||
|
cur = [p]
|
||||||
|
else:
|
||||||
|
cur.append(p)
|
||||||
|
if cur:
|
||||||
|
lines.append(" ".join(cur))
|
||||||
|
# indent wrapped lines with two spaces so they align under attribute
|
||||||
|
return "\n ".join(lines)
|
||||||
|
|
||||||
|
def repl(m):
|
||||||
|
tag = m.group(1)
|
||||||
|
attrs = m.group(2) or ""
|
||||||
|
closing = m.group(3) or ">"
|
||||||
|
attrs = attrs.strip()
|
||||||
|
if not attrs:
|
||||||
|
return f"<{tag}{closing}"
|
||||||
|
|
||||||
|
pieces = []
|
||||||
|
for am in attr_pair_re.finditer(attrs):
|
||||||
|
aname = am.group(1)
|
||||||
|
aval = am.group(2)
|
||||||
|
wval = wrap_value(aname, aval)
|
||||||
|
if "\n" in wval:
|
||||||
|
# keep newline inside quoted value; indent continuation lines
|
||||||
|
piece = f'{aname}="{wval}"'
|
||||||
|
else:
|
||||||
|
piece = f'{aname}="{wval}"'
|
||||||
|
pieces.append(piece)
|
||||||
|
|
||||||
|
# keep any remaining raw text (rare) appended
|
||||||
|
tail = attr_pair_re.sub("", attrs).strip()
|
||||||
|
if tail:
|
||||||
|
pieces.append(tail)
|
||||||
|
|
||||||
|
lines = [f"<{tag}"]
|
||||||
|
for p in pieces:
|
||||||
|
# if the attribute value contains internal newlines, ensure it's indented properly
|
||||||
|
if "\n" in p:
|
||||||
|
# split first line and continuation
|
||||||
|
idx = p.find('="')
|
||||||
|
name = p[:idx]
|
||||||
|
val = p[idx+2:-1]
|
||||||
|
first, *rest = val.split('\n')
|
||||||
|
lines.append(f" {name}=\"{first}\"")
|
||||||
|
for r in rest:
|
||||||
|
lines.append(f" {r}")
|
||||||
|
else:
|
||||||
|
lines.append(f" {p}")
|
||||||
|
|
||||||
|
lines.append(closing)
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
return re.sub(r"<(path|g)\b([^>]*)\s*(/?>)", repl, svg, flags=re.IGNORECASE | re.DOTALL)
|
||||||
|
|
||||||
|
|
||||||
|
def add_svg_attributes(svg: str) -> str:
|
||||||
|
"""Ensure the root <svg> tag has fill, stroke and stroke-width attributes.
|
||||||
|
|
||||||
|
This updates (or inserts) only the specified attributes on the opening
|
||||||
|
<svg ...> tag and preserves any other existing attributes.
|
||||||
|
"""
|
||||||
|
def repl(m):
|
||||||
|
start, attrs, end = m.group(1), m.group(2), m.group(3)
|
||||||
|
# remove existing occurrences of these specific attributes (only on svg)
|
||||||
|
attrs = re.sub(r"\sfill\s*=\s*\"[^\"]*\"", "", attrs, flags=re.IGNORECASE)
|
||||||
|
attrs = re.sub(r"\sstroke\s*=\s*\"[^\"]*\"", "", attrs, flags=re.IGNORECASE)
|
||||||
|
attrs = re.sub(r"\sstroke-width\s*=\s*\"[^\"]*\"", "", attrs, flags=re.IGNORECASE)
|
||||||
|
attrs = re.sub(r"\s+", " ", attrs).strip()
|
||||||
|
mid = f" {attrs}" if attrs else ""
|
||||||
|
# add/overwrite desired attributes
|
||||||
|
return f"{start}{mid} fill=\"#fff\" stroke=\"#000\" stroke-width=\"0.2\"{end}"
|
||||||
|
|
||||||
|
return re.sub(r"(<svg\b)([^>]*?)(/?>)", repl, svg, flags=re.IGNORECASE | re.DOTALL)
|
||||||
|
|
||||||
|
|
||||||
|
def collapse_newlines_after_svg(svg: str) -> str:
|
||||||
|
"""Ensure there is exactly one newline immediately after the opening <svg> tag.
|
||||||
|
|
||||||
|
This prevents the script from accumulating blank lines between the
|
||||||
|
opening <svg> and the first child element across multiple runs.
|
||||||
|
"""
|
||||||
|
pattern = re.compile(r'(<svg\b[^>]*>)\s*\n+', flags=re.IGNORECASE)
|
||||||
|
return pattern.sub(r'\1\n', svg, count=1)
|
||||||
|
|
||||||
|
|
||||||
|
def strip_whitespace_text_nodes(node):
|
||||||
|
"""Recursively remove text nodes that contain only whitespace from a DOM node.
|
||||||
|
|
||||||
|
This reduces extra blank lines produced by minidom.toprettyxml when
|
||||||
|
the source contains whitespace-only text nodes between elements.
|
||||||
|
"""
|
||||||
|
for child in list(node.childNodes):
|
||||||
|
if child.nodeType == Node.TEXT_NODE:
|
||||||
|
if not child.data.strip():
|
||||||
|
node.removeChild(child)
|
||||||
|
continue
|
||||||
|
if child.hasChildNodes():
|
||||||
|
strip_whitespace_text_nodes(child)
|
||||||
|
|
||||||
|
|
||||||
|
def remove_defs(svg: str) -> str:
|
||||||
|
"""Remove any <defs>...</defs> blocks from the SVG (case-insensitive)."""
|
||||||
|
return re.sub(r"<defs\b[^>]*>.*?</defs\s*>", "", svg, flags=re.IGNORECASE | re.DOTALL)
|
||||||
|
|
||||||
|
|
||||||
|
def remove_data_geo(svg: str) -> str:
|
||||||
|
# only operate on path and g opening tags
|
||||||
|
def repl(m):
|
||||||
|
"""Replace function used to strip data-geo* attributes from a tag match."""
|
||||||
|
start, attrs, end = m.group(1), m.group(2), m.group(3)
|
||||||
|
attrs2 = re.sub(r"\sdata-geo[-\w]*\s*=\s*\"[^\"]*\"", "", attrs, flags=re.IGNORECASE)
|
||||||
|
attrs2 = re.sub(r"\s+", " ", attrs2).strip()
|
||||||
|
return f"{start} {attrs2}{end}" if attrs2 else f"{start}{end}"
|
||||||
|
return re.sub(r"(<(?:path|g)\b)([^>]*?)(/?>)", repl, svg, flags=re.IGNORECASE | re.DOTALL)
|
||||||
|
|
||||||
|
|
||||||
|
def remove_original_strokewidth(svg: str) -> str:
|
||||||
|
"""Remove data-originalStrokeWidth attributes from the SVG text."""
|
||||||
|
return re.sub(r"\sdata-originalStrokeWidth\s*=\s*\"[^\"]*\"", "", svg, flags=re.IGNORECASE)
|
||||||
|
|
||||||
|
|
||||||
|
def uppercase_data_iso(svg: str) -> str:
|
||||||
|
"""Uppercase all data-iso attribute values for consistency."""
|
||||||
|
return re.sub(r'data-iso\s*=\s*"([^\"]*)"', lambda m: f'data-iso="{m.group(1).strip().upper()}"', svg, flags=re.IGNORECASE)
|
||||||
|
|
||||||
|
|
||||||
|
def clear_fill_stroke(svg: str) -> str:
|
||||||
|
"""Remove inline fill, stroke, stroke-width and filter/style entries from path/g tags."""
|
||||||
|
def repl(m):
|
||||||
|
start, attrs, end = m.group(1), m.group(2), m.group(3)
|
||||||
|
# remove explicit attributes
|
||||||
|
attrs = re.sub(r"\s(?:fill|stroke|stroke-width)\s*=\s*\"[^\"]*\"", "", attrs, flags=re.IGNORECASE)
|
||||||
|
# strip fill/stroke/filter from style
|
||||||
|
def style_repl(mm):
|
||||||
|
"""Clean style attribute content by removing fill/stroke/filter entries."""
|
||||||
|
style = mm.group(1)
|
||||||
|
props = [p.strip() for p in style.split(";") if p.strip()]
|
||||||
|
keep = [p for p in props if p.split(":", 1)[0].strip().lower() not in ("fill", "stroke", "filter", "stroke-width")]
|
||||||
|
if not keep:
|
||||||
|
return ""
|
||||||
|
return f'style="{";".join(keep)}"'
|
||||||
|
attrs = re.sub(r'style\s*=\s*"([^"]*)"', style_repl, attrs, flags=re.IGNORECASE)
|
||||||
|
attrs = re.sub(r"\s+", " ", attrs).strip()
|
||||||
|
return f"{start} {attrs}{end}" if attrs else f"{start}{end}"
|
||||||
|
return re.sub(r"(<(?:path|g)\b)([^>]*?)(/?>)", repl, svg, flags=re.IGNORECASE | re.DOTALL)
|
||||||
|
|
||||||
|
|
||||||
|
def remove_empty_groups(svg: str) -> str:
|
||||||
|
"""Remove empty <g>...</g> groups from the SVG to tidy the markup."""
|
||||||
|
return re.sub(r"<g\b([^>]*)>\s*</g>", "", svg, flags=re.IGNORECASE | re.DOTALL)
|
||||||
|
|
||||||
|
|
||||||
|
def add_data_iso(svg: str, iso_path: Path) -> str:
|
||||||
|
"""Try to infer and add data-iso attributes using an ISO JSON mapping.
|
||||||
|
|
||||||
|
Looks at id, name, data-name attributes or inner <title> to guess a country
|
||||||
|
name and maps it to an ISO alpha-2 code using the provided JSON.
|
||||||
|
"""
|
||||||
|
if not iso_path.exists():
|
||||||
|
return svg
|
||||||
|
try:
|
||||||
|
with iso_path.open(encoding="utf-8") as fh:
|
||||||
|
mapping = json.load(fh)
|
||||||
|
except Exception:
|
||||||
|
return svg
|
||||||
|
norm_map = { normalize_name(v): k.upper() for k, v in mapping.items() if v }
|
||||||
|
|
||||||
|
# Prefer an XML-aware edit: parse and modify elements, then serialize.
|
||||||
|
try:
|
||||||
|
root = ET.fromstring(svg)
|
||||||
|
# detect and register default namespace if present so ET doesn't
|
||||||
|
# emit ns0 prefixes when serializing
|
||||||
|
ns_uri = None
|
||||||
|
if isinstance(root.tag, str) and root.tag.startswith('{'):
|
||||||
|
ns_uri = root.tag.split('}')[0].strip('{')
|
||||||
|
elif 'xmlns' in root.attrib:
|
||||||
|
ns_uri = root.attrib.get('xmlns')
|
||||||
|
if ns_uri:
|
||||||
|
ET.register_namespace('', ns_uri)
|
||||||
|
|
||||||
|
# iterate over all elements and handle local tag names (ignore namespace)
|
||||||
|
for elem in root.iter():
|
||||||
|
tag = elem.tag
|
||||||
|
local = tag.split('}', 1)[1] if '}' in tag else tag
|
||||||
|
if local not in ('path', 'g'):
|
||||||
|
continue
|
||||||
|
if 'data-iso' in elem.attrib:
|
||||||
|
continue
|
||||||
|
# candidate sources
|
||||||
|
cand = elem.get('id') or elem.get('name') or elem.get('data-name')
|
||||||
|
if not cand:
|
||||||
|
title = None
|
||||||
|
for child in elem:
|
||||||
|
ctag = child.tag
|
||||||
|
c_local = ctag.split('}', 1)[1] if '}' in ctag else ctag
|
||||||
|
if c_local == 'title' and child.text:
|
||||||
|
title = child.text
|
||||||
|
break
|
||||||
|
cand = title
|
||||||
|
if not cand:
|
||||||
|
continue
|
||||||
|
code = norm_map.get(normalize_name(cand))
|
||||||
|
if code:
|
||||||
|
elem.set('data-iso', code)
|
||||||
|
|
||||||
|
# serialize back to a string; namespace registration prevents ns0 prefixes
|
||||||
|
return ET.tostring(root, encoding='unicode')
|
||||||
|
except Exception:
|
||||||
|
# fallback: keep the original regex-based approach (conservative)
|
||||||
|
# non-self-closing
|
||||||
|
def repl_pair(m):
|
||||||
|
start, attrs, inner = m.group(1), m.group(2), m.group(3)
|
||||||
|
if re.search(r'data-iso\s*=\s*"[^\"]*"', attrs, flags=re.IGNORECASE):
|
||||||
|
return m.group(0)
|
||||||
|
cand = None
|
||||||
|
for pat in (r'id\s*=\s*"([^\"]*)"', r'name\s*=\s*"([^\"]*)"', r'data-name\s*=\s*"([^\"]*)"'):
|
||||||
|
mm = re.search(pat, attrs, flags=re.IGNORECASE)
|
||||||
|
if mm:
|
||||||
|
cand = mm.group(1)
|
||||||
|
break
|
||||||
|
if not cand:
|
||||||
|
t = re.search(r"<title>(.*?)</title>", inner, flags=re.IGNORECASE | re.DOTALL)
|
||||||
|
if t:
|
||||||
|
cand = t.group(1)
|
||||||
|
if not cand:
|
||||||
|
return m.group(0)
|
||||||
|
code = norm_map.get(normalize_name(cand))
|
||||||
|
if not code:
|
||||||
|
return m.group(0)
|
||||||
|
tag = start[1:]
|
||||||
|
attrs_str = attrs.strip()
|
||||||
|
mid = f" {attrs_str}" if attrs_str else ""
|
||||||
|
return f"{start}{mid} data-iso=\"{code}\">{inner}</{tag}>"
|
||||||
|
|
||||||
|
svg = re.sub(r"(<(?:path|g)\b)([^>]*?)>(.*?)</(?:path|g)\s*>", repl_pair, svg, flags=re.IGNORECASE | re.DOTALL)
|
||||||
|
|
||||||
|
# self-closing
|
||||||
|
def repl_self(m):
|
||||||
|
start, attrs, tail = m.group(1), m.group(2), m.group(3)
|
||||||
|
if re.search(r'data-iso\s*=\s*"[^\"]*"', attrs, flags=re.IGNORECASE):
|
||||||
|
return m.group(0)
|
||||||
|
cand = None
|
||||||
|
for pat in (r'id\s*=\s*"([^\"]*)"', r'name\s*=\s*"([^\"]*)"', r'data-name\s*=\s*"([^\"]*)"'):
|
||||||
|
mm = re.search(pat, attrs, flags=re.IGNORECASE)
|
||||||
|
if mm:
|
||||||
|
cand = mm.group(1)
|
||||||
|
break
|
||||||
|
if not cand:
|
||||||
|
return m.group(0)
|
||||||
|
code = norm_map.get(normalize_name(cand))
|
||||||
|
if not code:
|
||||||
|
return m.group(0)
|
||||||
|
attrs_str = attrs.strip()
|
||||||
|
mid = f" {attrs_str}" if attrs_str else ""
|
||||||
|
return f"{start}{mid} data-iso=\"{code}\"{tail}"
|
||||||
|
|
||||||
|
svg = re.sub(r"(<(?:path|g)\b)([^>]*?)(/>)", repl_self, svg, flags=re.IGNORECASE | re.DOTALL)
|
||||||
|
return svg
|
||||||
|
|
||||||
|
# ---------------------- main flow ----------------------
|
||||||
|
|
||||||
|
def main(argv=None):
|
||||||
|
"""CLI entrypoint: parse arguments, run the pipeline and optionally write the file."""
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument("--in-place", action="store_true")
|
||||||
|
parser.add_argument("--file", type=Path, default=FILE_PATH)
|
||||||
|
args = parser.parse_args(argv)
|
||||||
|
|
||||||
|
svg_path = args.file
|
||||||
|
if not svg_path.exists():
|
||||||
|
print("SVG not found:", svg_path)
|
||||||
|
return 2
|
||||||
|
|
||||||
|
original = read_text(svg_path)
|
||||||
|
|
||||||
|
# extract text blocks to protect them
|
||||||
|
text_blocks = []
|
||||||
|
def extract_text(s):
|
||||||
|
"""Extract <text>...</text> blocks and replace them with unique markers."""
|
||||||
|
nonlocal text_blocks
|
||||||
|
pat = re.compile(r"(<text\b[^>]*>.*?</text\s*>)", flags=re.IGNORECASE | re.DOTALL)
|
||||||
|
def r(m):
|
||||||
|
idx = len(text_blocks)
|
||||||
|
text_blocks.append(m.group(1))
|
||||||
|
return f"<!--__TEXT_BLOCK_{idx}__-->"
|
||||||
|
return pat.sub(r, s)
|
||||||
|
|
||||||
|
def restore_text(s):
|
||||||
|
"""Restore previously extracted <text> blocks back into the SVG string."""
|
||||||
|
for i, b in enumerate(text_blocks):
|
||||||
|
s = s.replace(f"<!--__TEXT_BLOCK_{i}__-->", b)
|
||||||
|
return s
|
||||||
|
|
||||||
|
svg = extract_text(original)
|
||||||
|
|
||||||
|
steps = [
|
||||||
|
(extract_inner_svg, "extract_inner_svg"),
|
||||||
|
(add_svg_attributes, "add_svg_attributes"),
|
||||||
|
(collapse_path_tags, "collapse_path_tags"),
|
||||||
|
(remove_defs, "remove_defs"),
|
||||||
|
(lambda s: add_data_iso(s, ISO_JSON), "add_data_iso"),
|
||||||
|
(remove_data_geo, "remove_data_geo"),
|
||||||
|
(remove_original_strokewidth, "remove_original_strokewidth"),
|
||||||
|
(uppercase_data_iso, "uppercase_data_iso"),
|
||||||
|
(clear_fill_stroke, "clear_fill_stroke"),
|
||||||
|
(remove_empty_groups, "remove_empty_groups"),
|
||||||
|
(lambda s: s, "noop_compact"),
|
||||||
|
]
|
||||||
|
|
||||||
|
last_good = svg
|
||||||
|
for func, name in steps:
|
||||||
|
print(f"[stage] {name}")
|
||||||
|
try:
|
||||||
|
svg = func(svg)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"ERROR in {name}: {e}")
|
||||||
|
svg = last_good
|
||||||
|
break
|
||||||
|
ok, msg = validate_xml(svg)
|
||||||
|
if not ok:
|
||||||
|
print(f"Invalid XML after {name}: {msg}")
|
||||||
|
svg = last_good
|
||||||
|
break
|
||||||
|
last_good = svg
|
||||||
|
|
||||||
|
# pretty print
|
||||||
|
print("[stage] pretty_format")
|
||||||
|
try:
|
||||||
|
dom = minidom.parseString(svg)
|
||||||
|
# remove whitespace-only text nodes to avoid accumulating blank lines
|
||||||
|
strip_whitespace_text_nodes(dom)
|
||||||
|
pretty = dom.toprettyxml(indent=" ")
|
||||||
|
# remove xml decl if present
|
||||||
|
if pretty.startswith("<?xml"):
|
||||||
|
pretty = "\n".join(pretty.splitlines()[1:]) + "\n"
|
||||||
|
svg_out = pretty
|
||||||
|
except Exception:
|
||||||
|
svg_out = svg
|
||||||
|
|
||||||
|
# restore text blocks after pretty-format so their internal formatting
|
||||||
|
# is preserved and not mangled by the DOM pass.
|
||||||
|
svg_out = restore_text(svg_out)
|
||||||
|
|
||||||
|
if args.in_place:
|
||||||
|
bak = backup(svg_path)
|
||||||
|
write_text(svg_path, svg_out)
|
||||||
|
print(f"Wrote {svg_path} (backup: {bak})")
|
||||||
|
else:
|
||||||
|
print("Dry run - changes ready. Preview (head):\n")
|
||||||
|
print(svg_out[:2000])
|
||||||
|
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
@@ -166,8 +166,7 @@
|
|||||||
<div class="right-column">
|
<div class="right-column">
|
||||||
{#if logo.tags && logo.tags.some((tagObj) => (tagObj.text || tagObj) === "Country") && logo.meta && logo.meta["ISO code"]}
|
{#if logo.tags && logo.tags.some((tagObj) => (tagObj.text || tagObj) === "Country") && logo.meta && logo.meta["ISO code"]}
|
||||||
<CountryMap
|
<CountryMap
|
||||||
countryCodes={[logo.meta["ISO code"]]}
|
countryCodes={[String(logo.meta["ISO code"]).trim().toUpperCase()]}
|
||||||
countryNames={[logo.meta["country"]]}
|
|
||||||
countryScale={true}
|
countryScale={true}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -317,15 +316,15 @@
|
|||||||
|
|
||||||
.preview-container {
|
.preview-container {
|
||||||
flex: 3;
|
flex: 3;
|
||||||
display: flex;
|
/* display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding: 2rem;
|
padding: 2rem;
|
||||||
background-color: var(--color-card);
|
background-color: var(--color-card); */
|
||||||
color: var(--color-text);
|
color: var(--color-text);
|
||||||
overflow: hidden;
|
/* overflow: hidden;
|
||||||
position: relative;
|
position: relative;
|
||||||
contain: layout style paint; /* CSS containment */
|
contain: layout style paint; */
|
||||||
}
|
}
|
||||||
|
|
||||||
.preview-container img {
|
.preview-container img {
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
import { afterUpdate, onMount } from "svelte";
|
import { afterUpdate, onMount } from "svelte";
|
||||||
import InlineSvg from "./InlineSvg.svelte";
|
import InlineSvg from "./InlineSvg.svelte";
|
||||||
export let countryCodes = [];
|
export let countryCodes = [];
|
||||||
export let countryNames = [];
|
|
||||||
export let forceCenterKey = 0;
|
export let forceCenterKey = 0;
|
||||||
export let mapPath = "/data/world.svg";
|
export let mapPath = "/data/world.svg";
|
||||||
export let countryScale = false;
|
export let countryScale = false;
|
||||||
@@ -58,174 +57,67 @@
|
|||||||
let svgSeen = false; // whether we've already run the initial highlight for the inlined SVG
|
let svgSeen = false; // whether we've already run the initial highlight for the inlined SVG
|
||||||
|
|
||||||
function highlightCountries() {
|
function highlightCountries() {
|
||||||
// backward-compatible signature: allow passing a force flag
|
|
||||||
const args = Array.from(arguments);
|
const args = Array.from(arguments);
|
||||||
const forceCenter = args && args[0] === true;
|
const forceCenter = args && args[0] === true;
|
||||||
if (!wrapperRef) return;
|
if (!wrapperRef) return;
|
||||||
const svgEl = wrapperRef.querySelector("svg");
|
const svgEl = wrapperRef.querySelector("svg");
|
||||||
if (!svgEl) return;
|
if (!svgEl) return;
|
||||||
// Clear previous highlights robustly. Try to restore any saved inline
|
// Simple cleanup: remove any inline fill/stroke/filter and any
|
||||||
// attributes; if they are missing (e.g. due to re-mounts) also remove
|
// data-geo* helper attributes on all path and g elements. We don't
|
||||||
// the highlight styling if present.
|
// keep or restore previous values; just clear styling before
|
||||||
|
// applying new highlight styles.
|
||||||
try {
|
try {
|
||||||
const candidateSelectors = 'path, g, polygon, rect, circle, ellipse, use';
|
const els = Array.from(svgEl.querySelectorAll('path, g'));
|
||||||
const allEls = Array.from(svgEl.querySelectorAll(candidateSelectors));
|
els.forEach((el) => {
|
||||||
allEls.forEach((el) => {
|
|
||||||
try {
|
try {
|
||||||
const prevFill = el.getAttribute('data-geo-prev-fill');
|
// Clear basic inline styling
|
||||||
const prevStroke = el.getAttribute('data-geo-prev-stroke');
|
el.removeAttribute('fill');
|
||||||
const prevFilter = el.getAttribute('data-geo-prev-filter');
|
el.removeAttribute('stroke');
|
||||||
|
if (el.style) el.style.filter = '';
|
||||||
|
|
||||||
// If we saved explicit previous values, restore them (empty string means remove attr)
|
// Remove any attributes that start with 'data-geo'
|
||||||
if (prevFill !== null) {
|
|
||||||
if (prevFill === '') el.removeAttribute('fill');
|
|
||||||
else el.setAttribute('fill', prevFill);
|
|
||||||
} else {
|
|
||||||
// no saved value: if the element currently has the highlight color, remove it
|
|
||||||
try {
|
|
||||||
const curFill = el.getAttribute && el.getAttribute('fill');
|
|
||||||
if (curFill && curFill.toLowerCase() === '#4f8cff') el.removeAttribute('fill');
|
|
||||||
} catch (e) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (prevStroke !== null) {
|
|
||||||
if (prevStroke === '') el.removeAttribute('stroke');
|
|
||||||
else el.setAttribute('stroke', prevStroke);
|
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
const curStroke = el.getAttribute && el.getAttribute('stroke');
|
|
||||||
if (curStroke && curStroke.toLowerCase() === '#222') el.removeAttribute('stroke');
|
|
||||||
} catch (e) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (prevFilter !== null) {
|
|
||||||
if (prevFilter === '') el.style.filter = '';
|
|
||||||
else el.style.filter = prevFilter;
|
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
const cf = el.style && el.style.filter;
|
|
||||||
if (cf && cf.indexOf('4f8cff') !== -1) el.style.filter = '';
|
|
||||||
} catch (e) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clean up helper attributes if present
|
|
||||||
try {
|
try {
|
||||||
el.removeAttribute('data-geo-prev-fill');
|
const attrs = Array.from(el.attributes || []);
|
||||||
el.removeAttribute('data-geo-prev-stroke');
|
attrs.forEach((a) => {
|
||||||
el.removeAttribute('data-geo-prev-filter');
|
try {
|
||||||
el.removeAttribute('data-geo-highlight');
|
if (a && typeof a.name === 'string' && a.name.indexOf('data-geo') === 0) {
|
||||||
|
el.removeAttribute(a.name);
|
||||||
|
}
|
||||||
|
} catch (e) {}
|
||||||
|
});
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
} catch (err) {}
|
} catch (e) {}
|
||||||
});
|
});
|
||||||
} catch (err) {}
|
} catch (err) {}
|
||||||
|
|
||||||
// Highlight by country code (id)
|
// Highlight by country ISO using data-iso attribute only (case-insensitive via uppercasing)
|
||||||
let highlighted = [];
|
let highlighted = [];
|
||||||
countryCodes.forEach((code) => {
|
countryCodes.forEach((code) => {
|
||||||
const countryPath = svgEl.querySelector(`#${code}`);
|
try {
|
||||||
if (countryPath) {
|
if (!code) return;
|
||||||
// Save previous inline attributes so we can restore later
|
const iso = String(code).trim().toUpperCase();
|
||||||
const prevFill = countryPath.getAttribute('fill');
|
// Prefer elements that explicitly declare data-iso
|
||||||
const prevStroke = countryPath.getAttribute('stroke');
|
let countryPath = null;
|
||||||
const prevFilter = countryPath.style.filter || '';
|
try {
|
||||||
if (prevFill !== null) countryPath.setAttribute('data-geo-prev-fill', prevFill);
|
countryPath = svgEl.querySelector(`[data-iso="${iso}"]`);
|
||||||
else countryPath.setAttribute('data-geo-prev-fill', '');
|
} catch (err) {
|
||||||
if (prevStroke !== null) countryPath.setAttribute('data-geo-prev-stroke', prevStroke);
|
// Fallback: try lowercase attribute selector
|
||||||
else countryPath.setAttribute('data-geo-prev-stroke', '');
|
|
||||||
countryPath.setAttribute('data-geo-prev-filter', prevFilter);
|
|
||||||
|
|
||||||
countryPath.setAttribute("fill", "#4f8cff");
|
|
||||||
countryPath.setAttribute("stroke", "#222");
|
|
||||||
countryPath.style.filter = "drop-shadow(0 0 4px #4f8cff44)";
|
|
||||||
countryPath.setAttribute('data-geo-highlight', '1');
|
|
||||||
highlighted.push(countryPath);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
// Highlight by country name (try many fallbacks: attributes, class names, <title> children)
|
|
||||||
const names = Array.isArray(countryNames) ? countryNames : countryNames ? [countryNames] : [];
|
|
||||||
if (names.length > 0) {
|
|
||||||
// candidate elements to check for name matches
|
|
||||||
const candidateSelectors = 'path, g, polygon, rect, circle, ellipse, use';
|
|
||||||
const candidates = Array.from(svgEl.querySelectorAll(candidateSelectors));
|
|
||||||
|
|
||||||
names.forEach((nameRaw) => {
|
|
||||||
if (!nameRaw) return;
|
|
||||||
const name = String(nameRaw).trim();
|
|
||||||
if (!name) return;
|
|
||||||
const nameLower = name.toLowerCase();
|
|
||||||
|
|
||||||
const matched = new Set();
|
|
||||||
|
|
||||||
// First pass: strict attribute/title/id matches (case-insensitive)
|
|
||||||
try {
|
try {
|
||||||
const attrsToCheck = ['name', 'data-name', 'id', 'title', 'inkscape:label'];
|
countryPath = svgEl.querySelector(`[data-iso='${iso.toLowerCase()}']`);
|
||||||
candidates.forEach((el) => {
|
} catch (err2) {
|
||||||
for (const a of attrsToCheck) {
|
countryPath = null;
|
||||||
try {
|
|
||||||
const val = el.getAttribute && el.getAttribute(a);
|
|
||||||
if (val && String(val).trim().toLowerCase() === nameLower) {
|
|
||||||
matched.add(el);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} catch (err) {}
|
|
||||||
}
|
|
||||||
// child <title> element (redundant with 'title' attr check but kept safe)
|
|
||||||
try {
|
|
||||||
const titleEl = el.querySelector && el.querySelector('title');
|
|
||||||
if (titleEl && titleEl.textContent && titleEl.textContent.trim().toLowerCase() === nameLower) {
|
|
||||||
matched.add(el);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} catch (err) {}
|
|
||||||
});
|
|
||||||
} catch (err) {}
|
|
||||||
|
|
||||||
// If we didn't find any strict matches, fall back to class/slug heuristics
|
|
||||||
if (matched.size === 0) {
|
|
||||||
try {
|
|
||||||
// Try direct selectors only when safe
|
|
||||||
try {
|
|
||||||
const byNameAttr = svgEl.querySelectorAll(`[name='${name}']`);
|
|
||||||
byNameAttr.forEach((el) => matched.add(el));
|
|
||||||
} catch (err) {}
|
|
||||||
try {
|
|
||||||
const byClassDot = svgEl.querySelectorAll(`.${name.replace(/\s+/g, '.')}`);
|
|
||||||
byClassDot.forEach((el) => matched.add(el));
|
|
||||||
} catch (err) {}
|
|
||||||
|
|
||||||
// class list heuristics: slug (lower, dashes)
|
|
||||||
candidates.forEach((el) => {
|
|
||||||
try {
|
|
||||||
const cls = (el.className && (typeof el.className === 'string' ? el.className : el.className.baseVal)) || '';
|
|
||||||
if (cls) {
|
|
||||||
const parts = cls.split(/\s+/).map((c) => c.trim()).filter(Boolean);
|
|
||||||
const slug = nameLower.replace(/[^a-z0-9]+/g, '-').replace(/^\-+|\-+$/g, '');
|
|
||||||
if (parts.includes(name) || parts.includes(nameLower) || parts.includes(slug)) matched.add(el);
|
|
||||||
}
|
|
||||||
} catch (err) {}
|
|
||||||
});
|
|
||||||
} catch (err) {}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Apply highlighting to matched elements
|
if (countryPath) {
|
||||||
matched.forEach((countryPath) => {
|
|
||||||
const prevFill = countryPath.getAttribute('fill');
|
|
||||||
const prevStroke = countryPath.getAttribute('stroke');
|
|
||||||
const prevFilter = countryPath.style.filter || '';
|
|
||||||
if (prevFill !== null) countryPath.setAttribute('data-geo-prev-fill', prevFill);
|
|
||||||
else countryPath.setAttribute('data-geo-prev-fill', '');
|
|
||||||
if (prevStroke !== null) countryPath.setAttribute('data-geo-prev-stroke', prevStroke);
|
|
||||||
else countryPath.setAttribute('data-geo-prev-stroke', '');
|
|
||||||
countryPath.setAttribute('data-geo-prev-filter', prevFilter);
|
|
||||||
|
|
||||||
countryPath.setAttribute('fill', '#4f8cff');
|
countryPath.setAttribute('fill', '#4f8cff');
|
||||||
countryPath.setAttribute('stroke', '#222');
|
countryPath.setAttribute('stroke', '#222');
|
||||||
countryPath.style.filter = 'drop-shadow(0 0 4px #4f8cff44)';
|
countryPath.style.filter = 'drop-shadow(0 0 4px #4f8cff44)';
|
||||||
countryPath.setAttribute('data-geo-highlight', '1');
|
countryPath.setAttribute('data-geo-highlight', '1');
|
||||||
highlighted.push(countryPath);
|
highlighted.push(countryPath);
|
||||||
});
|
}
|
||||||
});
|
} catch (err) {}
|
||||||
}
|
});
|
||||||
// Smart scale/center if enabled and at least one country is highlighted
|
// Smart scale/center if enabled and at least one country is highlighted
|
||||||
if (countryScale && highlighted.length > 0) {
|
if (countryScale && highlighted.length > 0) {
|
||||||
// Compute bounding box of all highlighted paths
|
// Compute bounding box of all highlighted paths
|
||||||
|
|||||||
@@ -228,7 +228,7 @@
|
|||||||
|
|
||||||
.svg-wrapper :global(svg) {
|
.svg-wrapper :global(svg) {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: auto;
|
height: 100%;
|
||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
display: block;
|
display: block;
|
||||||
transform-origin: center;
|
transform-origin: center;
|
||||||
|
|||||||
@@ -144,10 +144,6 @@
|
|||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
.game-card.scheduled .play-button {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.coming-soon-text {
|
.coming-soon-text {
|
||||||
margin-top: 1.5rem;
|
margin-top: 1.5rem;
|
||||||
font-size: 1.1rem;
|
font-size: 1.1rem;
|
||||||
@@ -187,38 +183,6 @@
|
|||||||
background: var(--color-primary-dark);
|
background: var(--color-primary-dark);
|
||||||
}
|
}
|
||||||
|
|
||||||
.coming-soon {
|
|
||||||
text-align: center;
|
|
||||||
padding: 2rem;
|
|
||||||
background: var(--color-bg-secondary);
|
|
||||||
border-radius: 12px;
|
|
||||||
border: 1px dashed var(--color-border);
|
|
||||||
}
|
|
||||||
|
|
||||||
.coming-soon h3 {
|
|
||||||
color: var(--color-text-secondary);
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.upcoming-games {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 2rem;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.upcoming-game {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.5rem;
|
|
||||||
color: var(--color-text-secondary);
|
|
||||||
font-size: 0.9rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.upcoming-game .icon {
|
|
||||||
font-size: 1.2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.container {
|
.container {
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
@@ -233,10 +197,5 @@
|
|||||||
font-size: 2.5rem;
|
font-size: 2.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.upcoming-games {
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
gap: 1rem;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -50,10 +50,14 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function isFlagOnMap(flag) {
|
function isFlagOnMap(flag) {
|
||||||
if (!mapParsed) return true; // allow through if we couldn't parse map
|
// Prefer ISO comparison against data-iso values. If map couldn't be parsed, allow.
|
||||||
|
if (!mapParsed) return true;
|
||||||
try {
|
try {
|
||||||
const iso = flag?.meta?.["ISO code"] || flag?.meta?.["ISO Code"] || flag?.meta?.["ISO"] || flag?.ISO;
|
const isoRaw = flag?.meta?.["ISO code"] || flag?.meta?.["ISO Code"] || flag?.meta?.["ISO"] || flag?.ISO;
|
||||||
if (iso && availableIso.has(String(iso).trim())) return true;
|
const iso = isoRaw ? String(isoRaw).trim().toUpperCase() : '';
|
||||||
|
if (iso && availableIso.has(iso)) return true;
|
||||||
|
|
||||||
|
// Fallbacks to name/slug checks remain, but only used if no ISO matches.
|
||||||
const name = getCountryName(flag);
|
const name = getCountryName(flag);
|
||||||
const n = normalizeNameForLookup(name);
|
const n = normalizeNameForLookup(name);
|
||||||
if (n && availableNames.has(n)) return true;
|
if (n && availableNames.has(n)) return true;
|
||||||
@@ -206,8 +210,13 @@
|
|||||||
const all = Array.from(doc.querySelectorAll('*'));
|
const all = Array.from(doc.querySelectorAll('*'));
|
||||||
all.forEach((el) => {
|
all.forEach((el) => {
|
||||||
try {
|
try {
|
||||||
|
// Prefer data-iso attribute for ISO lookup. Normalize to uppercase.
|
||||||
|
const dataIso = el.getAttribute && (el.getAttribute('data-iso') || el.getAttribute('data-ISO'));
|
||||||
|
if (dataIso) availableIso.add(String(dataIso).trim().toUpperCase());
|
||||||
|
// Backwards-compatible: also include id if present (older maps)
|
||||||
const id = el.getAttribute && el.getAttribute('id');
|
const id = el.getAttribute && el.getAttribute('id');
|
||||||
if (id) availableIso.add(String(id).trim());
|
if (id) availableIso.add(String(id).trim().toUpperCase());
|
||||||
|
|
||||||
const nameAttr = el.getAttribute && (el.getAttribute('name') || el.getAttribute('data-name') || el.getAttribute('inkscape:label'));
|
const nameAttr = el.getAttribute && (el.getAttribute('name') || el.getAttribute('data-name') || el.getAttribute('inkscape:label'));
|
||||||
if (nameAttr) availableNames.add(String(nameAttr).trim().toLowerCase());
|
if (nameAttr) availableNames.add(String(nameAttr).trim().toLowerCase());
|
||||||
// title child
|
// title child
|
||||||
@@ -570,18 +579,12 @@
|
|||||||
currentQuestion.correct.meta["ISO code"]) ||
|
currentQuestion.correct.meta["ISO code"]) ||
|
||||||
currentQuestion.correct?.ISO ||
|
currentQuestion.correct?.ISO ||
|
||||||
"",
|
"",
|
||||||
].filter(Boolean)
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.map((c) => (c ? String(c).trim().toUpperCase() : c))
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
// Reactive country names array for CountryMap (fallback when no ISO present)
|
|
||||||
$: countryNames = currentQuestion
|
|
||||||
? [
|
|
||||||
currentQuestion.correct?.name ||
|
|
||||||
(currentQuestion.correct?.meta && currentQuestion.correct.meta.country) ||
|
|
||||||
currentQuestion.correct?.meta?.country ||
|
|
||||||
"",
|
|
||||||
].filter(Boolean)
|
|
||||||
: [];
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
@@ -660,8 +663,7 @@
|
|||||||
|
|
||||||
<div class="map-display">
|
<div class="map-display">
|
||||||
<CountryMap
|
<CountryMap
|
||||||
{countryCodes}
|
{countryCodes}
|
||||||
{countryNames}
|
|
||||||
countryScale={true}
|
countryScale={true}
|
||||||
scalePadding={90}
|
scalePadding={90}
|
||||||
forceCenterKey={questionKey}
|
forceCenterKey={questionKey}
|
||||||
|
|||||||