From 84eb64ff671c0161cb48d62c7d290738679401b1 Mon Sep 17 00:00:00 2001 From: octospacc Date: Sat, 5 Nov 2022 12:17:23 +0100 Subject: [PATCH] Add CSS minification, internal changes --- README.md | 15 +- Source/Build.py | 25 ++- Source/Libs/rcssmin.py | 421 ++++++++++++++++++++++++++++++++++++++++ Source/Modules/Feed.py | 4 +- Source/Modules/Site.py | 20 +- Source/Modules/Utils.py | 10 +- TODO | 1 + 7 files changed, 470 insertions(+), 26 deletions(-) create mode 100644 Source/Libs/rcssmin.py diff --git a/README.md b/README.md index 0e6c925..4cf52b1 100644 --- a/README.md +++ b/README.md @@ -2,12 +2,12 @@ **staticoso** (_from Italian statico [static] + coso [thingy]_) is an unpretentious **static site generator**. -I'm making this because I need a simple and kind-of-minimal program to serve me as a static site generator with (limited) CMS features. Just the features I need. +I'm making this because I need a simple and kind-of-minimal program to serve me as a static site generator with (limited) mixed-CMS+Wiki features. Just the ones I need, of which some are unique, however. This won't replace any of the big projects out there that do the same thing but, as all of my projects, I'm releasing it as free libre software, in the hope that someone would find it useful. -Also, this software is needed for someone to edit and compile my personal sub-website [sitoctt](https://gitlab.com/octtspacc/sitoctt) from its source. Being that site too released under a libre license that allows modifications, I have to also release the tools I use to build it. +Also, this software is needed for someone to edit and compile my personal website [sitoctt](https://gitlab.com/octtspacc/sitoctt) from its source. Being that site too released under a libre license that allows modifications, I have to also release the tools I use to build it, or really people will never be able to make the most out of it. -**Everything is still an heavy WIP**, and features might break across commits, but feel free to experiment with all of this stuff! +**Everything is still kind of a WIP**, and some bugs might appear across commits, but feel free to experiment with all of this stuff! If you really want to use this tool for your website, I suggest you don't do like I do in my build process, running always with the latest code commit; instead, either download a snapshot of the code and keep that, or always clone from a specific commit, until you have time when I do an update to check if your site actually builds correctly with the new version. ## Documentation @@ -20,18 +20,18 @@ Keep in mind that, currently, it's still very incomplete. **Any help**, from wri - [Python >= 3.10](https://python.org) - (Included) [Python Markdown == 3.3.7](https://pypi.org/project/Markdown) - - (Included) Third-Party Extensions for Python Markdown: [markdown_del_ins](https://github.com/honzajavorek/markdown-del-ins), [mdx_subscript](https://github.com/jambonrose/markdown_subscript_extension), [mdx_superscript](https://github.com/jambonrose/markdown_superscript_extension) - (Included) [Beautiful Soup == 4.11.1](https://pypi.org/project/beautifulsoup4) - (Included) [feedgen == 0.9.0](https://pypi.org/project/feedgen) - - [lxml == 4.9.1](https://pypi.org/project/lxml) + - (Not included) [lxml == 4.9.1](https://pypi.org/project/lxml) - (Included) [htmlmin == 0.1.12](https://pypi.org/project/htmlmin) +- (Included) [rcssmin == 1.1.1](https://pypi.org/project/rcssmin) ### Optional dependencies Needed for Pug input support: - [node == 12.22.5](https://nodejs.org) - [npm == 7.5.2](https://www.npmjs.com) -- (Included, must be manually installed in system $PATH) [pug-cli == 1.0.0-alpha6](https://npmjs.com/package/pug-cli) +- (Included, but must be manually installed in system $PATH) [pug-cli == 1.0.0-alpha6](https://npmjs.com/package/pug-cli) Needed for Gemtext output support: @@ -61,7 +61,7 @@ Needed for Gemtext output support: - [ ] Polished Gemtext generation - [x] Autodetection of pages and posts - [x] Info for posts shown on their page -- [x] HTML minification +- [x] HTML and CSS minification - [x] Full Open Graph support - [x] Custom categories for posts - [x] Custom static and dynamic page parts @@ -77,7 +77,6 @@ Needed for Gemtext output support: - [x] Generation of page (titles) menu as a tree - [x] Auto-detection of titles in a page - [x] **HTML**, **TXT**, **Extended Markdown**, and **Pug** supported as input page files -- [ ] Out of heavy-WIP state ## Known issues (might need further investigation) diff --git a/Source/Build.py b/Source/Build.py index 629cf51..d5dc94d 100755 --- a/Source/Build.py +++ b/Source/Build.py @@ -28,6 +28,8 @@ try: except: logging.warning("⚠ Can't load the ActivityPub module. Its use is disabled. Make sure the 'requests' library is installed.") ActivityPub = False +from Libs import rcssmin +cssmin = rcssmin._make_cssmin(python_only=True) def ResetOutDir(OutDir): for e in (OutDir, f"{OutDir}.gmi"): @@ -161,7 +163,8 @@ def Main(Args, FeedEntries): MarkdownExts = Flags['MarkdownExts'] = literal_eval(OptionChoose(str(MarkdownExtsDefault), Args.MarkdownExts, ReadConf(SiteConf, 'Markdown', 'Exts'))) SitemapOutput = Flags['SitemapOutput'] = StrBoolChoose(True, Args.SitemapOutput, ReadConf(SiteConf, 'Sitemap', 'Output')) - Minify = Flags['Minify'] = StrBoolChoose(False, Args.Minify, ReadConf(SiteConf, 'Minify', 'Minify')) + MinifyOutput = Flags['MinifyOutput'] = StrBoolChoose(False, Args.MinifyOutput, ReadConf(SiteConf, 'Minify', 'Output')) + MinifyAssets = Flags['MinifyAssets'] = StrBoolChoose(False, Args.MinifyAssets, ReadConf(SiteConf, 'Minify', 'Assets')) MinifyKeepComments = Flags['MinifyKeepComments'] = StrBoolChoose(False, Args.MinifyKeepComments, ReadConf(SiteConf, 'Minify', 'KeepComments')) ImgAltToTitle = Flags['ImgAltToTitle'] = StrBoolChoose(True, Args.ImgAltToTitle, ReadConf(SiteConf, 'Site', 'ImgAltToTitle')) @@ -267,8 +270,21 @@ def Main(Args, FeedEntries): logging.info("Generating Sitemap") MakeSitemap(Flags, Pages) - logging.info("Copying Assets") - os.system(f"cp -R Assets/* {OutDir}/") + logging.info("Preparing Assets") + #os.system(f"cp -R Assets/* {OutDir}/") + if Flags['MinifyAssets']: + shutil.copytree('Assets', OutDir, ignore=IgnoreFiles, dirs_exist_ok=True) + for File in Path('Assets').rglob('*'): + if os.path.isfile(File): + Dest = f"{OutDir}/{str(File)[len('Assets')+1:]}" + if str(File).lower().endswith(FileExtensions['HTML']): + WriteFile(Dest, DoMinifyHTML(ReadFile(File), MinifyKeepComments)) + elif str(File).lower().endswith('.css'): + WriteFile(Dest, cssmin(ReadFile(File), MinifyKeepComments)) + else: + shutil.copy2(File, Dest) + else: + shutil.copytree('Assets', OutDir, dirs_exist_ok=True) if __name__ == '__main__': StartTime = time.time() @@ -286,7 +302,8 @@ if __name__ == '__main__': Parser.add_argument('--BlogName', type=str) Parser.add_argument('--SiteTemplate', type=str) Parser.add_argument('--SiteDomain', type=str) - Parser.add_argument('--Minify', type=str) + Parser.add_argument('--MinifyOutput', type=str) + Parser.add_argument('--MinifyAssets', type=str) Parser.add_argument('--MinifyKeepComments', type=str) Parser.add_argument('--NoScripts', type=str) Parser.add_argument('--ImgAltToTitle', type=str) diff --git a/Source/Libs/rcssmin.py b/Source/Libs/rcssmin.py new file mode 100644 index 0000000..ebcb457 --- /dev/null +++ b/Source/Libs/rcssmin.py @@ -0,0 +1,421 @@ +#!/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 - 2022 + 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.1' +__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 = unused-variable, possibly-unused-variable + # pylint: disable = too-many-locals, too-many-statements + + if not python_only: + try: + import _rcssmin # pylint: disable = import-outside-toplevel + 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 # pylint: disable = import-outside-toplevel + + 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() diff --git a/Source/Modules/Feed.py b/Source/Modules/Feed.py index 795c4fe..6652ffb 100644 --- a/Source/Modules/Feed.py +++ b/Source/Modules/Feed.py @@ -57,5 +57,5 @@ def MakeFeed(Flags, Pages, FullSite=False): if not os.path.exists(f"{Flags['OutDir']}/feed"): os.mkdir(f"{Flags['OutDir']}/feed") FeedType = 'site.' if FullSite else '' - Feed.atom_file(f"{Flags['OutDir']}/feed/{FeedType}atom.xml", pretty=(not Flags['Minify'])) - Feed.rss_file(f"{Flags['OutDir']}/feed/{FeedType}rss.xml", pretty=(not Flags['Minify'])) + Feed.atom_file(f"{Flags['OutDir']}/feed/{FeedType}atom.xml", pretty=(not Flags['MinifyOutput'])) + Feed.rss_file(f"{Flags['OutDir']}/feed/{FeedType}rss.xml", pretty=(not Flags['MinifyOutput'])) diff --git a/Source/Modules/Site.py b/Source/Modules/Site.py index 5888727..fde691b 100644 --- a/Source/Modules/Site.py +++ b/Source/Modules/Site.py @@ -378,14 +378,16 @@ def PatchHTML(File, HTML, StaticPartsText, DynamicParts, DynamicPartsText, HTMLP HTML = DictReplWithEsc(HTML, { f"[staticoso:CustomPath:{e}]": FolderRoots[e], f"": FolderRoots[e], + #DEPRECATED f"[staticoso:Folder:{e}:AbsoluteRoot]": FolderRoots[e], f"": FolderRoots[e]}) for e in Categories: HTML = DictReplWithEsc(HTML, { - f"[staticoso:Category:{e}]": Categories[e], - f"[staticoso:Category:{e}]": Categories[e], - f"": Categories[e], - f"": Categories[e]}) + f"[staticoso:Category:{e}]": Categories[e], + f"": Categories[e], + f"": Categories[e], + #DEPRECATED + f"[staticoso:Category:{e}]": Categories[e]}) # TODO: Clean this doubling? ContentHTML = Content @@ -407,14 +409,16 @@ def PatchHTML(File, HTML, StaticPartsText, DynamicParts, DynamicPartsText, HTMLP ContentHTML = DictReplWithEsc(ContentHTML, { f"[staticoso:CustomPath:{e}]": FolderRoots[e], f"": FolderRoots[e], + #DEPRECATED f"[staticoso:Folder:{e}:AbsoluteRoot]": FolderRoots[e], f"": FolderRoots[e]}) for e in Categories: ContentHTML = DictReplWithEsc(ContentHTML, { - f"[staticoso:Category:{e}]": Categories[e], f"[staticoso:Category:{e}]": Categories[e], f"": Categories[e], - f"": Categories[e]}) + f"": Categories[e], + #DEPRECATED + f"[staticoso:Category:{e}]": Categories[e]}) return HTML, ContentHTML, Description, Image @@ -506,7 +510,7 @@ def HandlePage(Flags, Page, Pages, Categories, LimitFiles, Snippets, ConfMenu, L MenuStyle='Flat') HTML = ReplWithEsc(HTML, f"", DirectoryList) - if Flags['Minify']: + if Flags['MinifyOutput']: if not LightRun: HTML = DoMinifyHTML(HTML, MinifyKeepComments) ContentHTML = DoMinifyHTML(ContentHTML, MinifyKeepComments) @@ -543,7 +547,7 @@ def MultiprocHandlePage(d): def MakeSite(Flags, LimitFiles, Snippets, ConfMenu, GlobalMacros, Locale, Threads): PagesPaths, PostsPaths, Pages, MadePages, Categories = [], [], [], [], {} PoolSize = cpu_count() if Threads <= 0 else Threads - OutDir, MarkdownExts, Sorting, MinifyKeepComments = Flags['OutDir'], Flags['MarkdownExts'], Flags['Sorting'], Flags['MinifyKeepComments'] + OutDir, MarkdownExts, Sorting = Flags['OutDir'], Flags['MarkdownExts'], Flags['Sorting'] SiteName, BlogName, SiteTagline = Flags['SiteName'], Flags['BlogName'], Flags['SiteTagline'] SiteTemplate, SiteLang = Flags['SiteTemplate'], Flags['SiteLang'] SiteDomain, SiteRoot, FolderRoots = Flags['SiteDomain'], Flags['SiteRoot'], Flags['FolderRoots'] diff --git a/Source/Modules/Utils.py b/Source/Modules/Utils.py index 15ea789..41a2c94 100644 --- a/Source/Modules/Utils.py +++ b/Source/Modules/Utils.py @@ -12,13 +12,16 @@ import os from datetime import datetime from pathlib import Path -ReservedPaths = ('Site.ini', 'Assets', 'Pages', 'Posts', 'Templates', 'StaticParts', 'DynamicParts') +ReservedPaths = ('Site.ini', 'Assets', 'Resources', 'Pages', 'Posts', 'Templates', 'StaticParts', 'DynamicParts') FileExtensions = { 'Pages': ('htm', 'html', 'markdown', 'md', 'pug', 'txt'), 'HTML': ('.htm', '.html'), 'Markdown': ('.markdown', '.md'), 'Tmp': ('htm', 'markdown', 'md', 'pug', 'txt')} +def SureList(e): + return e if type(e) == list else [e] + def ReadFile(p): try: with open(p, 'r') as f: @@ -39,14 +42,13 @@ def WriteFile(p, c): def FileToStr(File, Truncate=''): return str(File)[len(Truncate):] -# https://stackoverflow.com/a/15664273 +# With shutil.copytree copy only folder struct, no files; https://stackoverflow.com/a/15664273 def IgnoreFiles(Dir, Files): return [f for f in Files if os.path.isfile(os.path.join(Dir, f))] def LoadFromDir(Dir, Matchs): Contents = {} - if type(Matchs) != list: - Matchs = [Matchs] + Matchs = SureList(Matchs) for Match in Matchs: for File in Path(Dir).rglob(Match): File = str(File)[len(Dir)+1:] diff --git a/TODO b/TODO index ad19cdd..85d7b9c 100644 --- a/TODO +++ b/TODO @@ -24,6 +24,7 @@ - Handle file extensions with any case sensitivity, not just lowercase; currently the bulk of the issue is finding the files on disk - Test sorting by date for files not starting with date, and dated folders - Fix arguments - some are only callable from CLI and not Site.ini - make them coherent with INI categories +- Change FolderRoots arg name to CustomPaths - Accept Macros as CLI arguments + Deprecate FolderRoots (Macros make it redundant) - Fix ordering menu in Site.ini (not working for inner pages) - Feed generation optionally without native libraries