code / chum / chum

1#!/bin/sh
2trampoline=; define()(:) # -*- makefile -*-
3###########################################
4## C H U M : Code Htmlizer Using Make(1) ##
5## Make a website out of a code repo, ez ##
6## Quickstart: cd project; vi .chum ---> ##
7## [change variables given below].  see  ##
8## README for details #####################
9###########################################
10define trampoline
11if command -v gmake >/dev/null 2>&1
12then exec gmake -kr -f "$0" "$@"
13else exec make -kr -f "$0" "$@"
14fi
15endef
16####################### code starts here ##
17# Project metadata
18NAME = $(notdir $(PWD))
19AUTHOR =
20DESCRIPTION =
21
22# Build config
23OUTDIR = out
24SERVER = @printf 'Generated files are in %s/\n'
25VDUMPF = # used by chums
26
27# Project files
28SOURCES =
29README =
30READMEFILTER = cat
31LICENSE =
32TARBALL = $(OUTDIR)/$(NAME).tar.gz
33STATICS =
34
35# Project web stuff
36REMOTE =
37URL_ROOT = /
38URL_CLONE =
39URL_DOWNLOAD = $(URL_ROOT)$(notdir $(TARBALL))
40
41define TEMPLATE =
42<!DOCTYPE html>
43<html><head>
44<meta charset="utf-8">
45<title>{{FILENAME}}</title>
46<style>{{STYLE}}</style></head>
47<body>
48<h1><!--NORMAL-->{{DIRECTORY}}/<!--/NORMAL-->{{FILENAME}}</h1>
49<!--INDEX-->{{DESCRIPTION}}<!--/INDEX-->
50<!--NORMAL-->
51<nav><ul>
52<li><a href="{{ROOT}}">index</a></li>
53<li><a href="{{RAWFILE}}">source</a></li>
54</ul></nav>
55<!--/NORMAL-->
56<main>{{CONTENT}}</main>
57<footer><p>(C) {{AUTHOR}}.
58<a href="{{CLONE}}">clone</a>
59<a href="{{DOWNLOAD}}">download</a>
60</p></footer>
61</body>
62</html>
63endef
64
65define STYLE =
66.ll{display:inline-block;width:3em;
67padding-right:0.5em;margin-right:0.5em;}
68endef
69
70# Internal stuff
71
72-include .chum
73
74_FILES = $(README) $(LICENSE) $(SOURCES)
75
76define _CHUM
77BEGIN {
78  if (template) TEMPLATE = template
79  else TEMPLATE = "<h1>{{FILENAME}}</h1>{{CONTENT}}"
80  TMPLV["STYLE"] = style
81  TMPLV["CLONE"] = escape("$(URL_CLONE)")
82  TMPLV["DOWNLOAD"] = escape("$(URL_DOWNLOAD)")
83  TMPLV["DESCRIPTION"] = "$(DESCRIPTION)"
84  TMPLV["AUTHOR"] = "$(AUTHOR)"
85  TMPLV["ROOT"] = escape("$(URL_ROOT)")
86  "pwd" | getline TMPLV["DIRECTORY"]; close("pwd")
87  TMPLV["DIRECTORY"] = outfn(TMPLV["DIRECTORY"], 3)
88  for (f in ARGV)
89    if (f && system("test -f " ARGV[f]))
90	delete ARGV[f]
91}
92
93FNR == 1 {
94  if (NR > 1) finish()
95  OUTFILE = outfn(FILENAME)
96  sub(/^\.\.?\//, "", FILENAME)
97  TMPLV["FILENAME"] = FILENAME
98  TMPLV["OUTFILE"] = OUTFILE
99  FILES[f++] = FILENAME
100  printf("%-24s -> %s\n", FILENAME, OUTFILE)
101  if (system("mkdir -p $(OUTDIR)/" outfn(FILENAME, 2)))
102    die(1, "Can't make directory: " outfn(FILENAME, 2))
103  if (!system("cp " FILENAME " " outfn(FILENAME, 4) ".txt"))
104    TMPLV["RAWFILE"] = outfn(FILENAME, 3) ".txt"
105  OUTSTR = ""
106}
107
108END {
109  if (dead) exit dead
110  finish()
111  doindex()
112  copy_statics()
113}
114
115{
116  gsub(/&/, "\&amp;"); gsub(/</, "\&lt;"); gsub(/>/, "\&gt;")
117  OUTSTR = OUTSTR sprintf("<span class=\"ln\" id=\"l%d\">" \
118        "<a class=\"ll\" href=\"#l%d\">%d</a>" \
119        "%s</span>\n",
120        FNR, FNR, FNR, $$0)
121}
122
123function slurp (file,  o) {
124  if (!file) return 0
125  while ((getline < file) > 0)
126    o = o (o?"\n":"") $$0
127  return o
128}
129
130function outfn (file, mod) {
131  if (!mod) { # foo.txt => $(OUTDIR)/foo.txt.html
132    sub(/^/, "$(OUTDIR)/", file)
133    sub(/$$/, ".html", file)
134  } else if (mod == 1) { # foo.txt => foo.txt.html
135    sub(/$$/, ".html", file)
136  } else if (mod == 2) { # foo.txt => / ; foo/bar.txt => foo/
137    if (!sub(/\/[^\/]*$$/, "/", file))
138      file = "/"
139  } else if (mod == 3) { # foo/bar.txt => bar.txt (basename)
140    sub(/.*\//, "", file)
141  } else if (mod == 4) { # foo/bar.txt => $(OUTDIR)/foo/bar.txt
142    sub(/^/, "$(OUTDIR)/", file)
143  }
144  return file
145}
146
147function template_replace (str) {
148  if (OUTFILE == "$(OUTDIR)/index.html") {
149    gsub(/<!--\/NORMAL/, "", str)
150    gsub(/NORMAL-->/, "", str)
151  } else {
152    gsub(/<!--\/INDEX/, "", str)
153    gsub(/INDEX-->/, "", str)
154  }
155  for (ts in TMPLV) {
156    gsub("{{"ts"}}", TMPLV[ts], str)
157  }
158  gsub(/&/, "\\&", OUTSTR)
159  sub("{{CONTENT}}", OUTSTR, str)
160  return str
161}
162
163function finish () {
164  sub("\n$$", "", OUTSTR)
165  if (OUTFILE == "$(OUTDIR)/index.html") {
166    TMPLV["FILENAME"] = TMPLV["DIRECTORY"]
167    OUTSTR = "<section id=\"files\"><ul>" \
168      OUTSTR            \
169      "</ul></section>"
170    if ("$(README)") {
171      OUTSTR = OUTSTR "<section id=\"readme\">"
172      while ((("$(READMEFILTER) $(README)") | getline) > 0)
173        OUTSTR = OUTSTR "\n" $$0
174      OUTSTR = OUTSTR "</section>"
175    }
176  } else {
177    OUTSTR = "<pre><code>" OUTSTR "</pre></code>"
178  }
179  print(template_replace(TEMPLATE)) > OUTFILE
180  close(OUTFILE)
181}
182
183function doindex () {
184  OUTFILE = "$(OUTDIR)/index.html"
185  OUTSTR = ""
186  printf("[build]                     %s\n", OUTFILE)
187  for (f in FILES) {
188    OUTSTR = OUTSTR \
189      sprintf("<li><a href=\"%s\">%s</a></li>\n",
190        escape(outfn(FILES[f], 1)), FILES[f])
191  }
192  finish()
193}
194
195function die(code, message) {
196  print(message) > "/dev/stderr"
197  dead = code
198  exit code
199}
200
201function escape(str, c, len, res) {
202    ord[" "] = 32; ord["!"] = 33; ord["\""] = 34; ord["#"] = 35; ord["$$"] = 36
203    ord["%"] = 37; ord["&"] = 38; ord["'"] = 39; ord["("] = 40; ord[")"] = 41
204    ord["*"] = 42; ord["+"] = 43; ord[","] = 44; ord["-"] = 45; ord["."] = 46
205    ord[":"] = 58; ord[";"] = 59; ord["<"] = 60; ord["="] = 61; ord[">"] = 62
206    ord["?"] = 63; ord["@"] = 64; ord["["] = 91; ord["\\"] = 92; ord["]"] = 93
207    ord["^"] = 94; ord["_"] = 95; ord["`"] = 96; ord["{"] = 123; ord["|"] = 124
208    ord["}"] = 125; ord["~"] = 126
209    len = length(str)
210    res = ""
211    for (i = 1; i <= len; i++) {
212	c = substr(str, i, 1);
213	if (c ~ /[0-9A-Za-z\/]/)
214	    res = res c
215	else
216	    res = res "%" sprintf("%02X", ord[c])
217    }
218    return res
219}
220
221function copy_statics () {
222  split(static, STATICS, /  */)
223  for (s in STATICS) {
224    printf("Copying %s\n", STATICS[s])
225    if (system("cp " STATICS[s] " $(OUTDIR)/" outfn(STATICS[s], 3)))
226      die(2, "Can't copy static file: " STATICS[s])
227  }
228}
229endef
230
231# Internal recipes
232
233$(OUTDIR): export _TMPL:=-vtemplate=$(TEMPLATE)
234$(OUTDIR): export _STYLE:=-vstyle=$(STYLE)
235$(OUTDIR): export _STATICS:=-vstatic=$(STATICS)
236$(OUTDIR): export _CHUM:=$(_CHUM)
237$(OUTDIR): $(_FILES) .chum
238	@awk "$${_TMPL}" "$${_STYLE}" "$${_STATICS}" "$${_CHUM}" $(_FILES)
239ifdef VDUMPF
240	printf '%s\t%s\t%s\n' $(NAME) $(URL_ROOT) "$(DESCRIPTION)" >> $(VDUMPF)
241endif
242
243$(TARBALL): $(_FILES) .chum
244	@printf '[build]                     %s\n' $(TARBALL)
245	@tar -cf $@ $(_FILES)
246
247.PHONY: build serve publish clean
248
249build: $(OUTDIR) $(TARBALL)
250
251serve: $(OUTDIR)
252	$(SERVER) $(OUTDIR)
253
254publish: $(TARBALL)
255	rsync -zaP $(OUTDIR)/ $(REMOTE)/
256
257clean:
258	rm -rf $(OUTDIR)