1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
|
#!/usr/bin/env python3
"""Thrust is a static site generator."""
import sys
import textwrap
from typing import Any, Dict
import jinja2
import jinja2.ext
import jinja2.nodes
import markdown
import yaml
# cribbed from https://jinja.palletsprojects.com/en/2.11.x/extensions/#cache.
class MarkdownExtension(jinja2.ext.Extension):
"""MarkdownExtension provides {% markdown %} and {% endmarkdown %}."""
# a set of names that trigger the extension.
tags = {'markdown'}
def __init__(self, environment):
super(MarkdownExtension, self).__init__(environment)
# add the defaults to the environment
environment.extend(markdown=markdown.Markdown())
def parse(self, parser):
# the first token is the token that started the tag.
# we only listen to `markdown`, so it will always be that.
# we keep the line number for the nodes we create later on.
lineno = next(parser.stream).lineno
# parse the body of the `{% markdown %}` up to `{% endmarkdown %}`.
body = parser.parse_statements(['name:endmarkdown'], drop_needle=True)
# return a `CallBlock` node that calls self._markdown_support.
return jinja2.nodes.CallBlock(
self.call_method('_markdown_support'), [], [], body).set_lineno(lineno)
def _markdown_support(self, caller):
"""Helper callback."""
block = caller()
block = textwrap.dedent(block)
return self.environment.markdown.convert(block)
class Thrust:
"""Thrust is a document templater."""
def __init__(self, template: str, data: Dict[str, Any]):
self.template = template
self.data = data
@classmethod
def from_path(cls, path: str) -> 'Thrust':
with open(path) as f:
return cls.parse(f.read())
@classmethod
def parse(cls, src: str) -> 'Thrust':
"""Parse a source file to maybe return a valid Thrust object."""
lines = src.split('\n')
parts = []
for line in lines:
if line == '---':
parts.append([])
else:
parts[-1].append(line)
if len(parts) != 2:
raise ValueError(
f'file needs 2 "---" deliniated parts, found {len(parts)}')
data = yaml.safe_load('\n'.join(parts[0]).replace('\t', ' '))
if data is None:
data = {}
template = '\n'.join(parts[1])
return cls(template, data)
@property
def _environment(self) -> jinja2.Environment:
environment = jinja2.Environment(
loader=jinja2.FileSystemLoader('.'),
extensions=[MarkdownExtension],
trim_blocks=True,
)
environment.markdown = markdown.Markdown(
extensions=['attr_list', 'extra', 'toc'])
def loadthrust_filter(path: str) -> str:
return Thrust.from_path(path)
environment.filters['loadthrust'] = loadthrust_filter
def markdown_filter(src: str) -> str:
return environment.markdown.convert(src)
environment.filters['markdown'] = markdown_filter
def template_filter(tmpl: str) -> str:
return environment.from_string(tmpl).render(self.data)
environment.filters['template'] = template_filter
def prefix_filter(s: str, prefix: str, first=True) -> str:
if not first:
lines = s.splitlines()
return '\n'.join(lines[0:1] + [f'{prefix}{l}' for l in lines[1:]])
return '\n'.join([f'{prefix}{l}' for l in s.splitlines()])
environment.filters['prefix'] = prefix_filter
return environment
def render(self) -> str:
"""Render the data in the template."""
return self._environment.from_string(self.template).render(self.data)
if __name__ == '__main__':
if len(sys.argv) != 2:
print(f'usage: {sys.argv[0]} <in-path>')
sys.exit(1)
in_path = sys.argv[1]
try:
thrust = Thrust.from_path(in_path)
print(thrust.render())
except ValueError as error:
print(f'{in_path}: {error}', file=sys.stderr)
sys.exit(1)
except jinja2.exceptions.TemplateError as error:
print(error, file=sys.stderr)
sys.exit(1)
|