~aleteoryx/gloss

898681c2532beaac134259024aaa83b794af13e4 — Aleteoryx 2 months ago 63e33c3
build system
6 files changed, 132 insertions(+), 120 deletions(-)

M README.md
A gloss/__init__.py
M gloss/__main__.py
M gloss/markup/__init__.py
M gloss/modes/glossary.py
A gloss/util.py
M README.md => README.md +1 -31
@@ 6,34 6,4 @@ it is designed to maximize nonlinear browsing.

***

the central gloss.py script translates each .gls file in its input directory to a .html in its output.
to run it, you will need a python runtime and a C compiler. make sure to download the entire repo.

.gls files are composed of a names section, a set of blocks, and an optional "see also" section.

the names section consists of the first non-empty non-comment lines in a file.
each line becomes a name for the file.
a file's names are automatically turned into links,
the first time it is referenced in another document.
matching is performed at word boundaries: a file with name "foo" will not be linked from "foobar".
the names section is terminated with a blank line.

blocks can be either paragraph blocks or quote blocks, and they are terminated by blank lines.
a block is a quote block if its first line begins with a '>'.
to begin a paragraph block with '>', escape it with a backslash.

if any line in a block begins with ~, it will become the block's metadata.
only quotes have metadata, for authorship info.
the format is thus: `<source> @@ <time> // <url>`.
if `@@` is omitted, the signature will have no time associated.
if `//` is omitted, the signature will have no URL associated.

within a block, one can write a link of the form `<https://example.com>`, or `<https://example.com|example page>`.
one can also escape `<` and `>` with backslashes. regions of text can be italicized by putting them `/in slashes/`.

if a block begins with `***`, the "see also" section is entered.
each subsequent non-empty line should be the slug (filename, minus .gls) of an article.
it will be linked in a "see also" list at the bottom.

if a template.html is present in the source directory, it will be the template for all rendered pages.
run gloss.py with no arguments for more information.
I'll document this another time. maybe. it's for personal use mostly

A gloss/__init__.py => gloss/__init__.py +84 -0
@@ 0,0 1,84 @@
#!/bin/env python3

import yaml
from pathlib import Path
from typing import List, Tuple, Callable
from dataclasses import dataclass

from .modes import glossary
from .util import die, copy

### MODES ###

def copymode(sfmt, srcdir, outdir):
	i = 0
	for srcfile in srcdir.iterdir():
		if not srcfile.is_file() or srcfile.name == '.gloss':
			continue
		i += 1
		outfile = Path(outdir, srcfile.name)
		copy(srcfile, outfile)
	print(sfmt.format('###', f'copied {i} files'))

modes = {
	'copy': copymode,
	'gloss': glossary.gloss
}


### FS WALK ###

@dataclass
class Step:
	srcdir: Path
	outdir: Path
	reldir: Path
	process: Callable[[str, Path, Path], None]
	
	def test(self):
		if not self.outdir.is_dir() and self.outdir.exists():
			die(f'can\'t output "{self.outdir}": non-directory already exists', 3)


	def exec(self, sfmt):
		if not self.outdir.exists():
			self.outdir.mkdir()
		self.process(sfmt, self.srcdir, self.outdir)

def get_steps(srcdir, outdir) -> List[Step]:
	global modes

	dirs = [srcdir]
	ret = []
	for adir in dirs:
		reldir = adir.relative_to(srcdir)

		glossfile = Path(adir, '.gloss')
		out = mode = None
		if glossfile.is_file():
			parsed = yaml.safe_load(open(glossfile, 'rt'))
			out = parsed.get('out', None)
			mode = parsed.get('mode', None)
		
		if out is None:
			out = reldir
		if mode is None:
			mode = 'copy'
		
		if mode == 'ignore':
			continue
		
		if mode not in modes:
			die(f'fatal: unknown mode in "{glossfile}": "{mode}"', 2)

		empty = True
		for what in adir.iterdir():
			if what.is_dir():
				dirs.append(what)
			elif what.is_file():
				empty = False
		
		if mode != 'copy' or not empty:
			ret.append(Step(adir, Path(outdir, out), reldir, modes[mode]))
	
	return ret

M gloss/__main__.py => gloss/__main__.py +26 -73
@@ 1,14 1,15 @@
#!/bin/env python3

from time import time
start = time()

from sys import argv
import yaml
from pathlib import Path
from typing import List, Tuple, Callable
from dataclasses import dataclass
from math import log10

from . import markup
from . import get_steps
from .util import die

from .modes import glossary

usage = f'''
usage: {argv[0]} <SRCDIR> <OUTDIR>


@@ 26,78 27,30 @@ said file's 'mode' key is missing, the mode defaults to 'copy'.
		SRCDIR.
'''[1:]

def die(why, code=1):
	print(why, file=stderr)
	exit(code)


### MODES ###

def copymode(srcdir, outdir):
	for srcfile in srcdir.iterdir():
		if not srcfile.is_file() or srcfile.name == '.gloss':
			continue
		outfile = outdir + srcfile.name
		outfile.write_bytes(srcfile.read_bytes())

modes = {
	'copy': copymode,
	'gloss': glossary.gloss
}


### FS WALK ###

@dataclass
class SrcDir:
	srcdir: Path
	outdir: Path
	process: Callable[[Path, Path], None]
argv = argv[1:]
if len(argv) != 2 or argv[0][0] == '-' or argv[1][0] == '-':
	die(usage)
srcdir = Path(argv[0]).absolute()
outdir = Path(argv[1]).absolute()

def get_dirs(srcdir, outdir) -> List[SrcDir]:
	global modes

	dirs = [srcdir]
	ret = []
	for adir in dirs:
		if not adir.is_dir():
			continue
print('collecting steps...')
steps = get_steps(srcdir, outdir)

		glossfile = Path(adir, '.gloss')
		out = mode = None
		if glossfile.is_file():
			parsed = yaml.safe_load(open(glossfile, 'rt'))
			out = parsed.get('out', None)
			mode = parsed.get('mode', None)
		
		if out is None:
			out = adir.relative_to(srcdir)
		if mode is None:
			mode = 'copy'
		
		if mode == 'ignore':
			continue
		
		if mode not in modes:
			die(f'fatal: unknown mode in "{glossfile}": "{mode}"')
		
		ret.append(SrcDir(adir, Path(outdir, out), modes[mode]))
		dirs.extend(adir.iterdir())
	
	return ret
numlen = int(log10(len(steps))+1)
nfmt = f'[{{0:{numlen}}}/{len(steps)}]' # calculates the correct width for e.g. [ 5/52]
sfmt = f'<{{0:^{numlen*2+1}}}> {{1}}'   # and for the status, e.g.              < END >

print(sfmt.format('...', 'testing!'))
for n, step in enumerate(steps):
	print(f'{nfmt} testing {{1}}'.format(n+1, Path('/', step.reldir)))
	step.test()

if __name__ == '__main__':
	argv = argv[1:]
	if len(argv) != 2 or argv[0][0] == '-' or argv[1][0] == '-':
		die(usage)
print(sfmt.format('...', 'building!'))
for n, step in enumerate(steps):
	print(f'{nfmt} building {{1}}'.format(n+1, Path('/', step.reldir)))
	step.exec(sfmt)

	srcdir = Path(argv[0]).absolute()
	outdir = Path(argv[1]).absolute()
	
	steps = get_dirs(srcdir, outdir)
	print(steps)
	for step in steps:
		if not step.outdir.exists():
			step.outdir.mkdir()
		step.process(step.srcdir, step.outdir)
stop = time()
print(sfmt.format('END', f'build complete in {stop-start:.2}s! :3'))

M gloss/markup/__init__.py => gloss/markup/__init__.py +3 -0
@@ 14,6 14,9 @@ class Markup:
		
		self.proc = Popen([str(self.bfile), 'convert'], stdin=PIPE, stdout=PIPE, text=True)
	
	def __del__(self):
		self.proc.kill()

	def process(self, text):
		self.proc.stdin.write(text+"\n")
		self.proc.stdin.flush()

M gloss/modes/glossary.py => gloss/modes/glossary.py +8 -16
@@ 7,9 7,10 @@ from os import stat, system
from glob import glob
from typing import List, Optional, Union, Dict, Set, Tuple
from dataclasses import dataclass
from sys import argv, stderr, exit
from sys import argv, stderr

from .. import markup
from gloss.markup import get as getfmt
from gloss.util import die

usage = f'''
usage: {argv[0]} <SRCDIR> <OUTDIR>


@@ 26,10 27,6 @@ interpolated as in str.format, and the following keys are provided:
	{{modtime}} - The datetime of the article's last edit.
'''[1:]

def die(why, code=1):
	print(why, file=stderr)
	exit(code)


### TYPES ###



@@ 151,7 148,7 @@ class Indexes:
				pattern = re.compile(f"((?<=\\W)|^){re.escape(name)}((?=\\W)|$)", re.IGNORECASE)
				self.names_sorted.append((name, pattern))
		
		sorted(self.names_sorted, key=lambda x: len(x[0]))
		self.names_sorted = sorted(self.names_sorted, key=lambda x: len(x[0]), reverse=True)
			
def gen_inner_html(fmt, file, idx):
	for block in file.blocks:		# format text, listify it


@@ 229,7 226,7 @@ def gen_inner_html(fmt, file, idx):

### ENTRYPOINT ###

def gloss(srcdir, outdir):
def gloss(sfmt, srcdir, outdir):
	try:
		with open('{srcdir}/template.html', 'rt') as fp:
			template = fp.read()


@@ 252,7 249,7 @@ def gloss(srcdir, outdir):
</html>
'''

	fmt = markup.get()
	fmt = getfmt()

	files = []
	for fn in glob('*.gls', root_dir=srcdir):


@@ 273,10 270,5 @@ def gloss(srcdir, outdir):
				'modtime': datetime.fromtimestamp(stat(f'{srcdir}/{file.slug}.gls').st_mtime)
			}
			fp.write(template.format(**ctx))

if __name__ == '__main__':
	argv = argv[1:]
	if len(argv) != 2 or argv[0][0] == '-' or argv[1][0] == '-':
		die(usage)

	gloss(argv[0], argv[1])
	
	print(sfmt.format('###', f'generated {len(files)} entries'))

A gloss/util.py => gloss/util.py +10 -0
@@ 0,0 1,10 @@
from sys import exit

def die(why, code=1):
	print(why, file=stderr)
	exit(code)

def copy(a, b):
	with open(a, 'rb') as af, open(b, 'wb') as bf:
		while buf := af.read(2**20):
			bf.write(buf)