~rbn/tlog

b23fd6d0ddf11127cadaeac29ca4ca4f612eadd7 — Ruben Schuller a month ago
initial commit
A  => .build.yml +30 -0
@@ 1,30 @@
arch: null
artifacts: []
environment:
  GIT_SSH_COMMAND: '"ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no"'
image: debian/stable
packages:
- wget
- tcl
- tcllib
- tcl-tls
repositories: {}
secrets:
- 13f9170c-f6db-4659-8f9e-a2f233dfe450
- e5756bb8-64dc-44b8-a043-cacbb148daad
shell: false
tasks:
- nagelfar: |-
    wget https://iweb.dl.sourceforge.net/project/nagelfar/Rel_130/nagelfar130.tar.gz
    tar xf nagelfar130.tar.gz
- check: |-
    nagelfar130/nagelfar.tcl -exitcode tlog/tlog.tcl
- example: |-
    cd tlog/example
    cp -r output output_orig
    make clean
    make output/index.html
    make output/test.html
    diff -q output/index.html output_orig/index.html
    diff -q output/test.html output_orig/test.html
triggers: []

A  => README.md +123 -0
@@ 1,123 @@
# tlog

[![builds.sr.ht status](https://builds.sr.ht/~rbn/tlog.svg)](https://builds.sr.ht/~rbn/tlog?)

simple & flexible templating.

## what?

i've gotten annoyed by other static site generators doing a million things.
i just want to generate some html pages. so this.

## how?

create a hierarchy like the following:

- `content`
	- `page`
		- `content.md`
		- `data.tcl`
- `global`
	- `date.tcl`

- `template`
	- `template.html`


then run tlog:

	tlog.tcl -t template/template.html global page

this uses `template.html` as template, reading contents to be inserted from
`global` and `page`.

### templates

a template can be anything plaintext. as 
[subst](https://www.tcl.tk/man/tcl8.6/TclCmd/subst.htm) 
is the mechanism used, it's rules apply.

a template thus looks kind of like this:

	<html>
	<head>
		<title>$title</title>
	</head>
	<body>
	<h1>[ insert title ]</h1>
	<p><b>[ insert date ]</b></p>
	<div id="content">[ insert content ]</div>
	</body>
	</html>

the `insert` command is another way to insert variables, as a way to have a
fixed way to insert stuff in case the internal data representation changes
(which it has done multiple times already :P) without requiring touching
contents.

### contents

content is always consumed as whole directory, the names of the files **without** extension are being used to reference the contents from a template. 

a tradeof which is being made is that contents can be preprocessed. while
this could be done in an additional external step, it's common enough to 
be included. currently there are three types of content:

- markdown (extension `.md`) parsed & formatted as html with
[tcllib "Markdown"](https://core.tcl-lang.org/tcllib/doc/tcllib-1-19/embedded/www/tcllib/files/modules/markdown/markdown.html)
- tcl (extension `.tcl`) is evaluated and can reign freely. the output is also
  stored.
- html (extension `.html`) is just copied.

as contents are a whole directory, we can do without the thing other 
systems call "frontmatter" by evaluating tcl scripts in the content directory.

there can't be contents of the same name, so data.md and data.tcl aren't
permitted, not even when in two different directories (as we can load multiple
ones). reading the contents of the same name will error out to avoid confusion.

examples for contents:

	$ cat content/page0/content.md
	# foobar
	
	it's markdown!

	$ cat content/page0/data.tcl
	dict set context title "the title"

	$ cat global/date.tcl
	proc date {} {
		set date 0
		foreach { path } [ glob -type f * ] {
			set ndate [ file mtime $path ]
			if [ expr $ndate > $date ] {
				set date $ndate
			}
		}
		return [ clock format $date -format %Y-%m-%d ]
	}

	dict set context date [ date ]

## internals

contents are read into the `context` dictionary, with their filenames
without extension used as keys. tcl script contents can modify the 
`context` dictionary as in the example above.

this `context` dictionary gets unpacked using 
[dict with](https://www.tcl.tk/man/tcl8.6/TclCmd/dict.htm#M27)
before running `subst` with the template as argument.

## makefile

to tie everything together, you can use make:

	output/%.html: template/template.html global/date.tcl content/%/content.md content/%/data.tcl
		tlog.tcl -t template/template.html global content/$* > $@

## example

in the example directory is a somewhat complete way to build a simple blog.


A  => example/Makefile +9 -0
@@ 1,9 @@
clean:
	rm output/*

output/index.html: template/index.html index/data.tcl
	../tlog.tcl -t template/index.html index > output/index.html

output/%.html: template/template.html content/%/content.md content/%/data.tcl
	../tlog.tcl -t template/template.html content/$* > $@


A  => example/content/test/content.md +3 -0
@@ 1,3 @@
## another heading

this seems to be _markdown_.

A  => example/content/test/data.tcl +2 -0
@@ 1,2 @@
dict set context title "test content"
dict set context date "123456789"

A  => example/index/data.tcl +59 -0
@@ 1,59 @@
# collect all pages from directory "content"
proc pages {} {
	upvar #0 converters convs
	lappend pages

	set contentdirs [ glob -type d -directory content "*" ] 

	foreach { dir } $contentdirs {
		# set teaser text up to <!--more--> or 150 characters
		set contents [ contents [ content_files $dir $convs ] $convs ]
		set mark [ string first {<!--more-->} [ dict get $contents content ] ]
		incr mark -1
		if { [ expr $mark < 0 ] } {
			set mark 150
		}
		dict set contents teaser [ string range [ dict get $contents content ] 0 $mark ]

		# set filename for linking, this assumes that the output is saved as $dirname.html
		set filename "[ file tail $dir ].html"
		dict set contents link $filename
		lappend pages $contents
	}
	return $pages
}

# helper command to sort posts dict by date
proc datecompare { a b } {
	set adate [ dict get $a date ]
	set bdate [ dict get $b date ]

	if {$adate < $bdate} {
		return -1
	} elseif {$adate > $bdate} {
		return 1
	}

	return 0
}

# create listing of posts
proc content {} {
	lappend out

	# iterate pages in reverse order (newest first)
	foreach { page } [ lsort -decreasing -command datecompare [ pages ] ] {
		lappend out "
		<div class=\"post\">
			<a href=\"[ dict get $page link ]\"><h1>[ dict get $page title ]</h1></a>
			<span class=\"date\">[ clock format [ dict get $page date ] -format %Y-%m-%d ]</span>
		</div>
		<p>[ dict get $page teaser ]</p>"
	}

	return [ join $out "\n" ]
}

dict set context title "tlog index"
# set page contents
dict set context content [ content ]

A  => example/output/index.html +18 -0
@@ 1,18 @@
<!DOCTYPE html>
<html>
<head>
	<meta charset="utf-8">
	<title>tlog index</title>
</head>
<body>
	
		<div class="post">
			<a href="test.html"><h1>test content</h1></a>
			<span class="date">1973-11-29</span>
		</div>
		<p><h2>another heading</h2>

<p>this seems to be <em>markdown</em>.</p></p>
</body>
</html>


A  => example/output/test.html +13 -0
@@ 1,13 @@
<!DOCTYPE html>
<html>
<head>
	<title>test content</title>
</head>
<body>
	<h1>test content</h1>
	<h2>another heading</h2>

<p>this seems to be <em>markdown</em>.</p>
</body>
</html>


A  => example/template/index.html +10 -0
@@ 1,10 @@
<!DOCTYPE html>
<html>
<head>
	<meta charset="utf-8">
	<title>[ insert title ]</title>
</head>
<body>
	[ insert content ]
</body>
</html>

A  => example/template/template.html +10 -0
@@ 1,10 @@
<!DOCTYPE html>
<html>
<head>
	<title>[ insert title ]</title>
</head>
<body>
	<h1>[ insert title ]</h1>
	[ insert content ]
</body>
</html>

A  => tlog.tcl +91 -0
@@ 1,91 @@
#!/usr/bin/env tclsh
package require cmdline
package require Markdown

proc verbatim { in } {
	return $in
}

proc markdown { in } {
	##nagelfar ignore Unknown command
	return [ ::Markdown::convert $in ]
}

proc tcl { in } {
	upvar 1 context context
	return [ eval $in ]
}

proc extensions { converters } {
	return [ dict keys $converters ]
}

proc page { template contents } {
	##nagelfar ignore Found constant
	return [ dict create template $template contents $contents ]
}

proc content_files { dir converters } {
	# find all files with registered processors
	set globs "*{[join [ extensions $converters ] , ]}"
	foreach name [ glob -directory $dir $globs ] {
		lappend files $name
	}
	return $files
}

proc contents { files converters } {
	set context [ dict create ]
	foreach { path } $files {
		set f [ open $path r ]
		set converter [ dict get $converters [ file extension $path ] ]
		set name [ file rootname [ lindex [ file split $path ] end ] ]
		if { [ dict exists $context $name ] } {
			error "duplicate content with name: $name"
		}
		dict set context $name [ $converter [ read $f ] ]
		close $f
	}

	return $context
}

proc insert { name } {
	upvar 1 $name _name
	return $_name 
}

proc execute { template { contextvar context } } {
	upvar 1 $contextvar context
	dict with context {}
	return [ subst $template ]
}

set options {
            { t.arg "templates/main.html"	"template file" }
}

set usage {[options] [content_dir content_dir ...]}

try {
	set opts [ ::cmdline::getoptions argv $options $usage ]
} trap { CMDLINE USAGE } { msg o } {
	puts $msg
	exit 0
}

set template_file [ dict get $opts t ]

set converters [ dict create .html verbatim .md markdown .tcl tcl ]

set f [ open $template_file r ]
set template [ read $f ]
close $f

set contents {}
foreach {dir} $argv {
	lappend contents {*}[ content_files $dir $converters ]
}

set context [ contents $contents $converters ]
puts [ execute $template ]