8 Commits

Author SHA1 Message Date
sHa
4ee8757577 Add color and target definitions for Nikamebel brand in logos.json; update SVG file to new design
Some checks failed
Deploy to GitHub Pages / build-and-deploy (push) Has been cancelled
Deploy to GitHub Pages / deploy (push) Has been cancelled
2025-12-07 15:51:05 +02:00
sHa
1cd6764078 Add CLAUDE.md guidance and add targets/sets for Dalnoboy Service in logos.json
Some checks failed
Deploy to GitHub Pages / build-and-deploy (push) Has been cancelled
Deploy to GitHub Pages / deploy (push) Has been cancelled
2025-10-30 14:43:21 +02:00
sHa
fc172cfa36 Add SVG cleanup utility for worldmap.svg
This commit introduces a new script, cleanup_worldmap.py, which provides a utility for cleaning and normalizing the SVG file worldmap.svg. The script performs various transformations including:

- Normalizing path tags and attributes
- Adding data-iso attributes based on a provided JSON mapping
- Pretty-printing the SVG output
- Creating a backup before overwriting the original file when using the --in-place option

The script is designed for ease of use with command-line arguments and includes error handling for XML validation.
2025-09-01 02:07:34 +03:00
sHa
58ab00f08d Fix layout issue in preview container by adjusting flex display properties 2025-09-01 01:44:36 +03:00
sHa
4dd4317d66 Refactor CardFull and InlineSvg components for improved layout and SVG handling 2025-09-01 01:37:09 +03:00
sHa
fe07f166cf Add ISO 3166-1 country codes JSON file and refactor Game.svelte to remove upcoming games section 2025-08-16 01:35:03 +03:00
sHa
fba47c142c Refactor CountryMap and CardFull components for ISO code handling
- Updated CardFull to ensure ISO codes are trimmed and uppercased before passing to CountryMap.
- Removed unused countryNames prop from CountryMap and adjusted highlighting logic to use data-iso attributes for country identification.
- Enhanced GeographyQuiz to prioritize ISO code checks and normalize values for consistency.
- Cleaned up code by removing legacy attribute handling and ensuring robust attribute management during country highlighting.
2025-08-16 00:01:01 +03:00
sHa
3ee3ffeb17 Implement code changes to enhance functionality and improve performance 2025-08-15 23:09:19 +03:00
16 changed files with 8156 additions and 1173 deletions

6
.gitignore vendored
View File

@@ -43,6 +43,11 @@ Temporary Items
*.sln
*.sw?
# Backup files
*.bak
*.bak.*
*.tmp
# Svelte related
.svelte-kit/
@@ -56,3 +61,4 @@ Temporary Items
# Make favicon generation script executable
chmod +x ./scripts/generate-favicons.js
chmod +x ./scripts/update-data.js
chmod +x ./scripts/*

132
CLAUDE.md Normal file
View 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
View 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

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 1.2 MiB

View File

@@ -1354,7 +1354,23 @@
],
"brand": "Dalnoboy Service",
"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": [
"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",

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 148 KiB

After

Width:  |  Height:  |  Size: 223 KiB

1473
public/data/world_prev.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 155 KiB

562
public/data/worldmap.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 2.1 MiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 2.2 MiB

View File

@@ -1,13 +1,34 @@
<svg width="100%" height="100%" viewBox="0 0 2786 400" xmlns="http://www.w3.org/2000/svg">
<g id="g4138">
<path id="path4150" fill="#fbc02d" stroke="none"
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" />
<text xml:space="preserve"
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"
x="246" y="309" transform="scale(0.95881214,1.0429572)">
<tspan id="tspan4168" x="246" y="309"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Ubuntu;-inkscape-font-specification:Ubuntu">
MEBEL</tspan>
</text>
</g>
<svg width="100%" height="100%" viewBox="0 0 11667 1667" version="1.1" xmlns="http://www.w3.org/2000/svg">
<path id="nika-path" fill="#fbc02d"
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" />
<g id="ru-group" fill="none">
<path
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"
style="fill-rule:nonzero;" />
<path
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"
style="fill-rule:nonzero;" />
<path
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"
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
View 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())

View File

@@ -166,8 +166,7 @@
<div class="right-column">
{#if logo.tags && logo.tags.some((tagObj) => (tagObj.text || tagObj) === "Country") && logo.meta && logo.meta["ISO code"]}
<CountryMap
countryCodes={[logo.meta["ISO code"]]}
countryNames={[logo.meta["country"]]}
countryCodes={[String(logo.meta["ISO code"]).trim().toUpperCase()]}
countryScale={true}
/>
{/if}
@@ -317,15 +316,15 @@
.preview-container {
flex: 3;
display: flex;
/* display: flex;
align-items: center;
justify-content: center;
padding: 2rem;
background-color: var(--color-card);
background-color: var(--color-card); */
color: var(--color-text);
overflow: hidden;
/* overflow: hidden;
position: relative;
contain: layout style paint; /* CSS containment */
contain: layout style paint; */
}
.preview-container img {

View File

@@ -2,7 +2,6 @@
import { afterUpdate, onMount } from "svelte";
import InlineSvg from "./InlineSvg.svelte";
export let countryCodes = [];
export let countryNames = [];
export let forceCenterKey = 0;
export let mapPath = "/data/world.svg";
export let countryScale = false;
@@ -58,174 +57,67 @@
let svgSeen = false; // whether we've already run the initial highlight for the inlined SVG
function highlightCountries() {
// backward-compatible signature: allow passing a force flag
const args = Array.from(arguments);
const forceCenter = args && args[0] === true;
if (!wrapperRef) return;
const svgEl = wrapperRef.querySelector("svg");
if (!svgEl) return;
// Clear previous highlights robustly. Try to restore any saved inline
// attributes; if they are missing (e.g. due to re-mounts) also remove
// the highlight styling if present.
// Simple cleanup: remove any inline fill/stroke/filter and any
// data-geo* helper attributes on all path and g elements. We don't
// keep or restore previous values; just clear styling before
// applying new highlight styles.
try {
const candidateSelectors = 'path, g, polygon, rect, circle, ellipse, use';
const allEls = Array.from(svgEl.querySelectorAll(candidateSelectors));
allEls.forEach((el) => {
const els = Array.from(svgEl.querySelectorAll('path, g'));
els.forEach((el) => {
try {
const prevFill = el.getAttribute('data-geo-prev-fill');
const prevStroke = el.getAttribute('data-geo-prev-stroke');
const prevFilter = el.getAttribute('data-geo-prev-filter');
// Clear basic inline styling
el.removeAttribute('fill');
el.removeAttribute('stroke');
if (el.style) el.style.filter = '';
// If we saved explicit previous values, restore them (empty string means remove attr)
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
// Remove any attributes that start with 'data-geo'
try {
el.removeAttribute('data-geo-prev-fill');
el.removeAttribute('data-geo-prev-stroke');
el.removeAttribute('data-geo-prev-filter');
el.removeAttribute('data-geo-highlight');
const attrs = Array.from(el.attributes || []);
attrs.forEach((a) => {
try {
if (a && typeof a.name === 'string' && a.name.indexOf('data-geo') === 0) {
el.removeAttribute(a.name);
}
} catch (e) {}
});
} catch (e) {}
} catch (err) {}
} catch (e) {}
});
} catch (err) {}
// Highlight by country code (id)
// Highlight by country ISO using data-iso attribute only (case-insensitive via uppercasing)
let highlighted = [];
countryCodes.forEach((code) => {
const countryPath = svgEl.querySelector(`#${code}`);
if (countryPath) {
// Save previous inline attributes so we can restore later
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("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 {
if (!code) return;
const iso = String(code).trim().toUpperCase();
// Prefer elements that explicitly declare data-iso
let countryPath = null;
try {
countryPath = svgEl.querySelector(`[data-iso="${iso}"]`);
} catch (err) {
// Fallback: try lowercase attribute selector
try {
const attrsToCheck = ['name', 'data-name', 'id', 'title', 'inkscape:label'];
candidates.forEach((el) => {
for (const a of attrsToCheck) {
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) {}
countryPath = svgEl.querySelector(`[data-iso='${iso.toLowerCase()}']`);
} catch (err2) {
countryPath = null;
}
}
// Apply highlighting to matched elements
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);
if (countryPath) {
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);
});
});
}
}
} catch (err) {}
});
// Smart scale/center if enabled and at least one country is highlighted
if (countryScale && highlighted.length > 0) {
// Compute bounding box of all highlighted paths

View File

@@ -228,7 +228,7 @@
.svg-wrapper :global(svg) {
width: 100%;
height: auto;
height: 100%;
object-fit: contain;
display: block;
transform-origin: center;

View File

@@ -144,10 +144,6 @@
opacity: 0.5;
}
.game-card.scheduled .play-button {
display: none;
}
.coming-soon-text {
margin-top: 1.5rem;
font-size: 1.1rem;
@@ -187,38 +183,6 @@
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) {
.container {
padding: 1rem;
@@ -233,10 +197,5 @@
font-size: 2.5rem;
}
.upcoming-games {
flex-direction: column;
align-items: center;
gap: 1rem;
}
}
</style>

View File

@@ -50,10 +50,14 @@
}
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 {
const iso = flag?.meta?.["ISO code"] || flag?.meta?.["ISO Code"] || flag?.meta?.["ISO"] || flag?.ISO;
if (iso && availableIso.has(String(iso).trim())) return true;
const isoRaw = flag?.meta?.["ISO code"] || flag?.meta?.["ISO Code"] || flag?.meta?.["ISO"] || flag?.ISO;
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 n = normalizeNameForLookup(name);
if (n && availableNames.has(n)) return true;
@@ -206,8 +210,13 @@
const all = Array.from(doc.querySelectorAll('*'));
all.forEach((el) => {
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');
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'));
if (nameAttr) availableNames.add(String(nameAttr).trim().toLowerCase());
// title child
@@ -570,18 +579,12 @@
currentQuestion.correct.meta["ISO code"]) ||
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>
<svelte:head>
@@ -660,8 +663,7 @@
<div class="map-display">
<CountryMap
{countryCodes}
{countryNames}
{countryCodes}
countryScale={true}
scalePadding={90}
forceCenterKey={questionKey}