Check-in by ben on 2025-11-12 03:37:15

Initial commit for coprolit version 1

 INSERTED    DELETED
      982          0 coprolit.awk
       18          0 geomyidae.sh
       23          0 map2gph.awk
       31          0 readme.txt
     1054          0 TOTAL over 4 changed files

ADDED   coprolit.awk
Index: coprolit.awk
==================================================================
--- /dev/null
+++ coprolit.awk
@@ -0,0 +1,982 @@
+#!/usr/bin/awk -f
+# coprolit.awk version 1 by Ben Collver
+#
+# Static page generator for gopher and fossil SCM.
+# Usage: awk -f coprolit.awk
+# Requirements: fossil webdump
+
+function exists(filename,     result, retval) {
+    result = getline < filename
+    close(filename)
+    if (result == -1) {
+        retval = 0
+    } else {
+        retval = 1
+    }
+    return retval
+}
+
+function fossil_configuration(arr,    cmd, fsout, k, v) {
+    fsout = gettemp()
+    cmd = sprintf("fossil configuration export project %s -R %s",
+        fsout, _repo)
+    system(cmd)
+    while ((getline <fsout) > 0) {
+        if (/^#/ || /^config/) {
+            # ignore comments & config lines
+        } else {
+            k = $2
+            v = $4
+            sub(/^'/, "", k)
+            sub(/'$/, "", k)
+            sub(/^'/, "", v)
+            sub(/'$/, "", v)
+            arr[k] = v
+        }
+   }
+   close(fsout)
+   unlink(fsout)
+   return
+}
+
+function fossil_remote(     cmd, retval) {
+    # get remote repository URL
+    retval = "unknown"
+    cmd = sprintf("fossil remote -R %s 2>&1", _repo)
+    while ((cmd | getline) > 0) {
+        # remove user name from the remote repository URL
+        sub(/\/[^/]*@/, "/")
+        retval = $0
+    }
+    close(cmd)
+    return retval
+}
+
+function generate_branches(     branches, cmd, commits, fsout, i, m,
+    out, refs, sel)
+{
+    mkdir(_work "/branches")
+    out = _work "/branches/gophermap"
+    unlink(out)
+    fsout = gettemp()
+    cmd = sprintf("fossil branch ls -R %s >%s 2>&1", _repo, fsout)
+    system(cmd)
+    refs["count"] = 1
+    refs[1] = "fossil branch ls"
+    info(out, sprintf("# %s / Branches", _conf["project-name"]))
+    menu(out, "Branches")
+    m = 0
+    while ((getline < fsout) > 0) {
+         m++
+         branches[m] = $1
+         sel = _root "/timeline/" $1
+         item(out, 1, $1, sel, _server, _port)
+    }
+    close(fsout)
+    unlink(fsout)
+
+    reference(out, refs)
+    close(out)
+
+    for (i = 1; i <= m; i++) {
+        generate_timeline(branches[i])
+    }
+    return
+}
+
+function generate_commit(commit,     branch, cmd, comment, file,
+    fsout, has_downloads, hash, is_checkin, m, out, parents, refs,
+    slug, type)
+{
+    out = sprintf("%s/info/%s/gophermap", _work, commit)
+    if (exists(out)) {
+        # This has already been generated, don't do again
+        return
+    }
+
+    mkdir(_work "/info/" commit)
+    mkdir(_work "/patch")
+
+    fsout = gettemp()
+    cmd = sprintf("fossil timeline %s -n 1 --full -R %s >%s 2>&1",
+        commit, _repo, fsout)
+    system(cmd)
+    m = 1
+    refs[m] = sprintf("fossil timeline %s -n 1 --full", commit)
+    info(out, sprintf("# %s / Check-in [%s]",
+        _conf["project-name"], commit))
+    menu(out, "Commit")
+    while ((getline < fsout) > 0) {
+        if ($1 == "Commit:") {
+            hash = $2
+        } else if ($1 == "Comment:") {
+            comment = $0
+            sub(/^Comment:  */, "", comment)
+        } else if ($1 == "Branch") {
+            branch = $2
+        }
+    }
+    close(fsout)
+    unlink(fsout)
+
+    cmd = sprintf("fossil whatis %s -R %s >%s 2>&1", commit, _repo, fsout)
+    system(cmd)
+    m++
+    refs[m] = sprintf("fossil whatis %s", commit)
+
+    type = "unknown"
+    while ((getline < fsout) > 0) {
+        if (/^type:/) {
+            sub(/^type:  */, "")
+            type = $0
+        }
+    }
+    close(fsout)
+    unlink(fsout)
+
+    if (type ~ /^Check-in/) {
+        is_checkin = 1
+    } else {
+        is_checkin = 0
+    }
+
+    has_downloads = 0
+    parents = 0
+
+    if (is_checkin) {
+        if (_download_count < _download_max || _download_max == 0) {
+            has_downloads = 1
+            _download_count++
+
+            # export tarball
+            m++
+            refs[m] = generate_tarball(commit)
+
+            # export zip
+            m++
+            refs[m] = generate_zip(commit)
+        }
+
+        cmd = sprintf("fossil timeline parents %s -n 0 --oneline -R %s >%s",
+            commit, _repo, fsout)
+        system(cmd)
+        while ((getline <fsout) > 0) {
+            if (/^... end of timeline/) {
+                sub(/^\(/, "", $5)
+                sub(/\)$/, "", $5)
+                parents = $5
+            }
+        }
+        close(fsout)
+        unlink(fsout)
+
+        if (parents > 1) {
+            # export patch
+            file = sprintf("%s/patch/%s.txt", _work, commit)
+            printf "%s\n\n", type >file
+            if (length(comment) < 66) {
+                println(file, comment)
+            } else {
+                print_wrap(file, comment, 65)
+            }
+            printf "\n" >>file
+            close(file)
+
+            cmd = sprintf("fossil diff -v --checkin %s --numstat -R %s >>%s",
+                commit, _repo, file)
+            system(cmd)
+
+            printf "\n" >>file
+            close(file)
+
+            cmd = sprintf("fossil diff -v --checkin %s -R %s >>%s",
+                commit, _repo, file)
+            system(cmd)
+
+            m++
+            refs[m] = sprintf("fossil diff -v --checkin %s --numstat",
+                commit)
+            m++
+            refs[m] = sprintf("fossil diff -v --checkin %s", commit)
+        }
+    }
+
+    info(out, "# Overview")
+    info(out, "")
+    if (length(comment) < 66) {
+        info(out, sprintf("Comment:     %s", comment))
+    } else {
+        info(out, "Comment:")
+        info_wrap(out, comment, 65)
+    }
+    if (is_checkin && has_downloads) {
+        info(out, "")
+        info(out, "Downloads:")
+        slug = sprintf("%s/tarball/%s/%s-%s.tar.gz",
+            _root, commit, _conf["short-project-name"], commit)
+        item(out, "9", "Tarball", slug, _server, _port)
+        slug = sprintf("%s/zip/%s/%s-%s.zip",
+            _root, commit, _conf["short-project-name"], commit)
+        item(out, "9", "Zip archive", slug, _server, _port)
+    }
+
+    info(out, "")
+    info(out, "SHA3-256:")
+    info(out, "  " hash)
+    info(out, sprintf("Type:        %s", type))
+
+    if (is_checkin && parents > 1) {
+        info(out, "")
+        info(out, "# Changes")
+        info(out, "")
+        file = sprintf("%s/patch/%s.txt", _root, commit)
+        item(out, 0, "Patch", file, _server, _port)
+    }
+
+    refs["count"] = m
+    reference(out, refs)
+    close(out)
+    return
+}
+
+function generate_files(     cmd, file) {
+    # export tip tarball & zip
+    generate_tarball("tip")
+    generate_zip("tip")
+
+    # extract tip tarball
+    file = sprintf("%s/tarball/tip/%s-tip.tar.gz",
+        _work, _conf["short-project-name"])
+    cmd = sprintf("tar zxf %s -C %s --transform 's,%s,files,'",
+        file, _work, _conf["short-project-name"])
+    system(cmd)
+    return
+}
+
+function generate_home(     cmd, fsout, out) {
+    out = _work "/gophermap"
+    unlink(out)
+    fsout = gettemp()
+    cmd = sprintf("fossil wiki export -h %s -R %s >%s 2>&1",
+        _conf["project-name"], _repo, fsout)
+    system(cmd)
+    info(out, "# Home - " _conf["project-name"])
+    menu(out, "Home")
+    cmd = sprintf("webdump -ilr -w 60 <%s", fsout)
+    while ((cmd | getline) > 0) {
+        info(out, $0)
+    }
+    close(fsout)
+    unlink(fsout)
+    close(out)
+    return
+}
+
+function generate_tarball(commit,     cmd, file, name, retval) {
+    mkdir(_work "/tarball/" commit)
+
+    name = _conf["short-project-name"]
+    file = sprintf("%s/tarball/%s/%s-%s.tar.gz",
+        _work, commit, name, commit)
+    cmd = sprintf("fossil tarball %s %s --name %s -R %s",
+        commit, file, name, _repo)
+    system(cmd)
+    retval = sprintf("fossil tarball %s %s-%s.tar.gz --name %s",
+        commit, name, commit, name)
+    return retval
+}
+
+function generate_tags(    tags, cmd, commits, fsout, i, m, out,
+    query, refs, sel)
+{
+    mkdir(_work "/tags")
+    out = _work "/tags/gophermap"
+    unlink(out)
+    fsout = gettemp()
+
+    query = "SELECT "                                         \
+        "    substr(tag.tagname, 5) AS tagname, "             \
+        "    datetime(MAX(tagxref.mtime)) AS mtime "          \
+        "FROM repository.tag "                                \
+        "JOIN repository.tagxref ON tagxref.tagid=tag.tagid " \
+        "WHERE "                                              \
+        "    tag.tagname LIKE 'sym-%%' AND "                  \
+        "    tagxref.tagtype = 1 "                            \
+        "GROUP BY tag.tagid, tag.tagname "                    \
+        "ORDER BY MAX(tagxref.mtime) DESC"
+
+    # cmd = sprintf("fossil tag ls -R %s >%s 2>&1", _repo, fsout)
+    cmd = sprintf("fossil sql --readonly -R %s >%s 2>&1", _repo, fsout)
+    printf ".mode tabs\n%s\n", query | cmd
+    close(cmd)
+
+    refs["count"] = 3
+    refs[1] = "fossil tag ls"
+    refs[1] = "fossil sql --readonly"
+    refs[3] = query
+
+    info(out, sprintf("# %s / Tags", _conf["project-name"]))
+    menu(out, "Tags")
+    m = 0
+    while ((getline < fsout) > 0) {
+         m++
+         tags[m] = $1
+         sel = _root "/tags/" $1
+         item(out, 1, $1 " -- " $2, sel, _server, _port)
+    }
+    close(fsout)
+    unlink(fsout)
+
+    reference(out, refs)
+    close(out)
+
+    for (i = 1; i <= m; i++) {
+        generate_tag_timeline(tags[i])
+    }
+    return
+}
+
+function generate_tag_timeline(tag,     author, authors, cmd,
+     comment_abbrev, commit, commits, date, dates, fsout, i, label,
+     line, lines, m, out, refs, sel, time, total_commits)
+{
+    out = sprintf("%s/tags/%s/gophermap", _work, tag)
+    if (exists(out)) {
+        # This has already been generated, don't do again.
+        return
+    }
+
+    _download_count = 0
+    _download_max = _download
+
+    mkdir(_work "/tags/" tag)
+    fsout = gettemp()
+    cmd = sprintf("fossil tag find -n 0 -t ci %s -R %s >%s 2>&1",
+            tag, _repo, fsout)
+    system(cmd)
+    refs["count"] = 1
+    refs[1] = sprintf("fossil tag find -n 0 -t ci %s", tag)
+
+    total_commits = 0
+    line = ""
+    while ((getline < fsout) > 0) {
+        if ($1 == "===") {
+            date = $2
+        } else if (/^[0-9]/) {
+            # check whether line buffer is present from prior iterations
+            if (length(line) > 0) {
+                if (match(line, /user: [^ ]*/)) {
+                    author = substr(line, RSTART + 7, RLENGTH - 6)
+                } else {
+                    author = "unknown"
+                }
+                sub(/ \(user:.*\)/, "", line)
+                if (m < _timeline) {
+                    m++
+                    authors[m] = author
+                    commits[m] = commit
+                    lines[m] = line
+                    dates[m] = date " " time
+                }
+            }
+            time = $1
+            commit = $2
+            sub(/^\[/, "", commit)
+            sub(/\]$/, "", commit)
+            line = $0
+            sub(/^[^ ]* [^ ]* /, "", line)
+        } else if (/^ /) {
+            sub(/^  */, "")
+            line = line $0
+        } else if (/^... end of timeline /) {
+            sub(/^\(/, "", $5)
+            sub(/\)$/, "", $5)
+            total_commits = $5
+        }
+    }
+    if (length(line) > 0 && m < _timeline) {
+        if (match(line, /user: [^ ]*/)) {
+            author = substr(line, RSTART + 6, RLENGTH - 6)
+        } else {
+            author = "unknown"
+        }
+        sub(/ \(user:.*\)/, "", line)
+        m++
+        authors[m] = author
+        commits[m] = commit
+        lines[m] = line
+        dates[m] = date " " time
+    }
+    close(fsout)
+    unlink(fsout)
+
+    info(out, sprintf("# %s / Timeline / Tag %s",
+        _conf["project-name"], tag))
+    menu(out, "Timeline")
+    for (i = 1; i <= m; i++) {
+        if (length(comment) < 42) {
+            comment_abbrev = comment
+        } else {
+            comment_abbrev = substr(comment, 1, 37) "..."
+        }
+        label = sprintf("%-17s %-41s %s",
+            substr(dates[i], 1, 16), comment_abbrev, authors[i])
+        sel = _root "/info/" commits[i]
+        item(out, 1, label, sel, _server, _port)
+    }
+
+    if (m < total_commits) {
+        info(out, "        " total_commits - m \
+            " more commits remaining, fetch the repository")
+    }
+
+    reference(out, refs)
+    close(out)
+
+    for (i = 1; i <= m; i++) {
+        generate_commit(commits[i])
+    }
+    return
+}
+
+function generate_ticket(ticket, ctime, foundin, mtime, priority,
+    resolution, severity, status, title, uuid, type,    cmd, out)
+{
+    out = sprintf("%s/tickets/%s.txt", _work, ticket)
+    unlink(out)
+    printf "# %s\n\n", _conf["project-name"] >out
+    printf "## View Ticket\n\n" >>out
+    printf "Ticket Hash: %s\n", uuid >>out
+    if (length(title) < 66) {
+        printf "Title:       %s\n", title >>out
+    } else {
+        printf "Title:\n" >>out
+        print_wrap(out, title, 65)
+    }
+    printf "Status:      %s\n", status >>out
+    printf "Type:        %s\n", type >>out
+    printf "Severity:    %s\n", severity >>out
+    printf "Priority:    %s\n", priority >>out
+    printf "Resolution:  %s\n", resolution >>out
+    printf "Modified:    %s\n", mtime >>out
+    printf "Created:     %s\n", ctime >>out
+    printf "Found In:    %s\n", foundin >>out
+    cmd = sprintf("fossil ticket history %s -R %s 2>&1", ticket, _repo)
+    while ((cmd | getline) > 0) {
+        if (/^Ticket Change/) {
+            printf "\n" >>out
+        }
+        print $0 >>out
+    }
+    close(cmd)
+    printf "\nReference:\n\n" >>out
+    printf "fossil ticket history %s\n", ticket >>out
+    close(out)
+    return
+}
+
+function generate_tickets(    ctime, cmd, count, field, fnr, fsout,
+    i, label, mtime, oldfs, out, query, refs, sel, tags, ticket)
+{
+    mkdir(_work "/tickets")
+    out = _work "/tickets/gophermap"
+    unlink(out)
+    fsout = gettemp()
+
+    query = "SELECT "                            \
+            "    tkt_id, "                       \
+            "    tkt_uuid, "                     \
+            "    datetime(tkt_mtime) AS mtime, " \
+            "    datetime(tkt_ctime) AS ctime, " \
+            "    type, "                         \
+            "    status, "                       \
+            "    subsystem, "                    \
+            "    priority, "                     \
+            "    severity, "                     \
+            "    foundin, "                      \
+            "    resolution, "                   \
+            "    title, "                        \
+            "    comment "                       \
+            "FROM repository.ticket"
+
+    # cmd = sprintf("fossil ticket show 0 -R %s >%s 2>&1", _repo, fsout)
+    cmd = sprintf("fossil sql --readonly -R %s >%s 2>&1", _repo, fsout)
+    printf ".headers on\n.mode tabs\n%s\n", query | cmd
+    close(cmd)
+
+    refs["count"] = 3
+    refs[1] = "fossil ticket show 0"
+    refs[2] = "fossil sql --readonly"
+    refs[3] = query
+
+    info(out, sprintf("# %s / Tickets", _conf["project-name"]))
+    menu(out, "Tickets")
+    oldfs = FS
+    FS = "\t"
+    while ((getline < fsout) > 0) {
+         fnr++
+         if (fnr == 1) {
+             count = NF
+             for (i = 1; i <= count; i++) {
+                 field[$i] = i
+             }
+         } else {
+             ticket = substr($field["tkt_uuid"], 1, 10)
+
+             label = "Ticket: " ticket
+             sel = sprintf("%s/tickets/%s.txt", _root, ticket)
+             item(out, 0, label, sel, _server, _port)
+             info(out, "Mtime:  " $field["mtime"])
+             info(out, "Type:   " $field["type"])
+             info(out, "Status: " $field["status"])
+             if (length($field["title"]) < 61) {
+                 info(out, "Title:  " $field["title"])
+             } else {
+                 info(out, "Title:")
+                 info_wrap(out, $field["title"], 65)
+             }
+             info(out, "")
+             generate_ticket(ticket,
+                 $field["ctime"],
+                 $field["foundin"],
+                 $field["mtime"],
+                 $field["priority"],
+                 $field["resolution"],
+                 $field["severity"],
+                 $field["status"],
+                 $field["title"],
+                 $field["tkt_uuid"],
+                 $field["type"])
+         }
+    }
+    FS = oldfs
+    close(fsout)
+    unlink(fsout)
+
+    reference(out, refs)
+    close(out)
+
+    return
+}
+
+function generate_timeline(branch,     author, branch_opt, cmd,
+    comment, comment_abbrev, commit, commits, date, download_count,
+    download_max, fsout, i, label, m, out, refs, sel, total_commits)
+{
+    _download_count = 0
+    if (length(branch) > 0) {
+        _download_max = -1
+        branch_opt = "-b " branch " "
+        out = sprintf("%s/timeline/%s/gophermap", _work, branch)
+    } else {
+        _download_max = _download
+        branch_opt = ""
+        out = sprintf("%s/timeline/gophermap", _work)
+    }
+    if (exists(out)) {
+        # This has already been generated, don't do again.
+        return
+    }
+
+    if (length(branch) > 0) {
+        mkdir(_work "/timeline/" branch)
+    } else {
+        mkdir(_work "/timeline")
+    }
+    fsout = gettemp()
+
+    cmd = sprintf("fossil timeline -n 0 %s-t ci --oneline -R %s >%s 2>&1",
+        branch_opt, _repo, fsout)
+    system(cmd)
+    total_commits = 0
+    while ((getline <fsout) > 0) {
+        if (/^... end of timeline /) {
+            sub(/^\(/, "", $5)
+            sub(/\)$/, "", $5)
+            total_commits = $5
+        }
+    }
+    close(fsout)
+    unlink(fsout)
+
+    refs["count"] = 1
+    refs[1] = sprintf("fossil timeline %s%s-t ci --medium",
+            branch_opt, _timeline_opt)
+
+    cmd = sprintf("fossil timeline %s%s-t ci --medium -R %s >%s 2>&1",
+            branch_opt, _timeline_opt, _repo, fsout)
+    system(cmd)
+    if (length(branch) > 0) {
+        info(out, sprintf("# %s / Timeline / Branch %s",
+            _conf["project-name"], branch))
+    } else {
+        info(out, sprintf("# %s / Timeline", _conf["project-name"]))
+    }
+    menu(out, "Timeline")
+    author = "unknown"
+    commit = ""
+    date = "unknown"
+    comment = ""
+    while ((getline < fsout) > 0) {
+        if ($1 == "Commit:") {
+            commit = $2
+            m++
+            commits[m] = commit
+        } else if ($1 == "Date:") {
+            sub(/^Date:  */, "")
+            date = $0
+        } else if ($1 == "Author:") {
+            sub(/^Author:  */, "")
+            author = $0
+        } else if ($1 == "Comment:") {
+            sub(/^Comment:  */, "")
+            comment = $0
+        } else if (length($0) == 0) {
+            if (length(comment) < 42) {
+                comment_abbrev = comment
+            } else {
+                comment_abbrev = substr(comment, 1, 37) "..."
+            }
+            label = sprintf("%-17s %-41s %s",
+                substr(date, 1, 16), comment_abbrev, author)
+            sel = _root "/info/" commit
+            item(out, 1, label, sel, _server, _port)
+        }
+    }
+    close(fsout)
+    unlink(fsout)
+
+    if (m < total_commits) {
+        info(out, "        " total_commits - m \
+            " more commits remaining, fetch the repository")
+    }
+
+    reference(out, refs)
+    close(out)
+
+    for (i = 1; i <= m; i++) {
+        generate_commit(commits[i])
+    }
+    return
+}
+
+function generate_wiki(    cmd, fsout, i, m, out, pages, refs, sel) {
+    mkdir(_work "/wiki")
+    out = _work "/wiki/gophermap"
+    unlink(out)
+    fsout = gettemp()
+    cmd = sprintf("fossil wiki ls -R %s >%s 2>&1", _repo, fsout)
+    system(cmd)
+    refs["count"] = 1
+    refs[1] = "fossil wiki ls"
+    info(out, sprintf("# %s / Wiki", _conf["project-name"]))
+    menu(out, "Wiki")
+    m = 0
+    while ((getline < fsout) > 0) {
+         m++
+         pages[m] = $0
+         sel = _root "/wiki/" safe_filename($0) ".txt"
+         item(out, 0, $0, sel, _server, _port)
+    }
+    close(fsout)
+    unlink(fsout)
+
+    reference(out, refs)
+    close(out)
+
+    for (i = 1; i <= m; i++) {
+        generate_wiki_page(pages[i])
+    }
+
+    return
+}
+
+function generate_wiki_page(page,     cmd, fsout, out) {
+    out = sprintf("%s/wiki/%s.txt", _work, safe_filename(page))
+    unlink(out)
+    fsout = gettemp()
+    cmd = sprintf("fossil wiki export -h \"%s\" -R %s >%s 2>&1",
+        page, _repo, fsout)
+    system(cmd)
+    printf "# %s\n\n", page >out
+    cmd = sprintf("webdump -ilr -w 60 <%s", fsout)
+    while ((cmd | getline) > 0) {
+        print >>out
+    }
+    printf "\nReference:\n\n" >>out
+    printf "fossil wiki export -h \"%s\"\n", page >>out
+    close(out)
+    close(fsout)
+    unlink(fsout)
+    return
+}
+
+function generate_zip(commit,     cmd, file, name, retval) {
+    mkdir(_work "/zip/" commit)
+
+    name = _conf["short-project-name"]
+    file = sprintf("%s/zip/%s/%s-%s.zip", _work, commit, name, commit)
+    cmd = sprintf("fossil zip %s %s --name %s -R %s",
+        commit, file, name, _repo)
+    system(cmd)
+    retval = sprintf("fossil zip %s %s-%s.zip --name %s",
+        commit, name, commit, name)
+    return retval
+}
+
+function gettemp(     cmd, result, retval) {
+    cmd = "mktemp"
+    while ((cmd | getline) > 0) {
+        retval = $0
+    }
+    result = close(cmd)
+    if (result != 0) {
+        print "Error: mktemp failed exit status: " result
+        exit
+    }
+    if (length(retval) == 0) {
+        print "Error: mktemp failed, no tmpfile"
+        exit
+    }
+    return retval
+}
+
+function info(out, str) {
+    if (length(out) == 0) {
+        printf "i%s\tErr\t%s\t%s\r\n", str, _server, _port
+    } else {
+        printf "i%s\tErr\t%s\t%s\r\n", str, _server, _port >>out
+    }
+    return
+}
+
+# info_wrap() will break long lines into line continuations
+
+function info_wrap(out, str, len) {
+    line = 1
+    buf = str
+    while (length(buf) > len) {
+        chunk = substr(buf, 1, len)
+        if (match(chunk, / [^ ]*$/)) {
+            before = substr(buf, 1, RSTART-1)
+            after = substr(buf, RSTART+1)
+            info(out, "  " before)
+            buf = after
+        } else if (match(chunk, /-[^-]*$/)) {
+            before = substr(buf, 1, RSTART)
+            after = substr(buf, RSTART+1)
+            info(out, "  " before)
+            buf = after
+        } else {
+            break
+        }
+        line++
+    }
+    info(out, "  " buf)
+    return
+}
+
+function item(out, type, label, sel, host, port,     line) {
+    line = item_str(type, label, sel, host, port)
+    if (length(out) == 0) {
+        printf "%s\r\n", line
+    } else {
+        printf "%s\r\n", line >>out
+    }
+    return
+}
+
+function item_str(type, label, sel, host, port) {
+    retval = sprintf("%s%s\t%s\t%s\t%s", type, label, sel, host, port)
+    return retval
+}
+
+function main(     cmd, format) {
+    if (ARGC < 3) {
+        usage()
+        exit 0
+    }
+    _repo = ARGV[1]
+    _root = ARGV[2]
+
+    _download = 5
+    _port = 70
+    _server = "localhost"
+    _timeline = 100
+    _work = "output"
+    format = "gopher"
+
+    for (i = 3; i < ARGC; i++) {
+        if (ARGV[i] == "--dir") {
+            _work = ARGV[i + 1]
+            i++
+        } else if (ARGV[i] == "--download") {
+            _download = ARGV[i + 1]
+            i++
+        } else if (ARGV[i] == "--format") {
+            format = ARGV[i + 1]
+            i++
+            if (format != "geomyidae" && format != "gopher") {
+                print "Error: Unknown format: " format
+                exit 1
+            }
+        } else if (ARGV[i] == "--port") {
+            _port = ARGV[i + 1]
+            i++
+        } else if (ARGV[i] == "--server") {
+            _server = ARGV[i + 1]
+            i++
+        } else if (ARGV[i] == "--timeline") {
+            _timeline = ARGV[i + 1]
+            i++
+        } else {
+            print "Error: Unrecognized option: " ARGV[i]
+            exit 1
+        }
+    }
+    if (_timeline > 0) {
+        _timeline_opt = "-n " _timeline " "
+    } else {
+        _timeline_opt = ""
+    }
+    _download_count = 0
+
+    fossil_configuration(_conf)
+    if ($_conf["project-name"] == "unknown") {
+        print "Error: Could not find fossil project name"
+        exit 1
+    }
+    _remote = fossil_remote()
+
+    mkdir(_work)
+    generate_home()
+    generate_timeline("")
+    generate_files()
+    generate_branches()
+    generate_tags()
+    generate_tickets()
+    generate_wiki()
+
+    if (format == "geomyidae") {
+        cmd = sprintf("./geomyidae.sh \"%s\"", _work)
+        system(cmd)
+    }
+    return
+}
+
+function menu(out, cur,     i, label, opts, path) {
+    opts[1] = "Timeline"
+    opts[2] = "Files"
+    opts[3] = "Branches"
+    opts[4] = "Tags"
+    opts[5] = "Tickets"
+    opts[6] = "Wiki"
+    path[1] = "timeline"
+    path[2] = "files"
+    path[3] = "branches"
+    path[4] = "tags"
+    path[5] = "tickets"
+    path[6] = "wiki"
+    item(out, "h", "fossil clone " _remote, "URL:" _remote, _server, _port)
+    for (i = 1; i < 7; i++) {
+        if (cur == opts[i]) {
+            label = sprintf("(%s)", opts[i])
+        } else {
+            label = sprintf(" %s ", opts[i])
+        }
+        item(out, 1, label, _root "/" path[i], _server, _port)
+    }
+    info(out, "---")
+    return
+}
+
+function mkdir(dir) {
+    system("mkdir -p " dir)
+    return
+}
+
+function println(out, str) {
+    if (length(out) == 0) {
+        print str
+    } else {
+        print str >>out
+    }
+    return
+}
+
+# print_wrap() will break long lines into line continuations
+
+function print_wrap(out, str, len) {
+    line = 1
+    buf = str
+    while (length(buf) > len) {
+        chunk = substr(buf, 1, len)
+        if (match(chunk, / [^ ]*$/)) {
+            before = substr(buf, 1, RSTART-1)
+            after = substr(buf, RSTART+1)
+            println(out, "  " before)
+            buf = after
+        } else if (match(chunk, /-[^-]*$/)) {
+            before = substr(buf, 1, RSTART)
+            after = substr(buf, RSTART+1)
+            println(out, "  " before)
+            buf = after
+        } else {
+            break
+        }
+        line++
+    }
+    println(out, "  " buf)
+    return
+}
+
+function reference(out, refs,     i) {
+    info(out, "")
+    info(out, "# Reference")
+    info(out, "")
+    for (i = 1; i <= refs["count"]; i++) {
+        if (length(refs[i]) > 65) {
+            info_wrap(out, refs[i], 65)
+        } else {
+            info(out, refs[i])
+        }
+    }
+    return
+}
+
+function safe_filename(name) {
+    gsub(/:/, "_", name)
+    gsub(/\\/, "_", name)
+    gsub(/\//, "_", name)
+    gsub(/ /, "_", name)
+    return name
+}
+
+function unlink(filename,     cmd) {
+    cmd = sprintf("rm %s 2>/dev/null", filename)
+    system(cmd)
+    return
+}
+
+function usage() {
+    print "Usage: coprolit.awk REPO ROOT options"
+    print ""
+    print "REPO = file.fossil"
+    print "ROOT = Gopher root selector"
+    print ""
+    print "Options:"
+    print "--dir (default: output)"
+    print "--download (default: 5)"
+    print "  limit number of tarballs per section"
+    print "--format (geomyidae | gopher)"
+    print "--port portnum"
+    print "--server hostname"
+    print "--timeline (default: 100)"
+    print "  limit number of items in timeline"
+    print ""
+    return
+}
+
+BEGIN {
+    main()
+}

ADDED   geomyidae.sh
Index: geomyidae.sh
==================================================================
--- /dev/null
+++ geomyidae.sh
@@ -0,0 +1,18 @@
+#!/bin/sh
+dir="$1"
+if [ -z "$dir" ]
+then
+    echo "Usage: geomyidae.sh DIR"
+    echo ""
+    echo "Converts all gophermap files in DIR to index.gph for geomyidae."
+    echo ""
+    exit 0
+fi
+
+find "$dir" -type f -name gophermap | while read f
+do
+    d=$(dirname $f)
+    awk -f map2gph "$f" >"$d/index.gph"
+done
+
+find "$dir" -type f -name gophermap -print0 | xargs -0 rm

ADDED   map2gph.awk
Index: map2gph.awk
==================================================================
--- /dev/null
+++ map2gph.awk
@@ -0,0 +1,23 @@
+#!/usr/bin/awk -f
+# map2gph.awk version 1 by Ben Collver
+# Convert gophernicus to geomyidae gophermap
+# usage: awk -f map2gph.awk gophermap >index.gph
+
+BEGIN {
+    FS = "\t"
+}
+
+/^i/ {
+    sub(/\r$/, "")
+    sub(/^i/, "", $1)
+    print $1
+    next
+}
+
+{
+    sub(/\r$/, "")
+    type = substr($1, 1, 1)
+    userstr = substr($1, 2)
+    gsub(/\|/, "\\|", userstr)
+    printf "[%s|%s|%s|%s|%s]\n", type, userstr, $2, $3, $4
+}

ADDED   readme.txt
Index: readme.txt
==================================================================
--- /dev/null
+++ readme.txt
@@ -0,0 +1,31 @@
+Coprolit
+========
+
+Static page generator for gopher and fossil SCM.
+
+Requires:
+
+* awk & unix
+* fossil SCM <https://www.fossil-scm.org/>
+* webdump    <gopher://codemadness.org/1/phlog/webdump>
+
+Usage:
+
+First clone the fossil repository locally, then run coprolit.awk.
+
+    $ awk -f coprolit.awk
+
+    Usage: coprolit.awk REPO ROOT options
+
+    REPO = file.fossil
+    ROOT = Gopher root selector
+
+    Options:
+    --dir (default: output)
+    --download (default: 5)
+      limit number of tarballs per section
+    --format (geomyidae | gopher)
+    --port portnum
+    --server hostname
+    --timeline (default: 100)
+      limit number of items in timeline