diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..f7969b8 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,31 @@ +name: Build and Deploy + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + + workflow_dispatch: + +jobs: + page_build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Build + run: | + sudo apt update + sudo apt install -y software-properties-common + sudo add-apt-repository ppa:deadsnakes/ppa + sudo apt update + sudo apt install -y python3.10 wget tar p7zip + python3.10 ./Build.py + ./Tools/DeployPages.sh + + - name: Deploy + uses: JamesIves/github-pages-deploy-action@v4 + with: + folder: public diff --git a/.gitignore b/.gitignore index 9c58b34..d9ca378 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ +*.pyc +twemoji.tar.gz twemoji/ -twemoji-astonishing.css -twemoji-astonishing.min.css +twemoji-astonishing/ +Build/ +public/ diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000..5e9498f --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,16 @@ +image: alpine:latest + +before_script: | + apk update + apk add python3 wget tar p7zip + +pages: + stage: deploy + script: | + python3.10 ./Build.py + ./Tools/DeployPages.sh + artifacts: + paths: + - public + rules: + - if: $CI_COMMIT_REF_NAME == $CI_DEFAULT_BRANCH diff --git a/Build.py b/Build.py index e6b8543..f4438de 100755 --- a/Build.py +++ b/Build.py @@ -1,11 +1,12 @@ #!/usr/bin/env python3 +import argparse import os import unicodedata from time import ctime from urllib.request import urlopen +from Tools import rcssmin -Name = "twemoji-astonishing" -EmojiVer = "15.0" +cssmin = rcssmin._make_cssmin(python_only=True) # https://stackoverflow.com/a/518232 def StripAccents(s): @@ -17,6 +18,17 @@ def ReplaceList(Text, Match, Replace): Text = Text.replace(m, Replace) return Text +def GetEmojiData(EmojiVer): + Response = urlopen(f"https://unicode.org/Public/emoji/{EmojiVer}/emoji-test.txt") + return Response.read().decode("utf-8") + +def ParseEmojiData(Data): + Emojis = [] + for Line in Data.splitlines(): + if CheckEmojiLine(Line): + Emojis += [GetEmojiMeta(Line)] + return Emojis + def CheckEmojiLine(Line): if Line.startswith("#"): return False @@ -41,40 +53,61 @@ def GetEmojiMeta(Line): return {"Code":Code, "Char":Char, "Name":Name} -def MinifyCSS(CSS): - return ReplaceList(CSS, ["\n","\t"," "], "") - -def Main(): - #print("[I] Cloning Twemoji repo") - #os.system("git clone --depth 1 https://github.com/twitter/twemoji") - - print(f"[I] Getting v{EmojiVer} emoji data") - Response = urlopen(f"https://unicode.org/Public/emoji/{EmojiVer}/emoji-test.txt") - - print("[I] Parsing emoji data") - Emojis = [] - Data = Response.read().decode("utf-8") - for Line in Data.splitlines(): - if CheckEmojiLine(Line): - Emojis += [GetEmojiMeta(Line)] - EmojiCount = str(len(Emojis)) - - print(f"[I] Writing CSS") - CSS = "" +def WriteCSS(Emojis, URLPrefix): with open("Preamble.css", "r") as f: - CSS += f.read() + "\n" + Preamble = f.read() + "\n" with open("Comment.css", "r") as f: - Comment = f.read().replace("{BuildTime}", ctime()).replace("{EmojiCount}", EmojiCount) - for Emoji in Emojis: - CSS += f"""\ -.twa-{Emoji["Name"]}, .twa-{Emoji["Char"]} {{ - background-image: url("{Emoji["Code"]}.svg"); + Comment = f.read().format(BuildTime=ctime(), EmojiCount=str(len(Emojis))) + + try: + os.mkdir("Build") + except FileExistsError: + pass + + for Type in ["Chars Names", "Chars", "Names"]: + CSS = Preamble + Line = "" + if "Chars" in Type: + Line = ".twa-{EmojiChar} " + Line + if "Names" in Type: + Line = ".twa-{EmojiName} " + Line + if Type == "Chars Names": + Line = Line.replace(" .twa-", ", .twa-") + + for Emoji in Emojis: + NewLine = Line.format(EmojiChar=Emoji["Char"], EmojiName=Emoji["Name"]) + CSS += f"""\ +{NewLine}{{ + background-image: url("{URLPrefix}{Emoji["Code"]}.svg"); }} """ - with open(f"{Name}.css", "w") as f: - f.write(CSS.replace("/*{CommentBlock}*/", Comment)) - with open(f"{Name}.min.css", "w") as f: - f.write(MinifyCSS(CSS).replace("/*{CommentBlock}*/", "\n"+Comment)) + FileName = "twemoji-astonishing" + if Type == "Chars": + FileName += ".chars" + elif Type == "Names": + FileName += ".names" + + with open(f"Build/{FileName}.css", "w") as f: + f.write(CSS.replace("{CommentBlock}", Comment)) + with open(f"Build/{FileName}.min.css", "w") as f: + f.write(cssmin(CSS).replace("{CommentBlock}", "\n"+Comment)) + +def Main(Args): + EmojiVer = Args.EmojiVer if Args.EmojiVer else "15.0" + URLPrefix = Args.URLPrefix if Args.URLPrefix else "" + + print(f"[I] Getting v{EmojiVer} emoji data") + Data = GetEmojiData(EmojiVer) + + print("[I] Parsing emoji data") + Emojis = ParseEmojiData(Data) + + print(f"[I] Writing CSS") + WriteCSS(Emojis, URLPrefix) if __name__ == "__main__": - Main() + Parser = argparse.ArgumentParser() + Parser.add_argument('--EmojiVer', type=str) + Parser.add_argument('--URLPrefix', type=str) + + Main(Parser.parse_args()) diff --git a/Comment.min.css b/Comment.min.css deleted file mode 100644 index fd2ea10..0000000 --- a/Comment.min.css +++ /dev/null @@ -1,4 +0,0 @@ -/* - License: MIT and CC BY 4.0. - Details and Sources: https://gitlab.com/octtspacc/twemoji-astonishing. -*/ diff --git a/Preamble.css b/Preamble.css index a742585..14986b7 100644 --- a/Preamble.css +++ b/Preamble.css @@ -1,5 +1,5 @@ @charset "UTF-8"; -/*{CommentBlock}*/ +{CommentBlock} .twa { display: inline-block; @@ -13,7 +13,7 @@ } /* Prevent image and fallback text emoji overlap */ -.twa span { +.twa > span { font-size: 0px; } diff --git a/Tools/DeployPages.sh b/Tools/DeployPages.sh new file mode 100755 index 0000000..8d9c764 --- /dev/null +++ b/Tools/DeployPages.sh @@ -0,0 +1,25 @@ +#!/bin/sh + +echo "[I] Downloading Twemoji files" +wget -O ./twemoji.tar.gz https://github.com/twitter/twemoji/archive/refs/heads/master.tar.gz + +echo "[I] Extracting archive" +tar xvf ./twemoji.tar.gz +mv ./twemoji-master ./twemoji + +echo "[I] Preparing Pages build" +rm -rf ./public +mkdir -p ./public +cp ./Build/* ./public/ +cp ./twemoji/assets/svg/* ./Build/ + +echo "[I] Making archives" +mv ./Build ./twemoji-astonishing +cd ./public +7z a -mx9 -mmt$(nproc --all) twemoji-astonishing.zip ../twemoji-astonishing +7z a -mx9 -mmt$(nproc --all) twemoji-astonishing.7z ../twemoji-astonishing +tar cvJf twemoji-astonishing.tar.xz ../twemoji-astonishing +cd .. + +echo "[I] Cleaning up" +rm -rf ./Build ./twemoji ./twemoji-astonishing ./twemoji.tar.gz diff --git a/Tools/rcssmin.py b/Tools/rcssmin.py new file mode 100644 index 0000000..18e78fa --- /dev/null +++ b/Tools/rcssmin.py @@ -0,0 +1,420 @@ +#!/usr/bin/env python +# -*- coding: ascii -*- +u""" +============== + CSS Minifier +============== + +CSS Minifier. + +The minifier is based on the semantics of the `YUI compressor`_\\, which +itself is based on `the rule list by Isaac Schlueter`_\\. + +:Copyright: + + Copyright 2011 - 2021 + Andr\xe9 Malo or his licensors, as applicable + +:License: + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +This module is a re-implementation aiming for speed instead of maximum +compression, so it can be used at runtime (rather than during a preprocessing +step). RCSSmin does syntactical compression only (removing spaces, comments +and possibly semicolons). It does not provide semantic compression (like +removing empty blocks, collapsing redundant properties etc). It does, however, +support various CSS hacks (by keeping them working as intended). + +Here's a feature list: + +- Strings are kept, except that escaped newlines are stripped +- Space/Comments before the very end or before various characters are + stripped: ``:{});=>],!`` (The colon (``:``) is a special case, a single + space is kept if it's outside a ruleset.) +- Space/Comments at the very beginning or after various characters are + stripped: ``{}(=:>[,!`` +- Optional space after unicode escapes is kept, resp. replaced by a simple + space +- whitespaces inside ``url()`` definitions are stripped, except if it's a + quoted non-base64 data url +- Comments starting with an exclamation mark (``!``) can be kept optionally. +- All other comments and/or whitespace characters are replaced by a single + space. +- Multiple consecutive semicolons are reduced to one +- The last semicolon within a ruleset is stripped +- CSS Hacks supported: + + - IE7 hack (``>/**/``) + - Mac-IE5 hack (``/*\\*/.../**/``) + - The boxmodelhack is supported naturally because it relies on valid CSS2 + strings + - Between ``:first-line`` and the following comma or curly brace a space is + inserted. (apparently it's needed for IE6) + - Same for ``:first-letter`` + +rcssmin.c is a reimplementation of rcssmin.py in C and improves runtime up to +factor 100 or so (depending on the input). docs/BENCHMARKS in the source +distribution contains the details. + +Supported python versions are 2.7 and 3.4+. + +.. _YUI compressor: https://github.com/yui/yuicompressor/ + +.. _the rule list by Isaac Schlueter: https://github.com/isaacs/cssmin/ +""" +__author__ = u"Andr\xe9 Malo" +__license__ = "Apache License, Version 2.0" +__version__ = '1.1.0' +__all__ = ['cssmin'] + +import re as _re + + +def _make_cssmin(python_only=False): + """ + Generate CSS minifier. + + Parameters: + python_only (bool): + Use only the python variant. If true, the c extension is not even + tried to be loaded. + + Returns: + callable: Minifier + """ + # pylint: disable = too-many-locals + + if not python_only: + try: + import _rcssmin + except ImportError: + pass + else: + # Ensure that the C version is in sync + if getattr(_rcssmin, '__version__', None) == __version__: + return _rcssmin.cssmin + + nl = r'(?:[\n\f]|\r\n?)' # pylint: disable = invalid-name + spacechar = r'[\r\n\f\040\t]' + + unicoded = r'[0-9a-fA-F]{1,6}(?:[\040\n\t\f]|\r\n?)?' + escaped = r'[^\n\r\f0-9a-fA-F]' + escape = r'(?:\\(?:%(unicoded)s|%(escaped)s))' % locals() + + nmchar = r'[^\000-\054\056\057\072-\100\133-\136\140\173-\177]' + # nmstart = r'[^\000-\100\133-\136\140\173-\177]' + # ident = (r'(?:' + # r'-?(?:%(nmstart)s|%(escape)s)%(nmchar)s*(?:%(escape)s%(nmchar)s*)*' + # r')') % locals() + + comment = r'(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/)' + + # only for specific purposes. The bang is grouped: + _bang_comment = r'(?:/\*(!?)[^*]*\*+(?:[^/*][^*]*\*+)*/)' + + string1 = \ + r'(?:\047[^\047\\\r\n\f]*(?:\\[^\r\n\f][^\047\\\r\n\f]*)*\047)' + string2 = r'(?:"[^"\\\r\n\f]*(?:\\[^\r\n\f][^"\\\r\n\f]*)*")' + strings = r'(?:%s|%s)' % (string1, string2) + + nl_string1 = \ + r'(?:\047[^\047\\\r\n\f]*(?:\\(?:[^\r]|\r\n?)[^\047\\\r\n\f]*)*\047)' + nl_string2 = r'(?:"[^"\\\r\n\f]*(?:\\(?:[^\r]|\r\n?)[^"\\\r\n\f]*)*")' + nl_strings = r'(?:%s|%s)' % (nl_string1, nl_string2) + + uri_nl_string1 = r'(?:\047[^\047\\]*(?:\\(?:[^\r]|\r\n?)[^\047\\]*)*\047)' + uri_nl_string2 = r'(?:"[^"\\]*(?:\\(?:[^\r]|\r\n?)[^"\\]*)*")' + uri_nl_strings = r'(?:%s|%s)' % (uri_nl_string1, uri_nl_string2) + + nl_escaped = r'(?:\\%(nl)s)' % locals() + + space = r'(?:%(spacechar)s|%(comment)s)' % locals() + + ie7hack = r'(?:>/\*\*/)' + + uri = ( + # noqa pylint: disable = bad-option-value, bad-continuation + + r'(?:' + + r'(?:[^\000-\040"\047()\\\177]*' + r'(?:%(escape)s[^\000-\040"\047()\\\177]*)*)' + r'(?:' + r'(?:%(spacechar)s+|%(nl_escaped)s+)' + r'(?:' + r'(?:[^\000-\040"\047()\\\177]|%(escape)s|%(nl_escaped)s)' + r'[^\000-\040"\047()\\\177]*' + r'(?:%(escape)s[^\000-\040"\047()\\\177]*)*' + r')+' + r')*' + + r')' + ) % locals() + + nl_unesc_sub = _re.compile(nl_escaped).sub + + uri_data_plain = _re.compile(( + r'[\047"][dD][aA][tT][aA]:([^\000-\040\\"\047,]*),' + )).match + + uri_space_sub = _re.compile(( + r'(%(escape)s+)|%(spacechar)s+|%(nl_escaped)s+' + ) % locals()).sub + uri_space_subber = lambda m: m.groups()[0] or '' + + space_sub_simple = _re.compile(( + r'[\r\n\f\040\t;]+|(%(comment)s+)' + ) % locals()).sub + space_sub_banged = _re.compile(( + r'[\r\n\f\040\t;]+|(%(_bang_comment)s+)' + ) % locals()).sub + + post_esc_sub = _re.compile(r'[\r\n\f\t]+').sub + + main_sub = _re.compile(( + # noqa pylint: disable = bad-option-value, bad-continuation + + r'([^\\"\047u>@\r\n\f\040\t/;:{}+]+)' # 1 + r'|(?<=[{}(=:>[,!])(%(space)s+)' # 2 + r'|^(%(space)s+)' # 3 + r'|(%(space)s+)(?=(([:{});=>\],!])|$)?)' # 4, 5, 6 + r'|;(%(space)s*(?:;%(space)s*)*)(?=(\})?)' # 7, 8 + r'|(\{)' # 9 + r'|(\})' # 10 + r'|(%(strings)s)' # 11 + r'|(?@\r\n\f\040\t/;:{}+]*)' # 19 + ) % locals()).sub + + # print(main_sub.__self__.pattern) + + def main_subber(keep_bang_comments): + """ Make main subber """ + in_macie5, in_rule, at_group = [0], [0], [0] + + if keep_bang_comments: + space_sub = space_sub_banged + + def space_subber(match): + """ Space|Comment subber """ + if match.lastindex: + group1, group2 = match.group(1, 2) + if group2: + if group1.endswith(r'\*/'): + in_macie5[0] = 1 + else: + in_macie5[0] = 0 + return group1 + + if group1.endswith(r'\*/'): + if in_macie5[0]: + return '' + in_macie5[0] = 1 + return r'/*\*/' + elif in_macie5[0]: + in_macie5[0] = 0 + return '/**/' + return '' + else: + space_sub = space_sub_simple + + def space_subber(match): + """ Space|Comment subber """ + if match.lastindex: + if match.group(1).endswith(r'\*/'): + if in_macie5[0]: + return '' + in_macie5[0] = 1 + return r'/*\*/' + elif in_macie5[0]: + in_macie5[0] = 0 + return '/**/' + return '' + + def fn_space_post(group): + """ space with token after """ + if group(5) is None or ( + group(6) == ':' and not in_rule[0] and not at_group[0]): + return ' ' + space_sub(space_subber, group(4)) + return space_sub(space_subber, group(4)) + + def fn_semicolon(group): + """ ; handler """ + return ';' + space_sub(space_subber, group(7)) + + def fn_semicolon2(group): + """ ; handler """ + if in_rule[0]: + return space_sub(space_subber, group(7)) + return ';' + space_sub(space_subber, group(7)) + + def fn_open(_): + """ { handler """ + if at_group[0]: + at_group[0] -= 1 + else: + in_rule[0] = 1 + return '{' + + def fn_close(_): + """ } handler """ + in_rule[0] = 0 + return '}' + + def fn_url(group): + """ url() handler """ + uri = group(12) + data = uri_data_plain(uri) + if not data or data.group(1).lower().endswith(';base64'): + uri = uri_space_sub(uri_space_subber, uri) + return 'url(%s)' % (uri,) + + def fn_at_group(group): + """ @xxx group handler """ + at_group[0] += 1 + return group(13) + + def fn_ie7hack(group): + """ IE7 Hack handler """ + if not in_rule[0] and not at_group[0]: + in_macie5[0] = 0 + return group(14) + space_sub(space_subber, group(15)) + return '>' + space_sub(space_subber, group(15)) + + table = ( + # noqa pylint: disable = bad-option-value, bad-continuation + + None, + None, + None, + None, + fn_space_post, # space with token after + fn_space_post, # space with token after + fn_space_post, # space with token after + fn_semicolon, # semicolon + fn_semicolon2, # semicolon + fn_open, # { + fn_close, # } + lambda g: g(11), # string + fn_url, # url(...) + fn_at_group, # @xxx expecting {...} + None, + fn_ie7hack, # ie7hack + None, + lambda g: g(16) + ' ' + space_sub(space_subber, g(17)), + # :first-line|letter followed + # by [{,] (apparently space + # needed for IE6) + lambda g: nl_unesc_sub('', g(18)), # nl_string + lambda g: post_esc_sub(' ', g(19)), # escape + ) + + def func(match): + """ Main subber """ + idx, group = match.lastindex, match.group + if idx > 3: + return table[idx](group) + + # shortcuts for frequent operations below: + elif idx == 1: # not interesting + return group(1) + # else: # space with token before or at the beginning + return space_sub(space_subber, group(idx)) + + return func + + def cssmin(style, keep_bang_comments=False): + """ + Minify CSS. + + Parameters: + style (str): + CSS to minify + + keep_bang_comments (bool): + Keep comments starting with an exclamation mark? (``/*!...*/``) + + Returns: + str: Minified style + """ + # pylint: disable = redefined-outer-name + + is_bytes, style = _as_str(style) + style = main_sub(main_subber(keep_bang_comments), style) + if is_bytes: + style = style.encode('latin-1') + if is_bytes == 2: + style = bytearray(style) + return style + + return cssmin + +cssmin = _make_cssmin() + + +def _as_str(script): + """ Make sure the style is a text string """ + is_bytes = False + if str is bytes: + if not isinstance(script, basestring): # noqa pylint: disable = undefined-variable + raise TypeError("Unexpected type") + elif isinstance(script, bytes): + is_bytes = True + script = script.decode('latin-1') + elif isinstance(script, bytearray): + is_bytes = 2 + script = script.decode('latin-1') + elif not isinstance(script, str): + raise TypeError("Unexpected type") + + return is_bytes, script + + +if __name__ == '__main__': + def main(): + """ Main """ + import sys as _sys + + keep_bang_comments = ( + '-b' in _sys.argv[1:] + or '-bp' in _sys.argv[1:] + or '-pb' in _sys.argv[1:] + ) + if '-p' in _sys.argv[1:] or '-bp' in _sys.argv[1:] \ + or '-pb' in _sys.argv[1:]: + xcssmin = _make_cssmin(python_only=True) + else: + xcssmin = cssmin + _sys.stdout.write(xcssmin( + _sys.stdin.read(), keep_bang_comments=keep_bang_comments + )) + + main()