A Pelican plugin that minifies fontawesome css and fonts to include only used icons.
git clone https://mcol.xyz/code/pelican-minify-fontawesome
Log | Files | Refs | README

minify_fontawesome.py (4842B)


      1 # -*- coding: utf-8 -*-
      2 #
      3 # pelican-minify-fontawesome
      4 #
      5 # Copyright (C) 2019 mcol@posteo.net
      6 #
      7 # This program is free software: you can redistribute it and/or modify
      8 # it under the terms of the GNU General Public License as published by
      9 # the Free Software Foundation, either version 3 of the License, or
     10 # (at your option) any later version.
     11 #
     12 # This program is distributed in the hope that it will be useful,
     13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
     14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     15 # GNU General Public License for more details.
     16 #
     17 # You should have received a copy of the GNU General Public License
     18 # along with this program. If not, see <http://www.gnu.org/licenses/>.
     19 
     20 
     21 import fontforge
     22 import os
     23 import re
     24 
     25 from docutils import nodes
     26 from docutils.parsers.rst import roles
     27 from pelican import signals
     28 
     29 
     30 def copy_glyphs(source, dest, css_blocks):
     31     """
     32     Copy only used icons from the font files into the output folder.
     33     """
     34     icons = []
     35     for block in css_blocks:
     36         match = re.findall('\.fa-(.*?):', block)
     37         icons.append(match[0])
     38 
     39     if not os.path.isdir(dest):
     40         os.mkdir(dest)
     41     for root, dirs, files in os.walk(source):
     42         for f in files:
     43             if f.endswith('woff'):
     44                 font = fontforge.open(os.path.join(root, f))
     45 
     46                 selected = False
     47                 for icon in icons:
     48                     try:
     49                         font.selection.select(("more",), icon)
     50                         selected = True
     51                     except ValueError:
     52                         pass
     53 
     54                 if selected:
     55                     font.selection.invert()
     56                     font.clear()
     57                     font.generate(os.path.join(dest, f))
     58                 font.close()
     59 
     60     print(f'Font Awesome icons incorporated: {icons}')
     61 
     62 def get_classes(folder, extra_icons):
     63     """
     64     Gets a list of font awesome CSS classes defined in all html files within a
     65     folder i.e. those beginning with 'fa'.
     66     """
     67     fa_classes = extra_icons if extra_icons else []
     68     all_classes = []
     69 
     70     for root, dirs, files in os.walk(folder):
     71         for f in files:
     72             if f.endswith('html'):
     73                 with open(os.path.join(root, f), "r") as fd:
     74                     matches = re.findall('class=[\'"](.*?)[\'"]', fd.read())
     75                     if matches:
     76                         for match in matches:
     77                             all_classes.extend(match.split())
     78 
     79     for cls in set(all_classes):
     80         if cls.startswith('fa'):
     81             fa_classes.append(cls)
     82 
     83     return fa_classes
     84 
     85 
     86 def copy_css(output_path, css_file, extra_icons):
     87     """
     88     Copy css for only used icons over to output folder. Returns the css blocks
     89     corresponding to these icons so we know which icons to copy from the font
     90     files.
     91     """
     92     with open(css_file, 'r') as fd:
     93         contents = fd.read()
     94 
     95     base_css = re.sub('\.fa-[\w-]+:before.*?}', '', contents)
     96     for string in ['brands-400', 'regular-400', 'solid-900']:
     97         base_css = re.sub(
     98             f'src:url\(\.\./webfonts/fa-{string}\.eot\).*?\}}',
     99             f'src:url(../webfonts/fa-{string}.woff) format("woff")}}',
    100             base_css
    101         )
    102 
    103     css_blocks = []
    104     for cls in get_classes(output_path, extra_icons):
    105         match = re.search(f'\.{cls}:before.*?}}', contents)
    106         if match:
    107             css_blocks.append(match.group(0))
    108 
    109     css = base_css + ''.join(css_blocks)
    110     css_file = os.path.join(output_path, 'theme', 'css', 'fa.css')
    111     with open(css_file, 'w') as fd:
    112         fd.write(css)
    113 
    114     return css_blocks
    115 
    116 
    117 def output_font(instance):
    118     """
    119     Main function that identifies used icons and copies their CSS and font
    120     definitions into the output folder.
    121     """
    122     FONT_PATH = instance.settings.get('MINIFY_FONTAWESOME', None)
    123     if not FONT_PATH or not os.path.isdir(FONT_PATH):
    124         return
    125 
    126     output_path = instance.output_path
    127     css_file = os.path.join(FONT_PATH, 'css', 'all.min.css')
    128 
    129     css_blocks = copy_css(
    130         output_path,
    131         css_file,
    132         instance.settings.get('FONTAWESOME_EXTRA', None)
    133     )
    134 
    135     copy_glyphs(
    136         os.path.join(FONT_PATH, 'webfonts'),
    137         os.path.join(output_path, 'theme', 'webfonts'),
    138         css_blocks,
    139     )
    140 
    141 
    142 def rst_span(name, rawtext, text, lineno, inliner, options={}, content=[]):
    143     """
    144     A RST role that replaces inline glyphs e.g. :fas:`cat` with the appropriate
    145     HTML span tag.
    146     """
    147     return [nodes.raw('', f'<span class="{name} fa-{text}"></span>', format='html')], []
    148 
    149 
    150 def register():
    151     # Register main program
    152     signals.finalized.connect(output_font)
    153 
    154     # Register font awesome RST roles
    155     roles.register_local_role('fas', rst_span)
    156     roles.register_local_role('far', rst_span)
    157     roles.register_local_role('fab', rst_span)