├── 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 | ![logo](https://user-images.githubusercontent.com/547147/109163952-e4610100-779f-11eb-9aa6-2236f06d3022.png) 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 |

    82 | {{ e.name }} 83 |

    84 |

    {{ e.description }}

    85 | 86 |
    87 | {% for tx in e.taxonomies %} 88 | {% if (e.taxonomies[tx] | length) > 0 %} 89 |
    90 | {{ tx }} 91 | {% for t in e.taxonomies[tx] %} 92 | {{ t.name }} 93 | {% endfor %} 94 | 95 | {% if (e.taxonomies[tx] | length) > 4 %} 96 | 97 | ← View all ({{ e.taxonomies[tx] | length }}) 98 | 99 | {% endif %} 100 |
    101 | {% endif %} 102 | {% endfor %} 103 | 104 |
    105 |
  • 106 | {% endfor %} 107 |
108 | 109 | {% if pagination.total > 1 %} 110 |
    111 | {% for p in range(1, pagination.total + 1) %} 112 |
  • 113 | {{ p }} 114 |
  • 115 | {% endfor %} 116 |
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 | --------------------------------------------------------------------------------