├── LICENSE
├── MANIFEST.in
├── README.md
├── dirmaker
├── __init__.py
├── dirmaker.py
└── example
│ ├── config.yml
│ ├── data.yml
│ ├── static
│ ├── logo.svg
│ ├── main.js
│ ├── style.css
│ └── thumb.png
│ └── template.html
├── requirements.txt
└── setup.py
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License
2 |
3 | Copyright (c) 2021, Kailash Nadh. https://github.com/knadh
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | include dirmaker/example/*
2 | include dirmaker/example/static/*
3 | include requirements.txt
4 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 
2 |
3 | dirmaker is a simple, opinionated static site generator for publishing directory websites (eg: [Indic.page](https://indic.page), [env.wiki](https://env.wiki/directory) It takes entries from a YAML file and generates a categorised, paginated directory website.
4 |
5 | ### Install
6 | `pip3 install dirmaker`
7 |
8 | ### Build a site
9 | ```shell
10 | # Generate an example site in the `example` directory.
11 | dirmaker --new
12 |
13 | cd example
14 |
15 | # Edit config.yml and add data to data.yml
16 | # Customize static files and the template if necessary.
17 |
18 | # Build the static site into a directory called `site`.
19 | dirmaker --build
20 | ```
21 |
22 | Licensed under the MIT license.
23 |
--------------------------------------------------------------------------------
/dirmaker/__init__.py:
--------------------------------------------------------------------------------
1 | #!/bin/python
2 | import argparse
3 | import os
4 | import shutil
5 | import sys
6 |
7 | __version__ = "1.1.0"
8 |
9 | def main():
10 | """Run the CLI."""
11 | p = argparse.ArgumentParser(
12 | description="A simple static site generator for generating directory websites.",
13 | formatter_class=argparse.ArgumentDefaultsHelpFormatter)
14 |
15 | p.add_argument("-v", "--version", action="store_true", dest="version", help="display version")
16 |
17 | n = p.add_argument_group("new")
18 | n.add_argument("-n", "--new", action="store_true",
19 | dest="new", help="initialize a new site")
20 | n.add_argument("-p", "--path", action="store", type=str, default="example",
21 | dest="exampledir", help="path to create the example site")
22 |
23 | b = p.add_argument_group("build")
24 | b.add_argument("-b", "--build", action="store_true",
25 | dest="build", help="build a static site")
26 | b.add_argument("-c", "--config", action="store", type=str, default="config.yml",
27 | dest="config", help="path to the config file")
28 | b.add_argument("-t", "--template", action="store", type=str, default="template.html",
29 | dest="template", help="path to the template file")
30 | b.add_argument("-d", "--data", action="store", type=str, default="data.yml",
31 | dest="data", help="path to the data file")
32 | b.add_argument("-o", "--output", action="store", type=str, default="site",
33 | dest="output", help="path to the output directory")
34 |
35 | if len(sys.argv) == 1:
36 | p.print_help()
37 | p.exit()
38 |
39 | args = p.parse_args()
40 |
41 | if args.version:
42 | print("v{}".format(__version__))
43 | quit()
44 |
45 | if args.new:
46 | exdir = os.path.join(os.path.dirname(__file__), "example")
47 | if not os.path.isdir(exdir):
48 | print("unable to find bundled example directory")
49 | sys.exit(1)
50 |
51 | try:
52 | shutil.copytree(exdir, args.exampledir)
53 | except FileExistsError:
54 | print("the directory '{}' already exists".format(args.exampledir))
55 | sys.exit(1)
56 | except:
57 | raise
58 |
59 | if args.build:
60 | from .dirmaker import Builder
61 |
62 | print("building site from: {}".format(args.data))
63 |
64 | bu = Builder(args.config)
65 | bu.load_template(args.template)
66 | bu.load_data(args.data)
67 | bu.build(args.output)
68 |
69 | print("processed {} entries, {} categories, {} taxonomies".format(len(bu.entries), len(bu.all_categories), len(bu.all_taxonomies)))
70 |
71 | if len(bu.entries) > 0:
72 | print("published to directory: {}".format(args.output))
73 | else:
74 | print("no data to build site")
75 |
--------------------------------------------------------------------------------
/dirmaker/dirmaker.py:
--------------------------------------------------------------------------------
1 | #!/bin/python
2 |
3 | import math
4 | import os
5 | import shutil
6 | from copy import copy
7 | import sys
8 |
9 | from jinja2 import Template
10 | import yaml
11 |
12 |
13 | class Taxonomy:
14 | def __init__(self, name, slug, count):
15 | self.name = name
16 | self.slug = slug
17 | self.count = count
18 |
19 | def __str__(self):
20 | return self.name
21 |
22 |
23 | class Category:
24 | def __init__(self, name, slug, count):
25 | self.name = name
26 | self.slug = slug
27 | self.count = count
28 |
29 | def __str__(self):
30 | return self.name
31 |
32 |
33 | class Entry:
34 | def __init__(self, name, description, url, categories, taxonomies):
35 | self.name = name
36 | self.description = description
37 | self.url = url
38 | self.categories = categories
39 |
40 | # eg: {"tags": [...tags], "types": [...types]}
41 | self.taxonomies = taxonomies
42 |
43 | def __str__(self):
44 | return self.name
45 |
46 |
47 | class Builder:
48 | template = Template("")
49 | outdir = ""
50 | config = {
51 | "base_url": "https://mysite.com",
52 | "per_page": 50,
53 | "taxonomies": ["tags"],
54 | "static_dir": "static",
55 | "site_name": "Directory site",
56 | "page_title": "{category}",
57 | "meta_description": "{category}",
58 | }
59 |
60 | entries = []
61 | all_categories = []
62 | all_taxonomies = []
63 |
64 | def __init__(self, config_file):
65 | with open(config_file, "r") as f:
66 | self.config = {**self.config, **yaml.load(f.read(), Loader=yaml.FullLoader)}
67 |
68 | def build(self, outdir):
69 | # Create the output diretory.
70 | self.outdir = outdir
71 | self._create_dir(outdir)
72 |
73 | # For each category, render a page.
74 | for c in self.all_categories:
75 | self._render_page(
76 | cat=c,
77 | entries=self._filter_by_category(c, self.entries)
78 | )
79 |
80 | # Copy the first category as the index page.
81 | if len(self.all_categories) > 0:
82 | c = self.all_categories[0]
83 | shutil.copy(os.path.join(self.outdir, "{}.html".format(c.slug)),
84 | os.path.join(self.outdir, "index.html"))
85 |
86 | def load_data(self, infile):
87 | """Loads entries from the YAML data file."""
88 | entries = []
89 | with open(infile, "r") as f:
90 | items = yaml.load(f.read(), Loader=yaml.FullLoader)
91 | if type(items) is not list or len(items) == 0:
92 | return []
93 |
94 | for i in items:
95 | entries.append(Entry(
96 | name=i["name"],
97 | description=i["description"],
98 | url=i["url"],
99 | categories=self._make_categories(i["categories"]),
100 | taxonomies=self._make_taxonomies(i)
101 | ))
102 |
103 | self.entries = entries
104 |
105 | # Collate all unique tags and categories across all entries.
106 | self.all_categories = self._collate_categories(self.entries)
107 | self.all_taxonomies = self._collate_taxonomies(self.entries)
108 |
109 |
110 | def load_template(self, file):
111 | with open(file, "r") as f:
112 | self.template = Template(f.read())
113 |
114 | def _create_dir(self, dirname):
115 | # Clear the output directory.
116 | if os.path.exists(dirname):
117 | shutil.rmtree(dirname)
118 |
119 | # Re-create the output directory.
120 | os.mkdir(dirname)
121 |
122 | # Copy the static directory into the output directory.
123 | for f in [self.config["static_dir"]]:
124 | target = os.path.join(self.outdir, f)
125 | if os.path.isfile(f):
126 | shutil.copyfile(f, target)
127 | else:
128 | shutil.copytree(f, target)
129 |
130 | def _make_taxonomies(self, item):
131 | """
132 | Make a dict of array of all taxonomy items on the entry.
133 | eg: {"tags": [...tags], "types": [...types]}
134 | """
135 | out = {}
136 | for tx in self.config["taxonomies"]:
137 | out[tx] = {}
138 | if tx not in item:
139 | continue
140 |
141 | # Iterate through each taxonomy array in the entry.
142 | for v in item[tx]:
143 | if v not in out[tx]:
144 | id = v.strip().lower()
145 | if id == "":
146 | continue
147 |
148 | out[tx][id] = Taxonomy(
149 | name=v, slug=self._make_slug(v), count=0)
150 |
151 | out[tx] = sorted([out[tx][v]
152 | for v in out[tx]], key=lambda k: k.name)
153 |
154 | return out
155 |
156 | def _collate_taxonomies(self, entries):
157 | """
158 | Return the unique list of all taxonomies across the given entries with counts.
159 | eg: {"tags": [...tags], "types": [...types]}
160 | """
161 | out = {}
162 | for e in entries:
163 | for tx in self.config["taxonomies"]:
164 | if tx not in out:
165 | out[tx] = {}
166 |
167 | for t in e.taxonomies[tx]:
168 | id = t.name.strip().lower()
169 | if id == "":
170 | continue
171 |
172 | if id not in out[tx]:
173 | out[tx][id] = copy(t)
174 | out[tx][id].count += 1
175 |
176 | for tx in self.config["taxonomies"]:
177 | out[tx] = sorted([out[tx][v]
178 | for v in out[tx]], key=lambda k: k.name)
179 |
180 | return out
181 |
182 | def _make_categories(self, cats):
183 | """Make a list of Categories out of the given string tags."""
184 | out = {}
185 | for c in cats:
186 | id = c.lower()
187 | if id not in out:
188 | out[id] = Category(name=c, slug=self._make_slug(c), count=0)
189 |
190 | return sorted([out[c] for c in out], key=lambda k: k.name)
191 |
192 | def _collate_categories(self, entries):
193 | """Return the unique list of all categories across the given entries with counts."""
194 | cats = {}
195 | for e in entries:
196 | for c in e.categories:
197 | id = c.name.lower()
198 | if id not in cats:
199 | cats[id] = copy(c)
200 | cats[id].count += 1
201 |
202 | return sorted([cats[c] for c in cats], key=lambda k: k.name)
203 |
204 | def _filter_by_category(self, category, entries):
205 | out = []
206 | for e in entries:
207 | for c in e.categories:
208 | if c.slug == category.slug:
209 | out.append(e)
210 |
211 | return sorted([e for e in out], key=lambda k: k.name.lower())
212 |
213 | def _make_slug(self, file):
214 | return file.replace(" ", "-").lower()
215 |
216 | def _render_page(self, cat, entries):
217 | total_pages = math.ceil(len(entries) / self.config["per_page"])
218 | page = 1
219 |
220 | for items in self._paginate(entries, self.config["per_page"]):
221 | html = self.template.render(
222 | config=self.config,
223 | pagination={"current": page, "total": total_pages},
224 | all_categories=self.all_categories,
225 |
226 | # Current category being rendered.
227 | category=cat,
228 | all_taxonomies=self.all_taxonomies,
229 |
230 | # Taxonomies of all the entries currently being rendered.
231 | taxonomies=self._collate_taxonomies(items),
232 | entries=items)
233 |
234 | fname = "{}{}.html".format(
235 | cat.slug, "-" + str(page) if page > 1 else "")
236 | with open(os.path.join(self.outdir, fname), "w") as f:
237 | f.write(html)
238 |
239 | page += 1
240 |
241 | def _paginate(self, entries, size):
242 | for start in range(0, len(entries), size):
243 | yield entries[start:start + size]
244 |
--------------------------------------------------------------------------------
/dirmaker/example/config.yml:
--------------------------------------------------------------------------------
1 | ---
2 | base_url: "https://mysite.com"
3 | static_dir: "static"
4 | per_page: 50
5 |
6 | # Each entry can have multiple collection of tags (list of strings) in addition to the
7 | # categories. For instance, a directory of fonts could have multiple meaningful
8 | # tag collections per entry such as:
9 | # types = [serif, sans-serif, display, script]
10 | # mood = [retro, distressed, cool]
11 | # tag = [yep, nope, etc]
12 | taxonomies: ["style", "mood"]
13 |
14 | site_name: "My directory site"
15 | site_description: "My directory site"
16 | page_title: "{category} - Directory of cool things."
17 | meta_description: "Directory of {category} in cool things. "
18 |
--------------------------------------------------------------------------------
/dirmaker/example/data.yml:
--------------------------------------------------------------------------------
1 | ---
2 | - name: Roboto
3 | description: Roboto has a dual nature. It has a mechanical skeleton and the forms are largely geometric. At the same time, the font features friendly and open curves.
4 | url: "https://www.fontsquirrel.com/fonts/roboto"
5 | categories: [Professional]
6 | style: [sans-serif]
7 | mood: [headings, oblique, paragraph, sans, grotesque, sans, humanist]
8 |
9 | - name: Acre
10 | description: Acre is a geometric sans-serif family of eight weights that’s both inspired by and named after my great grandfather, Tex Acre.
11 | url: "https://www.fontsquirrel.com/fonts/acre"
12 | categories: [Professional]
13 | style: [sans-serif]
14 | mood: [corporate, monolinear, paragraph, sans, geometric]
15 |
16 | - name: AnuDaw
17 | description: A funky font designed by Nyek! Pinoy Komik Fonts.
18 | url: https://www.fontsquirrel.com/fonts/AnuDaw
19 | categories: [Funky]
20 | style: [comic]
21 | mood: [brush, comic, display, grunge, handwritten]
22 |
--------------------------------------------------------------------------------
/dirmaker/example/static/logo.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/dirmaker/example/static/main.js:
--------------------------------------------------------------------------------
1 | function intersection(setA, setB) {
2 | const _intersection = new Set();
3 | for (const elem of setB) {
4 | if (setA.has(elem)) {
5 | _intersection.add(elem);
6 | }
7 | }
8 | return _intersection;
9 | }
10 |
11 | function filterList(sel, itemSelector) {
12 | const noRes = document.querySelector("#no-results");
13 | // Hide all items.
14 | document.querySelectorAll(itemSelector).forEach(e => {
15 | e.style.display = "none";
16 | });
17 |
18 | if (sel.length === 0) {
19 | noRes.style.display = 'block';
20 | return;
21 | }
22 | noRes.style.display = 'none';
23 |
24 | let visible = new Set(document.querySelectorAll(itemSelector));
25 | for (taxonomy of new Set(sel.map((e) => e.taxonomy))) {
26 | // List of attribute selector queries for each value. eg:
27 | // #items li[data-language*=malayalam|], #items li[data-language*=kannada|] ...
28 | let q = sel.filter((e) => e.taxonomy === taxonomy).map((e) => `${itemSelector}[data-${taxonomy}*='${e.value}|']`)
29 | visible = intersection(visible, new Set(document.querySelectorAll(q.join(", "))));
30 |
31 | // const q = sel.map(v => `${itemSelector}[data-${v.taxonomy}*='${v.value}|']`);
32 | }
33 |
34 | // Show the matched items.
35 | visible.forEach(e => {
36 | e.style.display = "block";
37 | });
38 | }
39 |
40 | function onFilter() {
41 | // Get the value of all checked items for all the taxonomy groups.
42 | const taxItems = Array.from(document.querySelectorAll(`#filters input[type=checkbox]:checked`));
43 | const sel = taxItems.map((e) => {
44 | return { taxonomy: e.dataset.taxonomy, value: e.value }
45 | });
46 |
47 | const cls = document.querySelector("#items").classList;
48 | cls.remove("shake");
49 | window.setTimeout(() => {
50 | cls.add("shake");
51 | }, 50);
52 | filterList(sel, "#items .item");
53 | }
54 |
55 | const reClean = new RegExp(/[^a-z0-9\s]+/g);
56 | const reSpaces = new RegExp(/\s+/g);
57 | function tokenize(str) {
58 | return str.toLowerCase().replace(reClean, "").replace(reSpaces, " ").split(" ").filter((c) => c !== "");
59 | }
60 |
61 | // UI hooks.
62 | (function() {
63 | // Mobile burger menu.
64 | document.querySelector("#burger").onclick = (e) => {
65 | e.preventDefault();
66 | const f = document.querySelector("#sidebar");
67 | f.style.display = f.style.display === "block" ? "none" : "block";
68 | };
69 |
70 |
71 | // Text search.
72 | let isSearching = false;
73 | document.querySelector("#search").oninput = function(e) {
74 | if (isSearching) {
75 | return true;
76 | }
77 |
78 | isSearching = true;
79 | window.setTimeout(() => {
80 | isSearching = false;
81 | }, 100);
82 |
83 | if (e.target.value.length < 3) {
84 | document.querySelectorAll("#items .item").forEach(e => e.style.display = 'block')
85 | return;
86 | }
87 | const search = tokenize(e.target.value);
88 |
89 | document.querySelectorAll("#items .item").forEach(e => {
90 | // Tokenize the text title and description of all the items.
91 | let txt = tokenize(e.querySelector(".title").innerText + " " + e.querySelector(".description").innerText);
92 |
93 | // Search input against the item tokens. Every token in the search input should match.
94 | let has = 0;
95 | for (let i = 0; i < search.length; i++) {
96 | for (let j = 0; j < txt.length; j++) {
97 | if (txt[j].indexOf(search[i]) > -1) {
98 | has++;
99 | break;
100 | }
101 | }
102 | }
103 |
104 | e.style.display = has === search.length ? "block" : "none";
105 | });
106 | };
107 |
108 |
109 | // Filter display toggle.
110 | document.querySelector("#toggle-filters").onclick = (e) => {
111 | e.preventDefault();
112 |
113 | const f = document.querySelector("#filters");
114 | f.style.display = f.style.display === "block" ? "none" : "block";
115 | };
116 |
117 | // Toggle filter checkbox selections.
118 | document.querySelectorAll(".toggle-filters").forEach(el => {
119 | el.onclick = (e) => {
120 | e.preventDefault();
121 |
122 | // Check or uncheck all filter checkboxes with the toggle item's dataset.taxonomy.
123 | const tax = e.target.dataset.taxonomy;
124 | const filters = document.querySelectorAll(`#filters input[data-taxonomy=${tax}]`);
125 | if (filters.length === 0) {
126 | return;
127 | }
128 |
129 | filters.forEach(el => {
130 | el.checked = e.target.dataset.state === "on" ? false : true;
131 | });
132 |
133 | e.target.dataset.state = e.target.dataset.state === "on" ? "off" : "on";
134 |
135 | // Trigger the filter.
136 | onFilter();
137 | };
138 | });
139 |
140 | // Taxonomies filters.
141 | document.querySelectorAll("#filters input[type=checkbox]").forEach(el => {
142 | el.onchange = () => {
143 | onFilter();
144 | };
145 | });
146 |
147 | // 'View all' link on taxonomies.
148 | document.querySelectorAll(".reveal").forEach(el => {
149 | el.onclick = (e) => {
150 | e.preventDefault();
151 |
152 | const cls = e.target.parentNode.classList;
153 | if (cls.contains("visible")) {
154 | cls.remove("visible");
155 | } else {
156 | cls.add("visible");
157 | }
158 | };
159 | });
160 |
161 | })();
162 |
--------------------------------------------------------------------------------
/dirmaker/example/static/style.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --primary: #0055d4;
3 | --light: #888;
4 | --lighter: #bbb;
5 |
6 | --size-xsmall: 0.675em;
7 | }
8 |
9 | * {
10 | box-sizing: border-box;
11 | }
12 | body {
13 | font-family: "Fira Sans", "Helvetica Neue", "Segoe UI", sans-serif;
14 | font-size: 16px;
15 | line-height: 28px;
16 | color: #666;
17 | }
18 |
19 | input, textarea, select {
20 | font-family: "Fira Sans", "Helvetica Neue", "Segoe UI", sans-serif;
21 | border: 1px solid #ddd;
22 | padding: 10px 15px;
23 | border-radius: 3px;
24 | }
25 |
26 | h1, h2, h3, h4, h5 {
27 | font-weight: 400;
28 | margin: 0 0 20px 0;
29 | color: #111;
30 | }
31 | h1 {
32 | font-size: 2em;
33 | }
34 | h2 {
35 | font-size: 1.3em;
36 | margin-bottom: 5px;
37 | }
38 | h1 a, h2 a, h3 a {
39 | color: var(--primary);
40 | }
41 |
42 | a {
43 | color: #111;
44 | text-decoration: none;
45 | }
46 | a:hover {
47 | border-bottom: 2px solid var(--primary);
48 | }
49 |
50 | p {
51 | margin: 0 0 10px 0;
52 | }
53 |
54 | ul {
55 | margin: 0;
56 | padding: 0;
57 | list-style-type: none;
58 | }
59 |
60 | footer {
61 | font-size: var(--size-xsmall);
62 | border-top: 1px solid #eee;
63 | margin: 45px 0 15px 0;
64 | padding-top: 15px;
65 | color: #999;
66 | }
67 | footer a {
68 | color: #999;
69 | }
70 |
71 | .wrap {
72 | max-width: 1000px;
73 | margin: 0 auto;
74 | padding: 30px 30px 0 30px;
75 | }
76 | .header {
77 | width: 100%;
78 | display: flex;
79 | margin-bottom: 15px;
80 | }
81 | .logo a {
82 | border: 0;
83 | }
84 | .logo .slogan {
85 | margin-top: 15px;
86 | }
87 | #burger {
88 | float: right;
89 | display: none;
90 | }
91 | #burger span {
92 | background: #111;
93 | display: block;
94 | height: 3px;
95 | width: 25px;
96 | margin-bottom: 4px;
97 | border-radius: 2px;
98 | }
99 | #burger:hover {
100 | border: 0;
101 | }
102 | #burger:hover span {
103 | background: var(--primary);
104 | }
105 |
106 | .container {
107 | display: flex;
108 | }
109 | .sidebar {
110 | flex: 30%;
111 | }
112 | .sidebar li {
113 | margin-bottom: 10px;
114 | font-style: capitalize;
115 | }
116 | .sidebar li.selected a {
117 | border-bottom: 2px solid var(--primary);
118 | }
119 | .content {
120 | flex: 70%;
121 | }
122 |
123 | .index {
124 | margin: 0;
125 | }
126 | .intro {
127 | margin-left: auto;
128 | }
129 | .search {
130 | float: right;
131 | box-shadow: 2px 1px 2px #f1f1f1;
132 | }
133 |
134 | /* Entries */
135 | #toggle-filters {
136 | float: right;
137 | }
138 |
139 | #filters {
140 | margin-bottom: 30px;
141 | display: none;
142 | }
143 | #filters .taxonomy {
144 | margin: 0;
145 | color: var(--light);
146 | text-transform: uppercase;
147 | }
148 | #filters .group {
149 | margin-bottom: 15px;
150 | }
151 | #filters label {
152 | cursor: pointer;
153 | margin-right: 10px;
154 | display: inline-block;
155 | }
156 |
157 | .toggle-filters {
158 | font-size: var(--size-xsmall);
159 | float: right;
160 | }
161 |
162 | #items {
163 | box-shadow: 2px 1px 1px #eee;
164 | border: 1px solid #eee;
165 | }
166 | #items .item {
167 | padding: 20px 30px;
168 | }
169 | #items .item:hover {
170 | background: #f9f9f9;
171 | }
172 |
173 | #items .taxonomies {
174 | font-size: var(--size-xsmall);
175 | color: var(--lighter);
176 | }
177 | #items .taxonomies .name {
178 | text-transform: uppercase;
179 | margin-right: 5px;
180 | }
181 | #items .taxonomies .value {
182 | border: 1px solid #eee;
183 | color: var(--light);
184 | line-height: 1em;
185 | text-align: center;
186 | margin-right: 5px;
187 |
188 | display: inline-block;
189 | padding: 4px 10px;
190 | border-radius: 3px;
191 | }
192 | #items .taxonomies .group {
193 | display: inline-block;
194 | margin-right: 15px;
195 | }
196 |
197 | /* Show max 5 tags and the "Reveal" link next to it */
198 | #items .taxonomies .group .value:nth-child(n+6) {
199 | display: none;
200 | }
201 | #items .taxonomies .group.visible .value {
202 | display: inline-block !important;
203 | }
204 |
205 | #no-results {
206 | display: none;
207 | margin-top: 30px;
208 | }
209 |
210 | .pagination {
211 | margin-top: 30px;
212 | }
213 | .pagination li {
214 | display: inline-block;
215 | margin: 0 15px 0 0;
216 | }
217 | .pagination .active a {
218 | color: var(--primary);
219 | border-bottom: 2px solid var(--primary);
220 | }
221 |
222 | .shake {
223 | animation: shake 150ms cubic-bezier(.36,.07,.19,.97) both;
224 | transform: translate3d(0, 0, 0);
225 | backface-visibility: hidden;
226 | perspective: 1000px;
227 | }
228 | @keyframes shake {
229 | 0% {
230 | scale: 1;
231 | }
232 | 20% {
233 | scale: 1.01;
234 | }
235 | 40% {
236 | scale: 1.02;
237 | }
238 | 80% {
239 | scale: 1.01;
240 | }
241 | 100% {
242 | scale: 1;
243 | }
244 | }
245 |
246 | @media(max-width: 750px) {
247 | .wrap {
248 | padding: 15px;
249 | }
250 | .header {
251 | display: block;
252 | margin: 0;
253 | }
254 | #burger {
255 | display: block;
256 | }
257 | .search {
258 | float: none;
259 | width: 100%;
260 | }
261 | .intro {
262 | margin: auto;
263 | }
264 | .container {
265 | display: block;
266 | }
267 | .sidebar {
268 | text-align: center;
269 | display: none;
270 | }
271 | #items .item {
272 | padding: 15px 15px;
273 | }
274 | #items .taxonomies .group {
275 | display: block;
276 | }
277 | }
278 |
--------------------------------------------------------------------------------
/dirmaker/example/static/thumb.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/knadh/dirmaker/ae0deca6dc080b4f9563f23756345ca230e46130/dirmaker/example/static/thumb.png
--------------------------------------------------------------------------------
/dirmaker/example/template.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{ config.page_title.format(category=category.name) }}
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
36 |
37 |
38 |
39 |
48 |
49 |
50 | {% if (taxonomies | length) > 0 %}
51 | Filter →
52 | {% endif %}
53 |
54 | {{ category.name }} ({{ category.count }})
55 |
56 |
57 | {% if (taxonomies | length) > 0 %}
58 | {% for tx in taxonomies %}
59 |
60 |
{{ tx }}
61 |
← Toggle
62 | {% for t in taxonomies[tx] %}
63 |
68 | {% endfor %}
69 |
70 | {% endfor %}
71 | {% endif %}
72 |
73 |
74 |
75 | {% for e in entries %}
76 | -
81 |
84 |
{{ e.description }}
85 |
86 |
87 | {% for tx in e.taxonomies %}
88 | {% if (e.taxonomies[tx] | length) > 0 %}
89 |
101 | {% endif %}
102 | {% endfor %}
103 |
104 |
105 |
106 | {% endfor %}
107 |
108 |
109 | {% if pagination.total > 1 %}
110 |
117 | {% endif %}
118 |
119 |
120 | No results.
121 |
122 |
123 |
124 |
127 |
128 |
129 |
130 |
131 |
132 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | jinja2==2.11.3
2 | PyYAML==5.4.1
3 | markupsafe==2.0.1
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | from codecs import open
3 | from setuptools import setup
4 | from dirmaker import __version__
5 |
6 | README = open("README.md").read()
7 |
8 | def requirements():
9 | with open('requirements.txt') as f:
10 | return f.read().splitlines()
11 |
12 |
13 |
14 | setup(
15 | name="dirmaker",
16 | version=__version__,
17 | description="A simple static site generator for generating directory websites.",
18 | long_description=README,
19 | long_description_content_type="text/markdown",
20 | author="Kailash Nadh",
21 | author_email="kailash@nadh.in",
22 | url="https://github.com/knadh/dirmaker",
23 | packages=['dirmaker'],
24 | install_requires=requirements(),
25 | include_package_data=True,
26 | download_url="https://github.com/knadh/dirmaker",
27 | license="MIT License",
28 | entry_points={
29 | 'console_scripts': [
30 | 'dirmaker = dirmaker:main'
31 | ],
32 | },
33 | classifiers=[
34 | "Topic :: Text Editors :: Text Processing",
35 | "Topic :: Internet :: WWW/HTTP :: Site Management",
36 | "Topic :: Documentation"
37 | ],
38 | )
39 |
--------------------------------------------------------------------------------