Skip to content

Commit

Permalink
First commit
Browse files Browse the repository at this point in the history
  • Loading branch information
knadh committed Feb 25, 2021
0 parents commit a0ead4d
Show file tree
Hide file tree
Showing 12 changed files with 929 additions and 0 deletions.
21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
The MIT License

Copyright (c) 2021, Kailash Nadh. https://github.com/knadh

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
2 changes: 2 additions & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
include dirmaker/example/*
include dirmaker/example/static/*
30 changes: 30 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
![logo](https://user-images.githubusercontent.com/547147/109163952-e4610100-779f-11eb-9aa6-2236f06d3022.png)

dirmaker is a simple, opinionated static site generator for publishing directory websites like [Indic.page](https://indic.page). It takes entries from a YAML file and generates a categorised, paginated directory website.

--------------
Example site:

[![image](https://user-images.githubusercontent.com/547147/109164159-1eca9e00-77a0-11eb-9382-4f9d35aef72a.png)](https://indic.page)


## Usage

### Install
`pip3 install dirmaker`

### Build a site
```shell
# Generate an example site in the `example` directory.
dirmaker --new

cd example

# Edit config.yml and add data to data.yml
# Customize static files and the template if necessary.

# Build the static site into a directory called `site`.
dirmaker --build
```

Licensed under the MIT license.
294 changes: 294 additions & 0 deletions dirmaker/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,294 @@
#!/bin/python

import argparse
import math
import os
import shutil
from copy import copy
import sys

from jinja2 import Template

import yaml


class Taxonomy:
def __init__(self, name, slug, count):
self.name = name
self.slug = slug
self.count = count

def __str__(self):
return self.name


class Category:
def __init__(self, name, slug, count):
self.name = name
self.slug = slug
self.count = count

def __str__(self):
return self.name


class Entry:
def __init__(self, name, description, url, categories, taxonomies):
self.name = name
self.description = description
self.url = url
self.categories = categories

# eg: {"tags": [...tags], "types": [...types]}
self.taxonomies = taxonomies

def __str__(self):
return self.name


class Builder:
template = Template("")
outdir = ""
config = {
"base_url": "https://mysite.com",
"per_page": 50,
"taxonomies": ["tags"],
"static_dir": "static",
"site_name": "Directory site",
"page_title": "{category}",
"meta_description": "{category}",
}

entries = []
all_categories = []
all_taxonomies = []

def __init__(self, config_file):
with open(config_file, "r") as f:
self.config = {**self.config, **yaml.load(f.read())}

def build(self, outdir):
# Create the output diretory.
self.outdir = outdir
self._create_dir(outdir)

# For each category, render a page.
for c in self.all_categories:
self._render_page(
cat=c,
entries=self._filter_by_category(c, self.entries)
)

# Copy the first category as the index page.
if len(self.all_categories) > 0:
c = self.all_categories[0]
shutil.copy(os.path.join(self.outdir, "{}.html".format(c.slug)),
os.path.join(self.outdir, "index.html"))

def load_data(self, infile):
"""Loads entries from the YAML data file."""
entries = []
with open(infile, "r") as f:
items = yaml.load(f.read())
if type(items) is not list or len(items) == 0:
return []

for i in items:
entries.append(Entry(
name=i["name"],
description=i["description"],
url=i["url"],
categories=self._make_categories(i["categories"]),
taxonomies=self._make_taxonomies(i)
))

self.entries = entries

# Collate all unique tags and categories across all entries.
self.all_categories = self._collate_categories(self.entries)
self.all_taxonomies = self._collate_taxonomies(self.entries)


def load_template(self, file):
with open(file, "r") as f:
self.template = Template(f.read())

def _create_dir(self, dirname):
# Clear the output directory.
if os.path.exists(dirname):
shutil.rmtree(dirname)

# Re-create the output directory.
os.mkdir(dirname)

# Copy the static directory into the output directory.
for f in [self.config["static_dir"]]:
target = os.path.join(self.outdir, f)
if os.path.isfile(f):
shutil.copyfile(f, target)
else:
shutil.copytree(f, target)

def _make_taxonomies(self, item):
"""
Make a dict of array of all taxonomy items on the entry.
eg: {"tags": [...tags], "types": [...types]}
"""
out = {}
for tx in self.config["taxonomies"]:
out[tx] = {}
if tx not in item:
continue

# Iterate through each taxonomy array in the entry.
for v in item[tx]:
if v not in out[tx]:
id = v.lower()
out[tx][id] = Taxonomy(
name=v, slug=self._make_slug(v), count=0)
for tx in self.config["taxonomies"]:
out[tx] = sorted([out[tx][v]
for v in out[tx]], key=lambda k: k.name)

return out

def _collate_taxonomies(self, entries):
"""
Return the unique list of all taxonomies across the given entries with counts.
eg: {"tags": [...tags], "types": [...types]}
"""
out = {}
for e in entries:
for tx in self.config["taxonomies"]:
if tx not in out:
out[tx] = {}

for t in e.taxonomies[tx]:
id = t.name.lower()
if id not in out[tx]:
out[tx][id] = copy(t)
out[tx][id].count += 1

for tx in self.config["taxonomies"]:
out[tx] = sorted([out[tx][v]
for v in out[tx]], key=lambda k: k.name)

return out

def _make_categories(self, cats):
"""Make a list of Categories out of the given string tags."""
out = {}
for c in cats:
id = c.lower()
if id not in out:
out[id] = Category(name=c, slug=self._make_slug(c), count=0)

return sorted([out[c] for c in out], key=lambda k: k.name)

def _collate_categories(self, entries):
"""Return the unique list of all categories across the given entries with counts."""
cats = {}
for e in entries:
for c in e.categories:
id = c.name.lower()
if id not in cats:
cats[id] = copy(c)
cats[id].count += 1

return sorted([cats[c] for c in cats], key=lambda k: k.name)

def _filter_by_category(self, category, entries):
out = []
for e in entries:
for c in e.categories:
if c.slug == category.slug:
out.append(e)

return sorted([e for e in out], key=lambda k: k.name.lower())

def _make_slug(self, file):
return file.replace(" ", "-").lower()

def _render_page(self, cat, entries):
total_pages = math.ceil(len(entries) / self.config["per_page"])
page = 1

for items in self._paginate(entries, self.config["per_page"]):
html = self.template.render(
config=self.config,
pagination={"current": page, "total": total_pages},
all_categories=self.all_categories,

# Current category being rendered.
category=cat,
all_taxonomies=self.all_taxonomies,

# Taxonomies of all the entries currently being rendered.
taxonomies=self._collate_taxonomies(items),
entries=items)

fname = "{}{}.html".format(
cat.slug, "-" + str(page) if page > 1 else "")
with open(os.path.join(self.outdir, fname), "w") as f:
f.write(html)

page += 1

def _paginate(self, entries, size):
for start in range(0, len(entries), size):
yield entries[start:start + size]


def main():
"""Run the CLI."""
p = argparse.ArgumentParser(
description="A simple static site generator for generating directory websites.",
formatter_class=argparse.ArgumentDefaultsHelpFormatter)

n = p.add_argument_group("new")
n.add_argument("-n", "--new", action="store_true",
dest="new", help="initialize a new site")
n.add_argument("-p", "--path", action="store", type=str, default="example",
dest="exampledir", help="path to create the example site")

b = p.add_argument_group("build")
b.add_argument("-b", "--build", action="store_true",
dest="build", help="build a static site")
b.add_argument("-c", "--config", action="store", type=str, default="config.yml",
dest="config", help="path to the config file")
b.add_argument("-t", "--template", action="store", type=str, default="template.html",
dest="template", help="path to the template file")
b.add_argument("-d", "--data", action="store", type=str, default="data.yml",
dest="data", help="path to the data file")
b.add_argument("-o", "--output", action="store", type=str, default="site",
dest="output", help="path to the output directory")
args = p.parse_args()

if args.new:
exdir = os.path.join(os.path.dirname(__file__), "example")
if not os.path.isdir(exdir):
print("unable to find bundled example directory")
sys.exit(1)

try:
shutil.copytree(exdir, args.exampledir)
except FileExistsError:
print("the directory '{}' already exists".format(args.exampledir))
sys.exit(1)
except:
raise

if args.build:
print("building site from: {}".format(args.data))

bu = Builder(args.config)
bu.load_template(args.template)
bu.load_data(args.data)
bu.build(args.output)

print("processed {} entries, {} categories, {} taxonomies".format(len(bu.entries), len(bu.all_categories), len(bu.all_taxonomies)))

if len(bu.entries) > 0:
print("published to directory: {}".format(args.output))
else:
print("no data to build site")
17 changes: 17 additions & 0 deletions dirmaker/example/config.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
base_url: "https://mysite.com"
static_dir: "static"
per_page: 50

# Each entry can have multiple collection of tags (list of strings) in addition to the
# categories. For instance, a directory of fonts could have multiple meaningful
# tag collections per entry such as:
# types = [serif, sans-serif, display, script]
# mood = [retro, distressed, cool]
# tag = [yep, nope, etc]
taxonomies: ["style", "mood"]

site_name: "My directory site"
site_description: "My directory site"
page_title: "{category} - Directory of cool things."
meta_description: "Directory of {category} in cool things. "
21 changes: 21 additions & 0 deletions dirmaker/example/data.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
---
- name: Roboto
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.
url: "https://www.fontsquirrel.com/fonts/roboto"
categories: [Professional]
style: [sans-serif]
mood: [headings, oblique, paragraph, sans, grotesque, sans, humanist]

- name: Acre
description: Acre is a geometric sans-serif family of eight weights that’s both inspired by and named after my great grandfather, Tex Acre.
url: "https://www.fontsquirrel.com/fonts/acre"
categories: [Professional]
style: [sans-serif]
mood: [corporate, monolinear, paragraph, sans, geometric]

- name: AnuDaw
description: A funky font designed by Nyek! Pinoy Komik Fonts.
url: https://www.fontsquirrel.com/fonts/AnuDaw
categories: [Funky]
style: [comic]
mood: [brush, comic, display, grunge, handwritten]
1 change: 1 addition & 0 deletions dirmaker/example/static/logo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading

0 comments on commit a0ead4d

Please sign in to comment.