Skip to content

Commit

Permalink
Refactor for Pelican 4 + inline minification (#3)
Browse files Browse the repository at this point in the history
* sort imports

* Fix svg compress fault due to csscompressor
For at least all versions under 0.9.5 of csscompressor, there is a faulty
compression on the url() func of css rules; spaces are wrongly removed,
thus destroying the meaning of any embedded svg file.
See sprymix/csscompressor#9

* fix typos with black

* Add logger/logging

* Ability to minify inline JS & CSS tags
jsmin is used for JavaScript minification;
csscompressor is used as for css files

* fix typos

* packaging: add setup.cfg

* packaging: drop old setup.py (replaced by setup.cfg)

* add version to __init__

* Move src files to new pelican plugins architecture

* Drop python2.7 open method

* Set pelican 4 dep

* Add license text in src file as it should be

* README: update;
- simplify maintenance process by removing references to specific versions
of dependencies (See setup.cfg instead)
- Add jsmin dep
- Add script & style related minification

* Add Makefile for release & test process

* Remove useless lambda

* Update changelog

* README: More complete plugin description

* README: mention to beautifulsoup dep

* packaging: add beautifulsoup dep

* Fix rest typo
  • Loading branch information
ysard authored and dArignac committed Feb 19, 2022
1 parent 35f19c7 commit 312f162
Show file tree
Hide file tree
Showing 7 changed files with 257 additions and 120 deletions.
9 changes: 9 additions & 0 deletions CHANGELOG
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
0.2
---

* Ability to minify inline JS & CSS
* Refactor for Pelican 4.x
* Drop Python 2.7 support
* Fix CSS minification fail on embedded SVG in CSS rules (url() func)


0.1.1
-----

Expand Down
14 changes: 14 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@

# development & release cycle
fullrelease:
fullrelease

check_setups:
pyroma .

check_code:
prospector pelican/plugins/minification/
check-manifest

sdist:
python setup.py sdist
23 changes: 14 additions & 9 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,18 @@ pelican-minification
====================

Content minification for the `Pelican`_ site generator.
This plugin can compress HTML & CSS files as well as inline CSS and JavaScript in HTML files.


Installation and Usage
----------------------

pelican-minification depends on the following packages that will be installed automatically, see below:

* `htmlmin >= 0.1.5`_
* `csscompressor >= 0.9.1`_
* `htmlmin`_
* `csscompressor`_
* `jsmin`_
* `BeautifulSoup`_
* `Pelican`_


Expand All @@ -21,18 +24,20 @@ Install pelican-minification into your Python interpreter using pip:
pip install pelican-minification
Then add the plugin to the PLUGINS setting within your *pelicanconf.py*:
Then add the plugin to the ``PLUGINS`` setting within your *pelicanconf.py*:

.. code-block:: python
PLUGINS = [
...
'minification',
'pelican.plugins.minification',
]
Upon calling the *pelican* command now, all HTML and CSS files are compressed automatically.
Upon calling the *pelican* command now, all HTML and CSS files are compressed automatically;
including inline JavaScript and CSS rules in ``<script>`` and ``<style>`` tags.


.. _htmlmin >= 0.1.5: https://pypi.python.org/pypi/htmlmin/0.1.5
.. _csscompressor >= 0.9.1: https://pypi.python.org/pypi/csscompressor/0.9.3
.. _Pelican: https://pypi.python.org/pypi/pelican/3.3
.. _htmlmin: https://pypi.python.org/pypi/htmlmin
.. _csscompressor: https://pypi.python.org/pypi/csscompressor
.. _jsmin: https://pypi.org/project/jsmin
.. _BeautifulSoup: https://pypi.org/project/beautifulsoup4
.. _Pelican: https://pypi.python.org/pypi/pelican
73 changes: 0 additions & 73 deletions minification/__init__.py

This file was deleted.

167 changes: 167 additions & 0 deletions pelican/plugins/minification/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
# -*- coding: utf-8 -*-
# MIT License
#
# Copyright (c) 2014-2015 Alexander Herrmann <[email protected]>
# Copyright (c) 2022 Ysard <[email protected]>
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
# Standard imports
import os
from packaging import version
from functools import lru_cache
from fnmatch import fnmatch
import logging

# Custom imports
import csscompressor
from jsmin import jsmin
import htmlmin
from bs4 import BeautifulSoup

# Pelican imports
from pelican import signals

__version__ = "0.2.0"

LOGGER = logging.getLogger(__name__)

if version.parse(csscompressor.__version__) <= version.parse("0.9.5"):
# Monkey patch csscompressor 0.9.5
_preserve_call_tokens_original = csscompressor._preserve_call_tokens
_url_re = csscompressor._url_re

def my_new_preserve_call_tokens(*args, **kwargs):
"""If regex is for url pattern, switch the keyword remove_ws to False
Such configuration will preserve svg code in url() pattern of CSS file.
"""
if _url_re == args[1]:
kwargs["remove_ws"] = False
return _preserve_call_tokens_original(*args, **kwargs)

csscompressor._preserve_call_tokens = my_new_preserve_call_tokens

assert csscompressor._preserve_call_tokens == my_new_preserve_call_tokens


class Minification:
"""File content minification"""

def __init__(self, pelican):
"""Minifies the files
:param pelican: the pelican object
:type pelican: pelican.Pelican
"""
LOGGER.info("Minification in progress...")

for path, subdirs, files in os.walk(pelican.output_path):
for name in files:
path_file = os.path.join(path, name)

if fnmatch(name, "*.html"):
self.write_to_file(
path_file,
lambda content: self.minify_inline_script_style(
htmlmin.minify(
content,
remove_comments=True,
remove_empty_space=True,
reduce_boolean_attributes=True,
keep_pre=True,
remove_optional_attribute_quotes=False,
)
),
)
elif fnmatch(name, "*.css"):
self.write_to_file(path_file, csscompressor.compress)

def minify_inline_script_style(self, content):
"""Minify inline JavaScript and CSS in HTML content
:param content: HTML data
:type content: <str>
:return: HTML data with <script> and <style> tags minified
:rtype: <str>
"""
soup = BeautifulSoup(content, "html.parser")

# Compression methods according to specific HTML tags
tags_methods = {"script": jsmin, "style": csscompressor.compress}

content_modified = False
for tag, method in tags_methods.items():

found_tags = soup.find_all(tag)
if not found_tags:
continue

for found_tag in found_tags:
# Exclude empty tags
if not found_tag.string:
continue
content_modified = True
found_tag.string.replace_with(
minification_method(method, found_tag.string)
)

# Return content as is if there have been no changes
return str(soup) if content_modified else content

@staticmethod
def write_to_file(path_file, callback):
"""Read the content of the given file, put the content into the callback
and writes the result back to the file.
:param path_file: the path to the file
:type path_file: str
:param callback: the callback function
:type callback: function
"""
try:
with open(path_file, "r+", encoding="utf-8") as f:
content = callback(f.read())
f.seek(0)
f.write(content)
f.truncate()
except Exception as e:
raise Exception(
"unable to minify file %(file)s, exception was %(exception)r"
% {"file": path_file, "exception": e}
)


@lru_cache(maxsize=None)
def minification_method(method, content):
"""Cached wrapper for minification method
Some JavaScript or CSS tags may be similar from page to page;
so caching the return of this function can speed up the minification process.
:param content: JavaScript or CSS code
:type content: <str>
:return: Minified code
:rtype: <str>
"""
return method(content)


def register():
"""Register the plugin after the content was generated"""
signals.finalized.connect(Minification)
52 changes: 52 additions & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
[metadata]
name = pelican-minification
version = attr: pelican.plugins.minification.__version__
description = Minifies HTML, CSS and JS of generated Pelican content.
long_description = file: README.rst
long_description_content_type = text/x-rst
author = Alexander Herrmann
author_email = [email protected]
url = https://github.com/dArignac/pelican-minification

license_files = LICENSE
keywords = pelican, javascript, css, html, minification
classifiers =
Development Status :: 4 - Beta
Intended Audience :: Developers
Intended Audience :: System Administrators
Intended Audience :: End Users/Desktop
Natural Language :: English
Operating System :: POSIX :: Linux
Topic :: Text Processing :: Markup
Programming Language :: Python :: 3
Programming Language :: Python :: 3.6
Programming Language :: Python :: 3.7
Programming Language :: Python :: 3.8
Programming Language :: Python :: 3.9
Programming Language :: Python :: 3.10
License :: OSI Approved :: MIT License

[options]
zip_safe = False
include_package_data = True
packages = pelican.plugins.minification
install_requires =
csscompressor>=0.9.5
htmlmin>=0.1.12
jsmin>=3.0.1
beautifulsoup4
pelican>=4

[options.extras_require]
dev =
pytest-cov>=2.6.1
pytest>=5.2.0
pytest-runner
zest.releaser[recommended]

[zest.releaser]
create-wheel = no
python-file-with-version = pelican/plugins/minification/__init__.py

[aliases]
test=pytest
Loading

0 comments on commit 312f162

Please sign in to comment.