diff --git a/README.md b/README.md index 7adc1fb72f5466aec19c84e00937d56aa0955029..723fcbc5cd2e96080bcf4290ce79b8430e550bee 100644 --- a/README.md +++ b/README.md @@ -58,15 +58,72 @@ $ tree ``` Boop! basically just copied `content` files under a `output` directory. But it -can convert Markdown files in HTML too: +can convert Markdown files in HTML too. For that, you'll need a template file +for articles: + +```console +$ mkdir theme +$ echo '{{ ARTICLE_CONTENT }} theme/article.html +``` + +Then, you can create your Markdown file as you did with the HTML one: ```console $ echo '# Welcome!' > content/markdown-index.md $ boop.py $ echo output/markdown-index.html -

Welcome!

+

Welcome!

``` +You can define meta variables in the Markdown file which will be accessible +then in the template. Variables are uppercased and prepended by `ARTICLE_`. For +example, for the following Markdown file: + +```markdown +--- +title: Welcome! +author: Marien +--- + +This is my article. +``` + +And the HTML template: + +```html + + + {{ ARTICLE_TITLE }} + + +

{{ ARTICLE_TITLE }}

+

By {{ ARTICLE_AUTHOR }}

+ + {{ ARTICLE_CONTENT }} + + +``` + +It will produce the following file: + +```html + + + Welcome! + + +

Welcome!

+

By Marien

+ +

This is my article.

+ + +``` + +For now, you can use any valid Python expression between `{{ }}`, you have +access to the variables defined in the meta header, the Python builtin +functions and the `datetime` module. + ## Tests There are some tests (i.e. doctests) that can be run to check that everything diff --git a/boop.py b/boop.py index 2a25f419ded2371dcf9d159eb7745d8c08ec6ad5..770a776777e75869b82d1700c8ad29c64f96f930 100755 --- a/boop.py +++ b/boop.py @@ -3,9 +3,15 @@ import os import sys import shutil +import locale import markdown +import boopsy + + +locale.setlocale(locale.LC_ALL, "fr_FR.utf8") + class ProgramError(Exception): """Exception raised during the program execution. @@ -41,6 +47,20 @@ def mkdirs_for_file(filepath): os.makedirs(path, exist_ok=True) +def convert_meta_to_article_vars(meta): + """Convert Markdown Meta to vars for article layout. + """ + article_vars = {} + for key, values in meta.items(): + article_key = f"ARTICLE_{key.upper()}" + # The Markdown's meta extension extract all the values in arrays, even + # if there is only one value. Because there is no iteration system in + # the template system, for the moment we just get the first value of + # the array. + article_vars[article_key] = values[0] + return article_vars + + def main(): # Check that content directory exists on the filesystem content_path = os.path.join(os.curdir, "content") @@ -56,7 +76,7 @@ def main(): # ... but it also can be a simple file if user created it manually! os.remove(output_path) - md = markdown.Markdown() + md = markdown.Markdown(extensions=["meta"]) # And copy the files from ./content to ./output for filepath in dir_tree(content_path): @@ -74,9 +94,18 @@ def main(): with open(content_filepath, "r") as content_file: html = md.convert(content_file.read()) - # And we write it as a HTML file + # We initialize the template + article_template_filepath = os.path.join("theme", "article.html") + template = boopsy.Template(article_template_filepath) + + # We get the local variables from the Markdown file (metadata) + # which will be accessible in the article template + article_vars = convert_meta_to_article_vars(md.Meta) + article_vars["ARTICLE_CONTENT"] = html + + # And we write the content in the output file with open(output_filepath, "w") as output_file: - output_file.write(html) + output_file.write(template.render(article_vars)) else: # ... else, we simply copy the file to the output directory shutil.copyfile(content_filepath, output_filepath) diff --git a/boopsy.py b/boopsy.py new file mode 100644 index 0000000000000000000000000000000000000000..8bf540de277c3028e7c86005475a2f54a16a77e2 --- /dev/null +++ b/boopsy.py @@ -0,0 +1,46 @@ +import re +from datetime import datetime + + +class BoopsySyntaxError(Exception): + """Exception raised if template file is invalid. + """ + + pass + + +class Template: + def __init__(self, template_name): + """Initialize a Boopsy template from file content. + """ + self.template_name = template_name + with open(self.template_name, "r") as f: + self.template = f.read() + + def render(self, variables): + """Return a string corresponding to template content with vars replaced. + """ + rendering = [] + + # The template is splitted in tokens: + # - {{ a_python_expression }} + # - or all the other strings + tokens = re.split(r"(?s)({{.*?}})", self.template) + for token in tokens: + if token.startswith("{{"): + expression = token[2:-2].strip() + if len(expression) == 0: + raise BoopsySyntaxError("an expression is expected between {{ }}") + + # We evaluate a Python expression which can access the + # variables passed as an argument of `render` and the + # `datetime` module. + evaluated_expression = eval( + expression, {"datetime": datetime}, variables + ) + rendering.append(evaluated_expression) + elif token: + # default tokens must be added to the rendering array + rendering.append(token) + + return "".join(rendering)