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