feat: new py based generator for md and html

This commit is contained in:
2025-12-15 09:48:10 +02:00
parent a0cf8c245a
commit 2e272e1673
5 changed files with 3184 additions and 239 deletions

30
.github/workflows/generate-listings.yml vendored Normal file
View File

@@ -0,0 +1,30 @@
name: Generate Listings
on:
push:
paths:
- 'emoji/**'
- 'create_listing.py'
branches:
- master
jobs:
generate:
runs-on: ubuntu-latest
steps:
- name: Checkout Repo
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Generate listings
run: python3 create_listing.py
- name: Commit changes
run: |
git config --local user.email "github-actions[bot]@users.noreply.github.com"
git config --local user.name "github-actions[bot]"
git add README.md index.html
git diff --staged --quiet || git commit -m "Update listings"
git push

346
README.md

File diff suppressed because it is too large Load Diff

View File

@@ -1,66 +0,0 @@
<?php
$output = 'README.md';
$per_row = 10;
$files = glob('emoji/*.{png,gif,jpg,jpeg}', GLOB_BRACE);
$listing = [];
$per_row_width = floor(100 / $per_row) . '%';
sort($files);
if (count($files) < 1) {
die('No images to continue with.');
}
function get_basename(string $file)
{
$parts = explode(DIRECTORY_SEPARATOR, $file);
return end($parts);
}
foreach ($files as $file) {
$first = get_basename($file);
$first = str_replace('emoji/', '', $first);
$first = trim($first[0]);
if (preg_match('/([^a-zA-Z:])/', $first)) {
$first = '\[^a-zA-Z:\]';
}
if (!array_key_exists($first, $listing)) {
$listing[$first] = [];
}
$listing[$first][] = $file;
}
$contents = "# Emotes\n\n";
foreach ($listing as $header => $icons) {
$contents .= sprintf("## %s\n\n", $header);
$chunks = array_chunk($icons, $per_row);
$contents .= '<table style="text-align: center;width: 100%">' . "\n";
foreach ($chunks as $chunk_icons) {
$contents .= "<tr>\n";
foreach ($chunk_icons as $icon) {
$file = $icon;
[$name, $ext] = explode('.', get_basename($icon), 2);
$format = '<td style=\'width: %s\'><img width=\'30\' src="%2$s"'
. ' alt="%2$s" title=":%3$s:"></td>';
$contents .= sprintf($format, $per_row_width, $file, $name) . "\n";
}
$contents .= "</tr>\n";
}
$contents .= "</table>\n\n";
}
$contents .= "\n\n Generated: " . date('c');
file_put_contents($output, $contents);

231
create_listing.py Normal file
View File

@@ -0,0 +1,231 @@
#!/usr/bin/env python3
"""Generate README.md and index.html with emoji listings."""
import html
import re
from collections import defaultdict
from datetime import datetime, timezone
from pathlib import Path
from urllib.parse import quote
PER_ROW = 10
EMOJI_DIR = Path("emoji")
EXTENSIONS = (".png", ".gif", ".jpg", ".jpeg")
def generate_readme(files: list[Path]) -> None:
"""Generate README.md with HTML tables of all emoji images."""
listing = defaultdict(list)
for file in files:
first_char = file.name[0].lower()
if not re.match(r"[a-z]", first_char):
first_char = r"\[^a-zA-Z:\]"
listing[first_char].append(file)
per_row_width = f"{100 // PER_ROW}%"
contents = "# Emotes\n\n"
for header in sorted(listing.keys(), key=lambda x: (not x.startswith("\\"), x)):
icons = listing[header]
contents += f"## {header}\n\n"
contents += '<table style="text-align: center;width: 100%">\n'
for i in range(0, len(icons), PER_ROW):
chunk = icons[i:i + PER_ROW]
contents += "<tr>\n"
for icon in chunk:
name = icon.stem
encoded_path = f"emoji/{quote(icon.name)}"
display_path = f"emoji/{icon.name}"
contents += (
f"<td style='width: {per_row_width}'>"
f"<img width='30' src=\"{encoded_path}\" "
f"alt=\"{display_path}\" title=\":{name}:\"></td>\n"
)
contents += "</tr>\n"
contents += "</table>\n\n"
contents += f"\n\n Generated: {datetime.now(timezone.utc).isoformat()}"
Path("README.md").write_text(contents, encoding="utf-8")
print(f"Generated README.md with {len(files)} emojis")
def generate_html(files: list[Path]) -> None:
"""Generate index.html with searchable emoji grid grouped alphabetically."""
# Group files by first character
listing = defaultdict(list)
for file in files:
first_char = file.name[0].lower()
if not re.match(r"[a-z]", first_char):
first_char = "#"
listing[first_char].append(file)
# Build grouped HTML
sections = []
for header in sorted(listing.keys(), key=lambda x: (x != "#", x)):
display_header = "0-9 / Special" if header == "#" else header.upper()
emoji_items = []
for file in listing[header]:
name = file.stem
encoded_path = f"emoji/{quote(file.name)}"
escaped_name = html.escape(name)
emoji_items.append(
f' <div class="emoji" data-keyword="{escaped_name}">'
f'<img src="{encoded_path}" alt="{escaped_name}" title=":{escaped_name}:"></div>'
)
sections.append(
f' <section data-group="{html.escape(header)}">\n'
f' <h2>{display_header}</h2>\n'
f' <div class="grid">\n{chr(10).join(emoji_items)}\n </div>\n'
f' </section>'
)
contents = f'''<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Emotes</title>
<style>
* {{ box-sizing: border-box; }}
body {{
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
margin: 0;
padding: 20px;
background: #1a1a1a;
color: #fff;
}}
#search {{
width: 100%;
max-width: 400px;
padding: 12px 16px;
font-size: 16px;
border: 2px solid #333;
border-radius: 8px;
background: #2a2a2a;
color: #fff;
margin-bottom: 20px;
}}
#search:focus {{
outline: none;
border-color: #666;
}}
#search::placeholder {{
color: #888;
}}
section {{
margin-bottom: 24px;
}}
section.hidden {{
display: none;
}}
h2 {{
font-size: 18px;
font-weight: 600;
margin: 0 0 12px 0;
color: #ccc;
}}
.grid {{
display: grid;
grid-template-columns: repeat(auto-fill, minmax(50px, 1fr));
gap: 8px;
}}
.emoji {{
display: flex;
align-items: center;
justify-content: center;
padding: 8px;
background: #2a2a2a;
border-radius: 6px;
transition: background 0.15s;
}}
.emoji:hover {{
background: #3a3a3a;
}}
.emoji img {{
width: 32px;
height: 32px;
object-fit: contain;
}}
.emoji.hidden {{
display: none;
}}
#count {{
color: #888;
font-size: 14px;
margin-bottom: 12px;
}}
h1 {{
margin: 0 0 20px 0;
font-size: 24px;
}}
h1 a {{
color: #fff;
text-decoration: none;
}}
h1 a:hover {{
text-decoration: underline;
}}
</style>
</head>
<body>
<h1><a href="https://github.com/ivuorinen/emoji">ivuorinen/emoji</a></h1>
<input type="text" id="search" placeholder="Search emojis..." autofocus>
<div id="count">{len(files)} emojis</div>
<div id="content">
{chr(10).join(sections)}
</div>
<script>
let timeout;
const search = document.getElementById('search');
const emojis = document.querySelectorAll('.emoji');
const sections = document.querySelectorAll('section');
const count = document.getElementById('count');
const total = emojis.length;
search.addEventListener('input', function(e) {{
clearTimeout(timeout);
timeout = setTimeout(() => {{
const query = e.target.value.toLowerCase();
let visible = 0;
emojis.forEach(el => {{
const match = el.dataset.keyword.toLowerCase().includes(query);
el.classList.toggle('hidden', !match);
if (match) visible++;
}});
sections.forEach(sec => {{
const hasVisible = sec.querySelector('.emoji:not(.hidden)');
sec.classList.toggle('hidden', !hasVisible);
}});
count.textContent = query ? visible + ' of ' + total + ' emojis' : total + ' emojis';
}}, 150);
}});
</script>
</body>
</html>
'''
Path("index.html").write_text(contents, encoding="utf-8")
print(f"Generated index.html with {len(files)} emojis")
def main():
files = sorted(
f for f in EMOJI_DIR.iterdir()
if f.suffix.lower() in EXTENSIONS
)
if not files:
raise SystemExit("No images to continue with.")
generate_readme(files)
generate_html(files)
if __name__ == "__main__":
main()

2750
index.html Normal file

File diff suppressed because it is too large Load Diff