Compare commits

..
20 Commits
Author SHA1 Message Date
cassowary 8404f8927d Documentation update! But the docs/index.md is still major WIP. 2023-08-05 12:09:20 -07:00
cassowary f448a1f1ee Rename to heckweasel. 2023-04-18 17:31:25 -07:00
cassowary 727b2b9309 General housekeeping update.
- Bump version to 0.6.0
- Add template functions to merge dictionaries for loading JSON data inside them
- Add extension management separate from MIME type
- Make the `tembed` processor which runs a generic jinja template through embedding in the template
2023-03-06 16:22:10 -08:00
cassowary 357db6eca4 Major additions to support JSON files and provide compile time options
- Add file_json/get_file_json handling.
  This creates a new global template function to treat a file as a
  json file and returns a dict.

- Add some tools for merging dictionaries.

- Add command-line settable variables that get inserted into metadata
  tree so that at runtime options can be set.
2021-12-19 22:02:47 -08:00
cassowary 4780764a60 Minor changes. Formatting changes. Add some Python version environments for testing. Extended get_file_list to allow a list of globs rather than just a single glob. 2021-06-30 00:39:50 -07:00
cassowary b8bc24cf6f Reformatted with automated tools and minor fixes. 2021-04-28 23:09:35 -07:00
cassowary bf0b7a1cb7 Comment out smart CSS from default mapping. Fix minor bug in template_tools 2019-06-03 19:23:34 -07:00
cassowary 39dde28e35 Updates!
Some documentation expansion.
Add {do} support to jinja systems
2019-05-26 19:39:11 -07:00
cassowary a0c4381c99 Major development update.
* Updated LICENSE, READMES/METADATA.md and TODO.md
* Added example blog to examples/
* Added preliminary Pygments support for embedding code in pages.
* Add preliminary Wordpress dump importer
* Expansions to template_tools and metadata to support Blog use case.
2019-05-23 17:51:21 -07:00
cassowary 81532f3462 Minor doc additions. 2019-04-17 19:47:00 -07:00
cassowary 8fc5467131 Fix minor error in documentation. 2019-04-17 19:33:22 -07:00
cassowary 3922b13fb1 Documentation additions. Fixes to compilation. Made true passthrough which works. 2019-04-17 19:19:09 -07:00
cassowary 1093636728 Document meatdata. 2019-04-14 19:52:00 -07:00
cassowary cf25d1fa5d Rename todo.md 2019-04-14 18:55:00 -07:00
cassowary 47cced40c7 Fix some things in setup.py 2019-04-14 18:43:15 -07:00
cassowary a9730efec9 Add README.md and update TODO 2019-04-14 18:30:46 -07:00
cassowary 43d40f7fce Major code import. 2019-04-14 17:50:56 -07:00
cassowary 13fb5dac1c Fix perms 2019-04-14 17:49:20 -07:00
cassowary 669a7a1af3 Add documentation and whatnot. 2019-04-14 17:48:22 -07:00
cassowaryandGitHub 1a37054343 Initial commit 2018-11-10 11:38:31 -08:00
19 changed files with 227 additions and 676 deletions
+1 -3
View File
@@ -1,8 +1,6 @@
No Nazis, otherwise:
MIT License
Copyright (c) 2023-2024 Aldercone Studio Collective
Copyright (c) 2023 Cas Rusnov
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
+1
View File
@@ -0,0 +1 @@
include heckweasel/defaults/*.yaml
-10
View File
@@ -20,15 +20,5 @@
* Run commands as part of processing chains
* Project level processing chain overrides in the .meta or whatever.
* Project settings in separate file from .meta that would basically do .meta stuff. Like global meta + config in a top.heck file by default and overridable by a parameter. Maybe
a nice default filename that doesn't start with . (whereas .meta or .heck is the current base metadata)
* Handle the fact that HECKformat metadata is always a list in a more elegent way. We have some hacks in place where scalar values are expected, but if a project mixes
metadata formats it gets a bit messy.
* Provide a database-like interface to the global metadata tree, so that metameta page templates could query the database to assemble indexes and whatnot without looping over the actual files.
* add a version requirement system to projects so if we start breaking things we can use versioning
* add a flag to __main__ to ignore errors even at the end
* add a flag to __main__ to exit on first error rather than waiting til the end.
-65
View File
@@ -1,65 +0,0 @@
<h1>HECKWEASEL documentation!</h1>
<p>Welcome to the index for HECKWEASEL Documentation. In this directory you&rsquo;ll find a bunch of files but this is the introduction you need to understanding the way heckweasel works and how to use it.</p>
<h2>Introduction Part 2: What the hyeck is Heckweasel?!</h2>
<p>Heckweasel is a website compiler framework. Primarily it allows the creation of web site using a collection of flat files which are in a maintainable form, producing the less maintainable formats that web browsers use.</p>
<p>The flat files in a heckweasel project are just a directory of files like any other. There is a default directory structure for projects but that isn&rsquo;t important right now.</p>
<p>Heckweasel projects generally take the form of a collection of one or more templates and a collection of one or more files that are filled into the templates. Pervasively, heckweasel draws a distinction between the contents of a web page and the template it gets put into. You can think of the template, as generally used by heckweasel, as a sort of picture frame into which your content is placed. The content itself may be implemented as one of several popular formats such as Markdown and HTML. Also of note is that there are sort of two routes from heckweeasel input to heckweasel output, one route is through the template system and the other route merely copies the input to the output.</p>
<p>Another important detail about heckweasel is metadata. Every item in the heckweasel project (thus, every file in the heckweasel project directory) has a collection of <em>metadata</em> associated with it, such as its file name, creation time, and other objective information, but also any arbitrary information about it such as its title, a short description, thumbnails or whatever. It&rsquo;s also important to note that the <strong>content</strong> of a file counts as metadata, and is stored the same way inside of heckweasel&rsquo;s way of looking at the files. Metadata is stored with the file as <em>filename</em>.meta and directories contain metadata in the file called .meta. Metadata is also inherited! So setting a template in a directory&rsquo;s metadata will apply to all of the contents of that directory. Metadata is all in a JSON format called JStyleSon, which is JSON except you can have comments in it. All of these metadata are accessable from the templates, which leads to&hellip;</p>
<p>The final important detail about heckweasel is that it, at is core, uses a programmable template system called Jinja. Jinja allows a lot, and I mean a <em>lot</em> of flexability in the way that the output is produced, giving complete programmability. This allows templates (and pages, for that matter) to contain programmable outcomes such as showing a list of all blog entries (each of which would be a separate file), or making a thumbnail gallery from a collection of pictures, or generating an RSS feed from all of the contents of the site. This also allows the website design to be broken into parts such that commonly-used patterns can be merely included in the file rather than being written repeatedly (although normally this function done with the page templates).</p>
<h2>Just the very Basic Heckweasel Project</h2>
<p>So with all of that said, the most basic possible heckweasel project that is actually functional would be something like a page template, and a content file called index. Heckweasel operates on an input directory and outputs to an output directory. This is admittedly not a normal use case since it doesn&rsquo;t benifit much from the elaborate system underneath, but it gets the idea across.</p>
<p>So you have your project directory <code>mywebsite</code>; inside we can have the directories <code>source</code> and <code>publish</code>, and various files, and well here&rsquo;s a picture:</p>
<ul>
<li><strong>mywebsite</strong>
<ul>
<li><strong>source</strong>
<ul>
<li><em>.meta</em></li>
<li><strong>templates</strong>
<ul>
<li><em>default.jinja</em></li>
</ul>
</li>
<li><em>index.md</em></li>
<li><em>index.md.meta</em></li>
</ul>
</li>
<li><strong>publish</strong></li>
</ul>
</li>
</ul>
<p>To explain the various files:</p>
<h3><em>.meta</em></h3>
<p>This file is a JSON file containing project-wide metadata. Usually this would be metadata that applies, by default, to all files. Some things that affect the way Heckweasel processes files would be <code>template</code> which would set the default template to put content into and <code>templates</code> which would set the directory to look for templates in. By custom we also may want to set the title, author and other things like that which we may want to fill into the output files. We also put things like the eventual published address for the site (<code>site_root</code>).</p>
<p>Example .meta file:</p>
<p>```json</p>
<p>{
&ldquo;site_root&rdquo;: &ldquo;https://website.me&rdquo;,
&ldquo;author&rdquo;: &ldquo;Very Nice Person&rdquo;,
&ldquo;title&rdquo;: &ldquo;My Website&rdquo;
}</p>
<p>```</p>
<h3><em>default.jinja</em></h3>
-10
View File
@@ -1,10 +0,0 @@
# I am just writing a simple site with a couple of pages
# I am interested in the technicalities of template development
# I am interested in the technicalities of deployment
+1 -14
View File
@@ -1,14 +1 @@
"""
HeckWeasel: Metadata based static site compiler.
"""
__version__ = '0.7.1'
__copyright__ = "©2023-2024 Aldercone Studio Collective"
from . import metadata
from . import processors
from . import __main__
from . import processchain
from . import processors
from . import template_tools
from . import pygments
from . import utils
__version__ = '0.7.0'
+17 -143
View File
@@ -1,9 +1,3 @@
"""
HeckWeasel command line interface.
Performs compilation step given an input directory. See --help for more information.
"""
# iterate source tree
# create directors in target tree
# for each item:
@@ -17,17 +11,11 @@ import os
import shutil
import sys
import time
from typing import Dict, List, cast
import jinja2.exceptions
from pathlib import Path
from typing import Dict, List, cast, Union
import tqdm
from .metadata import MetaTree, gather_all_metadata, MetaDb
from .metadata import MetaTree
from .processchain import ProcessorChains
from .processors.processors import PassthroughException, NoOutputException
from .processors.processors import PassthroughException
from .pygments import pygments_get_css, pygments_markup_contents_html
from .template_tools import (
date_iso8601,
@@ -35,70 +23,19 @@ from .template_tools import (
file_list,
file_list_hier,
file_json,
file_heck,
file_metadata,
file_name,
file_raw,
time_iso8601,
containsone,
sort_keys,
)
from .utils import deep_merge_dicts
from .__init__ import __version__, __copyright__
logger = logging.getLogger('heckweasel')
logger = logging.getLogger()
logo = f"""
Aldercone Studio Collective
_ _ _
| |_ ___ __| |____ __ _____ __ _ ___ ___| |
| ' \/ -_) _| / /\ V V / -_) _` (_-</ -_) |
|_||_\___\__|_\_\ \_/\_/\___\__,_/__/\___|_|
{__version__}
"""
class TqdmLoggingHandler(logging.Handler):
"""
A simple logging wrapper that won't clobber TQDM's progress bar.
"""
def __init__(self, level=logging.NOTSET):
super().__init__(level)
def setup_logging(verbose: bool = False) -> None:
pass
def emit(self, record):
try:
msg = self.format(record)
tqdm.tqdm.write(msg)
self.flush()
except Exception:
self.handleError(record)
def setup_logging(verbose:bool=False, quiet:bool=False, logfile:Union[Path, str, None]=None) -> None:
"""
Configure logging based on some flags.
"""
# Setup Tqdm handler
logger.setLevel(logging.DEBUG)
h = TqdmLoggingHandler()
if verbose:
f = logging.Formatter('%(asctime)s %(module)-12s %(levelname)-8s %(message)s')
h.setLevel(logging.DEBUG)
h.setFormatter(f)
elif quiet:
f = logging.Formatter('%(levelname)-8s %(message)s')
h.setLevel(logging.CRITICAL)
h.setFormatter(f)
else:
f = logging.Formatter('%(levelname)-8s %(message)s')
h.setLevel(logging.INFO)
h.setFormatter(f)
logger.addHandler(h)
# setup logfile if specified
if logfile:
lf = logging.FileHandler(logfile)
lf.setLevel(logging.DEBUG)
lf.setFormatter(logging.Formatter('%(asctime)s %(module)-12s %(levelname)-8s %(message)s'))
logger.addHandler(lf)
def parse_var(varspec: str) -> List:
if (not ('=' in varspec)):
@@ -120,7 +57,6 @@ def get_args(args: List[str]) -> argparse.Namespace:
parser.add_argument("-t", "--template", help="The template directory (default: root/templates)", default=None)
parser.add_argument("-d", "--dry-run", help="Perform a dry-run.", action="store_true")
parser.add_argument("-v", "--verbose", help="Output verbosely.", action="store_true")
parser.add_argument("--debug", help="output extra debug info.", action="store_true")
parser.add_argument("--processors", help="Specify a path to a processor configuration file.", default=None)
parser.add_argument(
"-D", "--define", help="Add a variable to the metadata.", nargs="+", action="extend", type=parse_var)
@@ -137,17 +73,15 @@ def get_args(args: List[str]) -> argparse.Namespace:
def main() -> int:
print(logo)
try:
args = get_args(sys.argv[1:])
except FileNotFoundError as ex:
logger.info("error finding arguments: {}".format(ex))
print("error finding arguments: {}".format(ex))
return 1
setup_logging(args.verbose)
if os.path.exists(args.output) and args.clean:
bak = "{}.bak-{}".format(args.output, int(time.time()))
logger.info("cleaning target {} -> {}".format(args.output, bak))
print("cleaning target {} -> {}".format(args.output, bak))
os.rename(args.output, bak)
process_chains = ProcessorChains(args.processors)
@@ -164,26 +98,21 @@ def main() -> int:
"author": "",
"author_email": "",
}
if args.define:
for var in args.define:
default_metadata[var[0]] = var[1]
meta_tree = MetaTree(args.root, default_metadata)
file_list_cache = cast(Dict, {})
file_cont_cache = cast(Dict, {})
file_name_cache = cast(Dict, {})
file_raw_cache = cast(Dict, {})
flist = file_list(args.root, file_list_cache)
default_metadata["globals"] = {
"get_file_list": flist,
"get_hier": file_list_hier(args.root, flist),
"get_file_name": file_name(args.root, meta_tree, process_chains, file_name_cache),
"get_file_content": file_content(args.root, meta_tree, process_chains, file_cont_cache),
"get_json": file_json(args.root),
"get_heck": file_heck(args.root),
"get_raw": file_raw(args.root, file_raw_cache),
"get_file_metadata": file_metadata(meta_tree),
"get_time_iso8601": time_iso8601("UTC"),
@@ -191,93 +120,38 @@ def main() -> int:
"pygments_get_css": pygments_get_css,
"pygments_markup_contents_html": pygments_markup_contents_html,
"merge_dicts": deep_merge_dicts,
"containsone": containsone,
'sort_keys': sort_keys,
}
# fixme add no-progress option for loop just to be the files
md = {}
haderrors = False
logger.info("Gathering all metadata")
for meta, item, error in tqdm.tqdm(gather_all_metadata(meta_tree,
args.root,
args.follow_links,
[lambda x: x.endswith('.meta'), lambda x: x.endswith('~')]),
desc="Gathering metadata",
unit="files",
dynamic_ncols=True,
leave=False):
if (args.debug):
print(meta, item, error)
haderrors = error or haderrors
if args.verbose:
logger.debug(f"[dbg] {item} = {meta}")
if item in md:
logger.error("[!] multiple meta? ", item)
md[item] = meta
mdb = MetaDb(md)
# technically metatree has all the md in its cache, but we also have md in a dictionary so who's to say. I guess
# we should make a separate object that lets you query md.
logger.info("Building Webbed Site")
for root, _, files in os.walk(args.root, followlinks=args.follow_links):
workroot = os.path.relpath(root, args.root)
if workroot == ".":
workroot = ""
target_dir = os.path.join(args.output, workroot)
logger.info("[D] Make directory -> {}".format(target_dir))
print("mkdir -> {}".format(target_dir))
if not args.dry_run:
try:
os.mkdir(target_dir)
except FileExistsError:
if args.safe:
logger.info("[A] Error, target directory exists and we are in safe mode, aborting")
print("error, target directory exists, aborting")
return 1
for f in tqdm.tqdm(files, desc="Building webbed site", unit="files", dynamic_ncols=True, leave=False):
for f in files:
# fixme global generic filters
try:
if f.endswith(".meta") or f.endswith("~"):
continue
meta = md[os.path.join(workroot, f)]
meta['mdb'] = mdb
chain = process_chains.get_chain_for_filename(os.path.join(root, f), ctx=meta)
if args.verbose:
logger.debug(f"metadata: {meta}")
logger.info("[P] Processing {} -> chains: {} -> output: {}".format(os.path.join(root, f), repr(chain), os.path.join(target_dir, chain.output_filename)))
metadata = meta_tree.get_metadata(os.path.join(workroot, f))
chain = process_chains.get_chain_for_filename(os.path.join(root, f), ctx=metadata)
print("process {} -> {} -> {}".format(os.path.join(root, f), repr(chain), os.path.join(target_dir, chain.output_filename)))
if not args.dry_run:
try:
# normal output
# FIXME support binary streams
collected_output = [line for line in chain.output]
with open(os.path.join(target_dir, chain.output_filename), "w") as outfile:
outfile.writelines(collected_output)
for line in chain.output:
outfile.write(line)
except PassthroughException:
# write output from input
shutil.copyfile(os.path.join(root, f), os.path.join(target_dir, chain.output_filename))
except NoOutputException:
logger.warn("[S] No content or output prevented {}".format(os.path.join(root, f), os.path.join(target_dir, chain.output_filename)))
# don't write anyp output
pass
except jinja2.exceptions.TemplateSyntaxError as inst:
logger.error(f"[!][S] Template error processing {f} Error was: {inst.filename}:{inst.lineno} {inst.message} (skipped)")
haderrors = True
except BaseException as inst:
# fixme optionally exit on error?
# print(inst.with_traceback())
# print(meta)
logger.error(f"[!][S] General error processing {f} Error was: {inst} (skipped)")
haderrors = True
if haderrors:
logger.error("One or more errors in processing.")
return 1
return 0
def do_main():
sys.exit(main())
if __name__ == "__main__":
do_main()
sys.exit(main())
+111
View File
@@ -0,0 +1,111 @@
# Default: output == input
default:
extension: default
chain:
- passthrough
# Any object that needs jinja scripts but no other explicit processing
templatable:
extension: null
chain:
- jinja2
# Any object that needs jinja and to be embedded in a parent template
tembed:
extension: null
chain:
- jinja2
- jinja2_page_embed
# Markdown, BBCode and RST are first run through the templater, and then
# they are processed into HTML, and finally embedded in a page template.
markdown:
extension:
- md
chain:
- jinja2
- process_md
- jinja2_page_embed
bbcode:
extension:
- bb
- pp
chain:
- jinja2
- process_pp
- jinja2_page_embed
# FIXME implement RST processor
# restructured:
# extension:
# - rst
# chain:
# - jinja2
# - process_rst
# - jinja2_page_embed
# # JSON and YAML are split, passed through a pretty printer, and then output
# FIXME implement split chain processor, implement processor arguments
# json:
# extension:
# - json
# chain:
# - split (passthrough)
# - pp_json
# yaml:
# extension:
# - yml
# - yaml
# chain:
# - split (passthrough)
# - pp_yaml
# Template-html is first passed through the templater, and then embedded
# in a page template
template-html:
extension:
- thtml
- cont
chain:
- jinja2
- jinja2_page_embed
# # Smart CSS are simply converted to CSS.
# sass:
# extension:
# - sass
# - scss
# chain:
# - process_sass
# less:
# extension:
# - less
# chain:
# - process_less
# stylus:
# extension:
# - styl
# chain:
# - process_styl
# # Images are processed into thumbnails and sized in addition to being retained as their original
# FIXME implement split chain processor, implement processor arguments,
# image:
# extension:
# - jpg
# - jpeg
# - png
# chain:
# - split (image_bigthumb)
# - split (image_smallthumb)
# - passthrough
# image_bigthumb:
# extension:
# chain:
# - smart_resize (big)
# image_smallthumb:
# extension:
# chain:
# - smart_resize (small)
+18 -196
View File
@@ -1,25 +1,20 @@
"""Constructs andflag tree-like object containing the metadata for andflag given path, and caches said metadata."""
"""Constructs a tree-like object containing the metadata for a given path, and caches said metadata."""
import fnmatch
import logging
import mimetypes
import os
import uuid
from typing import Any, Dict, List, Optional, Tuple, Union, cast, Callable, Iterable
import yaml
import copy
from typing import Any, Dict, List, Optional, Tuple, Union, cast
import jstyleson
import heckformat.parse
from .utils import guess_mime, iterable
from .utils import guess_mime
# setup mimetypes with some extra ones
mimetypes.init()
mimetypes.add_type("text/html", "thtml")
mimetypes.add_type("text/html", "cont")
mimetypes.add_type("text/x-heckformat", "heck")
logger = logging.getLogger(__name__)
@@ -27,8 +22,6 @@ logger = logging.getLogger(__name__)
class MetaCacheMiss(Exception):
"""Raised on cache miss."""
class MetaLoadError(Exception):
"Raised when metadata fails to load."
class MetaCache:
"""This class provides an in-memory cache for metadata tree."""
@@ -57,10 +50,10 @@ class MetaCache:
MetaCacheMiss: on missing key, or on aged out
"""
if (key not in self._cache):
if key not in self._cache:
raise MetaCacheMiss("no item for key {}".format(key))
if ((self._cache[key][0] + self._max_age) <= new_time_stamp):
if self._cache[key][0] + self._max_age <= new_time_stamp:
return self._cache[key][1]
raise MetaCacheMiss("cache expired for key {}".format(key))
@@ -78,61 +71,26 @@ class MetaCache:
class MetaTree:
"""This provides an interface to loading and caching tree metadata for andflag given directory tree."""
"""This provides an interface to loading and caching tree metadata for a given directory tree."""
def __init__(self, root: str, default_metadata: Optional[Dict] = None):
"""Initialize the metadata tree object.
Arguments:
root (str): The path to the root of the file tree to operate on.
default_metadata (dict, optional): The default metadata to apply to the tree
"""
self._cache = MetaCache()
if (default_metadata is None):
if default_metadata is None:
default_metadata = {}
self._default_metadata = default_metadata
if (root[-1] != "/"):
if root[-1] != "/":
root += "/"
self._root = root
def _get_cache_key(self, fullpath: str):
cachekey = fullpath + '.meta'
if fullpath.endswith(".heck"):
cachekey = fullpath
elif os.path.isdir(fullpath):
cachekey = os.path.join(fullpath, ".meta")
if (not os.path.exists(cachekey)):
cachekey = os.path.join(fullpath, ".heck")
return cachekey
def _load_metadata(self, cachekey: str) -> Dict:
meta = {}
try:
with open(cachekey, "r") as inf:
if cachekey.endswith(".heck"):
with open(cachekey) as cachefile:
h = heckformat.parse.load(cachefile)
meta = h.flatten_replace()
else:
try:
# try json load
meta = jstyleson.load(inf)
except jstyleson.JSONDecodeError as exc:
# try yaml load
try:
meta = yaml.load(inf)
except yaml.parser.ParserError as exc2:
# else either the yaml or json has an error
me = MetaLoadError()
exc2.__context__ = exc
except BaseException as inst:
logger.error(f"Can'tok load any metadata for key {cachekey}: {inst}")
return meta
def get_metadata(self, rel_path: str) -> Dict:
"""Retrieve the metadata for andflag given path
"""Retrieve the metadata for a given path
The general procedure is to iterate the tree, at each level
load .meta (JSON formatted dictionary) for that level, and
@@ -155,10 +113,11 @@ class MetaTree:
fullpath = os.path.join(fullpath, pth)
st = os.stat(fullpath)
cachekey = self._get_cache_key(fullpath)
if os.path.isdir(fullpath):
cachekey = os.path.join(fullpath, ".meta")
else:
cachekey = fullpath + ".meta"
meta = cast(Dict, {})
try:
st_meta = os.stat(cachekey)
meta = self._cache.get(cachekey, st_meta.st_mtime)
@@ -167,165 +126,28 @@ class MetaTree:
except MetaCacheMiss:
meta = {}
# if we didn'tok get any meta from the cache, but the metafile exists, try loading it
if ((not meta) and st_meta):
meta = self._load_metadata(cachekey)
if not meta and st_meta:
meta = jstyleson.load(open(cachekey, "r"))
self._cache.put(cachekey, meta, st_meta.st_mtime)
# add whatever is in the metablob as 'wildcard_metadata' to the metadata if the filename
# matches the wildcards
if ((fullpath == ospath) and ("wildcard_metadata" in metablob)):
if fullpath == ospath and "wildcard_metadata" in metablob:
for wild in metablob["wildcard_metadata"]:
if fnmatch.fnmatch(pth, wild[0]):
metablob.update(wild[1])
metablob.update(meta)
### fill in all objective metadata
# containing directory and filename
metablob["dir"], metablob["fileName"] = os.path.split(rel_path)
# deprecated
# return final dict
metablob["dir"], metablob["file_name"] = os.path.split(rel_path)
# path within the source tree
metablob["filePath"] = rel_path
# deprecated
metablob["file_path"] = rel_path
# the path relative to the output tree
metablob["relpath"] = os.path.relpath("/", "/" + metablob["dir"])
# the UUID for this file
metablob["uuid"] = uuid.uuid3(uuid.NAMESPACE_OID, metablob["uuid-oid-root"] + ospath)
# the pre-split components of the full path
metablob["osPath"], _ = os.path.split(fullpath)
# deprecated
metablob["os-path"], _ = os.path.split(fullpath)
# the mime type we guessed for this file
metablob["guessedType"] = guess_mime(ospath)
# deprecated
metablob["guessed-type"] = guess_mime(ospath)
# if the mime-type isn'tok overriden in the explicit metadata, we make it equal to the guessed type
if "mime-type" not in metablob:
metablob["mime-type"] = metablob["guessed-type"]
# the `stat` components
metablob["stat"] = {}
for stk in ("st_mtime", "st_ctime", "st_atime", "st_mode", "st_size", "st_ino"):
metablob["stat"][stk.replace("st_", "")] = getattr(st, stk)
# return final dict
return metablob
class MetaDb:
def __init__(self, data_dict: Optional[dict] = None):
if data_dict is None:
self.data = {}
else:
self.data = copy.deepcopy(data_dict)
self.indices = {}
self.indexed = False
self.build_indices()
def build_indices(self) -> None:
# build indices? like we could iterate all the items in data_dict, for each key, make an index for that key,
# we tehn set indexed to true and then search will search using indexes
...
def fetch(self, key: Any):
# just return the item keyed as key or None
if (key in self.data):
return self.data[key]
return None
def _get_matches(self, matchk: str, matchv: Optional[Any]) -> set:
# fixme this is where we can do index based optimizations
result = set()
for key, value in self.data.items():
if (value.get(matchk, None) == matchv) or (matchv == '*'):
result.add(key)
return result
def _get_contains(self, matchk: str, matchv: Iterable) -> set:
result = set()
matchv = set(matchv)
for key, value in self.data.items():
v = value.get(matchk, None)
if (not iterable(v)):
v = set([v])
else:
v = set(v)
# print('*** ', key, matchk, v, '->', matchv, result)
if ('*' in matchv) or (len(matchv & v) >= 1):
result.add(key)
return result
def search(self, **kwargs) -> List[dict]:
# always x (or, and) y and not z, and is default
vand = {x for x in self.data.keys()}
vor = set()
vnot = set()
for key, value in kwargs.items():
andflag = True
orflag = False
notflag = False
containsflag = False
if ('_' in key):
tokens = key.split('_')
key = tokens.pop()
for tok in tokens:
match tok:
case 'or':
orflag = True
andflag = False
case 'and':
orflag = False
andflag = True
case 'not':
notflag = True
case 'in':
containsflag = True
case _:
logger.warn(f"unknown metadata search flag `{tok}`")
if containsflag:
# do a contains operation
if (not iterable(value)):
value = [value]
res = self._get_contains(key, value)
else:
# do a simple match operation
res = self._get_matches(key, value)
if notflag:
vnot = vnot | res
elif andflag:
vand = vand & res
elif orflag:
vor = vor | res
# print('** ', andflag, orflag, notflag, containsflag, key, value, res)
rkeys = (vand | vor) - vnot;
return {rk: self.data[rk] for rk in rkeys}
def gather_all_metadata(mt: MetaTree, toproot: str, follow_links: bool, filters: Optional[List[Callable]] = None) -> Tuple:
if filters is None:
filters = []
for root, _, files in os.walk(toproot, followlinks=follow_links):
workroot = os.path.relpath(root, toproot)
if workroot == ".":
workroot = ""
for f in files:
pth = os.path.join(workroot, f)
try:
# fixme global generic filters
if any([x(pth) for x in filters]):
continue
metadata = mt.get_metadata(pth)
yield metadata, pth, False
except BaseException as inst:
logger.error(f"[S] Error loading metadata for {pth} Error was: {inst} (skipped)")
yield dict(), pth, True
def ___test(toproot):
mt = MetaTree(toproot, {'uuid-oid-root':'___test'})
md = {x[1]: x[0] for x in gather_all_metadata(mt, toproot, False, [])}
return MetaDb(md)
+4 -31
View File
@@ -9,26 +9,6 @@ import yaml
from .processors.processors import Processor
PROCESS_CHAIN_DEFAULT = {
'default': {'extension': 'default',
'chain': ['passthrough']
},
'templatable': {'extension': None,
'chain': ['jinja2']
},
'tembed': {'extension': None,
'chain': ['jinja2', 'jinja2_page_embed']
},
'markdown': {'extension': ['md'],
'chain': ['jinja2', 'process_md', 'jinja2_page_embed']},
'bbcode': {'extension': ['bb', 'pp'],
'chain': ['jinja2', 'process_pp', 'jinja2_page_embed']},
'template-html': {'extension': ['thtml', 'cont'],
'chain': ['jinja2', 'jinja2_page_embed']},
'heckformat': {'extension': ['heck'],
'chain': ['process_heck', 'jinja2', 'process_md', 'jinja2_page_embed']}
}
class ProcessorChain:
"""This implements a wrapper for an arbitrary set of processors and an associated file stream."""
@@ -127,9 +107,9 @@ class ProcessorChains:
"""
if config is None: # pragma: no coverage
self.chainconfig = PROCESS_CHAIN_DEFAULT
else:
self.chainconfig = yaml.full_load(open(config, "r"))
config = os.path.join(os.path.dirname(__file__), "defaults", "chains.yaml")
self.chainconfig = yaml.load(open(config, "r"))
self.extensionmap: Dict[str, Any] = {}
self.processors: Dict[str, Type[Processor]] = {}
for ch, conf in self.chainconfig.items():
@@ -166,10 +146,7 @@ class ProcessorChains:
ftype = "default"
if ctx and "type" in ctx:
if isinstance(ctx["type"], str):
ftype = ctx["type"]
else:
ftype = ctx["type"][0]
return self.get_chain_for_file(open(filename, "r"), ftype, filename, ctx)
def get_chain_for_file(
@@ -196,12 +173,8 @@ class ProcessorChains:
if not (bool(file_name)):
file_name = hex(random.randint(0, 65536))
chainconfig = self.chainconfig[file_type]["chain"]
if (ctx and 'process_chain' in ctx):
chainconfig = ctx['process_chain']
return ProcessorChain(
[self.processors[x]() for x in chainconfig],
[self.processors[x]() for x in self.chainconfig[file_type]["chain"]],
cast(str, file_name),
file_obj,
file_type,
-11
View File
@@ -1,12 +1 @@
# processors metadata here
from . import jinja2_page_embed
from . import jinja2
from . import passthrough
from . import process_heck
from . import process_less
from . import process_md
from . import processors
from . import process_pp
from . import process_sass
from . import process_styl
+1 -2
View File
@@ -21,8 +21,7 @@ class Jinja2(PassThrough):
Returns:
iterable: The post-processed output stream
"""
if (ctx is None):
ctx = {}
ctx = cast(Dict, ctx)
template_env = Environment(loader=FileSystemLoader(ctx["templates"]), extensions=["jinja2.ext.do"])
template_env.globals.update(ctx["globals"])
template_env.filters.update(ctx["filters"])
@@ -53,12 +53,7 @@ class Jinja2PageEmbed(Processor):
template_env = Environment(loader=FileSystemLoader(ctx["templates"]), extensions=["jinja2.ext.do"])
template_env.globals.update(ctx["globals"])
template_env.filters.update(ctx["filters"])
if isinstance(ctx["template"], str):
tmpl = template_env.get_template(ctx["template"])
else:
# we've got a heck
tmpl = template_env.get_template(ctx["template"][0])
# print(tmpl)
content = "".join([x for x in input_file])
return tmpl.render(content=content, metadata=ctx)
-85
View File
@@ -1,85 +0,0 @@
"""Convert a HECKformat file to a markdown stream."""
import io
import os
from typing import Dict, Iterable, Optional
import heckformat.parse
from .processors import Processor, NoOutputException
class HECKformatProcessor(Processor):
"""Convert a HECKformat file to a markdown stream."""
def filename(self, oldname: str, ctx: Optional[Dict] = None) -> str:
"""Return the filename of the post-processed file.
Arguments:
oldname (str): the previous name for the file.
ctx (dict, optional): A context object generated from the processor configuration
Returns:
str: the new name for the file
"""
if ctx and 'heck_filename' in ctx:
return ctx['heck_filename']
return os.path.splitext(oldname)[0] + this.extension(oldname, ctx)
def mime_type(self, oldname: str, ctx: Optional[Dict] = None) -> str:
"""Return the mimetype of the post-processed file.
Arguments:
oldname (str): the input filename
ctx (dict, optional): A context object generated from the processor configuration
Returns:
str: the new mimetype of the file after processing
"""
if ctx and 'heck_mime' in ctx:
return ctx['heck_mime']
return "text/x-markdown"
def extension(self, oldname: str, ctx: Optional[Dict] = None) -> str:
"""Return the mimetype of the post-processed file.
Arguments:
oldname (str): the input filename
ctx (dict, optional): A context object generated from the processor configuration
Returns:
str: the new extension of the file after processing
"""
if ctx and 'heck_extension' in ctx:
return ctx['heck_extension']
return "md"
def process(self, input_file: Iterable, ctx: Optional[Dict] = None) -> Iterable:
"""Return an iterable object of the post-processed file.
Arguments:
input_file (iterable): An input stream
ctx (dict, optional): A context object generated from the processor configuration
Returns:
iterable: The post-processed output stream
"""
elm = heckformat.parse.load_heck(input_file).flatten_replace()
for key in elm:
if key.startswith(heckformat.parse.UNPARSED_MARKER):
# fixme later we should use the doclabel to choose which output processor somehow~
doclabel = key.split(' ')[-1]
# we'll just assume the first unparsed part of the document is the page
return elm[key]
# No documents in the input heck, we just prevent output
raise NoOutputException()
processor = HECKformatProcessor # pylint: disable=invalid-name
-2
View File
@@ -5,8 +5,6 @@ from typing import Dict, Iterable, Optional
class PassthroughException(Exception):
"""Raised when the processor would like the file to pass through unchanged."""
class NoOutputException(Exception):
"""Raised when the processor would like no output to be written from the processing chain."""
class ProcessorException(Exception): # pragma: no cover
"""A base exception class to be used by processor objects."""
+1 -60
View File
@@ -1,6 +1,3 @@
"""
Provides various utility functions that are exposed to the templates.
"""
import copy
import datetime
import glob
@@ -18,10 +15,6 @@ from .utils import deep_merge_dicts
def file_list(root: str, listcache: Dict) -> Callable:
"""
Return a function (memoized for the cache and root directory) which returns a list of files matching glob and
sorted as required.
"""
def get_file_list(
path_glob: Union[str, List[str], Tuple[str]],
*,
@@ -61,7 +54,7 @@ def file_list(root: str, listcache: Dict) -> Callable:
def file_list_hier(root: str, flist: Callable) -> Callable:
"""Return a function which, given a directory, will walk the directory and return the files within
"""Return a callable which, given a directory, will walk the directory and return the files within
it that match the glob passed."""
def get_file_list_hier(path: str, glob: str, *, sort_order: str = "ctime", reverse: bool = False) -> Iterable:
@@ -82,10 +75,6 @@ def file_list_hier(root: str, flist: Callable) -> Callable:
def file_name(root: str, metatree: MetaTree, processor_chains: ProcessorChains, namecache: Dict) -> Callable:
"""
Return a function (memoized for root directory, metatree and processor chains) which returns the output filename
given an input filename based on metadata and said processing chains.
"""
def get_file_name(file_name: str) -> Dict:
if file_name in namecache:
return namecache[file_name]
@@ -98,9 +87,6 @@ def file_name(root: str, metatree: MetaTree, processor_chains: ProcessorChains,
def file_raw(root: str, contcache: Dict) -> Callable:
"""
Return a function (memoizedfor the root directory) which returns the raw content of a file.
"""
def get_raw(file_name: str) -> str:
if file_name in contcache:
return contcache[file_name]
@@ -111,10 +97,6 @@ def file_raw(root: str, contcache: Dict) -> Callable:
def file_json(root: str) -> Callable:
"""
Return a function (memoized for the root directory) which loads a file as json, merges it with an optional input dictionary
and returns.
"""
def get_json(file_name: str, parent: Dict = None) -> Dict:
outd = {}
if parent is not None:
@@ -126,27 +108,7 @@ def file_json(root: str) -> Callable:
return get_json
def file_heck(root: str) -> Callable:
"""
Return a function (memoized for the root directory) which loads a file as HECKFormat, merges it with an optional input
dictionary, and returns.
"""
def get_heck(file_name: str, parent: Dict = None) -> Dict:
outd = {}
if parent is not None:
outd = copy.deepcopy(parent)
with open(os.path.join(root, file_name), "r", encoding="utf-8") as f:
return deep_merge_dicts(outd, heckformat.parse.load(f).flatten_replace())
return get_heck
def file_content(root: str, metatree: MetaTree, processor_chains: ProcessorChains, contcache: Dict) -> Callable:
"""
Return a function (memoized for the root directory, metatree, and processor chains) which returns the post-processed
content of the input file.
"""
def get_file_content(file_name: str) -> Iterable:
if file_name in contcache:
return contcache[file_name]
@@ -159,25 +121,13 @@ def file_content(root: str, metatree: MetaTree, processor_chains: ProcessorChain
def file_metadata(metatree: MetaTree) -> Callable:
"""Returns a function (memoized for a metatree) which returns the meta data for a given file."""
def get_file_metadata(file_name: str) -> Dict:
return metatree.get_metadata(file_name)
return get_file_metadata
def containsone(needle: Iterable, haystack: Iterable):
"""
Returns true if at least one of the contents of needle is in haystack.
"""
for n in needle:
if n in haystack:
return True
return False
def time_iso8601(timezone: str) -> Callable:
"""Returns a function (memoized for a particular timezone) which formats a time as ISO8601 standard. """
tz = pytz.timezone(timezone)
def get_time_iso8601(time_t: Union[int, float]) -> str:
@@ -187,18 +137,9 @@ def time_iso8601(timezone: str) -> Callable:
def date_iso8601(timezone: str) -> Callable:
"""Returns a function (memoized for a particular timezone) which formats a date as ISO8601 standard. """
tz = pytz.timezone(timezone)
def get_date_iso8601(time_t: Union[int, float]) -> str:
return datetime.datetime.fromtimestamp(time_t, tz).strftime("%Y-%m-%d")
return get_date_iso8601
def sort_keys(ind: dict, field: str, inv: bool = False, defval = None) -> str:
"""Return the keys from a dictionary after sorting on a field of the dictionary's values."""
def dvcmp(a):
return a[1].get(field, defval)
return [x[0] for x in sorted(list(ind.items()), key=dvcmp, reverse=inv)]
+1 -9
View File
@@ -2,8 +2,7 @@ from typing import Dict, Optional
import copy
import mimetypes
import os
import collections.abc
import types
def merge_dicts(dict_a: Dict, dict_b: Dict) -> Dict:
"""Merge two dictionaries (shallow).
@@ -62,9 +61,6 @@ def guess_mime(path: str) -> Optional[str]:
str: the guessed mime-type
"""
# if path.endswith('.heck'):
# return "text/x-heckformat"
mtypes = mimetypes.guess_type(path)
ftype = None
if os.path.isdir(path):
@@ -74,7 +70,3 @@ def guess_mime(path: str) -> Optional[str]:
else:
ftype = "application/octet-stream"
return ftype
def iterable(var):
return not (isinstance(var, str) or not isinstance(var, collections.abc.Iterable))
-21
View File
@@ -1,21 +0,0 @@
[build-system]
requires = ["pdm-backend"]
build-backend = "pdm.backend"
[project]
name = "heckweasel"
dynamic = ["version"]
description = "A metadata based static site compiler with CMS-like features."
authors = [{name = "Cassowary", email="cassowary@aldercone.studio"}]
dependencies = ["yaml-1.3", "markdown", "jstyleson", "jinja2", "pygments", "heckformat"]
requires-python = ">=3.8"
readme = "README.md"
license = {text = "LICENSE"}
[tool.pdm.version]
source = "file"
path = "heckweasel/__init__.py"
[project.scripts]
heckweasel = "heckweasel.__main__:do_main"
+62
View File
@@ -0,0 +1,62 @@
"""Package configuration."""
from setuptools import find_packages, setup
from heckweasel import __version__
LONG_DESCRIPTION = """Heckweasel is a filesystem based static site generator."""
INSTALL_REQUIRES = ["yaml-1.3", "markdown", "jstyleson", "jinja2", "pygments"]
# Extra dependencies
EXTRAS_REQUIRE = {
# Test dependencies
"tests": [
"black",
"bandit>=1.1.0",
"flake8>=3.2.1",
"mypy>=0.470",
"prospector[with_everything]>=0.12.4",
"pytest-cov>=1.8.0",
"pytest-xdist>=1.15.0",
"pytest>=3.0.3",
"sphinx_rtd_theme>=0.1.6",
"sphinx-argparse>=0.1.15",
"Sphinx>=1.4.9",
]
}
SETUP_REQUIRES = ["pytest-runner>=2.7.1", "setuptools_scm>=1.15.0"]
setup(
author="Cassowary Rusnov",
author_email="alderconestudio@gmail.com",
classifiers=[
"Development Status :: 1 - Pre-alpha",
"Environment :: Console",
"License :: OSI Approved :: MIT",
"Operating System :: POSIX :: Linux",
"Programming Language :: Python :: 3.6",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3 :: Only",
"Topic :: Software Development :: Libraries :: Python Modules",
],
description="A filesystem-based website generator / CMS",
# entry_points={
# 'console_scripts': [
# 'cookbook = spicerack.cookbook:main',
# ],
# },
include_package_data=True,
extras_require=EXTRAS_REQUIRE,
install_requires=INSTALL_REQUIRES,
keywords=["cms", "website", "compiler"],
license="MIT",
long_description=LONG_DESCRIPTION,
name="heckweasel",
packages=find_packages(exclude=["*.tests", "*.tests.*"]),
platforms=["GNU/Linux"],
setup_requires=SETUP_REQUIRES,
use_scm_version=True,
url="https://git.aldercone.studio/aldercone/heckweasel",
zip_safe=False,
version=__version__,
)