mirror of
https://github.com/ivuorinen/emoji.git
synced 2026-01-26 03:14:02 +00:00
feat: new py based generator for md and html
This commit is contained in:
30
.github/workflows/generate-listings.yml
vendored
Normal file
30
.github/workflows/generate-listings.yml
vendored
Normal 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
|
||||
@@ -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
231
create_listing.py
Normal 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
2750
index.html
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user