By using this site, you agree to have cookies stored on your device, strictly for functional purposes, such as storing your session and preferences.

Dismiss

 ampoule.html

View raw Download
text/html • 21.21 kiB
HTML document, Unicode text, UTF-8 text, with very long lines (394)
        
            
1
<!DOCTYPE html>
2
<html lang="en">
3
<head>
4
<meta charset="UTF-8">
5
<title>
6
Ampoule
7
</title>
8
<link rel="stylesheet" href="/static/style.css">
9
<meta name="viewport" content="width=device-width, initial-scale=1.0">
10
</head>
11
<body>
12
<header>
13
<a href="#main" id="skip-link">Skip navigation</a>
14
<nav>
15
<ul>
16
<li><a href="/">Home</a></li>
17
<li><a href="/projects">Projects</a></li>
18
<li><a href="/index">Index</a></li>
19
<li><a href="/about">About</a></li>
20
<li><a href="https://roundabout-host.com/roundabout">Roundabout-host</a></li>
21
</ul>
22
<ul>
23
<li><a href="mailto:root@roundabout-host.com" id="mail-link">root@roundabout-host.com</a></li>
24
</ul>
25
</nav>
26
</header>
27
<main id="main">
28
29
<div class="project-title">
30
<h1>
31
Ampoule
32
</h1>
33
<a href="https://roundabout-host.com/roundabout/ampoule" class="repository-button">Go to repository</a>
34
</div>
35
<p class="tags">
36
37
<a href="/index/software.html" class="tag">software</a>
38
39
<a href="/index/web.html" class="tag">web</a>
40
41
<a href="/index/python.html" class="tag">python</a>
42
43
<a href="/index/ampoule.html" class="tag">ampoule</a>
44
45
<a href="/index/jinja2.html" class="tag">jinja2</a>
46
47
<a href="/index/docs.html" class="tag">docs</a>
48
49
<a href="/index/gpl.html" class="tag">gpl</a>
50
51
</p>
52
<article class="content-area">
53
<p>Ampoule is a lightweight, simple yet flexible, static site generator written in Python.
54
It uses Jinja2 for templating. This site was generated using Ampoule.
55
</p><h2>Features</h2><ul><li><p><strong class="emphasis-2"><em class="emphasis-1">Extremely</em> simple and small</strong>, only a few hundred lines of code.
56
</p></li><li><p><em class="emphasis-1">Only</em> depends on Jinja2, Ruamel YAML, bs4, and colorama.
57
</p></li><li><p><strong class="emphasis-2">Jinja2 templating</strong> will be familiar to Flask users. Now you can use the same templates for
58
both dynamic and static sites.
59
</p></li><li><p>More of <strong class="emphasis-2">a framework</strong>. Sites are generated by a short <strong class="emphasis-2">Python script</strong> that you write to customise
60
what <strong class="emphasis-2">pages</strong> it loads, which <strong class="emphasis-2">templates</strong> it uses, and what <strong class="emphasis-2">data</strong> it passes to them, or create
61
custom <strong class="emphasis-2">filters</strong>, <strong class="emphasis-2">tests</strong> and more.
62
</p></li><li><p>Supports <strong class="emphasis-2">YAML front matter</strong> for pages. It can be accessed using indexing syntax.
63
</p></li><li><p><strong class="emphasis-2">Indexes</strong> can be sorted using a function, iterated and can index any directory, recursively
64
or not. They can also <strong class="emphasis-2">transform URLs</strong> to make them end in ".html".
65
</p></li><li><p><strong class="emphasis-2">Object-oriented</strong> design. The same objects used in that script can also be passed to the
66
templates.
67
</p></li><li><p>Any <strong class="emphasis-2">markup language</strong> can be used, as long as it can be converted to HTML. You just need to
68
configure a filter for it. You can even mix multiple markup languages in the same site.
69
</p></li><li><p>Ships with a light <strong class="emphasis-2">markdown</strong> implementation.
70
</p></li><li><p>Easy to use for <strong class="emphasis-2"><em class="emphasis-1">both</em> programmers and non-programmers</strong>. While you do need a script, you can
71
also use an off-the-shelf one.
72
</p></li><li><p><strong class="emphasis-2">Themes</strong> can be <em class="emphasis-1">exactly how you want</em>.
73
</p></li><li><p>Keeping <strong class="emphasis-2">static files</strong> is easy, because indexes can be static.
74
</p></li><li><p>Static files are always <strong class="emphasis-2">binary</strong> and not templated. The same happens for files that can't be
75
decoded.
76
</p></li><li><p><strong class="emphasis-2">URL</strong>-based definitions. Pages are added using the URL that will be used to access them.
77
</p></li><li><p>Reinforces the <strong class="emphasis-2">web</strong> as a <strong class="emphasis-2">publishing medium</strong>. Static sites are not for everyone, but if you
78
want to <strong class="emphasis-2">publish</strong> something, it's the best way.
79
</p></li><li><p>And GitHub will give you <strong class="emphasis-2">free hosting</strong>, because it's static and <em class="emphasis-1">very cheap to serve</em>.
80
Roundabout-host now also offers free hosting for static sites and will soon offer a way to
81
generate them using CI and the generator you prefer.
82
</p></li><li><p>It's <strong class="emphasis-2">free software</strong> and available under the <strong class="emphasis-2">GPLv3</strong>.
83
</p></li><li><p><strong class="emphasis-2">No JavaScript</strong> is required, but it can of course be used if you want.
84
</p></li><li><p>Decently <strong class="emphasis-2">fast</strong>: even if you've got a huge site, it should not take more than <em class="emphasis-1">30 seconds</em>.
85
Local rebuilding will also be added. And it's still much faster than any dynamic site.
86
</p></li><li><p>Beautiful logging thanks to colorama.
87
</p></li><li><p>Great for educational use; you can learn <strong class="emphasis-2">Python</strong>, <strong class="emphasis-2">HTML</strong>, <strong class="emphasis-2">CSS</strong>, <strong class="emphasis-2">JavaScript</strong>,
88
and <strong class="emphasis-2">Jinja2</strong> all at once.
89
</p></li><li><p>You can <strong class="emphasis-2">make your site</strong> in <em class="emphasis-1">an hour</em>, and then it's time to focus on writing what you want
90
to publish.
91
</p></li><li><p>If you see fit, it's easy to <strong class="emphasis-2">convert</strong> to a dynamic site. A <strong class="emphasis-2">Flask implementation</strong> is
92
planned.
93
</p></li><li><p>Clear and <strong class="emphasis-2">magic-free</strong>. You can see exactly what's happening and why. No magic, no
94
configuration files, no hidden behaviour. The code is so short you can read it.
95
</p></li></ul><h2>Minimal example</h2><pre data-language="python">import string
96
from datetime import datetime
97
import string
98
99
import ampoule_ssg as ampoule
100
from ampoule_ssg import markdown
101
102
# Create a site object. This is where we are adding pages to. The argument is the directory
103
# where the site will be built.
104
site = ampoule.Site("my_site")
105
106
107
# Use this as "| markdown" in Jinja2 templates to convert any Markdown source to HTML.
108
@site.filter("markdown")
109
def markdown_filter(text):
110
return markdown.markdown2html(text)
111
112
113
# Make the URLs web-friendly and make it end in ".html" so it will be correctly formatted
114
# by dumb servers.
115
def article_url(url):
116
url = url.lower().rpartition(".")[0]
117
118
new_url = ""
119
for i in url:
120
if i in string.ascii_lowercase:
121
new_url += i
122
elif i in string.whitespace:
123
new_url += "-"
124
125
return new_url + ".html"
126
127
128
# Set context that will be passed to all templates. You can still override this.
129
site.context["timestamp"] = datetime.now()
130
site.context["ampoule"] = ampoule
131
132
# Add the index of articles. In the template, we're looping over it to list them all.
133
articles = ampoule.Index("articles", url_transform=article_url, sort_by=lambda x: x.date)
134
# This makes it take all indexed files and put them under the /articles URL, keeping the
135
# index's URL transformation and placing all of them in the article.html template. This
136
# will be passed as "document" to the template.
137
site.add_from_index(articles, "/articles", "article.html")
138
139
# Create the main page which has access to the index so it can list all articles.
140
main_page = ampoule.Page(site, "home.html", articles=articles)
141
142
# Add the page. Note how we're binding it to a path; it will automatically be set as
143
# index.html in that directory, and the URL is site-relative, not the OS root.
144
site.add_page("/", main_page)
145
146
# Add static files using a recursive static index. It will add all files in the static
147
# directory and all its subdirectories, without putting them into templates. You could
148
# still use them in templates, so you can make a photo gallery or something.
149
site.add_from_index(
150
# We're excluding Markdown files because we're using them as licence information
151
# for when the site is distributed together with the fonts. You can exclude any
152
# file you want using regex.
153
ampoule.Index("static", recursive=True, exclude=r"\.md$", static=True),
154
"/static",
155
# There is no template, because the index is static.
156
)
157
158
# Makes Ampoule take all pages and put them in a directory.
159
site.build()
160
</pre><h2>More information</h2><h3>Name origin</h3><p>An ampoule is smaller than a flask. Because it is related to Flask (it uses Jinja2) but is
161
a much smaller static version of it, the name makes sense.
162
</p><h3>What about the other static site generators?</h3><p>There are many static site generators out there, but they all have their own problems.
163
In particular, I haven't seen one that uses code to describe the site, rather than a
164
configuration file. This makes it much more flexible and powerful.
165
</p><p>Also, Ampoule is familiar to Python programmers, because it's written in Python and uses
166
Jinja2, a templating engine that is also used in Flask. It's even the smallest static site
167
generator:
168
</p><ol><li><p>Hugo: written in Go, uses go html/template, and it has 133k lines of Go, not counting
169
comments or blanks.
170
</p></li><li><p>Jekyll: written in Ruby, uses Liquid, and it has 17300 lines of Ruby, not counting
171
comments or blanks. Interestingly, it's got more Markdown than Ruby.
172
</p></li><li><p>Gatsby: they call it a framework, and rightfully so, because it's overkill for actually
173
static (i.e. for publishing content) sites, even though JS people use it for precisely that
174
purpose. It's written in JavaScript, uses React, and it's git 380k lines of JavaScript and
175
TypeScript combined. (For comparison, it's over 1/100 of Linux itself, which is HUGE considering
176
it uses a high-level language and only has to do so much.)
177
</p></li><li><p>Pelican: written in Python, uses Jinja2, and it has 12400 lines of Python, not counting
178
comments or blanks.
179
</p></li><li><p>Docusaurus: written in TypeScript, uses React (of course, because it's made by Facebook),
180
and it has 140k lines of TypeScript and JavaScript combined.
181
</p></li><li><p>VuePress: written in JavaScript, uses Vue, and it has 11k lines of JavaScript, Vue and
182
TypeScript combined.
183
</p></li><li><p>Zola: written in Rust, uses Tera, and it has 17k lines of Rust, not counting comments or
184
blanks. Also, it's designed to be monolithic and not extensible at all.
185
</p></li></ol><p>Whereas I have only got 750 lines of Python, not counting comments or blanks. Add the script
186
to generate the site, and it's still under 1000 lines.
187
</p><p>I don't want to criticise other static site generators, they all do some things well, but
188
they're not what I want. I want a simple, small, flexible and versatile static site generator
189
that is low-maintenance and easy to use. I don't know about you, but maybe you want the same
190
thing.
191
</p><p>The JS-based ones are particularly unsuitable for most people, because they're slow, bloated,
192
hard to install, and most often actually generate an SPA, which is not what you want for a
193
blog or documentation or web book or anything like that.
194
</p><h3>Why generated static sites?</h3><p>If you don't want generated static sites, you've got two other options.
195
</p><h4>Dynamic sites</h4><ul><li><p>bloated;
196
</p></li><li><p>slow;
197
</p></li><li><p>requires smart server;
198
</p></li><li><p>requires maintenance;
199
</p></li><li><p>requires security;
200
</p></li><li><p>requires a database;
201
</p></li><li><p>hard to post content;
202
</p></li><li><p>databases can't be managed with git;
203
</p></li><li><p>hard to import content;
204
</p></li><li><p>no free hosting;
205
</p></li></ul><h4>Static sites</h4><ul><li><p>hard to manage layouts;
206
</p></li><li><p>hard to list the content;
207
</p></li><li><p>hard to update indexes;
208
</p></li><li><p>no support for metadata;
209
</p></li><li><p>markup languages must be manually converted;
210
</p></li></ul><p>With a <em class="emphasis-1">generated</em> static site, you get the best of both worlds. It's the best publishing
211
platform, because it's just files, but it still provides the convenience of just writing
212
content and having it magically appear on the site and formatted correctly.
213
</p><h2>How to install</h2><p>Please note that this is not yet available on PyPI. For now you'll need to download the code
214
(ideally using git) and install it with <code>pip</code> as a local package by giving it the path to the
215
directory containing <code>setup.py</code>.
216
</p><h2>Full documentation</h2><p>To demonstrate just how easy it is, the docs can all fit on one page.
217
</p><h3>class <code>ampoule_ssg.Site</code></h3><p><code>Site</code> is the main class of Ampoule; it represents a single website. It is responsible for
218
handling added pages, the template engine and features, as well as building it.
219
</p><h4>def <code>__init__(self, build_dir: typing.Union[str, bytes, os.PathLike], template_dir: typing.Union[str, bytes, os.PathLike] = "templates")</code></h4><p>Create a new site object. <code>build_dir</code> is the directory where the site will be built.
220
<code>template_dir</code> is the directory where the templates are stored. Both are relative to the
221
script current working directory.
222
</p><h4>def <code>add_page(self, location: typing.Union[str, bytes, os.PathLike], page: typing.union[Static, Page])</code></h4><p>Add a page object to the site at the server-relative URL <code>location</code>. The page object can be
223
either a <code>Static</code> or a <code>Page</code>.
224
</p><h4>def <code>add_from_index(self, index: Index, location: typing.Union[str, bytes, os.PathLike], template: str = None, **kwargs)</code></h4><p>Add all pages from an index to the site with the root at the server-relative URL <code>location</code>.
225
The pages will be rendered with the template <code>template</code> and the context <code>kwargs</code>. will be
226
passed to all of them. If the index is static, the pages will not be rendered with a template,
227
but rather copied as-is.
228
</p><p>For each page, the <code>document</code> object found in the index will be passed to the template under
229
that name.
230
</p><h4>def <code>filter(self, name: str)</code></h4><p>A decorator that registers a filter function with the site. The function should take at least
231
one argument, the value to be filtered, and return the filtered value.
232
</p><h4>def <code>test(self, name: str)</code></h4><p>A decorator that registers a test function with the site. The function should take at least
233
one argument, the value to be tested, and return a boolean.
234
</p><h4>def <code>build(self, dont_delete: typing.Optional[list[str]] = None)</code></h4><p>Build (save) the site to the build directory it was constructed with. This will create the
235
directory if it does not exist, clear it (but not delete it) and then write all the pages.
236
You can set <code>dont_delete</code> to a list of files that should not be deleted when the directory
237
is cleared, for example, the <code>.git</code>.
238
</p><h4><code>context: dict[str, typing.Any]</code></h4><p>A dictionary containing names that are available to all pages. It can be overriden by the
239
page's context or modified at any time.
240
</p><h3>class <code>ampoule_ssg.Page(str)</code></h3><p><code>Page</code> is a class that represents a single page on the site. A page is composed of a
241
template, a document and a context.
242
</p><h4>def <code>__new__(cls, site: Site, template: str, document: Document = None, **kwargs)</code></h4><p>Create a new page object. <code>site</code> is the site object that the page belongs to. <code>template</code> is
243
the template the document will be put in. <code>document</code> is the document object that will be
244
passed to the template. <code>kwargs</code> are names that will be available to the template for
245
additional context.
246
</p><p>If there's no document, it will not be available to the template. This is useful for single
247
pages with fully static content, like a contact page.
248
</p><h3>class <code>ampoule_ssg.Static(bytes)</code></h3><p><code>Static</code> is a class that represents a single static file on the site. A static file is
249
just the content, in binary format, and it doesn't use templating.
250
</p><h4>def <code>__new__(cls, site: Site, document: Document)</code></h4><p>Create a new static object. <code>site</code> is the site object that the static file belongs to.
251
<code>document</code> is the document object that will be written to the file; it can contain any
252
encoding, even text, and will be written as-is.
253
</p><h3>class <code>ampoule_ssg.Index</code></h3><p>An index is a collection of documents that can be iterated over or added to a site using
254
a common template (see <code>ampoule_ssg.Site.add_from_index</code>).
255
</p><h4>def <code>__init__(self, directory: typing.Union[str, bytes, os.PathLike], recursive: bool = False, url_transform: typing.Callable = lambda x: x, sort_by: typing.Callable = lambda x: x.file_name, exclude: typing.Union[str, NoneType] = None, static: bool = False)</code></h4><p>Create a new index. <code>directory</code> is the directory to get content from. If <code>recursive</code> is
256
true, the whole tree of that directory will be indexed. <code>url_transform</code> is a function that
257
will be applied to the file name to get the new file name. Generally you want to set it so
258
it makes them end in <code>.html</code> so dumb servers can serve them correctly. However, for static
259
files you most likely will not set it. <code>sort_by</code> is the key after which to sort the
260
documents after they are indexed; by default it is the file name. <code>exclude</code> is a regular
261
expression that will be used to exclude files from the index. If the index is <code>static</code>,
262
all documents will be parsed as-is, without removing front matter.
263
</p><h4>def <code>__iter__(self)</code></h4><p>Return an iterator for the index.
264
</p><h4>def <code>__next__(self)</code></h4><p>Get the next document in the index.
265
</p><h4>def <code>__repr__(self)</code></h4><p>Return a string representation of the index. It contains the directory and the names
266
of the documents in it.
267
</p><h4>def <code>__len__(self)</code></h4><p>Return the number of documents in the index, that is, its length.
268
</p><h3>class <code>ampoule_ssg.Document</code></h3><p>A document is a file, not rendered, but available for use. It is what is passed to the
269
template as <code>document</code> for processing. Generally, you won't create these yourself, but
270
rather use them as they are returned by an index. However, if you do need one, you can
271
create it manually and pass it to a page.
272
</p><p>Documents will parse YAML front matter for textual files, unless disabled. The front matter
273
is available as an attribute of the document, and can be accessed using indexing syntax.
274
</p><h4>def <code>__init__(self, file_name: typing.Union[str, bytes, os.PathLike], url_transform: typing.Callable = lambda x: x, front_matter_enabled: bool = True)</code></h4><p>Create a new document. <code>file_name</code> is the name of the file. <code>url_transform</code> is a function
275
that will be applied to the file name to get the new file name; it has the same meaning as
276
in the <code>Index</code>. <code>front_matter_enabled</code> is a boolean that determines whether the document
277
will parse YAML front matter.
278
</p><h4>def <code>__repr__(self)</code></h4><p>Return a string containing <code>Document</code> and the file name.
279
</p><h4>def <code>__getitem__(self, item: str)</code></h4><p>Access the document's front matter. If front matter is disabled or not available, this will
280
never work.
281
</p><h4>def <code>__setitem__(self, item: str, value: typing.Any)</code></h4><p>Change the document's front matter. It works even if it wasn't parsed, because YAML
282
behaves like a dictionary.
283
</p><h4>def <code>__delitem__(self, item: str)</code></h4><p>Delete an item from the document's front matter.
284
</p><h4>def <code>__contains__(self, item: str)</code></h4><p>Check if an item is in the document's front matter.
285
</p><h2>Licence</h2><p>This program is free software: you can redistribute it and/or modify it under the terms of
286
the GNU General Public License as published by the Free Software Foundation, either version 3
287
of the License, or (at your option) any later version.
288
</p><p>This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
289
without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See
290
the GNU General Public License for more details.
291
</p><p>You should have received a copy of the GNU General Public License along with this program.
292
If not, see <a href="https://www.gnu.org/licenses/">https://www.gnu.org/licenses/</a>.
293
</p>
294
</article>
295
296
</main>
297
<footer>
298
<p>Page generated on Sunday, 4 May 2025 at 15:06:42</p>
299
<p xmlns:cc="http://creativecommons.org/ns#" >This work is marked with <a href="https://creativecommons.org/publicdomain/zero/1.0/?ref=chooser-v1" target="_blank" rel="license noopener noreferrer" style="display:inline-block;">CC0 1.0 Universal</a> (🄍). No rights reserved.</p>
300
<p>Hosted at <a href="https://roundabout-host.com/roundabout">Roundabout-host</a> using the static site service, and generated with <a href="/projects/ampoule.html">Ampoule</a>.</p>
301
<a href="#">Back to top</a>
302
</footer>
303
</body>
304
</html>