~aleteoryx/gloss

63e33c3226ad62e8845b50d07807ede4e6fb77ac — Aleteoryx 2 months ago dfbc0b2
build system basics
M .gitignore => .gitignore +1 -0
@@ 1,1 1,2 @@
/markup
__pycache__

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

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

from . import markup

from .modes import glossary

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

recurses into SRCDIR, parsing each .gloss file as YAML and running the
requested parser. if a directory does not contain a '.gloss' file, or
said file's 'mode' key is missing, the mode defaults to 'copy'.

.gloss may include the following fields:
	mode:	sets the mode. may be one of 'copy', 'ignore', or
		'gloss'. if mode is 'ignore', the directory and all
		subdirectories will be skipped.
	out:	sets the output directory, relative to OUTDIR. if
		omitted, it defaults to the current path relative to
		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]

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

	dirs = [srcdir]
	ret = []
	for adir in dirs:
		if not adir.is_dir():
			continue

		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


if __name__ == '__main__':
	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()
	
	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)

A gloss/markup/__init__.py => gloss/markup/__init__.py +46 -0
@@ 0,0 1,46 @@
import html
from pathlib import Path
from subprocess import Popen, PIPE, run as runcmd

class Markup:
	def __init__(self, where, *, cfile='markup.c', bfile='markup'):
		self.cfile = Path(where, cfile)
		self.bfile = Path(where, bfile)

		if not self.bfile.exists() or self.cfile.stat().st_mtime > self.bfile.stat().st_mtime:
			print("recompiling markup subsystem...")
			runcmd(['cc', str(self.cfile), '-o', str(self.bfile), '-Wall'], check=True)
			print("recompiled!")
		
		self.proc = Popen([str(self.bfile), 'convert'], stdin=PIPE, stdout=PIPE, text=True)
	
	def process(self, text):
		self.proc.stdin.write(text+"\n")
		self.proc.stdin.flush()

		segments = []
		while (line := self.proc.stdout.readline()) not in ('NEXT\n', ''):
			ty = line[0:4]

			length = int(line[5:9])
			ltext = line[12:12+length]

			if ty == 'HTML':
				segments.append((ltext,))
			elif ty == 'IESC':
				segments.append((html.escape(ltext),))
			elif ty == 'TEXT':
				segments.append(ltext)
			else:
				print(f'read in unknown type "{ty}" from markup subprocess', file=stderr)

		return segments

markup = None

def get():
	global markup
	if markup is None:
		markup = Markup(Path(__file__).parent)
	
	return markup

A gloss/markup/markup => gloss/markup/markup +0 -0
R markup.c => gloss/markup/markup.c +0 -1
@@ 262,7 262,6 @@ segmentize(size_t length, const char *source)
		}
	}
	if(in_italic){
		PLINE();
		tail->next = italic_head->next;
		if(tail->next != NULL)
			tail = italic_tail;

R gloss.py => gloss/modes/glossary.py +12 -47
@@ 8,8 8,8 @@ from glob import glob
from typing import List, Optional, Union, Dict, Set, Tuple
from dataclasses import dataclass
from sys import argv, stderr, exit
from pathlib import Path
from subprocess import Popen, PIPE, run as runcmd

from .. import markup

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


@@ 116,42 116,7 @@ def first_pass(slug, fp):
	elif block_type is not None:
		blocks.append(Block(block_type, block_text, block_meta))
	
	return GlsFile(slug, title, [*names], blocks, see_also)

class Markup:
	def __init__(self, where, *, cfile='markup.c', bfile='markup'):
		self.cfile = Path(where, cfile)
		self.bfile = Path(where, bfile)

		if not self.bfile.exists() or self.cfile.stat().st_mtime > self.bfile.stat().st_mtime:
			print("recompiling markup subsystem...")
			runcmd(['cc', str(self.cfile), '-o', str(self.bfile), '-Wall'], check=True)
			print("recompiled!")
		
		self.proc = Popen([str(self.bfile), 'convert'], stdin=PIPE, stdout=PIPE, text=True)
	
	def process(self, text):
		self.proc.stdin.write(text+"\n")
		self.proc.stdin.flush()

		segments = []
		while (line := self.proc.stdout.readline()) not in ('NEXT\n', ''):
			ty = line[0:4]

			length = int(line[5:9])
			ltext = line[12:12+length]

			if ty == 'HTML':
				segments.append((ltext,))
			elif ty == 'IESC':
				segments.append((html.escape(ltext),))
			elif ty == 'TEXT':
				segments.append(ltext)
			else:
				print(f'read in unknown type "{ty}" from markup subprocess', file=stderr)

		return segments
			
	return GlsFile(slug, title, [*names], blocks, see_also)			

quote_pat = re.compile("((?:(?!@@)(?!//).)*)(?:@@((?:(?!//).)+))?(?://(.+))?")



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

### ENTRYPOINT ###

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

	srcdir = argv[0]
	outdir = argv[1]

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


@@ 294,7 252,7 @@ if __name__ == '__main__':
</html>
'''

	fmt = Markup(Path(__file__).parent)
	fmt = markup.get()

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


@@ 315,3 273,10 @@ if __name__ == '__main__':
				'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])

A run => run +3 -0
@@ 0,0 1,3 @@
#!/bin/sh

PYTHONPATH="$(dirname "$0")" python -m gloss $@

A testsrc/.gloss => testsrc/.gloss +1 -0
@@ 0,0 1,1 @@
mode: gloss

A testsrc/a.html => testsrc/a.html +24 -0
@@ 0,0 1,24 @@
<!doctype html>
<html>
<head>
	<meta encoding='utf-8'>
	<title>The Letter A</title>
</head>
<body>
<main>
<h1>The Letter A</h1>
<p>this is a paragraph block. these lines will be folded into one string and ultimately rendered roughly the same in the browser.</p>
<div>
	<blockquote cite="https://aleteoryx.me">this is a quote block</blockquote>
	<p>&mdash; <cite><a href="https://aleteoryx.me">aleteoryx</a></cite>, 2025</p>
</div>
<p>\&lt;http://example.com|example page&gt;</p>
</main>
<hr>
<h3>See also:</h3>
<ul>
	<li><a href="b.html">The Letter B</a></li>
</ul>
<footer>File last modified Fri, 2025-15-51 00:51:39 </footer>
</body>
</html>

A testsrc/b.html => testsrc/b.html +19 -0
@@ 0,0 1,19 @@
<!doctype html>
<html>
<head>
	<meta encoding='utf-8'>
	<title>The Letter B</title>
</head>
<body>
<main>
<h1>The Letter B</h1>
<p>preceeded by <a href="a.html">the letter a</a>.</p>
</main>
<hr>
<h3>See also:</h3>
<ul>
	<li><a href="a.html">The Letter A</a></li>
</ul>
<footer>File last modified Thu, 2025-14-40 18:40:52 </footer>
</body>
</html>