summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorglenda <glenda@9front.local>2020-11-15 15:13:27 +0000
committerglenda <glenda@9front.local>2020-11-15 15:13:27 +0000
commit39318169e0b50551db511851829f9337c5fa6313 (patch)
tree65a0ef5c1da9677532fa8105293d017919473057
Import site to git
-rw-r--r--README79
-rw-r--r--apps/blagh/app.rc142
-rw-r--r--apps/blagh/atom.tpl58
-rwxr-xr-xapps/blagh/convert.rc20
-rw-r--r--apps/blagh/jsonfeed.tpl35
-rw-r--r--apps/blagh/new_post.tpl11
-rw-r--r--apps/blagh/rss20.tpl43
-rwxr-xr-xapps/bridge/app.rc103
-rwxr-xr-xapps/bridge/comments_list.tpl13
-rwxr-xr-xapps/bridge/foot.tpl37
-rwxr-xr-xapps/dirdir/app.rc40
-rwxr-xr-xapps/dirdir/edit.tpl25
-rwxr-xr-xapps/dirdir/sidebar_controls.tpl3
-rw-r--r--apps/duckduckgo/HOWTO20
-rwxr-xr-xapps/duckduckgo/app.rc30
-rw-r--r--apps/duckduckgo/footer.inc.sample3
-rwxr-xr-xapps/hello/app.rc10
-rwxr-xr-xapps/paste/app.rc45
-rwxr-xr-xapps/wman/app.rc89
-rwxr-xr-xapps/wman/man_page.tpl3
-rwxr-xr-xapps/wman/page_list.tpl11
-rwxr-xr-xapps/wman/search.tpl20
-rwxr-xr-xapps/wman/section_list.tpl11
-rwxr-xr-xbin/aux/addwuser.rc33
-rwxr-xr-xbin/aux/bpst.rc64
-rwxr-xr-xbin/aux/gensitemaptxt.rc14
-rwxr-xr-xbin/aux/runtsts.rc16
-rwxr-xr-xbin/cgilib.rc236
-rwxr-xr-xbin/contrib/fix-rc-scripts27
-rwxr-xr-xbin/contrib/hgweb.config12
-rwxr-xr-xbin/contrib/hgwebdir.cgi47
-rwxr-xr-xbin/contrib/markdown.pl1447
-rwxr-xr-xbin/contrib/md2html.awk427
-rwxr-xr-xbin/contrib/rc-httpd/handlers/authorize6
-rwxr-xr-xbin/contrib/rc-httpd/handlers/cgi46
-rwxr-xr-xbin/contrib/rc-httpd/handlers/dir-index111
-rwxr-xr-xbin/contrib/rc-httpd/handlers/error43
-rwxr-xr-xbin/contrib/rc-httpd/handlers/redirect30
-rwxr-xr-xbin/contrib/rc-httpd/handlers/serve-static43
-rwxr-xr-xbin/contrib/rc-httpd/handlers/static-or-cgi14
-rwxr-xr-xbin/contrib/rc-httpd/handlers/static-or-index5
-rwxr-xr-xbin/contrib/rc-httpd/lib/urldecode.awk39
-rwxr-xr-xbin/contrib/rc-httpd/rc-httpd102
-rwxr-xr-xbin/contrib/rc-httpd/select-handler20
-rwxr-xr-xbin/contrib/tcp807
-rwxr-xr-xbin/contrib/urldecode.awk39
-rwxr-xr-xbin/contrib/urlencode.awk126
-rwxr-xr-xbin/contrib/webserver.rc30
-rwxr-xr-xbin/corehandlers.rc152
-rwxr-xr-xbin/fltr_cache.rc37
-rwxr-xr-xbin/template.awk55
-rwxr-xr-xbin/werc.rc138
-rwxr-xr-xbin/werc_errlog_wrap.rc5
-rwxr-xr-xbin/wercconf.rc19
-rwxr-xr-xbin/werclib.rc393
-rwxr-xr-xetc/initrc39
-rw-r--r--etc/users/GROUP_AND_USER_ACCOUNTS1
-rw-r--r--lib/404.tpl3
-rw-r--r--lib/default_master.tpl26
-rw-r--r--lib/footer.inc7
-rw-r--r--lib/headers.tpl29
-rw-r--r--lib/top_bar.inc15
-rwxr-xr-xpub/default_favicon.icobin0 -> 2150 bytes
-rwxr-xr-xpub/style/imgs/sgl.pngbin0 -> 14763 bytes
-rwxr-xr-xpub/style/sinorca-screen-alt.css292
-rw-r--r--pub/style/style.cat-v.css50
-rw-r--r--pub/style/style.css11
-rwxr-xr-xpub/style/style.werc140.css330
-rwxr-xr-xpub/style/style_old.css330
-rw-r--r--sites/pmikkelsen.com/_files/djv.tarbin0 -> 1822720 bytes
-rw-r--r--sites/pmikkelsen.com/_files/djvmono.tarbin0 -> 993280 bytes
-rw-r--r--sites/pmikkelsen.com/_werc/config3
-rw-r--r--sites/pmikkelsen.com/_werc/lib/footer.inc1
-rw-r--r--sites/pmikkelsen.com/_werc/lib/top_bar.inc10
-rw-r--r--sites/pmikkelsen.com/_werc/pub/style.css67
-rw-r--r--sites/pmikkelsen.com/contact.md3
-rw-r--r--sites/pmikkelsen.com/haskell/quine.md94
-rw-r--r--sites/pmikkelsen.com/images/2048.gifbin0 -> 4157248 bytes
-rw-r--r--sites/pmikkelsen.com/images/acme-in-action.pngbin0 -> 286658 bytes
-rw-r--r--sites/pmikkelsen.com/images/cdude.pngbin0 -> 20432 bytes
-rw-r--r--sites/pmikkelsen.com/images/cursed.pngbin0 -> 18433 bytes
-rw-r--r--sites/pmikkelsen.com/images/discordgif.gifbin0 -> 9196759 bytes
-rw-r--r--sites/pmikkelsen.com/images/djvfonts.pngbin0 -> 138021 bytes
-rw-r--r--sites/pmikkelsen.com/images/linuxreverse.gifbin0 -> 201797 bytes
-rw-r--r--sites/pmikkelsen.com/images/mordor.gifbin0 -> 1620703 bytes
-rw-r--r--sites/pmikkelsen.com/index.md28
-rw-r--r--sites/pmikkelsen.com/linux/eduroam.md15
-rw-r--r--sites/pmikkelsen.com/linux/wifi.md12
-rw-r--r--sites/pmikkelsen.com/me/how-i-started-using-acme.md117
-rw-r--r--sites/pmikkelsen.com/me/stuff-i-use.md46
-rw-r--r--sites/pmikkelsen.com/opinions/favourite-programming-languages.md1
-rw-r--r--sites/pmikkelsen.com/plan9/basic_9p_server.md58
-rw-r--r--sites/pmikkelsen.com/plan9/discord.md83
-rw-r--r--sites/pmikkelsen.com/plan9/dns.md41
-rw-r--r--sites/pmikkelsen.com/plan9/fonts.md29
-rw-r--r--sites/pmikkelsen.com/plan9/lets_encrypt.md59
-rw-r--r--sites/pmikkelsen.com/plan9/mounting-9p-over-drawterm.md58
-rw-r--r--sites/pmikkelsen.com/plan9/network_booting.md30
-rw-r--r--sites/pmikkelsen.com/plan9/trellofs.md347
-rw-r--r--sites/pmikkelsen.com/plan9/using_irc.md48
-rw-r--r--sites/pmikkelsen.com/web/the-setup-of-this-site.md3
-rw-r--r--tpl/_debug.tpl29
-rw-r--r--tpl/_users/login.tpl18
-rw-r--r--tpl/sitemap.tpl67
104 files changed, 7034 insertions, 0 deletions
diff --git a/README b/README
new file mode 100644
index 0000000..b4dc773
--- /dev/null
+++ b/README
@@ -0,0 +1,79 @@
+werc - a minimalist document management system
+----------------------------------------------
+
+Werc is a content management system and web (anti-)framework designed to be simple to
+use, simple to setup, simple to hack on, and not get in the way while allowing
+users easy customization.
+
+For more information see the official website: http://werc.cat-v.org/
+
+
+Installation
+------------
+
+Requirements:
+
+* An http server that can handle CGIs
+* Plan 9 from User Space: http://swtch.com/plan9port,
+ Or 9base-tip: http://tools.suckless.org/9base,
+ Or frontbase: http://openbsd.stanleylieber.com/frontbase
+
+Note: Werc by default expects the Plan 9 tools to be installed under
+/bin/, if you have installed them elsewhere you will need to edit the
+#! line in bin/werc.rc and customize the $plan9port variable in your
+etc/initrc.local.
+
+
+Instructions:
+
+Untar werc at your desired location, configure httpd to use
+/path-to-your-werc-installation/bin/werc.rc as a cgi-script, it is recommended
+that you make werc.rc handle all non-static files (this can be done by setting
+it up as your 404 handler) and setup your virtual hosts to handle static files
+by setting the document root for the domain to
+/path-to-werc-installation/sites/yourdomain.com/, and create a directory for
+your web site under sites/ where you can start adding content right away.
+
+If you will want to allow updates via the web interface (eg., for wiki or
+comments apps) make sure all files under sites/ are writable by the user your
+cgi will run as, usually www-data, for example by doing: chown -R :www-data
+sites/; chmod -R g+w sites/
+
+If your Plan 9 binaries are located somewhere else than the standard /bin/ you
+will need to edit the first line of bin/werc.rc (Note that p9p in particular is
+picky about where it is located, once you run ./INSTALL you should *not* move
+it to a different directory without running ./INSTALL again.)
+
+For general configuration options copy etc/initrc to etc/initrc.local and
+customize it as needed. Site (and directory) specific options can be set in a
+sites/example.com/_werc/config file inside the site's directory. To customize
+templates and included files you can store your own version of the files in
+lib/ under sites/example.com/_werc/lib.
+
+The source tree for the werc website is included under sites/werc.cat-v.org as
+an example, feel free to use it as a template for your own site.
+
+For more details see the documentation section of the website:
+http://werc.cat-v.org/docs/
+
+
+Contact
+-------
+
+For comments, suggestions, bug reports or patches join the werc mailing list
+at: http://werc.cat-v.org or the irc channel #cat-v in irc.freenode.org
+
+If you have a public website that uses werc I would love to hear about it and
+get feedback about you experience setting it up.
+
+Thanks
+------
+
+Garbeam, Kris Maglione, sqweek, soul9, mycroftiv, maht, yiyus, cinap_lenrek,
+khm and many others for their ideas, patches, testing and other contributions.
+
+
+License
+-------
+
+Werc is in the public domain.
diff --git a/apps/blagh/app.rc b/apps/blagh/app.rc
new file mode 100644
index 0000000..c63689d
--- /dev/null
+++ b/apps/blagh/app.rc
@@ -0,0 +1,142 @@
+fn conf_enable_blog {
+ blagh_uri=$conf_wd
+ blagh_dirs=$*
+ if(~ $#blagh_dirs 0)
+ blagh_dirs=( . )
+ conf_enable_app blagh
+
+ if(~ $"conf_blog_editors '')
+ conf_blog_editors=blog-editors
+
+ if(~ $"conf_max_posts_per_page '')
+ conf_max_posts_per_page=32
+}
+
+fn blagh_init {
+ if(~ $#blagh_dirs 0 && ~ $req_path */[bB]log/*) {
+ blagh_uri=`{echo $req_path | sed 's,(/[bB]log/).*,\1,'}
+ blagh_dirs=( . )
+ }
+
+ # Should not match sub-dirs!
+ if(! ~ $#blagh_dirs 0) {
+ # && test -d / `{echo '-a -d '^$blagh_root^$blagh_dirs}
+ blagh_url=$base_url^$blagh_uri
+ blagh_root=$sitedir^$blagh_uri
+ if(check_user $conf_blog_editors) {
+ editor_mode=on
+ if(~ $"post_arg_date '')
+ post_date=`{datei|sed 's,-,/,g'}
+ if not
+ post_date=$post_arg_date
+ ll_add handlers_bar_left echo '<a href="'$blagh_uri'new_post">Make a new post</a>'
+ }
+
+ if(~ $req_path $blagh_uri) {
+ handler_body_main=blagh_body
+ u=$blagh_uri'index'
+ extraHeaders=$"extraHeaders ^ \
+'<link rel="alternate" type="application/atom+xml" title="ATOM" href="'$"u'.atom" />
+<link rel="alternate" type="application/rss+xml" title="RSS" href="'$"u'.rss" />
+<link rel="alternate" type="application/json" title="JSON" href="'$"blagh_uri'feed.json" />'
+ }
+ if not if(~ $req_path $blagh_uri^index.atom)
+ blagh_setup_feed_handlers atom.tpl 'application/atom+xml'
+
+ if not if(~ $req_path $blagh_uri^index.rss)
+ blagh_setup_feed_handlers rss20.tpl 'text/xml; charset=utf-8'
+
+ if not if(~ $req_path $blagh_uri^feed.json)
+ blagh_setup_feed_handlers jsonfeed.tpl 'application/json; charset=utf-8'
+
+ if not if(~ $req_path $blagh_uri^new_post && ! ~ $#editor_mode 0) {
+ handler_body_main=( tpl_handler `{get_lib_file blagh/new_post.tpl apps/blagh/new_post.tpl} )
+ if(~ $REQUEST_METHOD POST) {
+ if(mkbpost $"post_arg_body $"post_date $"post_arg_title $post_arg_id)
+ post_redirect $blagh_uri
+ if not
+ notify_errors=$status
+ }
+ }
+
+ }
+}
+
+fn blagh_setup_feed_handlers {
+ handler_body_main=NOT_USED_by_blagh_feeds
+ res_tail=()
+ http_content_type=$2
+ headers=()
+ master_template=apps/blagh/$1 # Should we allow tempalte override?
+}
+
+fn blagh_body {
+ if (! ~ $"blogTitle '')
+ echo '<h1>'$"blogTitle'</h1>'
+
+ # Direct links to feeds are disabled because they are not very useful, add clutter and might waste pagerank.
+ # An user can add this on their own using handlers_body_head anyway.
+ #echo '<div style="text-align:right">(<a href="index.rss">RSS Feed</a>|<a href="index.atom">Atom Feed</a>)</div>'
+
+ # XXX Not sure why this fixes issues with blog setup, probably bug in fltr_cache!
+ for(p in `{get_post_list $blagh_root^$blagh_dirs}) {
+ l=`{echo -n $p|sed 's!'$sitedir^'/?(.*)([0-9][0-9][0-9][0-9]/[0-9][0-9]/[0-9][0-9])(/[^/]+/)!\2 /\1\2\3!'}
+ sed '1s!.*![&]('^$l(2)^') ('^$l(1)^')!' < $p/index.md
+ echo # Needed extra \n so markdown doesn't mess up the formatting, probably can be done in sed.
+ } | $formatter
+ # XXX BUG! Markdown [references] break because multiple markdown documents are merged. Should format each blog post independently.
+ # TODO: use fltr_cache directly, that can fix the previous bug plus provide a perf boost by caching title generation.
+}
+
+fn get_post_list {
+ # /./->/|/ done to sort -t| and order by date
+ # Note: $paths in blagh_dirs should not contain '/./' or '|'
+ ls -F $*^/./[0-9][0-9][0-9][0-9]/[0-9][0-9]/[0-9][0-9]/ >[2]/dev/null | sed -n '/'^$forbidden_uri_chars^'/d; s,/\./,/|/,; /\/$/p' | sort -r '-t|' +1 | sed -e 's,/+\|/+,/,' -e $conf_max_posts_per_page^'q'
+}
+
+fn mkbpost {
+ bptext=$1
+ bpdate=$2
+ bptitle=$3
+ bpid=$4
+ _status=()
+ if(~ $"bptext '')
+ _status=($_status 'You need to provide a post body.')
+ if(! ~ $"bpdate [0-9][0-9][0-9][0-9]/[0-9][0-9]/[0-9][0-9])
+ _status=($_status 'Invalid date: '''^$"bpdate^'''') # XXX Should make semantic check.
+
+ if(~ $#_status 0) {
+ umask 002 # Let group write
+ if(! ~ $"bpid '')
+ bpid=`{echo -n '-'^$bpid | sed 's/'$forbidden_uri_chars'+/_/g; 1q'}
+
+ ddir=$blagh_root^$bpdate^'/'
+ n=`{ls $ddir >[2]/dev/null |wc -l}
+
+ mkdir -p $ddir/$"n^$"bpid/
+ {
+ if(! ~ $"bptitle '') {
+ echo $bptitle
+ echo '========================================='
+ }
+ # TODO: Enable metadata
+ #echo '* Posted:' `{date}
+ #if(! ~ $#logged_user 0)
+ # echo '* Author: '$logged_user
+ echo
+ echo $bptext
+ }> $ddir/$"n^$"bpid/index.md
+
+ # Experimental support for http://pubsubhubbub.googlecode.com/
+ if(! ~ $"conf_blog_pubsubdub_hub '') {
+ ifs='' { p=`{echo $req_url|sed 's/new_post$/index.atom/'|url_encode } }
+ dprint hget -p 'hub.mode=publish&hub.url='^$"p $conf_blog_pubsubdub_hub
+ hget -d -h -p 'hub.mode=publish&hub.url='^$"p $conf_blog_pubsubdub_hub >[1=2] &
+ }
+ }
+ status=$_status
+}
+
+fn strip_title_from_md_file {
+ sed '1N; /^.*\n===*$/N; /.*\n===*\n$/d'
+}
diff --git a/apps/blagh/atom.tpl b/apps/blagh/atom.tpl
new file mode 100644
index 0000000..97c665f
--- /dev/null
+++ b/apps/blagh/atom.tpl
@@ -0,0 +1,58 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+%{
+# See for more info:http://www.tbray.org/ongoing/When/200x/2005/07/27/Atomic-RSS
+fn statpost {
+ f = $1
+
+ post_uri=$base_url^`{cleanname `{echo $f | sed -e 's!^'$sitedir'!!'}}^'/'
+ title=`{read $f/index.md}
+ by=`{ls -m $f | sed 's/^\[//g; s/].*$//g' >[2]/dev/null}
+ ifs=() { summary=`{cat $f/index.md | strip_title_from_md_file | ifs=$difs {$formatter} } }
+}
+# rfc3339 date when feed was last updated.
+fupdated = `{ndate -a `{date `{mtime `{ls $blagh_root$blagh_dirs/[0-9][0-9][0-9][0-9]/[0-9][0-9]/[0-9][0-9]/[0-9] | tail -1} | awk '{print $1}'}}}
+%}
+
+<feed xmlns="http://www.w3.org/2005/Atom"
+ xmlns:thr="http://purl.org/syndication/thread/1.0">
+
+% if(! ~ $"conf_blog_pubsubdub_hub '') {
+% echo '<link rel="hub" href="'$conf_blog_pubsubdub_hub'" />'
+% }
+
+ <link rel="self" href="%($base_url^$req_path%)"/>
+ <id>%($base_url^$req_path%)</id>
+ <icon><![CDATA[/favicon.ico]]></icon>
+
+ <title><![CDATA[%($siteTitle%)]]></title>
+ <subtitle><![CDATA[%($siteSubTitle%)]]></subtitle>
+
+ <updated>%($fupdated%)</updated>
+ <link href="."/>
+
+% for(f in `{get_post_list $blagh_root$blagh_dirs}) {
+% statpost $f
+
+ <entry>
+% # Maybe we should be smarter, see: http://diveintomark.org/archives/2004/05/28/howto-atom-id, example: <id>tag:intertwingly.net,2004:2899</id>
+ <id>%($post_uri%)</id>
+ <link href="%($post_uri%)"/>
+ <title><![CDATA[%($title%)]]></title>
+% # <link rel="replies" href="2899.atom" thr:count="0"/>
+ <author><name><![CDATA[%($by%)]]></name></author>
+
+ <content type="xhtml"><div xmlns="http://www.w3.org/1999/xhtml">
+ <![CDATA[%($summary%)]]>
+ </div></content>
+
+% # rfc3339 date when entry was last updated.
+% eupdated=`{ndate -a `{date `{mtime $f | awk '{print $1}'}}}
+ <updated>%($eupdated%)</updated>
+ </entry>
+
+% }
+
+</feed>
+
+% exit
diff --git a/apps/blagh/convert.rc b/apps/blagh/convert.rc
new file mode 100755
index 0000000..0640805
--- /dev/null
+++ b/apps/blagh/convert.rc
@@ -0,0 +1,20 @@
+#!/usr/bin/env rc
+
+path=($PLAN9/bin/ $path)
+
+for(p in *.md) {
+ echo
+ echo '========================='
+ echo p $p
+ pp=`{echo $p | sed 's/^([0-9][0-9][0-9][0-9])-([0-9][0-9])-([0-9][0-9])[\-_](.*).md$/\1 \2 \3 \4/' }
+ echo pp $pp
+
+ d=$pp(1)^'/'^$pp(2)^'/'^$pp(3)^'/'^$pp(4)^'/'
+
+ mkdir -p $d
+ echo $pp(4) | sed -e 's/^[0-9]_//; s/_/ /g;' > $d/index.md
+ echo '=================================' >> $d/index.md
+ echo >> $d/index.md
+ cat $p >> $d/index.md
+
+}
diff --git a/apps/blagh/jsonfeed.tpl b/apps/blagh/jsonfeed.tpl
new file mode 100644
index 0000000..fd97ed4
--- /dev/null
+++ b/apps/blagh/jsonfeed.tpl
@@ -0,0 +1,35 @@
+{
+"version": "https://jsonfeed.org/version/1",
+"title": "%($siteTitle%)",
+"home_page_url": "%($"base_url%)",
+"feed_url": "%($"base_url^$"req_path%)",
+"items": [
+%{
+fn statpost {
+ f = $1
+ post_uri=$base_url^`{cleanname `{echo $f | sed -e 's!^'$sitedir'!!'}}^'/'
+ title=`{read $f/index.md}
+ #ifs=() { summary=`{cat $f/index.md | crop_text 1024 ... | $formatter } }
+ ifs=() { summary=`{cat $f/index.md | strip_title_from_md_file | ifs=$difs {$formatter| sed 's/"/\\"/g' | tr -d '\012' } } }
+}
+%}
+% #for(f in `{get_post_list $blagh_root$blagh_dirs}) {
+%
+% postlist=`{get_post_list $blagh_root$blagh_dirs}
+% postcount=0
+% for(f in $postlist) {
+% statpost $f
+ {
+ "id": "%($post_uri%)",
+ "url": "%($post_uri%)",
+ "title": "%($title%)",
+ "content_html": "%($summary%)"
+ }
+% postcount = `{echo $postcount 1+p | dc}
+% if (! ~ $#postlist $postcount) { echo , }
+% }
+]
+}
+
+% exit
+
diff --git a/apps/blagh/new_post.tpl b/apps/blagh/new_post.tpl
new file mode 100644
index 0000000..bd521c4
--- /dev/null
+++ b/apps/blagh/new_post.tpl
@@ -0,0 +1,11 @@
+<div>
+% notices_handler
+<form method="POST"><fieldset>
+ <legend>Submit a new blog post</legend>
+ <textarea cols="94" rows=16" name="body">%($"post_arg_body%)</textarea><br />
+ <label>Title: <input size="64" type="text" name="title" value="%($"post_arg_title%)" /></label>
+ <label>Id: <input size="8" type="text" name="id" value="%($"post_arg_id%)" /></label>
+ <label>Date: <input size="10" maxlength="10" type="text" name="date" value="%($"post_date%)" /></label>
+ <input type="submit" value="Post" />
+</fieldset></form>
+</div>
diff --git a/apps/blagh/rss20.tpl b/apps/blagh/rss20.tpl
new file mode 100644
index 0000000..0cba818
--- /dev/null
+++ b/apps/blagh/rss20.tpl
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+%{
+fn statpost {
+ f = $1
+ post_uri = `{echo $f | sed 's,^'$sitedir',,'}
+ title=`{read $f/index.md}
+ post_uri=$base_url^`{cleanname `{echo $f | sed -e 's!^'$sitedir'!!'}}^'/'
+ by=`{ls -m $f | sed 's/^\[//g; s/].*$//g' >[2]/dev/null}
+ ifs=() {summary=`{ cat $f/index.md |strip_title_from_md_file| ifs=$difs {$formatter | escape_html} }}
+}
+
+%}
+
+<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
+ <channel>
+ <atom:link href="%($base_url^$req_path%)" rel="self" type="application/rss+xml" />
+ <title><![CDATA[%($siteTitle%)]]></title>
+ <link>%($base_url^$req_path%)</link>
+ <description><![CDATA[%($blogDesc%)]]></description>
+ <language>en-us</language>
+ <generator><![CDATA[Tom Duff's rc, and Kris Maglione's clever hackery]]></generator>
+%{
+ # <webMaster>uriel99+rss@gmail.com (Uriel)</webMaster>
+ # rfc2822 last time channel content changed.
+ lbd=`{ndate -m `{date `{mtime `{ls $blagh_root$blagh_dirs/[0-9][0-9][0-9][0-9]/[0-9][0-9]/[0-9][0-9]/[0-9] | tail -1} | awk '{print $1}'}}}
+ echo '<lastBuildDate>'$"lbd'</lastBuildDate>'
+ # rfc2822 publication date for content in the channel.
+ pubdate=`{ndate -m}
+ for(f in `{get_post_list $blagh_root$blagh_dirs}){
+ statpost $f
+%}
+ <item>
+ <title><![CDATA[%($title%)]]></title>
+ <author><![CDATA[%($by%)@noreply.cat-v.org (%($by%))]]></author>
+ <link>%($post_uri%)</link>
+ <guid isPermaLink="true">%($post_uri%)</guid>
+ <pubDate>%($pubdate%)</pubDate>
+ <description> %($summary%) </description>
+ </item>
+% }
+ </channel>
+</rss>
diff --git a/apps/bridge/app.rc b/apps/bridge/app.rc
new file mode 100755
index 0000000..40477ba
--- /dev/null
+++ b/apps/bridge/app.rc
@@ -0,0 +1,103 @@
+comment_file_types=(md html)
+
+fn conf_enable_comments {
+ if(~ $1 -n) {
+ allow_new_user_comments=yes
+ shift
+ }
+ if not if(~ $1 -a) {
+ bridge_anon_comments=yes
+ }
+ enable_comments=yes
+ groups_allowed_comments=$*
+ conf_enable_app bridge
+}
+
+fn bridge_init {
+ if(~ $#enable_comments 1 && ! ~ `{ls $local_path.$comment_file_types >[2]/dev/null|wc -l} 0) {
+
+ comments_dir=$sitedir$req_path'_werc/comments'
+ if(~ $REQUEST_METHOD GET && test -d $comments_dir)
+ ll_add handlers_body_foot template apps/bridge/comments_list.tpl
+
+ if(check_user $groups_allowed_comments || {~ $#logged_user 0 && ~ 1 $#allow_new_user_comments $#bridge_anon_comments}) {
+
+ if(~ $#post_arg_bridge_post 1) {
+ ll_add handlers_body_foot template apps/bridge/foot.tpl
+
+ if(mk_new_comment $comments_dir)
+ post_redirect $base_url^$req_path
+ if not
+ saved_comment_text=$post_arg_comment_text
+ }
+ if not if(~ $REQUEST_METHOD GET)
+ ll_add handlers_body_foot template apps/bridge/foot.tpl
+ }
+ if not if(~ $REQUEST_METHOD GET)
+ ll_add handlers_body_foot echo '<hr><p>To post a comment you need to <a href="/_users/login">login</a> first.</p>'
+ }
+}
+
+fn validate_new_user {
+ usr=$1; pass=$2; pass2=$3
+ _status=()
+
+ if(~ $"usr '' || ! echo $usr |sed 1q|grep -s '^'$allowed_user_chars'+$')
+ _status='Requested user name is invalid, must match: '^$allowed_user_chars^'+'
+ if not if(test -d etc/users/$usr)
+ _status='Sorry, user name '''^$usr^''' already taken, please pick a different one.'
+
+ if(~ $"pass '' || ! ~ $"pass $"pass2)
+ _status=($_status 'Provided passwords don''t match.')
+
+ status=$_status
+}
+
+
+fn mk_new_comment {
+ _status=()
+ dir=$1
+ if(~ $"post_arg_comment_text '')
+ _status='Provide a comment!'
+ if not if(~ $#logged_user 0) {
+ if(! ~ $#allow_new_user_comments 0) {
+ if(validate_new_user $"post_arg_comment_user $post_arg_comment_passwd $post_arg_comment_passwd2) {
+ u=$post_arg_comment_user':'$post_arg_comment_passwd
+ dir=$comments_dir^'_pending'
+ # XXX: This doesn't work because we then do a redirect.
+ notify_notes='Saved comment and registration info, they will be enabled when approved by an admin.'
+ }
+ if not
+ _status=$status
+ }
+ if not if(! ~ $#bridge_anon_comments 0) {
+ if(~ $"post_arg_ima_robot 'not')
+ u='Glenda' # Anonymous
+ if not
+ _status='You are a robot!'
+ }
+ if not
+ _status='You need to log in to comment.'
+ }
+ if not if(check_user $groups_allowed_comments)
+ u=$logged_user
+ if not
+ _status='You are not a member of a group allowed to comment.'
+
+ if(~ $#_status 0) {
+ umask 002
+
+ dir=$dir'/'`{date -n} # FIXME Obvious race
+ mkdir -m 775 -p $dir &&
+ echo $u > $dir/user &&
+ echo $current_date_time > $dir/posted &&
+ echo $post_arg_comment_text > $dir/body
+ _s=$status
+ if(! ~ $"_s '') {
+ dprint 'ERROR XXX: Could not create comment: ' $_s
+ _status='Could not post comment due internal error, sorry.'
+ }
+ }
+ notify_errors=$_status
+ status=$_status
+}
diff --git a/apps/bridge/comments_list.tpl b/apps/bridge/comments_list.tpl
new file mode 100755
index 0000000..03e0ddc
--- /dev/null
+++ b/apps/bridge/comments_list.tpl
@@ -0,0 +1,13 @@
+<hr>
+<h2>Comments</h2>
+
+% for(c in `{ls $comments_dir/}) {
+% if(test -s $c/body) {
+ <div class="comment">
+ <h5>By: <i>%(`{cat $c/user}%)</i></b> (%(`{cat $c/posted}%))
+ </h5>
+% cat $c/body | escape_html | sed 's,$,<br>,'
+ <hr></div>
+% }
+% }
+
diff --git a/apps/bridge/foot.tpl b/apps/bridge/foot.tpl
new file mode 100755
index 0000000..0dad21d
--- /dev/null
+++ b/apps/bridge/foot.tpl
@@ -0,0 +1,37 @@
+<hr>
+
+% notices_handler
+<form action="" method="post">
+ <textarea name="comment_text" id="comment_text" cols="80" rows="16">%($"saved_comment_text%)</textarea>
+ <br>
+ <input type="submit" name="bridge_post" value="Post a comment">
+
+% if(~ $#logged_user 0) {
+% if(~ $#allow_new_user_comments 1) {
+ <label>New user name:
+ <input type="text" name="comment_user" value="%($"post_arg_comment_user%)">
+ </label>
+
+ <label>Password:
+ <input type="password" name="comment_passwd" value="">
+ </label>
+
+ <label>Repeat password:
+ <input type="password" name="comment_passwd2" value="">
+ </label>
+ <div style="font-size: 70%">
+ Enter your desired user name/password and after your comment has been reviewed by an admin it will be posted and your account will be enabled. If you are already registered please <a href="/_users/login">login</a> before posting.
+ </div>
+% }
+% if not if(~ $#bridge_anon_comments 1) {
+ <label>Is <a href="http://glenda.cat-v.org">Glenda a cute bunny</a>?
+ <select name='ima_robot'>
+ <option value="yes">No</option>
+ <option value="not">Yes</option>
+ <option value="foobar">I hate bunnies!</option>
+ <option value="robot">I'm a robot!</option>
+ </select>
+ </label>
+% }
+% }
+</form>
diff --git a/apps/dirdir/app.rc b/apps/dirdir/app.rc
new file mode 100755
index 0000000..1aa9cbd
--- /dev/null
+++ b/apps/dirdir/app.rc
@@ -0,0 +1,40 @@
+fn conf_enable_wiki {
+ enable_wiki=yes
+ wiki_editors_groups=$*
+ conf_enable_app dirdir
+}
+
+fn dirdir_init {
+ if(! ~ $#enable_wiki 0 && check_user $wiki_editors_groups) {
+ lp=$local_path
+ # werc.rc doesn't append /index when $local_path doesn't exist
+ # maybe it should, but for now we can fix it up here.
+ if(~ $lp */)
+ lp=$lp^'index'
+ dirdir_file=$lp.md
+ dirdir_dir=$dirdir_file^'_werc/dirdir/'
+
+ if(~ 1 $#post_arg_dirdir_edit $#post_arg_dirdir_preview)
+ handler_body_main=(tpl_handler `{get_lib_file dirdir/edit.tpl apps/dirdir/edit.tpl})
+
+ if not if(! ~ '' $"post_arg_dirdir_save $"post_arg_edit_text)
+ save_page
+
+ if not if(~ $"handler_body_main '' || {~ $REQUEST_METHOD GET && test -f $local_path.md})
+ ll_add handlers_bar_left tpl_handler apps/dirdir/sidebar_controls.tpl
+ }
+}
+
+fn save_page {
+ dirdir_verdir=$dirdir_dir/^`{date -n}^/
+ mkdir -p $dirdir_verdir
+ umask 002
+
+ # XXX Use a tmp file and mv(1) to ensure updates are atomic?
+ echo $logged_user > $dirdir_verdir/author
+ echo $post_arg_edit_text > $dirdir_verdir/data
+ echo $post_arg_edit_text > $dirdir_file
+
+ post_redirect $base_url^$req_path
+ #notify_notes='Saved <a href="'$"req_path'">'$"req_path'</a>!'
+}
diff --git a/apps/dirdir/edit.tpl b/apps/dirdir/edit.tpl
new file mode 100755
index 0000000..1a5b206
--- /dev/null
+++ b/apps/dirdir/edit.tpl
@@ -0,0 +1,25 @@
+<div>
+ <h1>Editing: <a href="%($req_path%)">%($req_path%)</a></h1>
+ <br>
+ <form action="" method="POST">
+ <textarea name="edit_text" id="edit_text" cols="80" rows="43">%{
+# FIXME Extra trailing new lines get added to the content somehow, should avoid it.
+ if(~ $#post_arg_edit_text 0 && test -f $dirdir_file)
+ cat $dirdir_file | escape_html
+ if not
+ echo -n $post_arg_edit_text | escape_html
+
+ %}</textarea>
+ <br>
+ <input type="submit" name="dirdir_save" value="Save">
+ <input type="submit" name="dirdir_preview" value="Preview">
+ <small>DirDir documents are written using <a href="http://daringfireball.net/projects/markdown/syntax">Markdown syntax</a>.</small>
+ </form>
+</div>
+
+% if(! ~ $"post_arg_dirdir_preview '') {
+ <h2>Preview:</h2>
+ <div id="preview">
+% echo $post_arg_edit_text | $formatter
+ </div>
+% }
diff --git a/apps/dirdir/sidebar_controls.tpl b/apps/dirdir/sidebar_controls.tpl
new file mode 100755
index 0000000..a897fc1
--- /dev/null
+++ b/apps/dirdir/sidebar_controls.tpl
@@ -0,0 +1,3 @@
+<form action="" method="POST">
+<input type="submit" name="dirdir_edit" value="Edit page" />
+</form>
diff --git a/apps/duckduckgo/HOWTO b/apps/duckduckgo/HOWTO
new file mode 100644
index 0000000..8bb952c
--- /dev/null
+++ b/apps/duckduckgo/HOWTO
@@ -0,0 +1,20 @@
+The default path for site search is /_search/. Assuming you want to keep
+that default, you could enable site search like so:
+
+
+mkdir -p /www/werc/sites/MYSITE/_search/_werc/
+echo 'conf_enable_duckduckgo' > /www/werc/sites/MYSITE/_search/_werc/config
+mkdir -p /www/werc/sites/MYSITE/_werc/lib/
+cp /www/werc/apps/duckduckgo/footer.inc.sample /www/werc/sites/MYSITE/_werc/lib/footer.inc
+
+Searches will POST to /_search/ and from there get redirected to Duck Duck
+Go with a site:$SERVER_NAME prefix. To have the search path URL be some-
+thing different, you'll have to edit line 23 of app.rc to point to the new
+path.
+
+TODO:
+* Make it automatically work no matter which directory the app is enabled in.
+* OR make the search path a configuration option.
+* Provide a template for non-footer deployment
+* Enable the search path itself to serve a search form to GET requests
+
diff --git a/apps/duckduckgo/app.rc b/apps/duckduckgo/app.rc
new file mode 100755
index 0000000..72dd0ec
--- /dev/null
+++ b/apps/duckduckgo/app.rc
@@ -0,0 +1,30 @@
+fn conf_enable_duckduckgo {
+ enable_duckduckgo=yes
+ conf_enable_app duckduckgo
+ pageTitle='Site Search'
+}
+
+
+fn duckduckgo_init {
+ get_post_args q
+ if (! ~ $#q 0) {
+ redirect_string = 'https://duckduckgo.com/?q=site:'$SERVER_NAME^'+'^$"q
+ http_redirect $redirect_string '302 Found'
+ }
+ if not {
+ handler_body_main='duckduckgo_body'
+ }
+}
+
+fn duckduckgo_body {
+ echo '
+<h1>Site search</h1>
+<h2>using DuckDuckGo</h2>
+<form action="/_search/" method="POST">
+<label for="searchtext">Site search:</label>
+<input type="text" id="searchtext" name="q" placeholder="Search text...">
+<input type="submit" value="Search">
+</form>'
+
+}
+
diff --git a/apps/duckduckgo/footer.inc.sample b/apps/duckduckgo/footer.inc.sample
new file mode 100644
index 0000000..4dd671d
--- /dev/null
+++ b/apps/duckduckgo/footer.inc.sample
@@ -0,0 +1,3 @@
+<div><a href="http://werc.cat-v.org">Powered by werc</a></div>
+
+<div><form action="/_search/" method="POST"><label for="searchtext">Site search:</label> <input type="text" id="searchtext" name="q" placeholder="Enter search text..."><input type="submit" display="Search"></form></div>
diff --git a/apps/hello/app.rc b/apps/hello/app.rc
new file mode 100755
index 0000000..e6faaa8
--- /dev/null
+++ b/apps/hello/app.rc
@@ -0,0 +1,10 @@
+fn hello_init {
+ if(~ $req_path /hello) {
+ handler_body_main='hello_body'
+ pageTitle='Hi title!'
+ }
+}
+
+fn hello_body {
+ echo 'Hello world!'
+}
diff --git a/apps/paste/app.rc b/apps/paste/app.rc
new file mode 100755
index 0000000..af0c76d
--- /dev/null
+++ b/apps/paste/app.rc
@@ -0,0 +1,45 @@
+fn conf_enable_wercpaste {
+ paste_url=$conf_wd
+ if (~ $#paste_dir 0) { paste_dir=`{pwd} }
+ conf_enable_app wercpaste
+}
+
+fn wercpaste_init {
+ if (~ $REQUEST_METHOD POST && ~ $post_arg_url url && ~ $req_path $paste_url ) { # incoming paste
+ now=`{ date -n }
+ cksum=`{ echo $"post_arg_paste | sum | awk '{ print $1 }' }
+ if (~ $cksum '1715a8eb' ) { # empty paste; discard
+ post_redirect $base_url^$paste_url
+ }
+ if not { # save and redirect
+ # TODO: stop using echo
+ # env var size limit is 16kb, this thing dies with larger input.
+ echo $"post_arg_paste > $paste_dir^/^$now^.^$cksum
+ # uncomment the following line to redirect to the pasted file
+ #post_redirect $base_url^$paste_url^$now^.^$cksum
+ # uncomment the following line instead to just return the url
+ echo 'Content-type: text/plain'; echo ''; exec echo $base_url^$paste_url^$now^.^$cksum
+ }
+ }
+ if not { # show a paste if there is one
+ if (test -r $werc_root/$local_path && ~ $QUERY_STRING raw ) {
+ echo 'Content-type: text/plain'; echo ''; exec cat $werc_root/$local_path
+ }
+ }
+
+# drop a textbox
+ if (~ $REQUEST_METHOD GET ) { handler_body_main='begforpaste' }
+
+}
+
+fn begforpaste {
+ echo '<article class="pastebox">
+ <h3 style="text-align: center">pasted data is not publically indexed</h3>
+ <form action="'$paste_url'" method="post" style="margin:2em">
+ <textarea name="paste" cols="120" rows="20" required style="display: block; margin: 0 auto 0 auto" ></textarea><br>
+ <input type="submit" name="submit" value="SUBMIT" style="display: block; margin: 0 auto 0 auto" ><br><br>
+ <span style="display: none"><input type="text" name="url" value="url" > (do not change) </span>
+ </form>
+ </article>
+ '
+}
diff --git a/apps/wman/app.rc b/apps/wman/app.rc
new file mode 100755
index 0000000..8f0a150
--- /dev/null
+++ b/apps/wman/app.rc
@@ -0,0 +1,89 @@
+fn conf_enable_wman {
+ wman_tmac=an
+ wman_base_uri=$conf_wd
+ wman_man_path=$*
+ if(~ $#wman_man_path 0)
+ wman_man_path=$wman_base_uri
+ conf_enable_app wman
+}
+
+wman_junk_filter='/(\/(INDEX|\.cvsignore|_.*)|\.9p|\.html)$/d; s!/man([0-9]+/[^/]+)$!/\1!; '
+fn wman_ls_pages {
+ ls $* \
+ | sed $dirfilter^$wman_junk_filter^' s/\.([0-9]|9p)$//; s!/0intro$!/intro!' \
+ | sort -u
+}
+fn wman_init {
+ ifs=$ifs^'/' { p=`{echo $req_path | sed 's!^'^$wman_base_uri^'!!'} }
+ wman_cat=$p(1)
+ wman_page=$p(2)
+ if(~ $#wman_unix_mode 1) {
+ wman_cp='man'
+ wman_pe=.^$"wman_cat
+ }
+
+ if(! ~ $"wman_cat '') {
+ wman_cat_path=$wman_man_path^/^$"wman_cp^$p(1)
+ if(! ~ $"wman_page '') {
+ wman_page_file=$wman_page^$"wman_pe
+ # Hack to handle 0intro files.
+ if(~ $wman_page intro && test -f $wman_cat_path^/0^$"wman_page_file)
+ wman_page_file=0^$"wman_page_file
+ wman_page_file=$wman_cat_path^/^$"wman_page_file
+ x=`{echo $"req_path|sed 's%.*/([^/]+)/'$"wman_cat'/'^$"wman_page^'%\1%; s%_% %g'}
+ pageTitle=$wman_page' page from Section '$wman_cat' of the '^$"x' manual'
+ }
+ }
+
+ wman_cat_list=`{ls -F $wman_man_path/*/ \
+ | sed -e $wman_junk_filter -e 's!.*/([^/]+)/[^/]+$!\1!; /[0-9]+/!d' \
+ | sort -un}
+
+ synth_paths=($wman_base_uri$wman_cat_list'/')
+
+ if(~ $req_path $wman_base_uri && ~ $"handler_body_main '')
+ handler_body_main=(tpl_handler apps/wman/section_list.tpl)
+ if not if(~ $req_path $wman_base_uri^*) {
+ #^*/[a-z0-9]*[a-z]* $wman_base_uri^*/*[a-z]*[a-z0-9] $wman_base_uri^*/[a-z])
+ if(echo $req_path | grep -s '^'^$wman_base_uri^'/*[0-9]+/[0-9a-z\-\+\.]+$')
+ if(test -f $wman_page_file) # Check for 404
+ handler_body_main=(tpl_handler apps/wman/man_page.tpl)
+ if not if(~ $req_path $wman_base_uri^*/)
+ handler_body_main=(tpl_handler apps/wman/page_list.tpl)
+ if not if(~ $p(2) [A-Z]* [0-9][A-Z]*) # Correct badly capitalized links
+ perm_redirect $wman_base_uri^$p(1)^/^`{echo $p(2) |tr 'A-Z' 'a-z'}
+ }
+
+ # Search
+ ll_add handlers_body_head tpl_handler apps/wman/search.tpl
+ if(! ~ $"post_arg_wman_search '') {
+ s=`{echo $post_arg_wman_search | sed 's/[^a-zA-Z0-9\-\.]+//g; s/\.+/./g; 1q'}
+ ifs='' { wman_search_results=`{wman_ls_pages $wman_man_path/*/*^$"s^*} }
+ if(! ~ $"post_arg_go '' && ~ `{echo -n $wman_search_results|wc -l} 1)
+ post_redirect $wman_base_uri^`{echo $wman_search_results|awk -F/ '{print $(NF-1)"/"$NF}'}
+ }
+
+}
+
+fn wman_get_section_desc {
+ cat $wman_man_path/^$"wman_cp^$1/0intro* >[2]/dev/null| sed '1,2d; s!intro \\- [Ii]ntroduction to !!; 3q;'
+}
+
+fn wman_page_gen {
+ #troff -manhtml $1| troff2html -t 'Plan 9 from User Space'
+ troff -N -m$wman_tmac $1 | wman_out_filter
+}
+
+fn wman_out_filter {
+ wman_default_out_filter
+}
+
+fn wman_default_out_filter {
+ # col -x syntax is the same for UNIX and Plan 9.
+ escape_html \
+ | sed 's!([\.\-a-zA-Z0-9]+)\(('^`{echo $wman_cat_list|tr ' ' '|'}^')\)!<a href="../\2/\1">&</a>!g' \
+ | awk '/^$/ {if(n != 1) print; n=1; next} /./ {n=0; print}' \
+ | col -x
+}
+
+
diff --git a/apps/wman/man_page.tpl b/apps/wman/man_page.tpl
new file mode 100755
index 0000000..945e23a
--- /dev/null
+++ b/apps/wman/man_page.tpl
@@ -0,0 +1,3 @@
+<pre>
+% wman_page_gen $wman_page_file
+</pre>
diff --git a/apps/wman/page_list.tpl b/apps/wman/page_list.tpl
new file mode 100755
index 0000000..b98600d
--- /dev/null
+++ b/apps/wman/page_list.tpl
@@ -0,0 +1,11 @@
+% d=`{wman_get_section_desc $wman_cat}
+<h1>Manual pages - Section %($wman_cat%): %($"d%)</h1>
+
+<ul style="float:left">
+%{
+wman_ls_pages $wman_cat_path \
+ | awk -F/ '{ print "<li><a href=\""$(NF)"\">"$(NF)"</a></li>" }
+ NR%20 == 0 { print "</ul><ul style=\"float: left\">" }'
+%}
+</ul>
+
diff --git a/apps/wman/search.tpl b/apps/wman/search.tpl
new file mode 100755
index 0000000..a6c59e4
--- /dev/null
+++ b/apps/wman/search.tpl
@@ -0,0 +1,20 @@
+<form action="" method="POST">
+<fieldset>
+ <input type="text" name="wman_search" value="%($"s%)" />
+ <input type="submit" name="go" value="Feel Lucky" />
+ <input type="submit" value="Search" />
+
+% if(! ~ $"post_arg_wman_search '') {
+% if(~ $"wman_search_results '') {
+ No matches found for <i>'%($post_arg_wman_search%)'</i>.
+% }
+% if not {
+ <ul>
+% echo $wman_search_results|awk -F/ '$(NF-1) ~ "^[0-9]+$" {printf "<li><a href=\"'$wman_base_uri'%s/%s\" />%s(%s)</a></li>", $(NF-1),$NF, $NF, $(NF-1)}'
+ </ul>
+% }
+% }
+
+</fieldset>
+</form>
+
diff --git a/apps/wman/section_list.tpl b/apps/wman/section_list.tpl
new file mode 100755
index 0000000..299d613
--- /dev/null
+++ b/apps/wman/section_list.tpl
@@ -0,0 +1,11 @@
+<h1>Manual Sections</h1>
+
+<ul style="text-transform: capitalize;">
+% for(c in $wman_cat_list) {
+ <li><a href="%($c%)/"><b>Section: %($c%)</b></a>
+% wman_get_section_desc $c
+% if(~ $status '' '|')
+% echo '(<a href="'$c'/intro">intro</a>)'
+ </li>
+% }
+</ul>
diff --git a/bin/aux/addwuser.rc b/bin/aux/addwuser.rc
new file mode 100755
index 0000000..9364d39
--- /dev/null
+++ b/bin/aux/addwuser.rc
@@ -0,0 +1,33 @@
+#!/bin/rc
+
+if(! ~ $#werc_root 0)
+ cd $werc_root
+
+fn usage {
+ if(! ~ $#* 0)
+ echo $0: $* >[1=2]
+ echo 'Usage:' $0 'user_name user_password [groups ...]' >[1=2]
+ exit usage
+}
+
+if(! test -d etc/users/)
+ usage 'Run for root of werc installation or set $werc_root'
+
+user_name=$1
+shift
+user_pass=$1
+shift
+user_groups=$*
+
+if(~ $"user_name '' || ~ $"user_pass '')
+ usage
+
+mkdir etc/users/$user_name
+echo $user_pass > etc/users/$user_name/password
+
+if(! ~ $#user_groups 0)
+ for(g in $user_groups) {
+ mkdir -p etc/users/$g
+ echo $user_name >> etc/users/$g/members
+ }
+
diff --git a/bin/aux/bpst.rc b/bin/aux/bpst.rc
new file mode 100755
index 0000000..e60d034
--- /dev/null
+++ b/bin/aux/bpst.rc
@@ -0,0 +1,64 @@
+#!/bin/rc
+
+path=( $PLAN9/bin $path )
+base=.
+
+if(~ $#user 0)
+ user=`{whoami}
+
+file=(); title=();
+bloguser=$user
+while(! ~ $#* 0) {
+ switch($1) {
+ case -u
+ base=/gsoc/www/people/$user/blog/
+ case -b
+ shift
+ base=$1
+ case -f
+ shift
+ file=$1
+ }
+ shift
+}
+
+if(~ $"EDITOR '')
+ EDITOR=vi
+
+if(~ $#file 0 || ! test -f $file) {
+ file=/tmp/blogtmp.$pid
+ rm $file >[2]/dev/null
+ touch $file
+}
+
+$EDITOR $file
+ispell $file
+rm $file.bak >[2]/dev/null
+
+fn mkbpost {
+ umask 002 # Let group write
+ bptext=$1
+ if(! ~ $#2 0)
+ bpid=`{echo -n '-'^$"bpid | sed 's/'$forbidden_uri_chars'+/_/g; 1q'}
+ d=`{/bin/date +%F|sed 's,-,/,g'}
+
+ ddir=$blagh_root^$d^'/'
+ n=`{ls $ddir >[2]/dev/null |wc -l}
+
+ mkdir -p $ddir/$"n^$"bpid/
+ {
+ # TODO: Enable metadata
+ #echo '* Posted:' `{date}
+ #if(! ~ $#logged_user 0)
+ # echo '* Author: '$logged_user
+ cat $bptext
+ }> $ddir/$"n^$"bpid/index.md
+}
+
+forbidden_uri_chars='[^a-zA-Z0-9_+\-\/\.]'
+blagh_root=$base
+
+if(test -s $file)
+ mkbpost $file
+if not
+ echo Empty file!
diff --git a/bin/aux/gensitemaptxt.rc b/bin/aux/gensitemaptxt.rc
new file mode 100755
index 0000000..a1b349d
--- /dev/null
+++ b/bin/aux/gensitemaptxt.rc
@@ -0,0 +1,14 @@
+#!/bin/rc
+# DEPRECATED: sitemap.tpl now generates and updates a sitemap.txt when requested, and is also more smart than this simplistic script.
+
+for(d in sites/*/) {
+echo $d
+9 du -a $d | awk '/\.(md|html)$/ { print $2 }; {}' | 9 sed -e 's/\.(md|html)$//' -e 's,/index$,/,' -e 's,^sites/,http://,' > $d/sitemap.txt
+
+if(! test -f $d/robots.txt) {
+ echo generating missing robots.txt for $d
+ echo $d|sed 's,sites/,Sitemap: http://,; s/$/sitemap.txt/;' > $d/robots.txt
+ cat $d/robots.txt
+}
+
+}
diff --git a/bin/aux/runtsts.rc b/bin/aux/runtsts.rc
new file mode 100755
index 0000000..b5b1df7
--- /dev/null
+++ b/bin/aux/runtsts.rc
@@ -0,0 +1,16 @@
+#!/bin/rc
+
+tstdom='http://test.cat-v.org'
+
+cd sites/tst.cat-v.org
+
+tstfiles=`{du -a |awk '/\.tst$/ { print $2 }; {} ' | sed 's/^\.//; s/\.tst$//'}
+
+for(f in $tstfiles) {
+ ifs='
+' { tsts=`{cat ./$f.tst} }
+
+ for(t in $tsts) {
+ echo tst $t
+ }
+}
diff --git a/bin/cgilib.rc b/bin/cgilib.rc
new file mode 100755
index 0000000..4516bf9
--- /dev/null
+++ b/bin/cgilib.rc
@@ -0,0 +1,236 @@
+# Useful CGI stuff
+
+fn dprint { echo $* >[1=2] }
+fn dprintv { { for(v in $*) { echo -n $v^'#'^$#$v^'=' $$v '; ' }; echo } >[1=2] }
+fn echo {if(! ~ $1 -n || ! ~ $2 '') /bin/echo $*}
+fn escape_html { sed 's/&/\&amp;/g; s/</\&lt;/g; s/>/\&gt;/g' $* }
+
+fn http_redirect {
+ if(~ $1 http://* https://*)
+ t=$1
+ if not if(~ $1 /*)
+ t=$"base_url^$1
+ if not
+ t=$"base_url^$"req_path^$1
+ exec /bin/echo 'Status: '^$2^'
+Location: '^$t^'
+
+'
+ exit
+}
+fn perm_redirect { http_redirect $1 '301 Moved Permanantly' }
+fn post_redirect { http_redirect $1 '303 See Other' }
+
+
+# Note: should check if content type is application/x-www-form-urlencoded?
+# Should compare with http://www.shelldorado.com/scripts/cmds/urlgetopt.txt
+fn load_post_args {
+ if(~ $REQUEST_METHOD POST && ~ $#post_args 0) {
+ ifs='&
+' for(pair in `{cat}) {
+ ifs='=' { pair=`{echo -n $pair} }
+ n='post_arg_'^`{echo $pair(1)|nurldecode|tr -cd 'a-zA-Z0-9_'}
+ post_args=( $post_args $n )
+ ifs=() { $n=`{echo -n $pair(2)|nurldecode|tr -d ' '} }
+ }
+ pair=()
+ }
+ if not
+ status='No POST or post args already loaded'
+}
+# Status is () if at least one arg is found. DEPRECATED: access vars directly.
+fn get_post_args {
+ load_post_args
+ _status='No post arg matches'
+ for(n in $*) {
+ v=post_arg_$n
+ if(! ~ $#$v 0) {
+ $n=$$v
+ _status=()
+ }
+ }
+ status=$_status
+}
+
+# This seems slightly improve performance, but might depend on httpd buffering behavior.
+fn awk_buffer {
+ awk '{
+ buf = buf $0"\n"
+ if(length(buf) > 1400) {
+ printf "%s", buf
+ buf = ""
+ }
+ }
+ END { printf "%s", buf }'
+}
+
+fn nurldecode { urlencode -d || url_decode} # GROSS
+
+fn url_decode {
+awk '
+BEGIN {
+ hextab ["0"] = 0; hextab ["8"] = 8;
+ hextab ["1"] = 1; hextab ["9"] = 9;
+ hextab ["2"] = 2; hextab ["A"] = hextab ["a"] = 10
+ hextab ["3"] = 3; hextab ["B"] = hextab ["b"] = 11;
+ hextab ["4"] = 4; hextab ["C"] = hextab ["c"] = 12;
+ hextab ["5"] = 5; hextab ["D"] = hextab ["d"] = 13;
+ hextab ["6"] = 6; hextab ["E"] = hextab ["e"] = 14;
+ hextab ["7"] = 7; hextab ["F"] = hextab ["f"] = 15;
+}
+{
+ decoded = ""
+ i = 1
+ len = length ($0)
+ while ( i <= len ) {
+ c = substr ($0, i, 1)
+ if ( c == "%" ) {
+ if ( i+2 <= len ) {
+ c1 = substr ($0, i+1, 1)
+ c2 = substr ($0, i+2, 1)
+ if ( hextab [c1] == "" || hextab [c2] == "" ) {
+ print "WARNING: invalid hex encoding: %" c1 c2 | "cat >&2"
+ } else {
+ code = 0 + hextab [c1] * 16 + hextab [c2] + 0
+ c = sprintf ("%c", code)
+ i = i + 2
+ }
+ } else {
+ print "WARNING: invalid % encoding: " substr ($0, i, len - i)
+ }
+ } else if ( c == "+" ) {
+ c = " "
+ }
+ decoded = decoded c
+ ++i
+ }
+ printf "%s", decoded
+}
+'
+}
+
+fn nurlencode { urlencode || url_encode } # GROSS
+
+fn url_encode {
+ awk '
+ BEGIN {
+ # We assume an awk implementation that is just plain dumb.
+ # We will convert an character to its ASCII value with the
+ # table ord[], and produce two-digit hexadecimal output
+ # without the printf("%02X") feature.
+
+ EOL = "%0A" # "end of line" string (encoded)
+ split ("1 2 3 4 5 6 7 8 9 A B C D E F", hextab, " ")
+ hextab [0] = 0
+ for ( i=1; i<=255; ++i ) ord [ sprintf ("%c", i) "" ] = i + 0
+ if ("'^$"EncodeEOL^'" == "yes") EncodeEOL = 1; else EncodeEOL = 0
+ }
+ {
+ encoded = ""
+ for ( i=1; i<=length ($0); ++i ) {
+ c = substr ($0, i, 1)
+ if ( c ~ /[a-zA-Z0-9.-]/ ) {
+ encoded = encoded c # safe character
+ } else if ( c == " " ) {
+ encoded = encoded "+" # special handling
+ } else {
+ # unsafe character, encode it as a two-digit hex-number
+ lo = ord [c] % 16
+ hi = int (ord [c] / 16);
+ encoded = encoded "%" hextab [hi] hextab [lo]
+ }
+ }
+ if ( EncodeEOL ) {
+ printf ("%s", encoded EOL)
+ } else {
+ print encoded
+ }
+ }
+ END {
+ #if ( EncodeEOL ) print ""
+ }
+' $*
+}
+
+# Cookies
+fn set_cookie {
+ # TODO: should check input values more carefully
+ name=$1
+ val=$2
+ extraHttpHeaders=( $extraHttpHeaders 'Set-cookie: '^$"name^'='^$"val^'; path=/;' )
+}
+fn get_cookie {
+ ifs=';' { co=`{echo $HTTP_COOKIE} }
+
+ # XXX: we might be adding a trailing new line?
+ # The ' ?' is needed to deal with '; ' inter-cookie delimiter
+ { for(c in $co) echo $c } | sed -n 's/^ ?'$1'=//p'
+}
+
+
+fn static_file {
+ echo -n 'Content-Type: '
+ select_mime $1
+ echo
+ exec cat $1
+}
+
+fn select_mime {
+ m='text/plain'
+ if(~ $1 *.css)
+ m='text/css'
+ if not if(~ $1 *.ico)
+ m='image/x-icon'
+ if not if(~ $1 *.png)
+ m='image/png'
+ if not if(~ $1 *.jpg *.jpeg)
+ m='image/jpeg'
+ if not if(~ $1 *.gif)
+ m='image/gif'
+ if not if(~ $1 *.pdf)
+ m='application/pdf'
+ echo $m
+}
+
+##############################################
+# Generic rc programming helpers
+
+# Manage nested lists
+fn ll_add {
+ _l=$1^_^$#$1
+ $_l=$*(2-)
+ $1=( $$1 $_l )
+}
+# Add to the head: dangerous if you shrink list by hand!
+fn ll_addh {
+ _l=$1^_^$#$1
+ $_l=$*(2-)
+ $1=( $_l $$1 )
+}
+
+
+NEW_LINE='
+'
+
+# crop_text [max_lenght [ellipsis]]
+# TODO: Option to crop only at word-delimiters.
+fn crop_text {
+ m=512
+ e='...'
+ if(! ~ $#1 0)
+ m=$1
+ if(! ~ $#2 0)
+ e=$2
+
+ awk -v 'max='^$"m -v 'ellipsis='$e '
+ {
+ nc += 1 + length;
+ if(nc > max) {
+ print substr($0, 1, nc - max) " " ellipsis
+ exit
+ }
+ print
+ }'
+}
+
+
diff --git a/bin/contrib/fix-rc-scripts b/bin/contrib/fix-rc-scripts
new file mode 100755
index 0000000..beb21c5
--- /dev/null
+++ b/bin/contrib/fix-rc-scripts
@@ -0,0 +1,27 @@
+#!/usr/local/plan9/bin/rc
+
+# Fix rc shell scripts to find rc without launching env every time.
+# Invoke with rc and plan9 versions of grep and ed in $PATH
+
+# If your system lacks which (e.g. some gnu/linux)
+# substitute the full path to rc in this line:
+rc=/usr/local/plan9/bin/rc
+firstline='#!'$"rc
+
+if(~ $#* 0) files = *
+if not files = $*
+
+myname = `{basename $0}
+
+for(file in $files) {
+ if(test -d $file) $0 $file/*
+ if not if(~ $file *$myname) {}
+ if not if(sed 1q $file | grep '^#!/.*[/ ]rc$' > /dev/null) {
+ {
+ echo 1c
+ echo $firstline
+ echo .
+ echo wq
+ } | ed $file > /dev/null
+ }
+}
diff --git a/bin/contrib/hgweb.config b/bin/contrib/hgweb.config
new file mode 100755
index 0000000..fba802b
--- /dev/null
+++ b/bin/contrib/hgweb.config
@@ -0,0 +1,12 @@
+[web]
+style = gitweb
+allow_archive = bz2
+
+#[paths]
+#w9 = /gsoc/hg/w9/
+
+[collections]
+#allow_archive = bz2 zip
+/gsoc/hg = /gsoc/hg/
+#/var/hg = /var/hg/
+
diff --git a/bin/contrib/hgwebdir.cgi b/bin/contrib/hgwebdir.cgi
new file mode 100755
index 0000000..5fe4b16
--- /dev/null
+++ b/bin/contrib/hgwebdir.cgi
@@ -0,0 +1,47 @@
+#!/usr/bin/env python
+#
+# An example CGI script to export multiple hgweb repos, edit as necessary
+
+# send python tracebacks to the browser if an error occurs:
+import cgitb
+cgitb.enable()
+
+# adjust python path if not a system-wide install:
+#import sys
+#sys.path.insert(0, "/path/to/python/lib")
+
+# If you'd like to serve pages with UTF-8 instead of your default
+# locale charset, you can do so by uncommenting the following lines.
+# Note that this will cause your .hgrc files to be interpreted in
+# UTF-8 and all your repo files to be displayed using UTF-8.
+#
+#import os
+#os.environ["HGENCODING"] = "UTF-8"
+
+from mercurial.hgweb.hgwebdir_mod import hgwebdir
+from mercurial.hgweb.request import wsgiapplication
+import mercurial.hgweb.wsgicgi as wsgicgi
+
+# The config file looks like this. You can have paths to individual
+# repos, collections of repos in a directory tree, or both.
+#
+# [paths]
+# virtual/path = /real/path
+# virtual/path = /real/path
+#
+# [collections]
+# /prefix/to/strip/off = /root/of/tree/full/of/repos
+#
+# collections example: say directory tree /foo contains repos /foo/bar,
+# /foo/quux/baz. Give this config section:
+# [collections]
+# /foo = /foo
+# Then repos will list as bar and quux/baz.
+#
+# Alternatively you can pass a list of ('virtual/path', '/real/path') tuples
+# or use a dictionary with entries like 'virtual/path': '/real/path'
+
+def make_web_app():
+ return hgwebdir("hgweb.config")
+
+wsgicgi.launch(wsgiapplication(make_web_app))
diff --git a/bin/contrib/markdown.pl b/bin/contrib/markdown.pl
new file mode 100755
index 0000000..3758a87
--- /dev/null
+++ b/bin/contrib/markdown.pl
@@ -0,0 +1,1447 @@
+#!/usr/bin/env perl
+#
+# Markdown -- A text-to-HTML conversion tool for web writers
+#
+# Copyright (c) 2004 John Gruber
+# <http://daringfireball.net/projects/markdown/>
+#
+package Markdown;
+require 5.006_000;
+use strict;
+use warnings;
+
+use Digest::MD5 qw(md5_hex);
+use vars qw($VERSION);
+$VERSION = '1.0.1';
+# Tue 14 Dec 2004
+
+## Disabled; causes problems under Perl 5.6.1:
+# use utf8;
+# binmode( STDOUT, ":utf8" ); # c.f.: http://acis.openlib.org/dev/perl-unicode-struggle.html
+
+
+#
+# Global default settings:
+#
+my $g_empty_element_suffix = " />"; # Change to ">" for HTML output
+my $g_tab_width = 4;
+
+
+#
+# Globals:
+#
+
+# Regex to match balanced [brackets]. See Friedl's
+# "Mastering Regular Expressions", 2nd Ed., pp. 328-331.
+my $g_nested_brackets;
+$g_nested_brackets = qr{
+ (?> # Atomic matching
+ [^\[\]]+ # Anything other than brackets
+ |
+ \[
+ (??{ $g_nested_brackets }) # Recursive set of nested brackets
+ \]
+ )*
+}x;
+
+
+# Table of hash values for escaped characters:
+my %g_escape_table;
+foreach my $char (split //, '\\`*_{}[]()>#+-.!') {
+ $g_escape_table{$char} = md5_hex($char);
+}
+
+
+# Global hashes, used by various utility routines
+my %g_urls;
+my %g_titles;
+my %g_html_blocks;
+
+# Used to track when we're inside an ordered or unordered list
+# (see _ProcessListItems() for details):
+my $g_list_level = 0;
+
+
+#### Blosxom plug-in interface ##########################################
+
+# Set $g_blosxom_use_meta to 1 to use Blosxom's meta plug-in to determine
+# which posts Markdown should process, using a "meta-markup: markdown"
+# header. If it's set to 0 (the default), Markdown will process all
+# entries.
+my $g_blosxom_use_meta = 0;
+
+sub start { 1; }
+sub story {
+ my($pkg, $path, $filename, $story_ref, $title_ref, $body_ref) = @_;
+
+ if ( (! $g_blosxom_use_meta) or
+ (defined($meta::markup) and ($meta::markup =~ /^\s*markdown\s*$/i))
+ ){
+ $$body_ref = Markdown($$body_ref);
+ }
+ 1;
+}
+
+
+#### Movable Type plug-in interface #####################################
+eval {require MT}; # Test to see if we're running in MT.
+unless ($@) {
+ require MT;
+ import MT;
+ require MT::Template::Context;
+ import MT::Template::Context;
+
+ eval {require MT::Plugin}; # Test to see if we're running >= MT 3.0.
+ unless ($@) {
+ require MT::Plugin;
+ import MT::Plugin;
+ my $plugin = new MT::Plugin({
+ name => "Markdown",
+ description => "A plain-text-to-HTML formatting plugin. (Version: $VERSION)",
+ doc_link => 'http://daringfireball.net/projects/markdown/'
+ });
+ MT->add_plugin( $plugin );
+ }
+
+ MT::Template::Context->add_container_tag(MarkdownOptions => sub {
+ my $ctx = shift;
+ my $args = shift;
+ my $builder = $ctx->stash('builder');
+ my $tokens = $ctx->stash('tokens');
+
+ if (defined ($args->{'output'}) ) {
+ $ctx->stash('markdown_output', lc $args->{'output'});
+ }
+
+ defined (my $str = $builder->build($ctx, $tokens) )
+ or return $ctx->error($builder->errstr);
+ $str; # return value
+ });
+
+ MT->add_text_filter('markdown' => {
+ label => 'Markdown',
+ docs => 'http://daringfireball.net/projects/markdown/',
+ on_format => sub {
+ my $text = shift;
+ my $ctx = shift;
+ my $raw = 0;
+ if (defined $ctx) {
+ my $output = $ctx->stash('markdown_output');
+ if (defined $output && $output =~ m/^html/i) {
+ $g_empty_element_suffix = ">";
+ $ctx->stash('markdown_output', '');
+ }
+ elsif (defined $output && $output eq 'raw') {
+ $raw = 1;
+ $ctx->stash('markdown_output', '');
+ }
+ else {
+ $raw = 0;
+ $g_empty_element_suffix = " />";
+ }
+ }
+ $text = $raw ? $text : Markdown($text);
+ $text;
+ },
+ });
+
+ # If SmartyPants is loaded, add a combo Markdown/SmartyPants text filter:
+ my $smartypants;
+
+ {
+ no warnings "once";
+ $smartypants = $MT::Template::Context::Global_filters{'smarty_pants'};
+ }
+
+ if ($smartypants) {
+ MT->add_text_filter('markdown_with_smartypants' => {
+ label => 'Markdown With SmartyPants',
+ docs => 'http://daringfireball.net/projects/markdown/',
+ on_format => sub {
+ my $text = shift;
+ my $ctx = shift;
+ if (defined $ctx) {
+ my $output = $ctx->stash('markdown_output');
+ if (defined $output && $output eq 'html') {
+ $g_empty_element_suffix = ">";
+ }
+ else {
+ $g_empty_element_suffix = " />";
+ }
+ }
+ $text = Markdown($text);
+ $text = $smartypants->($text, '1');
+ },
+ });
+ }
+}
+else {
+#### BBEdit/command-line text filter interface ##########################
+# Needs to be hidden from MT (and Blosxom when running in static mode).
+
+ # We're only using $blosxom::version once; tell Perl not to warn us:
+ no warnings 'once';
+ unless ( defined($blosxom::version) ) {
+ use warnings;
+
+ #### Check for command-line switches: #################
+ my %cli_opts;
+ use Getopt::Long;
+ Getopt::Long::Configure('pass_through');
+ GetOptions(\%cli_opts,
+ 'version',
+ 'shortversion',
+ 'html4tags',
+ );
+ if ($cli_opts{'version'}) { # Version info
+ print "\nThis is Markdown, version $VERSION.\n";
+ print "Copyright 2004 John Gruber\n";
+ print "http://daringfireball.net/projects/markdown/\n\n";
+ exit 0;
+ }
+ if ($cli_opts{'shortversion'}) { # Just the version number string.
+ print $VERSION;
+ exit 0;
+ }
+ if ($cli_opts{'html4tags'}) { # Use HTML tag style instead of XHTML
+ $g_empty_element_suffix = ">";
+ }
+
+
+ #### Process incoming text: ###########################
+ my $text;
+ {
+ local $/; # Slurp the whole file
+ $text = <>;
+ }
+ print Markdown($text);
+ }
+}
+
+
+
+sub Markdown {
+#
+# Main function. The order in which other subs are called here is
+# essential. Link and image substitutions need to happen before
+# _EscapeSpecialChars(), so that any *'s or _'s in the <a>
+# and <img> tags get encoded.
+#
+ my $text = shift;
+
+ # Clear the global hashes. If we don't clear these, you get conflicts
+ # from other articles when generating a page which contains more than
+ # one article (e.g. an index page that shows the N most recent
+ # articles):
+ %g_urls = ();
+ %g_titles = ();
+ %g_html_blocks = ();
+
+
+ # Standardize line endings:
+ $text =~ s{\r\n}{\n}g; # DOS to Unix
+ $text =~ s{\r}{\n}g; # Mac to Unix
+
+ # Make sure $text ends with a couple of newlines:
+ $text .= "\n\n";
+
+ # Convert all tabs to spaces.
+ $text = _Detab($text);
+
+ # Strip any lines consisting only of spaces and tabs.
+ # This makes subsequent regexen easier to write, because we can
+ # match consecutive blank lines with /\n+/ instead of something
+ # contorted like /[ \t]*\n+/ .
+ $text =~ s/^[ \t]+$//mg;
+
+ # Turn block-level HTML blocks into hash entries
+ $text = _HashHTMLBlocks($text);
+
+ # Strip link definitions, store in hashes.
+ $text = _StripLinkDefinitions($text);
+
+ $text = _RunBlockGamut($text);
+
+ $text = _UnescapeSpecialChars($text);
+
+ return $text . "\n";
+}
+
+
+sub _StripLinkDefinitions {
+#
+# Strips link definitions from text, stores the URLs and titles in
+# hash references.
+#
+ my $text = shift;
+ my $less_than_tab = $g_tab_width - 1;
+
+ # Link defs are in the form: ^[id]: url "optional title"
+ while ($text =~ s{
+ ^[ ]{0,$less_than_tab}\[(.+)\]: # id = $1
+ [ \t]*
+ \n? # maybe *one* newline
+ [ \t]*
+ <?(\S+?)>? # url = $2
+ [ \t]*
+ \n? # maybe one newline
+ [ \t]*
+ (?:
+ (?<=\s) # lookbehind for whitespace
+ ["(]
+ (.+?) # title = $3
+ [")]
+ [ \t]*
+ )? # title is optional
+ (?:\n+|\Z)
+ }
+ {}mx) {
+ $g_urls{lc $1} = _EncodeAmpsAndAngles( $2 ); # Link IDs are case-insensitive
+ if ($3) {
+ $g_titles{lc $1} = $3;
+ $g_titles{lc $1} =~ s/"/&quot;/g;
+ }
+ }
+
+ return $text;
+}
+
+
+sub _HashHTMLBlocks {
+ my $text = shift;
+ my $less_than_tab = $g_tab_width - 1;
+
+ # Hashify HTML blocks:
+ # We only want to do this for block-level HTML tags, such as headers,
+ # lists, and tables. That's because we still want to wrap <p>s around
+ # "paragraphs" that are wrapped in non-block-level tags, such as anchors,
+ # phrase emphasis, and spans. The list of tags we're looking for is
+ # hard-coded:
+ my $block_tags_a = qr/p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|script|noscript|form|fieldset|iframe|math|ins|del/;
+ my $block_tags_b = qr/p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|script|noscript|form|fieldset|iframe|math/;
+
+ # First, look for nested blocks, e.g.:
+ # <div>
+ # <div>
+ # tags for inner block must be indented.
+ # </div>
+ # </div>
+ #
+ # The outermost tags must start at the left margin for this to match, and
+ # the inner nested divs must be indented.
+ # We need to do this before the next, more liberal match, because the next
+ # match will start at the first `<div>` and stop at the first `</div>`.
+ $text =~ s{
+ ( # save in $1
+ ^ # start of line (with /m)
+ <($block_tags_a) # start tag = $2
+ \b # word break
+ (.*\n)*? # any number of lines, minimally matching
+ </\2> # the matching end tag
+ [ \t]* # trailing spaces/tabs
+ (?=\n+|\Z) # followed by a newline or end of document
+ )
+ }{
+ my $key = md5_hex($1);
+ $g_html_blocks{$key} = $1;
+ "\n\n" . $key . "\n\n";
+ }egmx;
+
+
+ #
+ # Now match more liberally, simply from `\n<tag>` to `</tag>\n`
+ #
+ $text =~ s{
+ ( # save in $1
+ ^ # start of line (with /m)
+ <($block_tags_b) # start tag = $2
+ \b # word break
+ (.*\n)*? # any number of lines, minimally matching
+ .*</\2> # the matching end tag
+ [ \t]* # trailing spaces/tabs
+ (?=\n+|\Z) # followed by a newline or end of document
+ )
+ }{
+ my $key = md5_hex($1);
+ $g_html_blocks{$key} = $1;
+ "\n\n" . $key . "\n\n";
+ }egmx;
+ # Special case just for <hr />. It was easier to make a special case than
+ # to make the other regex more complicated.
+ $text =~ s{
+ (?:
+ (?<=\n\n) # Starting after a blank line
+ | # or
+ \A\n? # the beginning of the doc
+ )
+ ( # save in $1
+ [ ]{0,$less_than_tab}
+ <(hr) # start tag = $2
+ \b # word break
+ ([^<>])*? #
+ /?> # the matching end tag
+ [ \t]*
+ (?=\n{2,}|\Z) # followed by a blank line or end of document
+ )
+ }{
+ my $key = md5_hex($1);
+ $g_html_blocks{$key} = $1;
+ "\n\n" . $key . "\n\n";
+ }egx;
+
+ # Special case for standalone HTML comments:
+ $text =~ s{
+ (?:
+ (?<=\n\n) # Starting after a blank line
+ | # or
+ \A\n? # the beginning of the doc
+ )
+ ( # save in $1
+ [ ]{0,$less_than_tab}
+ (?s:
+ <!
+ (--.*?--\s*)+
+ >
+ )
+ [ \t]*
+ (?=\n{2,}|\Z) # followed by a blank line or end of document
+ )
+ }{
+ my $key = md5_hex($1);
+ $g_html_blocks{$key} = $1;
+ "\n\n" . $key . "\n\n";
+ }egx;
+
+
+ return $text;
+}
+
+
+sub _RunBlockGamut {
+#
+# These are all the transformations that form block-level
+# tags like paragraphs, headers, and list items.
+#
+ my $text = shift;
+
+ $text = _DoHeaders($text);
+
+ # Do Horizontal Rules:
+ $text =~ s{^[ ]{0,2}([ ]?\*[ ]?){3,}[ \t]*$}{\n<hr$g_empty_element_suffix\n}gmx;
+ $text =~ s{^[ ]{0,2}([ ]? -[ ]?){3,}[ \t]*$}{\n<hr$g_empty_element_suffix\n}gmx;
+ $text =~ s{^[ ]{0,2}([ ]? _[ ]?){3,}[ \t]*$}{\n<hr$g_empty_element_suffix\n}gmx;
+
+ $text = _DoLists($text);
+
+ $text = _DoCodeBlocks($text);
+
+ $text = _DoBlockQuotes($text);
+
+ # We already ran _HashHTMLBlocks() before, in Markdown(), but that
+ # was to escape raw HTML in the original Markdown source. This time,
+ # we're escaping the markup we've just created, so that we don't wrap
+ # <p> tags around block-level tags.
+ $text = _HashHTMLBlocks($text);
+
+ $text = _FormParagraphs($text);
+
+ return $text;
+}
+
+
+sub _RunSpanGamut {
+#
+# These are all the transformations that occur *within* block-level
+# tags like paragraphs, headers, and list items.
+#
+ my $text = shift;
+
+ $text = _DoCodeSpans($text);
+
+ $text = _EscapeSpecialChars($text);
+
+ # Process anchor and image tags. Images must come first,
+ # because ![foo][f] looks like an anchor.
+ $text = _DoImages($text);
+ $text = _DoAnchors($text);
+
+ # Make links out of things like `<http://example.com/>`
+ # Must come after _DoAnchors(), because you can use < and >
+ # delimiters in inline links like [this](<url>).
+ $text = _DoAutoLinks($text);
+
+ $text = _EncodeAmpsAndAngles($text);
+
+ $text = _DoItalicsAndBold($text);
+
+ # Do hard breaks:
+ $text =~ s/ {2,}\n/ <br$g_empty_element_suffix\n/g;
+
+ return $text;
+}
+
+
+sub _EscapeSpecialChars {
+ my $text = shift;
+ my $tokens ||= _TokenizeHTML($text);
+
+ $text = ''; # rebuild $text from the tokens
+# my $in_pre = 0; # Keep track of when we're inside <pre> or <code> tags.
+# my $tags_to_skip = qr!<(/?)(?:pre|code|kbd|script|math)[\s>]!;
+
+ foreach my $cur_token (@$tokens) {
+ if ($cur_token->[0] eq "tag") {
+ # Within tags, encode * and _ so they don't conflict
+ # with their use in Markdown for italics and strong.
+ # We're replacing each such character with its
+ # corresponding MD5 checksum value; this is likely
+ # overkill, but it should prevent us from colliding
+ # with the escape values by accident.
+ $cur_token->[1] =~ s! \* !$g_escape_table{'*'}!gx;
+ $cur_token->[1] =~ s! _ !$g_escape_table{'_'}!gx;
+ $text .= $cur_token->[1];
+ } else {
+ my $t = $cur_token->[1];
+ $t = _EncodeBackslashEscapes($t);
+ $text .= $t;
+ }
+ }
+ return $text;
+}
+
+
+sub _DoAnchors {
+#
+# Turn Markdown link shortcuts into XHTML <a> tags.
+#
+ my $text = shift;
+
+ #
+ # First, handle reference-style links: [link text] [id]
+ #
+ $text =~ s{
+ ( # wrap whole match in $1
+ \[
+ ($g_nested_brackets) # link text = $2
+ \]
+
+ [ ]? # one optional space
+ (?:\n[ ]*)? # one optional newline followed by spaces
+
+ \[
+ (.*?) # id = $3
+ \]
+ )
+ }{
+ my $result;
+ my $whole_match = $1;
+ my $link_text = $2;
+ my $link_id = lc $3;
+
+ if ($link_id eq "") {
+ $link_id = lc $link_text; # for shortcut links like [this][].
+ }
+
+ if (defined $g_urls{$link_id}) {
+ my $url = $g_urls{$link_id};
+ $url =~ s! \* !$g_escape_table{'*'}!gx; # We've got to encode these to avoid
+ $url =~ s! _ !$g_escape_table{'_'}!gx; # conflicting with italics/bold.
+ $result = "<a href=\"$url\"";
+ if ( defined $g_titles{$link_id} ) {
+ my $title = $g_titles{$link_id};
+ $title =~ s! \* !$g_escape_table{'*'}!gx;
+ $title =~ s! _ !$g_escape_table{'_'}!gx;
+ $result .= " title=\"$title\"";
+ }
+ $result .= ">$link_text</a>";
+ }
+ else {
+ $result = $whole_match;
+ }
+ $result;
+ }xsge;
+
+ #
+ # Next, inline-style links: [link text](url "optional title")
+ #
+ $text =~ s{
+ ( # wrap whole match in $1
+ \[
+ ($g_nested_brackets) # link text = $2
+ \]
+ \( # literal paren
+ [ \t]*
+ <?(.*?)>? # href = $3
+ [ \t]*
+ ( # $4
+ (['"]) # quote char = $5
+ (.*?) # Title = $6
+ \5 # matching quote
+ )? # title is optional
+ \)
+ )
+ }{
+ my $result;
+ my $whole_match = $1;
+ my $link_text = $2;
+ my $url = $3;
+ my $title = $6;
+
+ $url =~ s! \* !$g_escape_table{'*'}!gx; # We've got to encode these to avoid
+ $url =~ s! _ !$g_escape_table{'_'}!gx; # conflicting with italics/bold.
+ $result = "<a href=\"$url\"";
+
+ if (defined $title) {
+ $title =~ s/"/&quot;/g;
+ $title =~ s! \* !$g_escape_table{'*'}!gx;
+ $title =~ s! _ !$g_escape_table{'_'}!gx;
+ $result .= " title=\"$title\"";
+ }
+
+ $result .= ">$link_text</a>";
+
+ $result;
+ }xsge;
+
+ return $text;
+}
+
+
+sub _DoImages {
+#
+# Turn Markdown image shortcuts into <img> tags.
+#
+ my $text = shift;
+
+ #
+ # First, handle reference-style labeled images: ![alt text][id]
+ #
+ $text =~ s{
+ ( # wrap whole match in $1
+ !\[
+ (.*?) # alt text = $2
+ \]
+
+ [ ]? # one optional space
+ (?:\n[ ]*)? # one optional newline followed by spaces
+
+ \[
+ (.*?) # id = $3
+ \]
+
+ )
+ }{
+ my $result;
+ my $whole_match = $1;
+ my $alt_text = $2;
+ my $link_id = lc $3;
+
+ if ($link_id eq "") {
+ $link_id = lc $alt_text; # for shortcut links like ![this][].
+ }
+
+ $alt_text =~ s/"/&quot;/g;
+ if (defined $g_urls{$link_id}) {
+ my $url = $g_urls{$link_id};
+ $url =~ s! \* !$g_escape_table{'*'}!gx; # We've got to encode these to avoid
+ $url =~ s! _ !$g_escape_table{'_'}!gx; # conflicting with italics/bold.
+ $result = "<img src=\"$url\" alt=\"$alt_text\"";
+ if (defined $g_titles{$link_id}) {
+ my $title = $g_titles{$link_id};
+ $title =~ s! \* !$g_escape_table{'*'}!gx;
+ $title =~ s! _ !$g_escape_table{'_'}!gx;
+ $result .= " title=\"$title\"";
+ }
+ $result .= $g_empty_element_suffix;
+ }
+ else {
+ # If there's no such link ID, leave intact:
+ $result = $whole_match;
+ }
+
+ $result;
+ }xsge;
+
+ #
+ # Next, handle inline images: ![alt text](url "optional title")
+ # Don't forget: encode * and _
+
+ $text =~ s{
+ ( # wrap whole match in $1
+ !\[
+ (.*?) # alt text = $2
+ \]
+ \( # literal paren
+ [ \t]*
+ <?(\S+?)>? # src url = $3
+ [ \t]*
+ ( # $4
+ (['"]) # quote char = $5
+ (.*?) # title = $6
+ \5 # matching quote
+ [ \t]*
+ )? # title is optional
+ \)
+ )
+ }{
+ my $result;
+ my $whole_match = $1;
+ my $alt_text = $2;
+ my $url = $3;
+ my $title = '';
+ if (defined($6)) {
+ $title = $6;
+ }
+
+ $alt_text =~ s/"/&quot;/g;
+ $title =~ s/"/&quot;/g;
+ $url =~ s! \* !$g_escape_table{'*'}!gx; # We've got to encode these to avoid
+ $url =~ s! _ !$g_escape_table{'_'}!gx; # conflicting with italics/bold.
+ $result = "<img src=\"$url\" alt=\"$alt_text\"";
+ if (defined $title) {
+ $title =~ s! \* !$g_escape_table{'*'}!gx;
+ $title =~ s! _ !$g_escape_table{'_'}!gx;
+ $result .= " title=\"$title\"";
+ }
+ $result .= $g_empty_element_suffix;
+
+ $result;
+ }xsge;
+
+ return $text;
+}
+
+
+sub _DoHeaders {
+ my $text = shift;
+
+ # Setext-style headers:
+ # Header 1
+ # ========
+ #
+ # Header 2
+ # --------
+ #
+ $text =~ s{ ^(.+)[ \t]*\n=+[ \t]*\n+ }{
+ "<h1>" . _RunSpanGamut($1) . "</h1>\n\n";
+ }egmx;
+
+ $text =~ s{ ^(.+)[ \t]*\n-+[ \t]*\n+ }{
+ "<h2>" . _RunSpanGamut($1) . "</h2>\n\n";
+ }egmx;
+
+
+ # atx-style headers:
+ # # Header 1
+ # ## Header 2
+ # ## Header 2 with closing hashes ##
+ # ...
+ # ###### Header 6
+ #
+ $text =~ s{
+ ^(\#{1,6}) # $1 = string of #'s
+ [ \t]*
+ (.+?) # $2 = Header text
+ [ \t]*
+ \#* # optional closing #'s (not counted)
+ \n+
+ }{
+ my $h_level = length($1);
+ "<h$h_level>" . _RunSpanGamut($2) . "</h$h_level>\n\n";
+ }egmx;
+
+ return $text;
+}
+
+
+sub _DoLists {
+#
+# Form HTML ordered (numbered) and unordered (bulleted) lists.
+#
+ my $text = shift;
+ my $less_than_tab = $g_tab_width - 1;
+
+ # Re-usable patterns to match list item bullets and number markers:
+ my $marker_ul = qr/[*+-]/;
+ my $marker_ol = qr/\d+[.]/;
+ my $marker_any = qr/(?:$marker_ul|$marker_ol)/;
+
+ # Re-usable pattern to match any entirel ul or ol list:
+ my $whole_list = qr{
+ ( # $1 = whole list
+ ( # $2
+ [ ]{0,$less_than_tab}
+ (${marker_any}) # $3 = first list item marker
+ [ \t]+
+ )
+ (?s:.+?)
+ ( # $4
+ \z
+ |
+ \n{2,}
+ (?=\S)
+ (?! # Negative lookahead for another list item marker
+ [ \t]*
+ ${marker_any}[ \t]+
+ )
+ )
+ )
+ }mx;
+
+ # We use a different prefix before nested lists than top-level lists.
+ # See extended comment in _ProcessListItems().
+ #
+ # Note: There's a bit of duplication here. My original implementation
+ # created a scalar regex pattern as the conditional result of the test on
+ # $g_list_level, and then only ran the $text =~ s{...}{...}egmx
+ # substitution once, using the scalar as the pattern. This worked,
+ # everywhere except when running under MT on my hosting account at Pair
+ # Networks. There, this caused all rebuilds to be killed by the reaper (or
+ # perhaps they crashed, but that seems incredibly unlikely given that the
+ # same script on the same server ran fine *except* under MT. I've spent
+ # more time trying to figure out why this is happening than I'd like to
+ # admit. My only guess, backed up by the fact that this workaround works,
+ # is that Perl optimizes the substition when it can figure out that the
+ # pattern will never change, and when this optimization isn't on, we run
+ # afoul of the reaper. Thus, the slightly redundant code to that uses two
+ # static s/// patterns rather than one conditional pattern.
+
+ if ($g_list_level) {
+ $text =~ s{
+ ^
+ $whole_list
+ }{
+ my $list = $1;
+ my $list_type = ($3 =~ m/$marker_ul/) ? "ul" : "ol";
+ # Turn double returns into triple returns, so that we can make a
+ # paragraph for the last item in a list, if necessary:
+ $list =~ s/\n{2,}/\n\n\n/g;
+ my $result = _ProcessListItems($list, $marker_any);
+ $result = "<$list_type>\n" . $result . "</$list_type>\n";
+ $result;
+ }egmx;
+ }
+ else {
+ $text =~ s{
+ (?:(?<=\n\n)|\A\n?)
+ $whole_list
+ }{
+ my $list = $1;
+ my $list_type = ($3 =~ m/$marker_ul/) ? "ul" : "ol";
+ # Turn double returns into triple returns, so that we can make a
+ # paragraph for the last item in a list, if necessary:
+ $list =~ s/\n{2,}/\n\n\n/g;
+ my $result = _ProcessListItems($list, $marker_any);
+ $result = "<$list_type>\n" . $result . "</$list_type>\n";
+ $result;
+ }egmx;
+ }
+
+
+ return $text;
+}
+
+
+sub _ProcessListItems {
+#
+# Process the contents of a single ordered or unordered list, splitting it
+# into individual list items.
+#
+
+ my $list_str = shift;
+ my $marker_any = shift;
+
+
+ # The $g_list_level global keeps track of when we're inside a list.
+ # Each time we enter a list, we increment it; when we leave a list,
+ # we decrement. If it's zero, we're not in a list anymore.
+ #
+ # We do this because when we're not inside a list, we want to treat
+ # something like this:
+ #
+ # I recommend upgrading to version
+ # 8. Oops, now this line is treated
+ # as a sub-list.
+ #
+ # As a single paragraph, despite the fact that the second line starts
+ # with a digit-period-space sequence.
+ #
+ # Whereas when we're inside a list (or sub-list), that line will be
+ # treated as the start of a sub-list. What a kludge, huh? This is
+ # an aspect of Markdown's syntax that's hard to parse perfectly
+ # without resorting to mind-reading. Perhaps the solution is to
+ # change the syntax rules such that sub-lists must start with a
+ # starting cardinal number; e.g. "1." or "a.".
+
+ $g_list_level++;
+
+ # trim trailing blank lines:
+ $list_str =~ s/\n{2,}\z/\n/;
+
+
+ $list_str =~ s{
+ (\n)? # leading line = $1
+ (^[ \t]*) # leading whitespace = $2
+ ($marker_any) [ \t]+ # list marker = $3
+ ((?s:.+?) # list item text = $4
+ (\n{1,2}))
+ (?= \n* (\z | \2 ($marker_any) [ \t]+))
+ }{
+ my $item = $4;
+ my $leading_line = $1;
+ my $leading_space = $2;
+
+ if ($leading_line or ($item =~ m/\n{2,}/)) {
+ $item = _RunBlockGamut(_Outdent($item));
+ }
+ else {
+ # Recursion for sub-lists:
+ $item = _DoLists(_Outdent($item));
+ chomp $item;
+ $item = _RunSpanGamut($item);
+ }
+
+ "<li>" . $item . "</li>\n";
+ }egmx;
+
+ $g_list_level--;
+ return $list_str;
+}
+
+
+
+sub _DoCodeBlocks {
+#
+# Process Markdown `<pre><code>` blocks.
+#
+
+ my $text = shift;
+
+ $text =~ s{
+ (?:\n\n|\A)
+ ( # $1 = the code block -- one or more lines, starting with a space/tab
+ (?:
+ (?:[ ]{$g_tab_width} | \t) # Lines must start with a tab or a tab-width of spaces
+ .*\n+
+ )+
+ )
+ ((?=^[ ]{0,$g_tab_width}\S)|\Z) # Lookahead for non-space at line-start, or end of doc
+ }{
+ my $codeblock = $1;
+ my $result; # return value
+
+ $codeblock = _EncodeCode(_Outdent($codeblock));
+ $codeblock = _Detab($codeblock);
+ $codeblock =~ s/\A\n+//; # trim leading newlines
+ $codeblock =~ s/\s+\z//; # trim trailing whitespace
+
+ $result = "\n\n<pre><code>" . $codeblock . "\n</code></pre>\n\n";
+
+ $result;
+ }egmx;
+
+ return $text;
+}
+
+
+sub _DoCodeSpans {
+#
+# * Backtick quotes are used for <code></code> spans.
+#
+# * You can use multiple backticks as the delimiters if you want to
+# include literal backticks in the code span. So, this input:
+#
+# Just type ``foo `bar` baz`` at the prompt.
+#
+# Will translate to:
+#
+# <p>Just type <code>foo `bar` baz</code> at the prompt.</p>
+#
+# There's no arbitrary limit to the number of backticks you
+# can use as delimters. If you need three consecutive backticks
+# in your code, use four for delimiters, etc.
+#
+# * You can use spaces to get literal backticks at the edges:
+#
+# ... type `` `bar` `` ...
+#
+# Turns to:
+#
+# ... type <code>`bar`</code> ...
+#
+
+ my $text = shift;
+
+ $text =~ s@
+ (`+) # $1 = Opening run of `
+ (.+?) # $2 = The code block
+ (?<!`)
+ \1 # Matching closer
+ (?!`)
+ @
+ my $c = "$2";
+ $c =~ s/^[ \t]*//g; # leading whitespace
+ $c =~ s/[ \t]*$//g; # trailing whitespace
+ $c = _EncodeCode($c);
+ "<code>$c</code>";
+ @egsx;
+
+ return $text;
+}
+
+
+sub _EncodeCode {
+#
+# Encode/escape certain characters inside Markdown code runs.
+# The point is that in code, these characters are literals,
+# and lose their special Markdown meanings.
+#
+ local $_ = shift;
+
+ # Encode all ampersands; HTML entities are not
+ # entities within a Markdown code span.
+ s/&/&amp;/g;
+
+ # Encode $'s, but only if we're running under Blosxom.
+ # (Blosxom interpolates Perl variables in article bodies.)
+ {
+ no warnings 'once';
+ if (defined($blosxom::version)) {
+ s/\$/&#036;/g;
+ }
+ }
+
+
+ # Do the angle bracket song and dance:
+ s! < !&lt;!gx;
+ s! > !&gt;!gx;
+
+ # Now, escape characters that are magic in Markdown:
+ s! \* !$g_escape_table{'*'}!gx;
+ s! _ !$g_escape_table{'_'}!gx;
+ s! { !$g_escape_table{'{'}!gx;
+ s! } !$g_escape_table{'}'}!gx;
+ s! \[ !$g_escape_table{'['}!gx;
+ s! \] !$g_escape_table{']'}!gx;
+ s! \\ !$g_escape_table{'\\'}!gx;
+
+ return $_;
+}
+
+
+sub _DoItalicsAndBold {
+ my $text = shift;
+
+ # <strong> must go first:
+ $text =~ s{ (\*\*|__) (?=\S) (.+?[*_]*) (?<=\S) \1 }
+ {<strong>$2</strong>}gsx;
+
+ $text =~ s{ (\*|_) (?=\S) (.+?) (?<=\S) \1 }
+ {<em>$2</em>}gsx;
+
+ return $text;
+}
+
+
+sub _DoBlockQuotes {
+ my $text = shift;
+
+ $text =~ s{
+ ( # Wrap whole match in $1
+ (
+ ^[ \t]*>[ \t]? # '>' at the start of a line
+ .+\n # rest of the first line
+ (.+\n)* # subsequent consecutive lines
+ \n* # blanks
+ )+
+ )
+ }{
+ my $bq = $1;
+ $bq =~ s/^[ \t]*>[ \t]?//gm; # trim one level of quoting
+ $bq =~ s/^[ \t]+$//mg; # trim whitespace-only lines
+ $bq = _RunBlockGamut($bq); # recurse
+
+ $bq =~ s/^/ /g;
+ # These leading spaces screw with <pre> content, so we need to fix that:
+ $bq =~ s{
+ (\s*<pre>.+?</pre>)
+ }{
+ my $pre = $1;
+ $pre =~ s/^ //mg;
+ $pre;
+ }egsx;
+
+ "<blockquote>\n$bq\n</blockquote>\n\n";
+ }egmx;
+
+
+ return $text;
+}
+
+
+sub _FormParagraphs {
+#
+# Params:
+# $text - string to process with html <p> tags
+#
+ my $text = shift;
+
+ # Strip leading and trailing lines:
+ $text =~ s/\A\n+//;
+ $text =~ s/\n+\z//;
+
+ my @grafs = split(/\n{2,}/, $text);
+
+ #
+ # Wrap <p> tags.
+ #
+ foreach (@grafs) {
+ unless (defined( $g_html_blocks{$_} )) {
+ $_ = _RunSpanGamut($_);
+ s/^([ \t]*)/<p>/;
+ $_ .= "</p>";
+ }
+ }
+
+ #
+ # Unhashify HTML blocks
+ #
+ foreach (@grafs) {
+ if (defined( $g_html_blocks{$_} )) {
+ $_ = $g_html_blocks{$_};
+ }
+ }
+
+ return join "\n\n", @grafs;
+}
+
+
+sub _EncodeAmpsAndAngles {
+# Smart processing for ampersands and angle brackets that need to be encoded.
+
+ my $text = shift;
+
+ # Ampersand-encoding based entirely on Nat Irons's Amputator MT plugin:
+ # http://bumppo.net/projects/amputator/
+ $text =~ s/&(?!#?[xX]?(?:[0-9a-fA-F]+|\w+);)/&amp;/g;
+
+ # Encode naked <'s
+ $text =~ s{<(?![a-z/?\$!])}{&lt;}gi;
+
+ return $text;
+}
+
+
+sub _EncodeBackslashEscapes {
+#
+# Parameter: String.
+# Returns: The string, with after processing the following backslash
+# escape sequences.
+#
+ local $_ = shift;
+
+ s! \\\\ !$g_escape_table{'\\'}!gx; # Must process escaped backslashes first.
+ s! \\` !$g_escape_table{'`'}!gx;
+ s! \\\* !$g_escape_table{'*'}!gx;
+ s! \\_ !$g_escape_table{'_'}!gx;
+ s! \\\{ !$g_escape_table{'{'}!gx;
+ s! \\\} !$g_escape_table{'}'}!gx;
+ s! \\\[ !$g_escape_table{'['}!gx;
+ s! \\\] !$g_escape_table{']'}!gx;
+ s! \\\( !$g_escape_table{'('}!gx;
+ s! \\\) !$g_escape_table{')'}!gx;
+ s! \\> !$g_escape_table{'>'}!gx;
+ s! \\\# !$g_escape_table{'#'}!gx;
+ s! \\\+ !$g_escape_table{'+'}!gx;
+ s! \\\- !$g_escape_table{'-'}!gx;
+ s! \\\. !$g_escape_table{'.'}!gx;
+ s{ \\! }{$g_escape_table{'!'}}gx;
+
+ return $_;
+}
+
+
+sub _DoAutoLinks {
+ my $text = shift;
+
+ $text =~ s{<((https?|ftp):[^'">\s]+)>}{<a href="$1">$1</a>}gi;
+
+ # Email addresses: <address@domain.foo>
+ $text =~ s{
+ <
+ (?:mailto:)?
+ (
+ [-.\w]+
+ \@
+ [-a-z0-9]+(\.[-a-z0-9]+)*\.[a-z]+
+ )
+ >
+ }{
+ _EncodeEmailAddress( _UnescapeSpecialChars($1) );
+ }egix;
+
+ return $text;
+}
+
+
+sub _EncodeEmailAddress {
+#
+# Input: an email address, e.g. "foo@example.com"
+#
+# Output: the email address as a mailto link, with each character
+# of the address encoded as either a decimal or hex entity, in
+# the hopes of foiling most address harvesting spam bots. E.g.:
+#
+# <a href="&#x6D;&#97;&#105;&#108;&#x74;&#111;:&#102;&#111;&#111;&#64;&#101;
+# x&#x61;&#109;&#x70;&#108;&#x65;&#x2E;&#99;&#111;&#109;">&#102;&#111;&#111;
+# &#64;&#101;x&#x61;&#109;&#x70;&#108;&#x65;&#x2E;&#99;&#111;&#109;</a>
+#
+# Based on a filter by Matthew Wickline, posted to the BBEdit-Talk
+# mailing list: <http://tinyurl.com/yu7ue>
+#
+
+ my $addr = shift;
+
+ srand;
+ my @encode = (
+ sub { '&#' . ord(shift) . ';' },
+ sub { '&#x' . sprintf( "%X", ord(shift) ) . ';' },
+ sub { shift },
+ );
+
+ $addr = "mailto:" . $addr;
+
+ $addr =~ s{(.)}{
+ my $char = $1;
+ if ( $char eq '@' ) {
+ # this *must* be encoded. I insist.
+ $char = $encode[int rand 1]->($char);
+ } elsif ( $char ne ':' ) {
+ # leave ':' alone (to spot mailto: later)
+ my $r = rand;
+ # roughly 10% raw, 45% hex, 45% dec
+ $char = (
+ $r > .9 ? $encode[2]->($char) :
+ $r < .45 ? $encode[1]->($char) :
+ $encode[0]->($char)
+ );
+ }
+ $char;
+ }gex;
+
+ $addr = qq{<a href="$addr">$addr</a>};
+ $addr =~ s{">.+?:}{">}; # strip the mailto: from the visible part
+
+ return $addr;
+}
+
+
+sub _UnescapeSpecialChars {
+#
+# Swap back in all the special characters we've hidden.
+#
+ my $text = shift;
+
+ while( my($char, $hash) = each(%g_escape_table) ) {
+ $text =~ s/$hash/$char/g;
+ }
+ return $text;
+}
+
+
+sub _TokenizeHTML {
+#
+# Parameter: String containing HTML markup.
+# Returns: Reference to an array of the tokens comprising the input
+# string. Each token is either a tag (possibly with nested,
+# tags contained therein, such as <a href="<MTFoo>">, or a
+# run of text between tags. Each element of the array is a
+# two-element array; the first is either 'tag' or 'text';
+# the second is the actual value.
+#
+#
+# Derived from the _tokenize() subroutine from Brad Choate's MTRegex plugin.
+# <http://www.bradchoate.com/past/mtregex.php>
+#
+
+ my $str = shift;
+ my $pos = 0;
+ my $len = length $str;
+ my @tokens;
+
+ my $depth = 6;
+ my $nested_tags = join('|', ('(?:<[a-z/!$](?:[^<>]') x $depth) . (')*>)' x $depth);
+ my $match = qr/(?s: <! ( -- .*? -- \s* )+ > ) | # comment
+ (?s: <\? .*? \?> ) | # processing instruction
+ $nested_tags/ix; # nested tags
+
+ while ($str =~ m/($match)/g) {
+ my $whole_tag = $1;
+ my $sec_start = pos $str;
+ my $tag_start = $sec_start - length $whole_tag;
+ if ($pos < $tag_start) {
+ push @tokens, ['text', substr($str, $pos, $tag_start - $pos)];
+ }
+ push @tokens, ['tag', $whole_tag];
+ $pos = pos $str;
+ }
+ push @tokens, ['text', substr($str, $pos, $len - $pos)] if $pos < $len;
+ \@tokens;
+}
+
+
+sub _Outdent {
+#
+# Remove one level of line-leading tabs or spaces
+#
+ my $text = shift;
+
+ $text =~ s/^(\t|[ ]{1,$g_tab_width})//gm;
+ return $text;
+}
+
+
+sub _Detab {
+#
+# Cribbed from a post by Bart Lateur:
+# <http://www.nntp.perl.org/group/perl.macperl.anyperl/154>
+#
+ my $text = shift;
+
+ $text =~ s{(.*?)\t}{$1.(' ' x ($g_tab_width - length($1) % $g_tab_width))}ge;
+ return $text;
+}
+
+
+1;
+
+__END__
+
+
+=pod
+
+=head1 NAME
+
+B<Markdown>
+
+
+=head1 SYNOPSIS
+
+B<Markdown.pl> [ B<--html4tags> ] [ B<--version> ] [ B<-shortversion> ]
+ [ I<file> ... ]
+
+
+=head1 DESCRIPTION
+
+Markdown is a text-to-HTML filter; it translates an easy-to-read /
+easy-to-write structured text format into HTML. Markdown's text format
+is most similar to that of plain text email, and supports features such
+as headers, *emphasis*, code blocks, blockquotes, and links.
+
+Markdown's syntax is designed not as a generic markup language, but
+specifically to serve as a front-end to (X)HTML. You can use span-level
+HTML tags anywhere in a Markdown document, and you can use block level
+HTML tags (like <div> and <table> as well).
+
+For more information about Markdown's syntax, see:
+
+ http://daringfireball.net/projects/markdown/
+
+
+=head1 OPTIONS
+
+Use "--" to end switch parsing. For example, to open a file named "-z", use:
+
+ Markdown.pl -- -z
+
+=over 4
+
+
+=item B<--html4tags>
+
+Use HTML 4 style for empty element tags, e.g.:
+
+ <br>
+
+instead of Markdown's default XHTML style tags, e.g.:
+
+ <br />
+
+
+=item B<-v>, B<--version>
+
+Display Markdown's version number and copyright information.
+
+
+=item B<-s>, B<--shortversion>
+
+Display the short-form version number.
+
+
+=back
+
+
+
+=head1 BUGS
+
+To file bug reports or feature requests (other than topics listed in the
+Caveats section above) please send email to:
+
+ support@daringfireball.net
+
+Please include with your report: (1) the example input; (2) the output
+you expected; (3) the output Markdown actually produced.
+
+
+=head1 VERSION HISTORY
+
+See the readme file for detailed release notes for this version.
+
+1.0.1 - 14 Dec 2004
+
+1.0 - 28 Aug 2004
+
+
+=head1 AUTHOR
+
+ John Gruber
+ http://daringfireball.net
+
+ PHP port and other contributions by Michel Fortin
+ http://michelf.com
+
+
+=head1 COPYRIGHT AND LICENSE
+
+Copyright (c) 2003-2004 John Gruber
+<http://daringfireball.net/>
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+* Redistributions of source code must retain the above copyright notice,
+ this list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+* Neither the name "Markdown" nor the names of its contributors may
+ be used to endorse or promote products derived from this software
+ without specific prior written permission.
+
+This software is provided by the copyright holders and contributors "as
+is" and any express or implied warranties, including, but not limited
+to, the implied warranties of merchantability and fitness for a
+particular purpose are disclaimed. In no event shall the copyright owner
+or contributors be liable for any direct, indirect, incidental, special,
+exemplary, or consequential damages (including, but not limited to,
+procurement of substitute goods or services; loss of use, data, or
+profits; or business interruption) however caused and on any theory of
+liability, whether in contract, strict liability, or tort (including
+negligence or otherwise) arising in any way out of the use of this
+software, even if advised of the possibility of such damage.
+
+=cut
diff --git a/bin/contrib/md2html.awk b/bin/contrib/md2html.awk
new file mode 100755
index 0000000..81d1241
--- /dev/null
+++ b/bin/contrib/md2html.awk
@@ -0,0 +1,427 @@
+#!/bin/awk -f
+#
+# by: Jesus Galan (yiyus) 2009
+#
+# Usage: md2html.awk file.md > file.html
+# See: http://4l77.com/src/md2html.awk
+
+function eschtml(t) {
+ gsub("&", "\\&amp;", t);
+ gsub("<", "\\&lt;", t);
+ return t;
+}
+
+function oprint(t){
+ if(nr == 0)
+ print t;
+ else
+ otext = otext "\n" t;
+}
+
+function subref(id){
+ for(; nr > 0 && sub("<<" id, ref[id], otext); nr--);
+ if(nr == 0 && otext) {
+ print otext;
+ otext = "";
+ }
+}
+
+function nextil(t) {
+ if(!match(t, /[`<&\[*_\\-]|(\!\[)/))
+ return t;
+ t1 = substr(t, 1, RSTART - 1);
+ tag = substr(t, RSTART, RLENGTH);
+ t2 = substr(t, RSTART + RLENGTH);
+ if(ilcode && tag != "`")
+ return eschtml(t1 tag) nextil(t2);
+ # Backslash escaping
+ if(tag == "\\"){
+ if(match(t2, /^[\\`*_{}\[\]()#+\-\.!]/)){
+ tag = substr(t2, 1, 1);
+ t2 = substr(t2, 2);
+ }
+ return t1 tag nextil(t2);
+ }
+ # Dashes
+ if(tag == "-"){
+ if(sub(/^-/, "", t2))
+ tag = "&#8212;";
+ return t1 tag nextil(t2);
+ }
+ # Inline Code
+ if(tag == "`"){
+ if(sub(/^`/, "", t2)){
+ if(!match(t2, /``/))
+ return t1 "&#8221;" nextil(t2);
+ ilcode2 = !ilcode2;
+ }
+ else if(ilcode2)
+ return t1 tag nextil(t2);
+ tag = "<code>";
+ if(ilcode){
+ t1 = eschtml(t1);
+ tag = "</code>";
+ }
+ ilcode = !ilcode;
+ return t1 tag nextil(t2);
+ }
+ if(tag == "<"){
+ # Autolinks
+ if(match(t2, /^[^ ]+[\.@][^ ]+>/)){
+ url = eschtml(substr(t2, 1, RLENGTH - 1));
+ t2 = substr(t2, RLENGTH + 1);
+ linktext = url;
+ if(match(url, /@/) && !match(url, /^mailto:/))
+ url = "mailto:" url;
+ return t1 "<a href=\"" url "\">" linktext "</a>" nextil(t2);
+ }
+ # Html tags
+ if(match(t2, /^[A-Za-z\/!][^>]*>/)){
+ tag = tag substr(t2, RSTART, RLENGTH);
+ t2 = substr(t2, RLENGTH + 1);
+ return t1 tag nextil(t2);
+ }
+ return t1 "&lt;" nextil(t2);
+ }
+ # Html special entities
+ if(tag == "&"){
+ if(match(t2, /^#?[A-Za-z0-9]+;/)){
+ tag = tag substr(t2, RSTART, RLENGTH);
+ t2 = substr(t2, RLENGTH + 1);
+ return t1 tag nextil(t2);
+ }
+ return t1 "&amp;" nextil(t2);
+ }
+ # Images
+ if(tag == "!["){
+ if(!match(t2, /(\[.*\])|(\(.*\))/))
+ return t1 tag nextil(t2);
+ match(t2, /^[^\]]*/);
+ alt = substr(t2, 1, RLENGTH);
+ t2 = substr(t2, RLENGTH + 2);
+ if(match(t2, /^\(/)){
+ # Inline
+ sub(/^\(/, "", t2);
+ match(t2, /^[^\)]+/);
+ url = eschtml(substr(t2, 1, RLENGTH));
+ t2 = substr(t2, RLENGTH + 2);
+ title = "";
+ if(match(url, /[ ]+\".*\"[ ]*$/)) {
+ title = substr(url, RSTART, RLENGTH);
+ url = substr(url, 1, RSTART - 1);
+ match(title, /\".*\"/);
+ title = " title=\"" substr(title, RSTART + 1, RLENGTH - 2) "\"";
+ }
+ if(match(url, /^<.*>$/))
+ url = substr(url, 2, RLENGTH - 2);
+ return t1 "<img src=\"" url "\" alt=\"" alt "\"" title " />" nextil(t2);
+ }
+ else{
+ # Referenced
+ sub(/^ ?\[/, "", t2);
+ id = alt;
+ if(match(t2, /^[^\]]+/))
+ id = substr(t2, 1, RLENGTH);
+ t2 = substr(t2, RLENGTH + 2);
+ if(ref[id])
+ r = ref[id];
+ else{
+ r = "<<" id;
+ nr++;
+ }
+ return t1 "<img src=\"" r "\" alt=\"" alt "\" />" nextil(t2);
+ }
+ }
+ # Links
+ if(tag == "["){
+ if(!match(t2, /(\[.*\])|(\(.*\))/))
+ return t1 tag nextil(t2);
+ match(t2, /^[^\]]*(\[[^\]]*\][^\]]*)*/);
+ linktext = substr(t2, 1, RLENGTH);
+ t2 = substr(t2, RLENGTH + 2);
+ if(match(t2, /^\(/)){
+ # Inline
+ match(t2, /^[^\)]+(\([^\)]+\)[^\)]*)*/);
+ url = substr(t2, 2, RLENGTH - 1);
+ pt2 = substr(t2, RLENGTH + 2);
+ title = "";
+ if(match(url, /[ ]+\".*\"[ ]*$/)) {
+ title = substr(url, RSTART, RLENGTH);
+ url = substr(url, 1, RSTART - 1);
+ match(title, /\".*\"/);
+ title = " title=\"" substr(title, RSTART + 1, RLENGTH - 2) "\"";
+ }
+ if(match(url, /^<.*>$/))
+ url = substr(url, 2, RLENGTH - 2);
+ url = eschtml(url);
+ return t1 "<a href=\"" url "\"" title ">" nextil(linktext) "</a>" nextil(pt2);
+ }
+ else{
+ # Referenced
+ sub(/^ ?\[/, "", t2);
+ id = linktext;
+ if(match(t2, /^[^\]]+/))
+ id = substr(t2, 1, RLENGTH);
+ t2 = substr(t2, RLENGTH + 2);
+ if(ref[id])
+ r = ref[id];
+ else{
+ r = "<<" id;
+ nr++;
+ }
+ pt2 = t2;
+ return t1 "<a href=\"" r "\" />" nextil(linktext) "</a>" nextil(pt2);
+ }
+ }
+ # Emphasis
+ if(match(tag, /[*_]/)){
+ ntag = tag;
+ if(sub("^" tag, "", t2)){
+ if(stag[ns] == tag && match(t2, "^" tag))
+ t2 = tag t2;
+ else
+ ntag = tag tag
+ }
+ n = length(ntag);
+ tag = (n == 2) ? "strong" : "em";
+ if(match(t1, / $/) && match(t2, /^ /))
+ return t1 tag nextil(t2);
+ if(stag[ns] == ntag){
+ tag = "/" tag;
+ ns--;
+ }
+ else
+ stag[++ns] = ntag;
+ tag = "<" tag ">";
+ return t1 tag nextil(t2);
+ }
+}
+
+function inline(t) {
+ ilcode = 0;
+ ilcode2 = 0;
+ ns = 0;
+
+ return nextil(t);
+}
+
+function printp(tag) {
+ if(!match(text, /^[ ]*$/)){
+ text = inline(text);
+ if(tag != "")
+ oprint("<" tag ">" text "</" tag ">");
+ else
+ oprint(text);
+ }
+ text = "";
+}
+
+BEGIN {
+ blank = 0;
+ code = 0;
+ hr = 0;
+ html = 0;
+ nl = 0;
+ nr = 0;
+ otext = "";
+ text = "";
+ par = "p";
+}
+
+# References
+!code && /^ *\[[^\]]*\]:[ ]+/ {
+ sub(/^ *\[/, "");
+ match($0, /\]/);
+ id = substr($0, 1, RSTART - 1);
+ sub(id "\\]:[ ]+", "");
+ title = "";
+ if(match($0, /\".*\"$/))
+ title = "\" title=\"" substr($0, RSTART + 1, RLENGTH - 2);
+ sub(/[ ]+\".*\"$/, "");
+ url = eschtml($0);
+ ref[id] = url title;
+
+ subref(id);
+ next;
+}
+
+# html
+!html && /^<(address|blockquote|center|dir|div|dl|fieldset|form|h[1-6r]|\
+isindex|menu|noframes|noscript|ol|p|pre|table|ul|!--)/ {
+ if(code)
+ oprint("</pre></code>");
+ for(; !text && block[nl] == "blockquote"; nl--)
+ oprint("</blockquote>");
+ match($0, /^<(address|blockquote|center|dir|div|dl|fieldset|form|h[1-6r]|\
+ isindex|menu|noframes|noscript|ol|p|pre|table|ul|!--)/);
+ htag = substr($0, 2, RLENGTH - 1);
+ if(!match($0, "(<\\/" htag ">)|((^<hr ?\\/?)|(--)>$)"))
+ html = 1;
+ if(html && match($0, /^<hr/))
+ hr = 1;
+ oprint($0);
+ next;
+}
+
+html && (/(^<\/(address|blockquote|center|dir|div|dl|fieldset|form|h[1-6r]|\
+isindex|menu|noframes|noscript|ol|p|pre|table|ul).*)|(--)>$/ ||
+(hr && />$/)) {
+ html = 0;
+ hr = 0;
+ oprint($0);
+ next;
+}
+
+html {
+ oprint($0);
+ next;
+}
+
+# List and quote blocks
+
+# Remove indentation
+{
+ for(nnl = 0; nnl < nl; nnl++)
+ if((match(block[nnl + 1], /[ou]l/) && !sub(/^( | )/, "")) || \
+ (block[nnl + 1] == "blockquote" && !sub(/^> ?/, "")))
+ break;
+}
+nnl < nl && !blank && text && ! /^ ? ? ?([*+-]|([0-9]+\.)+)( +| )/ { nnl = nl; }
+# Quote blocks
+{
+ while(sub(/^> /, ""))
+ nblock[++nnl] = "blockquote";
+}
+# Horizontal rules
+{ hr = 0; }
+(blank || (!text && !code)) && /^ ? ? ?([-*_][ ]*)([-*_][ ]*)([-*_][ ]*)+$/ {
+ if(code){
+ oprint("</pre></code>");
+ code = 0;
+ }
+ blank = 0;
+ nnl = 0;
+ hr = 1;
+}
+# List items
+block[nl] ~ /[ou]l/ && /^$/ {
+ blank = 1;
+ next;
+}
+{ newli = 0; }
+!hr && (nnl != nl || !text || block[nl] ~ /[ou]l/) && /^ ? ? ?[*+-]( +| )/ {
+ sub(/^ ? ? ?[*+-]( +| )/, "");
+ nnl++;
+ nblock[nnl] = "ul";
+ newli = 1;
+}
+(nnl != nl || !text || block[nl] ~ /[ou]l/) && /^ ? ? ?([0-9]+\.)+( +| )/ {
+ sub(/^ ? ? ?([0-9]+\.)+( +| )/, "");
+ nnl++;
+ nblock[nnl] = "ol";
+ newli = 1;
+}
+newli {
+ if(blank && nnl == nl && !par)
+ par = "p";
+ blank = 0;
+ printp(par);
+ if(nnl == nl && block[nl] == nblock[nl])
+ oprint("</li><li>");
+}
+blank && ! /^$/ {
+ if(match(block[nnl], /[ou]l/) && !par)
+ par = "p";
+ printp(par);
+ par = "p";
+ blank = 0;
+}
+
+# Close old blocks and open new ones
+nnl != nl || nblock[nl] != block[nl] {
+ if(code){
+ oprint("</pre></code>");
+ code = 0;
+ }
+ printp(par);
+ b = (nnl > nl) ? nblock[nnl] : block[nnl];
+ par = (match(b, /[ou]l/)) ? "" : "p";
+}
+nnl < nl || (nnl == nl && nblock[nl] != block[nl]) {
+ for(; nl > nnl || (nnl == nl && pblock[nl] != block[nl]); nl--){
+ if(match(block[nl], /[ou]l/))
+ oprint("</li>");
+ oprint("</" block[nl] ">");
+ }
+}
+nnl > nl {
+ for(; nl < nnl; nl++){
+ block[nl + 1] = nblock[nl + 1];
+ oprint("<" block[nl + 1] ">");
+ if(match(block[nl + 1], /[ou]l/))
+ oprint("<li>");
+ }
+}
+hr {
+ oprint("<hr>");
+ next;
+}
+
+# Code blocks
+code && /^$/ {
+ if(blanK)
+ oprint("");
+ blank = 1;
+ next;
+}
+!text && sub(/^( | )/, "") {
+ if(blanK)
+ oprint("");
+ blank = 0;
+ if(!code)
+ oprint("<code><pre>");
+ code = 1;
+ $0 = eschtml($0);
+ oprint($0);
+ next;
+}
+code {
+ oprint("</pre></code>");
+ code = 0;
+}
+
+# Setex-style Headers
+text && /^=+$/ {printp("h1"); next;}
+text && /^-+$/ {printp("h2"); next;}
+
+# Atx-Style headers
+/^#+/ && (!newli || par=="p" || /^##/) {
+ for(n = 0; n < 6 && sub(/^# */, ""); n++)
+ sub(/#$/, "");
+ par = "h" n;
+}
+
+# Paragraph
+/^$/ {
+ printp(par);
+ par = "p";
+ next;
+}
+
+# Add text
+{ text = (text ? text " " : "") $0; }
+
+END {
+ if(code){
+ oprint("</pre></code>");
+ code = 0;
+ }
+ printp(par);
+ for(; nl > 0; nl--){
+ if(match(block[nl], /[ou]l/))
+ oprint("</li>");
+ oprint("</" block[nl] ">");
+ }
+ gsub(/<<[^\"]*/, "", otext);
+ print(otext);
+}
diff --git a/bin/contrib/rc-httpd/handlers/authorize b/bin/contrib/rc-httpd/handlers/authorize
new file mode 100755
index 0000000..ea4db3e
--- /dev/null
+++ b/bin/contrib/rc-httpd/handlers/authorize
@@ -0,0 +1,6 @@
+#!/bin/rc
+if(~ $REMOTE_USER ''){
+ extra_headers=($extra_headers 'WWW-Authenticate: Basic realm="'$"SERVER_NAME'"')
+ error 401
+ exit
+}
diff --git a/bin/contrib/rc-httpd/handlers/cgi b/bin/contrib/rc-httpd/handlers/cgi
new file mode 100755
index 0000000..2c9a9b9
--- /dev/null
+++ b/bin/contrib/rc-httpd/handlers/cgi
@@ -0,0 +1,46 @@
+#!/bin/rc
+fn filter_headers{
+ response=(200 OK)
+ lines=''
+ done=false
+ while(~ $done false){
+ line=`{getline}
+ head=`{echo $line | awk '{print tolower($1)}'}
+ if(~ $head status:*)
+ response=`{echo $line | awk '{$1="" ; print}'}
+ if not if(~ $line '')
+ done=true
+ if not
+ lines=$"lines^$"line^$cr^'
+'
+ }
+ echo 'HTTP/1.1' $"response^$cr
+ echo -n $"lines
+ do_log $response(1)
+}
+
+fn run_cgi {
+ path=$cgi_path exec $"cgi_bin $params || echo 'Status: 500'
+}
+
+cgi_bin=$1
+cgi_dir=.
+if(! ~ $#* 1)
+ cgi_dir=$*($#*)
+if not if(~ $"cgi_bin /*){
+ cgi_dir=`{basename -d $"cgi_bin}
+ cgi_dir=$"cgi_dir
+}
+if(! ~ $"cgi_bin */*)
+ cgi_bin=./$"cgi_bin
+if(! builtin cd $"cgi_dir >[2]/dev/null || ! test -x $"cgi_bin){
+ error 500
+ exit
+}
+
+run_cgi | {
+ filter_headers
+ emit_extra_headers
+ echo $cr
+ exec cat
+}
diff --git a/bin/contrib/rc-httpd/handlers/dir-index b/bin/contrib/rc-httpd/handlers/dir-index
new file mode 100755
index 0000000..00ff8ce
--- /dev/null
+++ b/bin/contrib/rc-httpd/handlers/dir-index
@@ -0,0 +1,111 @@
+#!/bin/rc
+PATH_INFO=`{echo $PATH_INFO | urldecode.awk}
+full_path=$"FS_ROOT^$"PATH_INFO
+full_path=$"full_path
+if(! test -d $full_path){
+ error 404
+ exit
+}
+if(! test -r $full_path -x $full_path){
+ error 503
+ exit
+}
+do_log 200
+builtin cd $full_path
+if(~ $"NOINDEXFILE ^ $"NOINDEX ''){
+ ifile=index.htm*
+ if(! ~ $ifile(1) *'*'){
+ PATH_INFO=$ifile(1)
+ FS_ROOT=''
+ exec serve-static
+ }
+}
+title=`{echo $SITE_TITLE | sed s,%s,^$"PATH_INFO^,}
+title=$"title
+lso=()
+switch($2){
+case size
+ # ls has no option to sort by size
+ # could pipe it through sort, I suppose
+case date
+ lso=-t
+}
+echo 'HTTP/1.1 200 OK'^$cr
+emit_extra_headers
+echo 'Content-type: text/html'^$cr
+echo $cr
+echo '<html>
+<head>
+<title>'^$title^'</title>
+<style type="text/css">
+ .size {
+ text-align: right;
+ padding-right: 4pt;
+ }
+ .day {
+ text-align: right;
+ padding-right: 3pt;
+ }
+ .datetime {
+ text-align: right;
+ }
+ .name {
+ text-align: right;
+ padding-left: 3pt;
+ }
+</style>
+</head>
+<body>'
+echo '<h1>'^$title^'</h1>'
+if(! ~ $PATH_INFO /)
+ echo '<a href="../">Parent directory</a>'
+echo '<table>'
+ls -lQ $lso | awk '
+function urlencode(loc){
+ # very minimal encoding, just enough for our static-file purposes
+ url=loc
+ gsub("%", "%25", url) # this one first!
+ gsub("\\$", "%24", url)
+ gsub("&", "%26", url)
+ gsub("\\+", "%2B", url)
+ gsub("\\?", "%3F", url)
+ gsub(" ", "%20", url)
+ gsub("\"", "%22", url)
+ gsub("#", "%23", url)
+ return url
+}
+function hrsize(size){
+ if(size > 1073741824) return sprintf("%.1fGB", size/1073741824)
+ if(size > 10485760) return sprintf("%iMB", size/1048576)
+ if(size > 1048576) return sprintf("%.1fMB", size/1048576)
+ if(size > 10240) return sprintf("%iKB", size/1024)
+ if(size > 1024) return sprintf("%.1fKB", size/1024)
+ return sprintf("%iB", size)
+}
+/^(-|a)/ {
+ print "<tr>"
+ print "<td class=\"size\">"hrsize($6)"</td>"
+ print "<td class=\"month\">"$7"</td>"
+ print "<td class=\"day\">"$8"</td>"
+ print "<td class=\"datetime\">"$9"</td>"
+ $1="" ; $2="" ; $3="" ; $4="" ; $5="" ; $6="" ; $7="" ; $8="" ; $9=""
+ sub("^ *?", "")
+ print "<td><a class=\"file name\" href=\""urlencode($0)"\">"$0"</a></td>"
+ print "</tr>"
+ $0=""
+}
+/^d/ {
+ print "<tr>"
+ print "<td class=\"size\"> </td>"
+ print "<td class=\"month\">"$7"</td>"
+ print "<td class=\"day\">"$8"</td>"
+ print "<td class=\"datetime\">"$9"</td>"
+ $1="" ; $2="" ; $3="" ; $4="" ; $5="" ; $6="" ; $7="" ; $8="" ; $9=""
+ sub("^ *?", "")
+ print "<td><a class=\"dir name\" href=\""urlencode($0)"/\">"$0"/</a></td>"
+ print "</tr>"
+}'
+echo '</table>
+
+</body>
+</html>'
diff --git a/bin/contrib/rc-httpd/handlers/error b/bin/contrib/rc-httpd/handlers/error
new file mode 100755
index 0000000..282d870
--- /dev/null
+++ b/bin/contrib/rc-httpd/handlers/error
@@ -0,0 +1,43 @@
+#!/bin/rc
+# DO NOT make this script callable directly from the web!
+fn do_error{
+ echo 'HTTP/1.1 '^$1^$cr
+ emit_extra_headers
+ echo 'Content-type: text/html'^$cr
+ echo $cr
+ echo '<html>
+<head>
+<title>'^$1^'</title>
+</head>
+<body>
+<h1>'^$1^'</h1>'
+ echo $2
+ echo '<p><i>rc-httpd at' $SERVER_NAME '</i>'
+ echo '
+ </body>
+ </html>
+ '
+}
+
+fn 401{
+ do_error '401 Unauthorized' \
+ 'The requested path '^$"location^' requires authorization.'
+}
+
+fn 404{
+ do_error '404 Not Found' \
+ 'The requested path '^$"location^' was not found on this server.'
+}
+
+fn 500{
+ do_error '500 Internal Server Error' \
+ 'The server has encountered an internal misconfiguration and is unable to satisfy your request.'
+}
+
+fn 503{
+ do_error '503 Forbidden' \
+ 'You do not have permission to access '^$"location^' on this server.'
+}
+
+do_log $1
+$1
diff --git a/bin/contrib/rc-httpd/handlers/redirect b/bin/contrib/rc-httpd/handlers/redirect
new file mode 100755
index 0000000..e223091
--- /dev/null
+++ b/bin/contrib/rc-httpd/handlers/redirect
@@ -0,0 +1,30 @@
+#!/bin/rc
+if(~ $#2 0){
+ error 500
+ exit
+}
+switch($1){
+case perm*
+ do_log 301
+ echo 'HTTP/1.1 301 Moved Permanently'^$cr
+case temp*
+ do_log 302
+ echo 'HTTP/1.1 302 Moved Temporarily'^$cr
+case seeother
+ do_log 303
+ echo 'HTTP/1.1 303 See Other'^$cr
+case *
+ error 500
+ exit
+}
+echo 'Location: ' ^ $2 ^ $cr
+emit_extra_headers
+echo 'Content-type: text/html'^$cr
+echo $cr
+echo '<html><body>'
+if(~ $#3 0)
+ echo 'Browser did not accept redirect.'
+if not
+ echo $3
+echo '<a href="'^$"location^'/">Click here</a>'
+echo '</body></html>'
diff --git a/bin/contrib/rc-httpd/handlers/serve-static b/bin/contrib/rc-httpd/handlers/serve-static
new file mode 100755
index 0000000..00cc70a
--- /dev/null
+++ b/bin/contrib/rc-httpd/handlers/serve-static
@@ -0,0 +1,43 @@
+#!/bin/rc
+full_path=`{echo $"FS_ROOT^$"PATH_INFO | urldecode.awk}
+full_path=$"full_path
+if(~ $full_path */)
+ error 503
+if(test -d $full_path){
+ redirect perm $"location^'/' \
+ 'URL not quite right, and browser did not accept redirect.'
+ exit
+}
+if(! test -e $full_path){
+ error 404
+ exit
+}
+if(! test -r $full_path){
+ error 503
+ exit
+}
+do_log 200
+switch($full_path){
+case *.html *.htm
+ type=text/html
+case *.css
+ type=text/css
+case *.txt
+ type='text/plain; charset=utf-8'
+case *.jpg *.jpeg
+ type=image/jpeg
+case *.gif
+ type=image/gif
+case *.png
+ type=image/png
+case *
+ type=`{file -m $full_path || file -i $full_path} # GROSS
+}
+max_age=3600 # 1 hour
+echo 'HTTP/1.1 200 OK'^$cr
+emit_extra_headers
+echo 'Content-type: '^$type^'; charset=utf-8'^$cr
+echo 'Content-length: '^`{ls -l $full_path | awk '{print $6}'}
+echo 'Cache-control: max-age='^$max_age^$cr
+echo $cr
+exec cat $full_path
diff --git a/bin/contrib/rc-httpd/handlers/static-or-cgi b/bin/contrib/rc-httpd/handlers/static-or-cgi
new file mode 100755
index 0000000..4d8a2d4
--- /dev/null
+++ b/bin/contrib/rc-httpd/handlers/static-or-cgi
@@ -0,0 +1,14 @@
+#!/bin/rc
+cgiargs=$*
+
+fn error{
+ if(~ $1 404)
+ exec cgi $cgiargs
+ if not
+ $rc_httpd_dir/handlers/error $1
+}
+
+if(~ $location */)
+ exec cgi $cgiargs
+if not
+ exec serve-static
diff --git a/bin/contrib/rc-httpd/handlers/static-or-index b/bin/contrib/rc-httpd/handlers/static-or-index
new file mode 100755
index 0000000..f0904f8
--- /dev/null
+++ b/bin/contrib/rc-httpd/handlers/static-or-index
@@ -0,0 +1,5 @@
+#!/bin/rc
+if(~ $PATH_INFO */)
+ exec dir-index $params
+if not
+ exec serve-static
diff --git a/bin/contrib/rc-httpd/lib/urldecode.awk b/bin/contrib/rc-httpd/lib/urldecode.awk
new file mode 100755
index 0000000..1dadd00
--- /dev/null
+++ b/bin/contrib/rc-httpd/lib/urldecode.awk
@@ -0,0 +1,39 @@
+# taken from werc
+BEGIN {
+ hextab ["0"] = 0; hextab ["8"] = 8;
+ hextab ["1"] = 1; hextab ["9"] = 9;
+ hextab ["2"] = 2; hextab ["A"] = hextab ["a"] = 10
+ hextab ["3"] = 3; hextab ["B"] = hextab ["b"] = 11;
+ hextab ["4"] = 4; hextab ["C"] = hextab ["c"] = 12;
+ hextab ["5"] = 5; hextab ["D"] = hextab ["d"] = 13;
+ hextab ["6"] = 6; hextab ["E"] = hextab ["e"] = 14;
+ hextab ["7"] = 7; hextab ["F"] = hextab ["f"] = 15;
+}
+{
+ decoded = ""
+ i = 1
+ len = length ($0)
+ while ( i <= len ) {
+ c = substr ($0, i, 1)
+ if ( c == "%" ) {
+ if ( i+2 <= len ) {
+ c1 = substr ($0, i+1, 1)
+ c2 = substr ($0, i+2, 1)
+ if ( hextab [c1] == "" || hextab [c2] == "" ) {
+ print "WARNING: invalid hex encoding: %" c1 c2 | "cat >&2"
+ } else {
+ code = 0 + hextab [c1] * 16 + hextab [c2] + 0
+ c = sprintf ("%c", code)
+ i = i + 2
+ }
+ } else {
+ print "WARNING: invalid % encoding: " substr ($0, i, len - i)
+ }
+ } else if ( c == "+" ) {
+ c = " "
+ }
+ decoded = decoded c
+ ++i
+ }
+ printf "%s", decoded
+}
diff --git a/bin/contrib/rc-httpd/rc-httpd b/bin/contrib/rc-httpd/rc-httpd
new file mode 100755
index 0000000..8e4fad9
--- /dev/null
+++ b/bin/contrib/rc-httpd/rc-httpd
@@ -0,0 +1,102 @@
+#!/bin/rc
+rc_httpd_dir=/home/sl/www/werc/bin/contrib/rc-httpd
+libdir = $rc_httpd_dir/lib
+path=($PLAN9/bin $rc_httpd_dir/handlers $PATH)
+cgi_path=$PLAN9/bin
+SERVER_PORT=80 # default for CGI scripts, may be overridden by the Host header
+extra_headers='Server: rc-httpd'
+cr=
+
+fn do_log{
+ echo `{date} :: $SERVER_NAME :: $request :: \
+ $HTTP_USER_AGENT :: $1 :: $HTTP_REFERER >[1=2]
+}
+
+fn emit_extra_headers{
+ for(header in $extra_headers)
+ echo $"header^$cr
+}
+
+fn getline{ read | sed 's/'^$"cr^'$//g' }
+
+fn terminate{
+ echo `{date} connection terminated >[1=2]
+ exit terminate
+}
+
+fn trim_input{ dd -bs 1 -count $CONTENT_LENGTH }
+
+request=`{getline}
+if(~ $#request 0)
+ terminate
+REQUEST_METHOD=$request(1)
+REQUEST_URI=$request(2)
+reqlines=''
+HTTP_COOKIE=''
+REMOTE_USER=''
+done=false
+chunked=no
+while(~ $"done false){
+ line=`{getline}
+ if(~ $#line 0)
+ done=true
+ reqlines=$"reqlines$"line'
+'
+ h=`{echo $line | awk '{print tolower($1)}'}
+ switch($h){
+ case ''
+ done=true
+ case host:
+ SERVER_NAME=$line(2)
+ case referer:
+ HTTP_REFERER=$line(2)
+ case user-agent:
+ HTTP_USER_AGENT=`{echo $line | sed 's;[^:]+:[ ]+;;'}
+ case content-length:
+ CONTENT_LENGTH=$line(2)
+ case content-type:
+ CONTENT_TYPE=$line(2)
+ case cookie:
+ cookie=`{echo $line | sed 's;^[^:]+:[ ]*;;'}
+ HTTP_COOKIE=$"HTTP_COOKIE^$"cookie^'; '
+ case authorization:
+ REMOTE_USER=`{auth/httpauth $line(3)}
+ case transfer-encoding:
+ ~ $line(2) chunked && chunked=yes
+ }
+}
+if(~ $REQUEST_URI *://* //*){
+ SERVER_NAME=`{echo $REQUEST_URI | sed '
+ s;^[^:]+:;;
+ s;^//([^/]+).*;\1;'}
+ REQUEST_URI=`{echo $REQUEST_URI | sed '
+ s;^[^:]+:;;
+ s;^//[^/]+/?;/;'}
+}
+QUERY_STRING=`{echo $REQUEST_URI | sed 's;[^?]*\??;;'}
+params=`{echo $QUERY_STRING | sed 's;\+; ;g'}
+location=`{echo $REQUEST_URI | sed 's;\?.*;;'}
+location=`{echo $location | sed '
+ s;[^/]+/\.\./;/;g
+ s;/\./;/;g
+ s;//+;/;g
+'}
+SERVER_NAME=`{echo $SERVER_NAME | sed 's;^(\[[^\]]+\]|[^:]+)\:([0-9]+)$;\1 \2;'}
+if(~ $#SERVER_NAME 2){
+ SERVER_PORT=$SERVER_NAME(2)
+ SERVER_NAME=$SERVER_NAME(1)
+}
+if(~ $REQUEST_METHOD (PUT POST)){
+ if(! ~ $"CONTENT_LENGTH '')
+ trim_input | exec $rc_httpd_dir/select-handler
+ if not{
+ if(~ $chunked yes){
+ echo 'HTTP/1.1 411 Length required'^$cr
+ echo $cr
+ exit
+ }
+ exec $rc_httpd_dir/select-handler
+ }
+}
+if not
+ . $rc_httpd_dir/select-handler
diff --git a/bin/contrib/rc-httpd/select-handler b/bin/contrib/rc-httpd/select-handler
new file mode 100755
index 0000000..ec819d4
--- /dev/null
+++ b/bin/contrib/rc-httpd/select-handler
@@ -0,0 +1,20 @@
+#!/bin/rc
+rfork n
+
+# Route requests to werc.
+# Change paths to match your system.
+
+if(~ $SERVER_NAME 9base.werc.cat-v.org)
+ PLAN9=/usr/local/9base
+if(~ $SERVER_NAME frontbase.werc.cat-v.org)
+ PLAN9=/usr/local/plan9front
+if(~ $SERVER_NAME plan9port.werc.cat-v.org)
+ PLAN9=/usr/local/plan9
+
+if(~ $SERVER_NAME *){
+ PATH_INFO=$location
+ FS_ROOT=/home/sl/www/werc/sites/$SERVER_NAME
+ exec static-or-cgi /home/sl/www/werc/bin/werc.rc
+}
+if not
+ error 503
diff --git a/bin/contrib/tcp80 b/bin/contrib/tcp80
new file mode 100755
index 0000000..ae111a0
--- /dev/null
+++ b/bin/contrib/tcp80
@@ -0,0 +1,7 @@
+#!/bin/rc
+# For use with listen(8).
+# Change paths to match your system.
+# Eitdit rc-httpd/rc-httpd to match your system.
+PLAN9=/usr/local/plan9
+PATH=($PATH /home/sl/www/werc/bin/contrib)
+exec /home/sl/www/werc/bin/contrib/rc-httpd/rc-httpd >>[2]/var/log/rc-httpd
diff --git a/bin/contrib/urldecode.awk b/bin/contrib/urldecode.awk
new file mode 100755
index 0000000..bd791e3
--- /dev/null
+++ b/bin/contrib/urldecode.awk
@@ -0,0 +1,39 @@
+#!/bin/awk -f
+BEGIN {
+ hextab ["0"] = 0; hextab ["8"] = 8;
+ hextab ["1"] = 1; hextab ["9"] = 9;
+ hextab ["2"] = 2; hextab ["A"] = hextab ["a"] = 10
+ hextab ["3"] = 3; hextab ["B"] = hextab ["b"] = 11;
+ hextab ["4"] = 4; hextab ["C"] = hextab ["c"] = 12;
+ hextab ["5"] = 5; hextab ["D"] = hextab ["d"] = 13;
+ hextab ["6"] = 6; hextab ["E"] = hextab ["e"] = 14;
+ hextab ["7"] = 7; hextab ["F"] = hextab ["f"] = 15;
+}
+{
+ decoded = ""
+ i = 1
+ len = length ($0)
+ while ( i <= len ) {
+ c = substr ($0, i, 1)
+ if ( c == "%" ) {
+ if ( i+2 <= len ) {
+ c1 = substr ($0, i+1, 1)
+ c2 = substr ($0, i+2, 1)
+ if ( hextab [c1] == "" || hextab [c2] == "" ) {
+ print "WARNING: invalid hex encoding: %" c1 c2 | "cat >&2"
+ } else {
+ code = 0 + hextab [c1] * 16 + hextab [c2] + 0
+ c = sprintf ("%c", code)
+ i = i + 2
+ }
+ } else {
+ print "WARNING: invalid % encoding: " substr ($0, i, len - i)
+ }
+ } else if ( c == "+" ) {
+ c = " "
+ }
+ decoded = decoded c
+ ++i
+ }
+ print decoded
+}
diff --git a/bin/contrib/urlencode.awk b/bin/contrib/urlencode.awk
new file mode 100755
index 0000000..d4d354d
--- /dev/null
+++ b/bin/contrib/urlencode.awk
@@ -0,0 +1,126 @@
+# Taken from http://www.shelldorado.com/scripts/cmds/urlencode
+##########################################################################
+# Title : urlencode - encode URL data
+# Author : Heiner Steven (heiner.steven@odn.de)
+# Date : 2000-03-15
+# Requires : awk
+# Categories : File Conversion, WWW, CGI
+# SCCS-Id. : @(#) urlencode 1.4 06/10/29
+##########################################################################
+# Description
+# Encode data according to
+# RFC 1738: "Uniform Resource Locators (URL)" and
+# RFC 1866: "Hypertext Markup Language - 2.0" (HTML)
+#
+# This encoding is used i.e. for the MIME type
+# "application/x-www-form-urlencoded"
+#
+# Notes
+# o The default behaviour is not to encode the line endings. This
+# may not be what was intended, because the result will be
+# multiple lines of output (which cannot be used in an URL or a
+# HTTP "POST" request). If the desired output should be one
+# line, use the "-l" option.
+#
+# o The "-l" option assumes, that the end-of-line is denoted by
+# the character LF (ASCII 10). This is not true for Windows or
+# Mac systems, where the end of a line is denoted by the two
+# characters CR LF (ASCII 13 10).
+# We use this for symmetry; data processed in the following way:
+# cat | urlencode -l | urldecode -l
+# should (and will) result in the original data
+#
+# o Large lines (or binary files) will break many AWK
+# implementations. If you get the message
+# awk: record `...' too long
+# record number xxx
+# consider using GNU AWK (gawk).
+#
+# o urlencode will always terminate it's output with an EOL
+# character
+#
+# Thanks to Stefan Brozinski for pointing out a bug related to non-standard
+# locales.
+#
+# See also
+# urldecode
+##########################################################################
+
+PN=`basename "$0"` # Program name
+VER='1.4'
+
+: ${AWK=awk}
+
+Usage () {
+ echo >&2 "$PN - encode URL data, $VER
+usage: $PN [-l] [file ...]
+ -l: encode line endings (result will be one line of output)
+
+The default is to encode each input line on its own."
+ exit 1
+}
+
+Msg () {
+ for MsgLine
+ do echo "$PN: $MsgLine" >&2
+ done
+}
+
+Fatal () { Msg "$@"; exit 1; }
+
+set -- `getopt hl "$@" 2>/dev/null` || Usage
+[ $# -lt 1 ] && Usage # "getopt" detected an error
+
+EncodeEOL=no
+while [ $# -gt 0 ]
+do
+ case "$1" in
+ -l) EncodeEOL=yes;;
+ --) shift; break;;
+ -h) Usage;;
+ -*) Usage;;
+ *) break;; # First file name
+ esac
+ shift
+done
+
+LANG=C export LANG
+$AWK '
+ BEGIN {
+ # We assume an awk implementation that is just plain dumb.
+ # We will convert an character to its ASCII value with the
+ # table ord[], and produce two-digit hexadecimal output
+ # without the printf("%02X") feature.
+
+ EOL = "%0A" # "end of line" string (encoded)
+ split ("1 2 3 4 5 6 7 8 9 A B C D E F", hextab, " ")
+ hextab [0] = 0
+ for ( i=1; i<=255; ++i ) ord [ sprintf ("%c", i) "" ] = i + 0
+ if ("'"$EncodeEOL"'" == "yes") EncodeEOL = 1; else EncodeEOL = 0
+ }
+ {
+ encoded = ""
+ for ( i=1; i<=length ($0); ++i ) {
+ c = substr ($0, i, 1)
+ if ( c ~ /[a-zA-Z0-9.-]/ ) {
+ encoded = encoded c # safe character
+ } else if ( c == " " ) {
+ encoded = encoded "+" # special handling
+ } else {
+ # unsafe character, encode it as a two-digit hex-number
+ lo = ord [c] % 16
+ hi = int (ord [c] / 16);
+ encoded = encoded "%" hextab [hi] hextab [lo]
+ }
+ }
+ if ( EncodeEOL ) {
+ printf ("%s", encoded EOL)
+ } else {
+ print encoded
+ }
+ }
+ END {
+ #if ( EncodeEOL ) print ""
+ }
+' "$@"
+
diff --git a/bin/contrib/webserver.rc b/bin/contrib/webserver.rc
new file mode 100755
index 0000000..8044565
--- /dev/null
+++ b/bin/contrib/webserver.rc
@@ -0,0 +1,30 @@
+#!/bin/rc
+
+# A web server in rc by maht
+# Originally from http://www.proweb.co.uk/~matt/rc/webserver.rc
+
+ifs=' '
+request=`{sed 1q}
+
+url=$request(2)
+file=`{echo $url | sed 's/http:\/\/[^\/]*//' | tr -d \012}
+
+if(test -d $file){
+ file=$file ^'/index.html'
+}
+if(test -e $file) {
+ response='200'
+}
+if not {
+ response='404'
+ file='404.html'
+}
+
+echo 'HTTP/1.1 ' ^$response
+echo 'Date: ' `{date}
+echo 'Server: rc shell'
+echo 'Content-Length: ' `{cat $file | wc -c | tr -d ' '}
+echo 'Content-Type: ' `{file -i $file | awk '{ print $2 }'}
+echo 'Connection: close'
+echo
+cat $file
diff --git a/bin/corehandlers.rc b/bin/corehandlers.rc
new file mode 100755
index 0000000..294b1a7
--- /dev/null
+++ b/bin/corehandlers.rc
@@ -0,0 +1,152 @@
+# Werc builtin handlers
+
+fn nav_tree {
+ if(! ~ $#sideBarNavTitle 0)
+ echo '<p class="sideBarTitle">'$"sideBarNavTitle':</p>'
+ # Ignore stderr, last path element might be a file that doesn't exist (eg., foo for foo.md)
+ # /./ to deal with p9p's ls failure to follow dir symlinks otherwise
+ ls -F $sitedir/./$req_paths_list >[2]/dev/null \
+ | {
+ sed $dirfilter'/\/[^_.\/][^\/]*(\.(md|txt|html)|\/)$/!d; s!^'$sitedir'!!; '$dirclean
+ if(! ~ $#synth_paths 0) echo $synth_paths | tr ' ' $NEW_LINE
+ } | sort -u | awk -F/ '
+ function p(x, y, s) { for(i=0; i < x-y; i+=1) print s }
+ BEGIN { lNF=2; print "<ul>" }
+ {
+ d = ""
+ if(match($0, "/$"))
+ d = "/"
+ sub("/$", "") # Strip trailing / for dirs so NF is consistent
+
+ p(NF, lNF, "<li><ul>")
+ p(lNF, NF, "</ul></li>")
+ lNF = NF
+
+ bname = $NF d
+ path = $0 d
+ gsub(/[\-_]/, " ", bname)
+
+ # To avoid false matches add trailing / even for plain files to act as delimiter
+ pa = path
+ gsub(/[^\/]$/, "&/", pa)
+
+ if(index(ENVIRON["req_path"] "/", pa) == 1)
+ print "<li><a href=\"" path "\" class=\"thisPage\">&raquo;<i> " bname "</i></a></li>"
+ else
+ print "<li><a href=\"" path "\">&rsaquo; " bname "</a></li>"
+ }
+ END { p(lNF, 2, "</ul></li>"); print "</ul>" }'
+}
+
+fn link_bar {
+ if(~ $1 -t) {
+ echo '<p class="sideBarTitle">'$2'</p>'
+ shift; shift
+ }
+ echo '<ul>'
+ while(! ~ $#* 0) {
+ echo '<li><a href="'$2'">- '$1'</a></li>'
+ shift; shift
+ }
+ echo '</ul>'
+}
+
+fn md_handler { $formatter $1 }
+
+fn tpl_handler { template $* }
+
+fn html_handler {
+ # body states: 0 = no <body> found, 2 = after <body>, 1 = after <body></body>, -1 = after </body>
+ awk 'gsub(".*<[Bb][Oo][Dd][Yy][^>]*>", "") > 0 {body=2}
+ gsub("</ *[Bb][Oo][Dd][Yy][^>]*>.*", "") > 0 {print; body=body-1}
+ body==2 {print}
+ body==0 {buf=buf "\n" $0}
+ END {if(body<=0) {print buf}}' < $1
+}
+
+fn txt_handler {
+ # Note: Words are not broken, even if they are way beyond 82 chars long
+ echo '<pre>'
+ sed 's/</\&lt;/g; s/>/\&gt;/g' < $1 | fmt -l 82 -j
+ echo '</pre>'
+}
+
+fn dir_listing_handler {
+ d=`{basename -d $1}
+ if(~ $#d 0)
+ d='/'
+ echo $d|sed 's,.*//,,g; s,/$,,; s,/, / ,g; s,.*,<h1 class="dir-list-head">&</h1> <ul class="dir-list">,'
+ # Symlinks suck: '/.' forces ls to list the linked dir if $d is a symlink.
+ ls -F $dir_listing_ls_opts $sitedir$d/. | sed $dirfilter$dirclean' s,.*/([^/]+/?)$,<li><a href="\1">\1</a></li>,'
+ echo '</ul>'
+}
+
+fn notices_handler {
+ for(type in notify_errors notify_notes notify_success)
+ for(n in $$type)
+ echo '<div class="'$type'"><b>'$"n'</b></div>'
+}
+
+fn setup_handlers {
+
+ if(test -f $local_path.md) {
+ local_file=$local_path.md
+ handler_body_main=(md_handler $local_file)
+ }
+ if not if(test -f $local_path.tpl) {
+ local_file=$local_path.tpl
+ handler_body_main=(tpl_handler $local_file)
+ }
+ if not if(test -f $local_path.html) {
+ local_file=$local_path.html
+ handler_body_main=(html_handler $local_file)
+ }
+ # Global tpl (eg sitemap.tpl), should take precedence over txt handler!
+ if not if(test -f tpl^$req_path^.tpl)
+ # XXX Should we set $local_file for global .tpls?
+ handler_body_main=(tpl_handler tpl^$req_path^.tpl)
+ if not if(test -f $local_path.txt) {
+ local_file=$local_path.txt
+ handler_body_main=(txt_handler $local_file)
+ }
+
+ # XXX Should check that $enabled_apps exist in $werc_apps?
+ # XXX Should split init of apps that provide main handler (eg., blog) and apps that don't (eg., comments)?
+ if(! ~ $#enabled_apps 0)
+ for(a in $enabled_apps)
+ $a^'_init'
+
+ if(! ~ $#handler_body_main 0)
+ { } # We are done
+ # Dir listing
+ if not if(~ $local_path */index) {
+ handler_body_main=(dir_listing_handler $req_path)
+ if(test -f $sitedir$req_path'_header.md')
+ ll_add handlers_body_head md_handler $sitedir$req_path'_header.md'
+ if(test -f $sitedir$req_path'_footer.md')
+ ll_add handlers_body_foot md_handler $sitedir$req_path'_footer.md'
+ }
+ # Canonize explicit .html urls, the web server might handle this first!
+ if not if(~ $local_path *.html && test -f $local_path)
+ perm_redirect `{ echo $req_path|sed 's/.html$//' }
+ # Fallback static file handler
+ if not if(test -f $local_path)
+ static_file $local_path
+ if not if(~ $req_path /pub/* && test -f .$req_path)
+ static_file .$req_path
+ # File not found
+ if not
+ setup_404_handler
+}
+
+# This function allows config files to define their own 404 handlers.
+fn setup_404_handler {
+ handler_body_main=(tpl_handler `{get_lib_file 404.tpl})
+ echo 'Status: 404 Not Found'
+ dprint 'NOT FOUND: '$SERVER_NAME^$"REQUEST_URI^' - '^$"HTTP_REFERER^' - '^$"HTTP_USER_AGENT
+}
+
+fn run_handlers { for(h in $*) run_handler $$h }
+fn run_handler { $*(1) $*(2-) }
+
+
diff --git a/bin/fltr_cache.rc b/bin/fltr_cache.rc
new file mode 100755
index 0000000..9394724
--- /dev/null
+++ b/bin/fltr_cache.rc
@@ -0,0 +1,37 @@
+#!/bin/rc
+
+fn fltr_cache {
+ a=()
+ tmpf=()
+
+ proc=$1
+ shift
+
+ if(~ $#* 0) {
+ tmpf=/tmp/fmttmp.$pid
+ f=$tmpf
+ score=`{{tee $tmpf || exit 1} | sha1sum}
+ }
+ if not {
+ f=$1
+ if(~ $f */) {
+ score=`{du -an $f | sha1sum || exit 1} # XXX using -n(bytes) instead of -t(lastmod) because sitemap proc touches files in tree.
+ a=$f
+ f=/dev/null
+ }
+ if not {
+ score=`{sha1sum $f || exit 1}
+ score=$score(1)
+ }
+ }
+ cachedir=/tmp/fltr_cache/$score
+ mkdir -p $cachedir >[2]/dev/null
+
+ if(test -s $cachedir/$proc)
+ cat $cachedir/$proc
+ if not
+ if($proc $a < $f | tee $cachedir/$pid)
+ mv $cachedir/$pid $cachedir/$proc
+
+ rm $tmpf $cachedir/$pid >[2]/dev/null &
+}
diff --git a/bin/template.awk b/bin/template.awk
new file mode 100755
index 0000000..8f02ebb
--- /dev/null
+++ b/bin/template.awk
@@ -0,0 +1,55 @@
+#!/bin/awk -f
+function pr(str) {
+ if(lastc !~ "[{(]")
+ gsub(/'/, "''", str)
+ printf "%s", str
+}
+function trans(c) {
+ printf "%s", end
+
+ lastc = c
+ end = "\n"
+ if(c == "%")
+ end = ""
+ else if(c == "(")
+ printf "echo -n "
+ else if(c ~ "[})]") {
+ end = "'\n"
+ printf "echo -n '"
+ }
+}
+
+BEGIN {
+ lastc = "{"
+ trans("}")
+}
+END {
+ print end
+}
+
+/^%/ && $0 !~ /^%[{()}%]/ && lastc !~ /[({]/ {
+ trans("%")
+ print substr($0, 2)
+ next
+}
+{
+ if(lastc == "%")
+ trans("}")
+ n = split($0, a, "%")
+ pr(a[1])
+ for(i=2; i<=n; i++) {
+ c = substr(a[i], 1, 1)
+ rest = substr(a[i], 2)
+
+ if((lastc !~ "[({]" && c ~ "[({]") ||
+ (lastc == "{" && c == "}") ||
+ (lastc == "(" && c == ")"))
+ trans(c)
+ else if(c == "%")
+ pr("%")
+ else
+ pr("%" c)
+ pr(rest)
+ }
+ pr("\n")
+}
diff --git a/bin/werc.rc b/bin/werc.rc
new file mode 100755
index 0000000..0d006a3
--- /dev/null
+++ b/bin/werc.rc
@@ -0,0 +1,138 @@
+#!/bin/rc
+. ./cgilib.rc
+. ./werclib.rc
+. ./wercconf.rc
+. ./corehandlers.rc
+. ./fltr_cache.rc
+cd ..
+
+forbidden_uri_chars='[^a-zA-Z0-9_+\-\/\.,:]'
+difs=$ifs # Used to restore default ifs when needed
+
+# Expected input: ls -F style, $sitedir/path/to/files/
+# <ls -F+x><symlink hack><Useless?><hiden files >
+dirfilter='s/\*$//; s,/+\./+,/,g; s,^\./,,; /\/[._][^\/]/d; /'$forbidden_uri_chars'/d; /\/sitemap\.xml$/d; /\/index\.(md|html|txt|tpl)$/d; /\/(robots|sitemap)\.txt$/d; /_werc\/?$/d; '
+dirclean=' s/\.(md|html|txt)$//; '
+
+# Careful, the proper p9p path might not be set until initrc.local is sourced
+path=(. /bin ./bin)
+
+res_tail='</body></html>'
+http_content_type='text/html'
+ll_add handlers_bar_left nav_tree
+werc_apps=( apps/* )
+werc_root=`{pwd}
+sitesdir=sites
+
+ . ./etc/initrc
+
+if(test -f etc/initrc.local)
+ . ./etc/initrc.local
+
+for(a in $werc_apps)
+ . ./$a/app.rc
+
+fn werc_exec_request {
+ site=$SERVER_NAME
+ base_url=http://$site:$SERVER_PORT
+ sitedir=$sitesdir/$site
+ headers=`{get_lib_file headers.tpl}
+ master_template=`{get_lib_file default_master.tpl}
+ current_date_time=`{date}
+
+ # Note: $REQUEST_URI is not officially in CGI 1.1, but seems to be de-facto
+ # Note: We only urldecode %5F->'_' because some sites (stackoverflow.com?) urlencode it in their links,
+ # perhaps we should completel urldecode the whole url.
+ req_path=`{echo -n $REQUEST_URI | sed 's/\?.*//; s!//+!/!g; s/%5[Ff]/_/g; s/'^$forbidden_uri_chars^'//g; s/\.\.*/./g; 1q'}
+ req_url=$base_url^$req_path
+ local_path=$sitedir$req_path
+ local_file=''
+ ifs='/' { args=`{echo -n $req_path} }
+
+ # Preload post args for templates where cgi's stdin is not accessible
+ if(~ $REQUEST_METHOD POST) {
+ load_post_args
+ login_user
+ }
+
+ if(~ $req_path */index)
+ perm_redirect `{echo $req_path | sed 's,/index$,/,'}
+
+ if(~ $local_path */) {
+ if(test -d $local_path)
+ local_path=$local_path^'index'
+ # XXX: This redir might step on apps with synthetic dirs.
+ if not if(ls `{basename -d $local_path}^* >/dev/null >[2]/dev/null)
+ perm_redirect `{echo $req_path|sed 's,/+$,,'}
+ }
+ if not if(~ $req_path *'.' *',' *';' *':')
+ perm_redirect `{echo $req_path | sed 's/[.,;:)]$//'}
+ if not if(test -d $local_path)
+ perm_redirect $req_path^'/'
+
+ if(! ~ $#args 0)
+ ifs=$NEW_LINE { pageTitle=`{ echo $args|sed -e 's/ / - /g' -e 's/([a-z])-([a-z])/\1 \2/g' -e 's/_/ /g' } }
+
+ cd $sitedir
+ req_paths_list='/' # Note: req_paths_list doesn't include 'stnythetic' dirs.
+ conf_wd='/' # Used in config files to know where we are in the document tree.
+ if(test -f _werc/config)
+ . _werc/config
+ for(i in $args) {
+ conf_wd=$conf_wd^$i
+ req_paths_list=($req_paths_list $conf_wd)
+ if(test -d $i) {
+ conf_wd=$conf_wd'/'
+ cd $i
+ if(test -f _werc/config)
+ . _werc/config
+ }
+ }
+ cd $werc_root
+
+ if(~ $#perm_redir_to 1)
+ perm_redirect $perm_redir_to
+ for(l in $perm_redir_patterns) {
+ p=$$l
+ r=$p(1)
+ # If target is absolute, then patern must match whole string
+ if(~ $p(2) http://* https://*)
+ r='^'$r
+ t=`{ echo $req_path | sed 's!'^$r^'!'^$p(2)^'!' } # Malicious danger!
+
+ if(! ~ $"t '' $req_path)
+ perm_redirect $t
+ }
+
+ setup_handlers
+
+
+ # Set Page title
+ if(! ~ $local_file '') {
+ t=`{get_file_title $local_file}
+ if(! ~ $"t '')
+ pageTitle=$t
+ }
+
+ # XXX Is this never true? because we set pageTitle earlier based on url.
+ if(~ $"pageTitle '')
+ pageTitle=$"siteTitle' '$"siteSubTitle
+# if not
+# pageTitle=$"pageTitle' | '$"siteTitle' '$"siteSubTitle
+
+ for(h in $extraHttpHeaders)
+ echo $h
+ echo Content-Type: $http_content_type
+ echo # End of HTTP headers
+
+ if(! ~ $#debug 0)
+ dprint $"SERVER_NAME^$"REQUEST_URI - $"HTTP_USER_AGENT - $"REQUEST_METHOD - $"handler_body_main - $"master_template
+
+ if(~ $REQUEST_METHOD HEAD)
+ exit
+
+ template $headers $master_template # | awk_buffer
+ echo $res_tail
+}
+
+werc_exec_request
diff --git a/bin/werc_errlog_wrap.rc b/bin/werc_errlog_wrap.rc
new file mode 100755
index 0000000..94bd18f
--- /dev/null
+++ b/bin/werc_errlog_wrap.rc
@@ -0,0 +1,5 @@
+#!/bin/rc
+
+# This is a wrapper script for broken http servers like recent lighttpd versions which throw away cgi's stderr.
+
+./werc.rc >>[2]/tmp/wlog.txt
diff --git a/bin/wercconf.rc b/bin/wercconf.rc
new file mode 100755
index 0000000..bb3422d
--- /dev/null
+++ b/bin/wercconf.rc
@@ -0,0 +1,19 @@
+# To be used from config files
+fn conf_perm_redirect {
+ if(~ $#* 1)
+ perm_redir_to=$1
+ if not
+ ll_addh perm_redir_patterns $1 $2
+}
+
+fn conf_hide_paths {
+ for(i in $*)
+ dirfilter=$dirfilter^'/'^`{echo $sitedir$conf_wd$i|sed 's!/+!\\/!g'}^'/d; '
+}
+
+# Usually will be called from within conf_enable_foo
+fn conf_enable_app {
+ # Note: maybe we should add test -d apps/$1/?
+ if(! ~ $1 $enabled_apps)
+ enabled_apps=( $enabled_apps $1 )
+}
diff --git a/bin/werclib.rc b/bin/werclib.rc
new file mode 100755
index 0000000..bcebf91
--- /dev/null
+++ b/bin/werclib.rc
@@ -0,0 +1,393 @@
+fn get_lib_file {
+ if(! ~ $#sitedir 0 && test -f $sitedir/_werc/lib/$1)
+ echo -n $sitedir/_werc/lib/$1
+ if not if(! ~ $#masterSite 0 && test -f $sitesdir/$masterSite/_werc/lib/$1)
+ echo -n $sitesdir/$masterSite/_werc/lib/$1
+ if not if(test -f lib/$1)
+ echo -n lib/$1
+ if not if(~ $#* 2)
+ echo -n $2
+ if not
+ status='Can''t find lib file: '$1
+}
+
+fn template { awk -f bin/template.awk $* | rc $rcargs }
+
+# Auth code
+# TODO: check http://cookies.lcs.mit.edu/pubs/webauth:tr.pdf
+allowed_user_chars='[a-zA-Z0-9_]'
+# Cookie format: WERC_USER: name:timestamp:hash(name.timestamp.password)
+# login_user can't be used from a template because it sets a cookie
+fn login_user {
+ # Note: we set the cookie even if it is already there.
+ if(get_user $*)
+ set_cookie werc_user $"logged_user^':0:'^$"logged_password
+}
+
+# Check login status, if called with group arg we check membership too
+fn check_user {
+ get_user
+ g=($* admin)
+ _status=$status
+ if(! ~ $"_status '')
+ _status=(Not logged in: $"_status)
+ if not if(! ~ $#* 0 && ! ~ $logged_user $* && ! grep -s '^'^$logged_user^'$' $werc_root/etc/users/$g/members >[2]/dev/null)
+ _status=(User $logged_user not in: $*)
+ status=$_status
+}
+
+# If not logged in, try to get user login info from POST or from cookie
+fn get_user {
+ if(~ $#logged_user 0) {
+ if(~ $#* 2) {
+ user_name=$1
+ user_password=$2
+ }
+ if not if(~ $REQUEST_METHOD POST)
+ get_post_args user_name user_password
+
+ if(~ $#user_name 0) {
+ ifs=':' { cu=`{ifs=$difs {get_cookie werc_user} | tr -d $NEW_LINE} }
+ if(! ~ $#cu 0) {
+ user_name=$cu(1)
+ user_password=$cu(3)
+ }
+ }
+ auth_user $user_name $user_password
+ }
+ if not
+ status=()
+}
+
+# Check if user_name and user_password represent a valid user account
+# If valid, 'log in' by setting logged_user
+fn auth_user {
+ user_name=$1
+ user_password=$2
+
+ pfile=$werc_root/etc/users/$"user_name/password
+ if(~ $#user_name 0 || ~ $#user_password 0)
+ status=('Auth: missing user name or pass: '^$"user_name^' / '^$"user_password)
+ if not if(! test -f $pfile)
+ status=('Auth: cant find '^$pfile)
+ if not if(! test -s $pfile || ! ~ $user_password `{cat $pfile})
+ status=('Auth: Pass '$user_password' doesnt match '^`{cat $pfile})
+ if not {
+ logged_user=$user_name
+ logged_password=$user_password
+ dprint Auth: success
+ status=()
+ }
+}
+
+fn user_controls {
+ echo User: $"logged_user
+}
+
+
+# .md '(meta-)data' extract
+fn get_md_file_attr {
+ sed -n '/^\* '$2': /p; /^\* '$2': /q; /^$/q' < $1
+}
+
+
+# File title extraction
+fn get_md_title {
+ #sed 's/^(................................................................[^ ]*).*$/\1/g; 1q' < $1
+ sed -n -e '1N; /^.*\n===*$/N; /.*\n===*\n *$/!b' -e 's/\n==*\n//p' < $1
+}
+
+fn get_html_title {
+ t=`{sed -n '32q; s/^.*<[Tt][Ii][Tt][Ll][Ee]> *([^<]+) *(<\/[Tt][Ii][Tt][Ll][Ee]>.*)?$/\1/p' < $1}
+
+ # As a backup we might want to pick the first 'non-tag' text in the file with:
+ if(~ $"t '')
+ t=`{sed -n -e 's/^(<[^>]+>)*([^<]+).*/\2/p; 32q' < $1 | sed 1q}
+
+ echo $t
+}
+
+fn get_file_title {
+ if (~ $1 *.md)
+ get_md_title $1
+ if not if(~ $1 *.html)
+ get_html_title $1
+ if not if(~ $1 */) {
+ if(test -f $1/index.md)
+ get_md_title $1/index.md
+ if not if(test -f $1/index.html)
+ get_html_title $1/index.html
+ }
+}
+
+fn ndate {
+ if(~ $#* 7)
+ date=$*(2-)
+ if not
+ date=`{date}
+ switch($date(2)){
+ case Jan; mo=01
+ case Feb; mo=02
+ case Mar; mo=03
+ case Apr; mo=04
+ case May; mo=05
+ case Jun; mo=06
+ case Jul; mo=07
+ case Aug; mo=08
+ case Sep; mo=09
+ case Oct; mo=10
+ case Nov; mo=11
+ case Dec; mo=12
+ }
+ switch($date(3)){
+ case [0-9]
+ da=0^$date(3)
+ case *
+ da=$date(3)
+ }
+ switch($date(5)){
+ case A; tz=+0100
+ case ADT; tz=-0300
+ case AFT; tz=+430
+ case AKDT; tz=-0800
+ case AKST; tz=-0900
+ case ALMT; tz=+0600
+ case AMST; tz=-0300
+ case AMT; tz=-0400
+ case ANAST; tz=+1200
+ case ANAT; tz=+1200
+ case AQTT; tz=+0500
+ case ART; tz=-0300
+ case AST; tz=-0400
+ case AZOST; tz=+0000
+ case AZOT; tz=-0100
+ case AZST; tz=+0500
+ case AZT; tz=+0400
+ case B; tz=+0200
+ case BNT; tz=+0800
+ case BOT; tz=-0400
+ case BRST; tz=-0200
+ case BRT; tz=-0300
+ case BST; tz=+0100
+ case BTT; tz=+0600
+ case C; tz=+0300
+ case CAST; tz=+0800
+ case CAT; tz=+0200
+ case CCT; tz=+0630
+ case CDT; tz=-0500
+ case CEST; tz=+0200
+ case CET; tz=+0100
+ case CHADT; tz=+1345
+ case CHAST; tz=+1245
+ case CKT; tz=-1000
+ case CLST; tz=-0300
+ case CLT; tz=-0400
+ case COT; tz=-0500
+ case CST; tz=-0600
+ case CVT; tz=-0100
+ case CXT; tz=+0700
+ case ChST; tz=+1000
+ case D; tz=+0400
+ case DAVT; tz=+0700
+ case E; tz=+0500
+ case EASST; tz=-0500
+ case EAST; tz=-0600
+ case EAT; tz=+0300
+ case ECT; tz=-0500
+ case EDT; tz=-0400
+ case EEST; tz=+0300
+ case EET; tz=+0200
+ case EGST; tz=+0000
+ case EGT; tz=-0100
+ case EST; tz=-0500
+ case ET; tz=-0500
+ case F; tz=+0600
+ case FJST; tz=+1300
+ case FJT; tz=+1200
+ case FKST; tz=-0300
+ case FKT; tz=-0400
+ case FNT; tz=-0200
+ case G; tz=+0700
+ case GALT; tz=-0600
+ case GAMT; tz=-0900
+ case GET; tz=+0400
+ case GFT; tz=-0300
+ case GILT; tz=+1200
+ case GMT; tz=+0000
+ case GST; tz=+0400
+ case GYT; tz=-0400
+ case H; tz=+0800
+ case HAA; tz=-0300
+ case HAC; tz=-0500
+ case HADT; tz=-0900
+ case HAE; tz=-0400
+ case HAP; tz=-0700
+ case HAR; tz=-0600
+ case HAST; tz=-1000
+ case HAT; tz=-0230
+ case HAY; tz=-0800
+ case HKT; tz=+0800
+ case HLV; tz=-0430
+ case HNA; tz=-0400
+ case HNC; tz=-0600
+ case HNE; tz=-0500
+ case HNP; tz=-0800
+ case HNR; tz=-0700
+ case HNT; tz=-0330
+ case HNY; tz=-0900
+ case HOVT; tz=+0700
+ case I; tz=+0900
+ case ICT; tz=+0700
+ case IDT; tz=+0300
+ case IOT; tz=+0600
+ case IRDT; tz=+0430
+ case IRKST; tz=+0900
+ case IRKT; tz=+0800
+ case IRST; tz=+0330
+ case IST; tz=+0200
+ case JST; tz=+0900
+ case K; tz=+1000
+ case KGT; tz=+0600
+ case KRAST; tz=+0800
+ case KRAT; tz=+0700
+ case KST; tz=+0900
+ case KUYT; tz=+0400
+ case L; tz=+1100
+ case LHDT; tz=+1100
+ case LHST; tz=+1030
+ case LINT; tz=+1400
+ case M; tz=+1200
+ case MAGST; tz=+1200
+ case MAGT; tz=+1100
+ case MART; tz=-0930
+ case MAWT; tz=+0500
+ case MDT; tz=-0600
+ case MHT; tz=+1200
+ case MMT; tz=+0630
+ case MSD; tz=+0400
+ case MSK; tz=+0300
+ case MST; tz=-0700
+ case MUT; tz=+0400
+ case MVT; tz=+0500
+ case MYT; tz=+0800
+ case N; tz=-0100
+ case NCT; tz=+1100
+ case NDT; tz=-0230
+ case NFT; tz=+1130
+ case NOVST; tz=+0700
+ case NOVT; tz=+0600
+ case NPT; tz=+0545
+ case NST; tz=-0330
+ case NUT; tz=-1100
+ case NZDT; tz=+1300
+ case NZST; tz=+1200
+ case O; tz=-0200
+ case OMSST; tz=+0700
+ case OMST; tz=+0600
+ case P; tz=-0300
+ case PDT; tz=-0700
+ case PET; tz=-0500
+ case PETST; tz=+1200
+ case PETT; tz=+1200
+ case PGT; tz=+1000
+ case PHOT; tz=+1300
+ case PHT; tz=+0800
+ case PKT; tz=+0500
+ case PMDT; tz=-0200
+ case PMST; tz=-0300
+ case PONT; tz=+1100
+ case PST; tz=-0800
+ case PT; tz=-0800
+ case PWT; tz=+0900
+ case PYST; tz=-0300
+ case PYT; tz=-0400
+ case Q; tz=-0400
+ case R; tz=-0500
+ case RET; tz=+0400
+ case S; tz=-0600
+ case SAMT; tz=+0400
+ case SAST; tz=+0200
+ case SBT; tz=+1100
+ case SCT; tz=+0400
+ case SGT; tz=+0800
+ case SRT; tz=-0300
+ case SST; tz=-1100
+ case T; tz=-0700
+ case TAHT; tz=-1000
+ case TFT; tz=+0500
+ case TJT; tz=+0500
+ case TKT; tz=-1000
+ case TLT; tz=+0900
+ case TMT; tz=+0500
+ case TVT; tz=+1200
+ case U; tz=-0800
+ case ULAT; tz=+0800
+ case UYST; tz=-0200
+ case UYT; tz=-0300
+ case UZT; tz=+0500
+ case V; tz=-0900
+ case VET; tz=-0430
+ case VLAST; tz=+1100
+ case VLAT; tz=+1000
+ case VUT; tz=+1100
+ case W; tz=-1000
+ case WAST; tz=+0200
+ case WAT; tz=+0100
+ case WDT; tz=+0900
+ case WEST; tz=+0100
+ case WET; tz=+0000
+ case WFT; tz=+1200
+ case WGST; tz=-0200
+ case WGT; tz=-0300
+ case WIB; tz=+0700
+ case WIT; tz=+0900
+ case WITA; tz=+0800
+ case WST; tz=+0800
+ case WT; tz=+0000
+ case X; tz=-1100
+ case Y; tz=-1200
+ case YAKST; tz=+1000
+ case YAKT; tz=+0900
+ case YAPT; tz=+1000
+ case YEKST; tz=+0600
+ case YEKT; tz=+0500
+ case Z; tz=+0000
+ }
+ switch($1){
+ case -a # rfc3339
+ tz=`{echo $tz | sed 's/00$/:00/'}
+ echo $date(6)^-$mo-$da^T^$date(4)^$tz
+ case -i # iso-8601 lite
+ echo $date(6)^-$mo-$da
+ case -m # rfc2822
+ echo $date(1)^, $da $date(2) $date(6) $date(4) $tz
+ case -t # iso-8601
+ echo $date(6)^-$mo-$da^T^$date(4)^$tz
+ }
+}
+
+##########################################################################
+##########################################################################
+#app_blog_methods = ( _post index.rss )
+#fn app_blog__post {
+# echo
+#}
+#
+#app_blog___default {
+# if (~ $blog)
+# call_app blogpost
+#}
+#
+## --
+#app_blogpost_methods = ( comment _edit )
+#
+#fn app_blogpost_comment {
+# call_app comments
+#}
+#
+## --
+#app_comments_methods = ( _post _edit )
+#
+#fn app_comments___default {
+#
+#}
diff --git a/etc/initrc b/etc/initrc
new file mode 100755
index 0000000..f0b26c2
--- /dev/null
+++ b/etc/initrc
@@ -0,0 +1,39 @@
+# This file contains the default werc settings.
+#
+# DO NOT EDIT, to customize copy to etc/initrc.local and edit at will.
+#
+# Some settings can also be set for a specific site or directory in their
+# respective _werc/config or their $masterSite/_werc/config file.
+
+# General options
+
+# Location of your Plan 9 from User Space installation (usually /usr/local/plan9)
+plan9port=$PLAN9
+
+# If you use 9base, it should point to your 9base root, try for example:
+#plan9port=/usr/lib/9base # This is the default 9base install path in Debian.
+
+# If rc has not been copied to /bin/rc you will also need to change
+# the #! line in bin/*.rc!
+
+# Path, make sure the plan9port /bin directory is included before /bin
+# Keep '.' in path! It is needed.
+path=($plan9port/bin . ./bin ./bin/contrib /bin /usr/bin)
+
+# Set this to your favorite markdown formatter, eg., markdown.pl (fltr_cache
+# takes as an argument a filter, in the default configuration markdown.pl, that
+# caches output) Note that some werc components assume a markdown-like
+# formatter, but all major functionality should should be formatter agnostic.
+#formatter=(fltr_cache markdown.pl)
+formatter=(fltr_cache md2html.awk) # no perl for old men
+
+# Enable debugging, to disable set to ()
+debug=true
+
+# Globally enabled apps
+enabled_apps=()
+
+# Default site variables, must be set in initrc.local or _werc/config, only siteTitle is required.
+#masterSite=cat-v.org # Not required!
+#siteTitle='cat-v'
+#siteSubTitle='Considered harmful'
diff --git a/etc/users/GROUP_AND_USER_ACCOUNTS b/etc/users/GROUP_AND_USER_ACCOUNTS
new file mode 100644
index 0000000..f563306
--- /dev/null
+++ b/etc/users/GROUP_AND_USER_ACCOUNTS
@@ -0,0 +1 @@
+This is just a dummy file to force hg to preserve this directory that is used to store user and group account information.
diff --git a/lib/404.tpl b/lib/404.tpl
new file mode 100644
index 0000000..f839439
--- /dev/null
+++ b/lib/404.tpl
@@ -0,0 +1,3 @@
+<h1>The requested document at '<i>%($base_url$"req_path%)</i>' doesn't exist</h1>
+<h4>Or take a look at the <a href="/sitemap">sitemap</a>.</h4>
+<hr>
diff --git a/lib/default_master.tpl b/lib/default_master.tpl
new file mode 100644
index 0000000..6742c49
--- /dev/null
+++ b/lib/default_master.tpl
@@ -0,0 +1,26 @@
+<header>
+ <nav>
+% cat `{ get_lib_file top_bar.inc }
+ </nav>
+ <h1><a href="/">%($"siteTitle%) <span id="headerSubTitle">%($"siteSubTitle%)</span></a></h1>
+</header>
+
+% if(! ~ $#handlers_bar_left 0) {
+ <nav id="side-bar">
+% for(h in $handlers_bar_left) {
+ <div>
+% run_handler $$h
+ </div>
+% }
+ </nav>
+% }
+
+<article>
+% run_handlers $handlers_body_head
+% run_handler $handler_body_main
+% run_handlers $handlers_body_foot
+</article>
+
+<footer>
+% cat `{ get_lib_file footer.inc }
+</footer>
diff --git a/lib/footer.inc b/lib/footer.inc
new file mode 100644
index 0000000..1eac7d8
--- /dev/null
+++ b/lib/footer.inc
@@ -0,0 +1,7 @@
+<div><a href="http://werc.cat-v.org/">Powered by werc</a></div>
+
+<div><a href="/_users/login">User Login</a>
+<!-- TODO: add duckduckgo site search
+<form action="/_search" method=get><input type=text name=q />
+-->
+</div>
diff --git a/lib/headers.tpl b/lib/headers.tpl
new file mode 100644
index 0000000..635f85e
--- /dev/null
+++ b/lib/headers.tpl
@@ -0,0 +1,29 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+
+ <title>%($pageTitle%)</title>
+
+ <link rel="stylesheet" href="/pub/style/style.css" type="text/css" media="screen, handheld" title="default">
+ <link rel="shortcut icon" href="/favicon.ico" type="image/vnd.microsoft.icon">
+% if(test -f $sitedir/_werc/pub/style.css)
+% echo ' <link rel="stylesheet" href="/_werc/pub/style.css" type="text/css" media="screen" title="default">'
+
+ <meta charset="UTF-8">
+% # Legacy charset declaration for backards compatibility with non-html5 browsers.
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
+
+% if(! ~ $#meta_description 0)
+% echo ' <meta name="description" content="'$"meta_description'">'
+% if(! ~ $#meta_keywords 0)
+% echo ' <meta name="keywords" content="'$"meta_keywords'">'
+
+% h = `{get_lib_file headers.inc}
+% if(! ~ $#h 0)
+% cat $h
+
+ %($"extraHeaders%)
+
+</head>
+<body>
+
diff --git a/lib/top_bar.inc b/lib/top_bar.inc
new file mode 100644
index 0000000..cbb89b8
--- /dev/null
+++ b/lib/top_bar.inc
@@ -0,0 +1,15 @@
+ <div>
+ <a href="http://gsoc.cat-v.org">gsoc</a> |
+ <a href="http://doc.cat-v.org">doc archive</a> |
+ <a href="http://repo.cat-v.org">software repo</a> |
+ <a href="http://ninetimes.cat-v.org">ninetimes</a> |
+ <a href="http://harmful.cat-v.org">harmful</a> |
+ <a href="http://9p.cat-v.org/">9P</a> |
+ <a href="http://cat-v.org">cat-v.org</a>
+ </div>
+
+ <div>
+ <a href="http://cat-v.org/update_log">site updates</a> |
+ <a href="/sitemap">site map</a>
+ </div>
+
diff --git a/pub/default_favicon.ico b/pub/default_favicon.ico
new file mode 100755
index 0000000..817f5fa
--- /dev/null
+++ b/pub/default_favicon.ico
Binary files differ
diff --git a/pub/style/imgs/sgl.png b/pub/style/imgs/sgl.png
new file mode 100755
index 0000000..d68580e
--- /dev/null
+++ b/pub/style/imgs/sgl.png
Binary files differ
diff --git a/pub/style/sinorca-screen-alt.css b/pub/style/sinorca-screen-alt.css
new file mode 100755
index 0000000..d11e9ad
--- /dev/null
+++ b/pub/style/sinorca-screen-alt.css
@@ -0,0 +1,292 @@
+/***********************************************
+ * TITLE: Sinorca Alterative Screen Stylesheet *
+ * URI : sinorca/sinorca-screen-alt.css *
+ * MODIF: 2003-May-13 18:48 +0800 *
+ ***********************************************/
+
+
+/* ##### Common Styles ##### */
+
+body {
+ color: black;
+ background-color: white;
+ font-family: verdana, helvetica, arial, sans-serif;
+ font-size: 71%; /* Enables font size scaling in MSIE */
+ margin: 0;
+ padding: 0;
+}
+
+html > body {
+ font-size: 8.5pt;
+}
+
+acronym, .titleTip {
+ border-bottom: 1px dotted rgb(153,153,153);
+ cursor: help;
+ margin: 0;
+ padding: 0 0 0.4px 0;
+}
+
+.doNotDisplay {
+ display: none;
+}
+
+.smallCaps {
+ font-size: 110%;
+ font-variant: small-caps;
+}
+
+
+/* ##### Header ##### */
+
+.superHeader {
+ color: white;
+ background-color: rgb(100,135,220);
+ height: 2em;
+}
+
+.superHeader a {
+ color: white;
+ background-color: transparent;
+ text-decoration: none;
+ font-size: 91%;
+ margin: 0;
+ padding: 0 0.5ex 0 0.25ex;
+}
+
+.superHeader a:hover {
+ text-decoration: underline;
+}
+
+.superHeader .left {
+ position: absolute;
+ left: 1.5mm;
+ top: 0.75ex;
+}
+
+.superHeader .right {
+ position: absolute;
+ right: 1.5mm;
+ top: 0.75ex;
+}
+
+.midHeader {
+ color: rgb(39,78,144);
+ background-color: rgb(140,170,230);
+}
+
+.headerTitle {
+ font-size: 337%;
+ font-weight: normal;
+ margin: 0 0 0 4mm;
+ padding: 0.25ex 0;
+}
+
+.subHeader {
+ color: white;
+ background-color: rgb(0,51,153);
+ margin: 0;
+ padding: 1ex 1ex 1ex 1.5mm;
+}
+
+.subHeader a {
+ color: white;
+ background-color: transparent;
+ text-decoration: none;
+ font-weight: bold;
+ margin: 0;
+ padding: 0 0.75ex 0 0.5ex;
+}
+
+.subHeader a:hover {
+ text-decoration: underline;
+}
+
+.superHeader .highlight, .subHeader .highlight {
+ color: rgb(253,160,91);
+ background-color: transparent;
+}
+
+
+/* ##### Side Boxes ##### */
+
+#side-bar {
+ width: 14em;
+ margin: 2.5em 0 0 1.25mm;
+ float: left;
+ clear: left;
+}
+
+body > #side-bar {
+ margin-left: 2.5mm; /* Circumvents a rendering bug in MSIE (6.0) */
+}
+
+.sideBarTitle {
+ color: white;
+ background-color: rgb(100,135,220);
+ font-weight: bold;
+ margin: 0;
+ padding: 0.4ex 0 0.4ex 0.6ex;
+}
+
+#side-bar ul {
+ list-style-type: none;
+ list-style-position: outside;
+ margin: 0;
+ padding: 0 0 2.25em 0;
+}
+
+#side-bar li {
+ margin: 0;
+ padding: 0.1ex 0; /* Circumvents a rendering bug (?) in MSIE (6.0) */
+}
+
+#side-bar a, .thisPage {
+ color: rgb(0,102,204);
+ background-color: transparent;
+ text-decoration: none;
+ font-weight: bold;
+ margin: 0;
+ padding: 1.3ex 2ex;
+ display: block;
+}
+
+.thisPage {
+ color: black;
+ background-color: transparent;
+}
+
+#side-bar a:hover {
+ color: white;
+ background-color: rgb(100,135,220);
+ text-decoration: none;
+}
+
+.sideBarText {
+ line-height: 1.5em;
+ margin: 0 0 2.5em 0;
+ padding: 1ex 0.5ex 0 0.5ex;
+ display: block;
+}
+
+.sideBarText + .sideBarText { /* Not recognised by MSIE (6.0) */
+ margin-top: -1.5em;
+}
+
+#side-bar .sideBarText a {
+ text-decoration: underline;
+ font-weight: normal;
+ margin: 0;
+ padding: 0;
+ display: inline;
+}
+
+#side-bar .sideBarText a:hover {
+ color: rgb(0,102,204);
+ background-color: transparent;
+ text-decoration: none;
+}
+
+
+/* ##### Main Copy ##### */
+
+#main-copy {
+ color: black;
+ background-color: transparent;
+ text-align: justify;
+ line-height: 1.5em;
+ margin: -1em 0 0 15em;
+ padding: 0.5mm 5mm 5mm 5mm;
+}
+
+#bodyText {
+ margin: 0 0 0 15.5em;
+ padding: 2mm 5mm 2mm 5mm;
+}
+
+
+#main-copy p {
+ margin: 1em 1ex 2em 1ex;
+ padding: 0;
+}
+
+#main-copy a {
+ color: rgb(0,102,204);
+ background-color: transparent;
+ text-decoration: underline;
+}
+
+#main-copy a:hover {
+ text-decoration: none;
+}
+
+#main-copy h1 {
+ color: rgb(0,102,204);
+ background-color: transparent;
+ font-size: 145.5%;
+ font-weight: bold;
+ margin: 2em 0 0 0;
+ padding: 0.5ex 0 0.5ex 0.6ex;
+ border-bottom: 1px solid rgb(0,102,204);
+}
+
+#main-copy .topOfPage {
+ color: rgb(0,102,204);
+ background-color: transparent;
+ font-size: 91%;
+ font-weight: bold;
+ text-decoration: none;
+ margin: 3ex 1ex 0 0;
+ padding: 0;
+ float: right;
+}
+
+dl {
+ margin: 1em 1ex 2em 1ex;
+ padding: 0;
+}
+
+dt {
+ font-weight: bold;
+ margin: 0 0 0 0;
+ padding: 0;
+}
+
+dd {
+ margin: 0 0 2em 2em;
+ padding: 0;
+}
+
+
+/* ##### Footer ##### */
+
+#footer {
+ color: white;
+ background-color: rgb(100,135,220);
+ font-size: 91%;
+ margin: 0;
+ padding: 1em 2.5mm 2.5ex 2.5mm;
+ clear: both;
+}
+
+#footer .left {
+ text-align: left;
+ line-height: 1.45em;
+ float: left;
+ clear: left;
+}
+
+#footer .right {
+ text-align: right;
+ line-height: 1.45em;
+}
+
+#footer a {
+ color: white;
+ background-color: transparent;
+ text-decoration: underline;
+}
+
+#footer a:hover {
+ text-decoration: none;
+} \ No newline at end of file
diff --git a/pub/style/style.cat-v.css b/pub/style/style.cat-v.css
new file mode 100644
index 0000000..3d3d4b6
--- /dev/null
+++ b/pub/style/style.cat-v.css
@@ -0,0 +1,50 @@
+body { display: flex; flex-wrap: wrap; font-family: sans; }
+header { flex-basis: 100%; flex-shrink: 0; }
+article { flex-basis: 60%; padding-left: 1em; }
+footer { flex-basis: 100%; flex-shrink: 0; }
+header nav { display: flex; justify-content: space-between; }
+nav a, header a { text-decoration: none ; color: inherit; }
+header h1 span { margin-left: 1em; font-size: 50%; font-style: italic; }
+body > nav { flex-basis: content; padding-right: 1vw; min-width: 16em; }
+nav ul { display: flex; flex-direction: column; list-style-type: none; list-style-position: outside; padding-left: 0; }
+nav li ul { padding-left: 0.6em }
+footer { display: flex; justify-content: space-between; }
+
+/* cut here to leave vanity behind */
+
+body { margin:0; padding: 0; font-size: 84%; font-family: Helvetica, Verdana, Arial, 'Liberation Sans', FreeSans, sans-serif; }
+a { text-decoration: none; color: }
+a:hover { text-decoration: underline; }
+.thisPage { color: black; }
+
+/* header and top bar */
+header nav { background-color: rgb(100,135,220); color: white; padding: 0.3em; border-bottom: 2px solid black; font-size: 91%; }
+header h1 { background-color: #ff6d06; color: black; margin: 0; border-bottom: 2px solid black; font-weight: normal; padding: 0.25ex; font-size: 233%; }
+header a:hover { text-decoration: none; }
+
+/* sidebar */
+body > nav { border-right: 1px solid #ddd; padding: 0; }
+body > nav > div { border-bottom: 1px solid #ddd; }
+body > nav > div a { color: rgb(0, 102, 204); display: block; text-transform: capitalize; font-weight: bold; padding: 0.25em 1ex 0.25em 2mm; font-size: 102%}
+body > nav > div a:hover { color: white; background-color: rgb(100,135,220); border-left: black solid 0.2em; text-decoration: none; }
+body > nav > div p { font-weight: bold; margin: 0 0 0.5em 2mm; padding: 1em 0 0 0; }
+
+/* main copy */
+article { padding: 0.5ex 0 5vh 1vw; }
+article h1, article h2 { color: rgb(0,102,204); font-weight: bold; margin: 2em 0 0 0; border-bottom: 2px solid rgb(0,102,204); }
+article h3, article h4, article h5 { color: rgb(0,102,204); font-weight: bold; margin: 2em 0 0 0; }
+article h6, article h7, article h8 { color: rgb(0,102,204); font-weight: bold; margin: 2em 0 0 0; }
+article a { color: rgb(0,102,204); }
+article a:hover { color: rgb(100,135,220); }
+article pre { font-size: 1.2em; }
+
+/* footer */
+footer { color: white; background-color: rgb(100,135,220); }
+footer a { color: inherit; }
+footer div { padding: 1em; }
+
+/* tables */
+table { border: 1px solid rgba(128,128,128,0.5); padding: 0; }
+th { color: white; background-color: rgb(100,135,220); }
+tr:nth-child(odd) { background-color: rgba(128,128,128,0.1) }
+
diff --git a/pub/style/style.css b/pub/style/style.css
new file mode 100644
index 0000000..91f060e
--- /dev/null
+++ b/pub/style/style.css
@@ -0,0 +1,11 @@
+body { display: flex; flex-wrap: wrap; font-family: sans; }
+header { flex-basis: 100%; flex-shrink: 0; }
+article { flex-basis: 60%; padding-left: 1em; }
+footer { flex-basis: 100%; flex-shrink: 0; }
+header nav { display: flex; justify-content: space-between; }
+nav a, header a { text-decoration: none ; color: inherit; }
+header h1 span { margin-left: 1em; font-size: 50%; font-style: italic; }
+body > nav { flex-basis: content; padding-right: 1vw; min-width: 16em; }
+nav ul { display: flex; flex-direction: column; list-style-type: none; list-style-position: outside; padding-left: 0; }
+nav li ul { padding-left: 0.6em }
+footer { display: flex; justify-content: space-between; }
diff --git a/pub/style/style.werc140.css b/pub/style/style.werc140.css
new file mode 100755
index 0000000..e3261e9
--- /dev/null
+++ b/pub/style/style.werc140.css
@@ -0,0 +1,330 @@
+/* Default werc style */
+
+body {
+ color: black;
+ background-color: white;
+ font-family: Helvetica, Verdana, Arial, 'Liberation Sans', FreeSans, sans-serif;
+ font-size: 84%; /* Enables font size scaling in MSIE */
+ margin: 0;
+ padding: 0;
+}
+
+
+/* # Header # */
+.superHeader {
+ color: white;
+ background-color: rgb(100,135,220);
+ height: 1.6em;
+}
+
+.superHeader img { vertical-align: bottom; }
+
+.superHeader a {
+ color: white;
+ background-color: transparent;
+ font-size: 91%;
+ margin: 0;
+ padding: 0 0.5ex 0 0.25ex;
+}
+
+a { text-decoration: none; }
+a:hover { text-decoration: underline; }
+
+.superHeader div {
+ position: absolute;
+ top: 0.40ex;
+}
+
+.superHeader .left { left: 0.4em; }
+.superHeader .right { right: 0.4em; }
+
+.midHeader {
+ color: rgb(39,78,144);
+ background-color: rgb(140,170,230);
+ background-color: #ff6d06;
+ border: solid 0 black;
+ border-width: 2px 0;
+}
+
+.headerTitle {
+ color: black;
+ font-size: 233%;
+ font-weight: normal;
+ margin: 0 0 0 4mm;
+ padding: 0.25ex 0;
+}
+#headerSubTitle {
+ font-size: 50%;
+ font-style: italic;
+ margin-left: 1em;
+}
+
+.headerTitle a { color: black; }
+.headerTitle a:hover { text-decoration: none; }
+
+.subHeader {
+ display: none;
+ color: white;
+ background-color: rgb(0,51,153);
+ margin: 0;
+ padding: 1ex 1ex 1ex 1.5mm;
+}
+
+.subHeader a {
+ color: white;
+ background-color: transparent;
+ font-weight: bold;
+ margin: 0;
+ padding: 0 0.75ex 0 0.5ex;
+}
+
+.superHeader .highlight, .subHeader .highlight {
+ color: rgb(253,160,91);
+ background-color: transparent;
+}
+
+
+/* # Side # */
+#side-bar {
+ width: 16em;
+ float: left;
+ clear: left;
+ border-right: 1px solid #ddd;
+}
+
+#side-bar div {
+ border-bottom: 1px solid #ddd;
+}
+
+.sideBarTitle {
+ font-weight: bold;
+ margin: 0 0 0.5em 2mm;
+ padding: 1em 0 0 0;
+}
+
+#side-bar ul {
+ list-style-type: none;
+ list-style-position: outside;
+ margin: 0;
+ padding: 0 0 0.3em 0;
+}
+
+li ul {
+ padding-left: 0.6em !important;
+}
+
+#side-bar li {
+ margin: 0;
+ padding: 0.1ex 0; /* Circumvents a rendering bug (?) in MSIE 6.0 XXX should move to iehacks.css, this causes an ugly gap */
+}
+
+#side-bar a {
+ color: rgb(0,102,204);
+ background-color: transparent;
+ margin: 0;
+ padding: 0.25em 1ex 0.25em 2mm;
+ display: block;
+ text-transform: capitalize;
+ font-weight: bold!important;
+ font-size: 102%;
+ border-left: white solid 0.2em;
+}
+
+.thisPage, .thisPage a {
+ color: black!important;
+ background-color: white;
+ padding-left: 5mm;
+}
+
+#side-bar a:hover {
+ color: white;
+ background-color: rgb(100,135,220);
+ border-left: black solid 0.2em;
+ text-decoration: none;
+}
+
+.sideBarText {
+ line-height: 1.5em;
+ margin: 0 0 1em 0;
+ padding: 0 1.5ex 0 2.5mm;
+ display: block;
+}
+
+#side-bar .sideBarText a {
+ margin: 0;
+ padding: 0;
+ display: inline;
+}
+
+#side-bar .sideBarText a:hover {
+ color: rgb(0,102,204);
+ background-color: transparent;
+ text-decoration: none;
+}
+
+
+/* # Main Copy # */
+#main-copy {
+ max-width: 70em;
+ color: black;
+ background-color: transparent;
+ text-align: justify;
+ line-height: 1.5em;
+ margin: 0em 0 0 16em;
+ padding: 0.5mm 5mm 5mm 5mm;
+ border-left: 1px solid #ddd;
+}
+
+#bodyText {
+ margin: 0 0 0 15.5em;
+ padding: 2mm 5mm 2mm 5mm;
+}
+
+#main-copy p {
+ margin: 1em 1ex 1em 1ex !important; /* Need !important so troff-generated pages don't look totally squezed */
+ padding: 0;
+}
+
+#main-copy a {
+ color: rgb(0,102,204);
+ background-color: transparent;
+}
+
+#main-copy a:hover {
+ color: rgb(100,135,220);
+}
+
+#main-copy h1, #main-copy h2 {
+ color: rgb(0,102,204);
+ background-color: transparent;
+ font-size: 145.5%;
+ font-weight: bold;
+ margin: 2em 0 0 0;
+ padding: 0.5ex 0 0.5ex 0.6ex;
+ border-bottom: 2px solid rgb(0,102,204);
+}
+
+#main-copy h2 {
+ font-size: 115.5%;
+ border-bottom: 1px solid rgb(0,102,204);
+}
+
+#main-copy .topOfPage {
+ color: rgb(0,102,204);
+ background-color: transparent;
+ font-size: 91%;
+ font-weight: bold;
+ text-decoration: none;
+ margin: 3ex 1ex 0 0;
+ padding: 0;
+ float: right;
+}
+
+dl {
+ margin: 1em 1ex 2em 1ex;
+ padding: 0;
+}
+
+dt {
+ font-weight: bold;
+ margin: 0 0 0 0;
+ padding: 0;
+}
+
+dd {
+ margin: 0 0 2em 2em;
+ padding: 0;
+}
+
+
+/* # Footer # */
+#footer {
+ color: white;
+ background-color: rgb(100,135,220);
+ padding: 1em;
+ clear: both;
+}
+
+#footer .left {
+ text-align: left;
+ line-height: 1.55em;
+ float: left;
+ clear: left;
+}
+
+#footer .right {
+ text-align: right;
+ line-height: 1.45em;
+}
+
+#footer a {
+ color: white;
+ background-color: transparent;
+}
+
+
+/* GENERAL */
+
+table {
+ border: solid 1px black;
+}
+th {
+ background-color: #abc;
+ border: solid 1px black;
+ text-align: center;
+}
+td {
+ background-color: #def;
+ border: solid 1px black;
+}
+
+hr {
+ border-width: 0px 0px 0.1em 0px;
+ border-color: black;
+}
+
+acronym, .titleTip {
+ border-bottom: 1px solid #ddd;
+ cursor: help;
+ margin: 0;
+ padding: 0 0 0.4px 0;
+}
+
+pre {
+ margin-left: 2em;
+ font-size: 1.2em;
+}
+
+blockquote {
+ border-left: 1px solid blue;
+ font-style: italic;
+}
+
+.smallCaps {
+ font-size: 110%;
+ font-variant: small-caps;
+}
+
+.doNotDisplay { display: none; }
+
+
+.notify_errors,
+.notify_notes,
+.notify_success { padding: .8em; margin-bottom: 1em; border: 2px solid #ddd; }
+
+.notify_errors { background: #FBE3E4; color: #8a1f11; border-color: #FBC2C4; }
+.notify_notes { background: #FFF6BF; color: #514721; border-color: #FFD324; }
+.notify_success { background: #E6EFC2; color: #264409; border-color: #C6D880; }
+.notify_errors a { color: #8a1f11; }
+.notify_notes a { color: #514721; }
+.notify_success a { color: #264409; }
+
+
+/* # Page/Handler specific # */
+h1.dir-list-head, ul.dir-list {
+ text-transform: capitalize;
+ font-weight: bold;
+}
+ul.sitemap-list a {
+ text-transform: capitalize;
+}
diff --git a/pub/style/style_old.css b/pub/style/style_old.css
new file mode 100755
index 0000000..e4a41fe
--- /dev/null
+++ b/pub/style/style_old.css
@@ -0,0 +1,330 @@
+/* Old Default style */
+/* ##### Common Styles ##### */
+
+body {
+ color: black;
+ XXXbackground-color: rgb(240,240,240);
+ background-color: white;
+ font-family: verdana, helvetica, arial, sans-serif;
+ font-size: 71%; /* Enables font size scaling in MSIE */
+ margin: 0;
+ padding: 0;
+}
+
+html > body {
+ font-size: 8.5pt;
+}
+
+acronym, .titleTip {
+ border-bottom: 1px dotted rgb(153,153,153);
+ cursor: help;
+ margin: 0;
+ padding: 0 0 0.4px 0;
+}
+
+.doNotDisplay {
+ display: none;
+}
+
+.smallCaps {
+ font-size: 110%;
+ font-variant: small-caps;
+}
+
+
+/* ##### Header ##### */
+
+.superHeader {
+ color: white;
+ background-color: rgb(100,135,220);
+ height: 2em;
+}
+
+.superHeader a {
+ color: white;
+ background-color: transparent;
+ text-decoration: none;
+ font-size: 91%;
+ margin: 0;
+ padding: 0 0.5ex 0 0.25ex;
+}
+
+.superHeader a:hover {
+ text-decoration: underline;
+}
+
+.superHeader .left {
+ position: absolute;
+ left: 1.5mm;
+ top: 0.75ex;
+}
+
+.superHeader .right {
+ position: absolute;
+ right: 1.5mm;
+ top: 0.75ex;
+}
+
+.midHeader {
+ color: rgb(39,78,144);
+ background-color: rgb(140,170,230);
+ border: solid 0 black;
+ border-width: 0.3em 0;
+}
+
+.headerTitle {
+ color: black;
+ font-size: 337%;
+ font-weight: normal;
+ margin: 0 0 0 4mm;
+ padding: 0.25ex 0;
+}
+#headerSubTitle {
+ font-size: 50%;
+ font-style: italic;
+}
+
+.subHeader {
+display: none;
+ color: white;
+ background-color: rgb(0,51,153);
+ margin: 0;
+ padding: 1ex 1ex 1ex 1.5mm;
+}
+
+.subHeader a {
+ color: white;
+ background-color: transparent;
+ text-decoration: none;
+ font-weight: bold;
+ margin: 0;
+ padding: 0 0.75ex 0 0.5ex;
+}
+
+.subHeader a:hover {
+ text-decoration: underline;
+}
+
+.superHeader .highlight, .subHeader .highlight {
+ color: rgb(253,160,91);
+ background-color: transparent;
+}
+
+
+/* ##### Side Bar ##### */
+
+#side-bar {
+ width: 15em;
+ float: left;
+ clear: left;
+ border-right: 1px solid rgb(153,153,153);
+}
+
+#side-bar div {
+ border-bottom: 1px solid rgb(153,153,153);
+}
+
+.sideBarTitle {
+ font-weight: bold;
+ margin: 0 0 0.5em 2.5mm;
+ padding: 1em 0 0 0;
+}
+
+#side-bar ul {
+ list-style-type: none;
+ list-style-position: outside;
+ margin: 0;
+ padding: 0 0 1.1em 0;
+}
+
+#side-bar li {
+ margin: 0;
+ padding: 0.1ex 0; /* Circumvents a rendering bug (?) in MSIE 6.0 */
+}
+
+#side-bar a, .thisPage {
+ color: rgb(0,102,204);
+ background-color: transparent;
+ XXXtext-decoration: none;
+ margin: 0;
+ padding: 0.55em 1ex 0.55em 5mm;
+ display: block;
+}
+
+.thisPage {
+ color: black;
+ background-color: white;
+ padding-left: 5mm;
+ XXXborder-top: 1px solid rgb(153,153,153);
+ XXXborder-bottom: 1px solid rgb(153,153,153);
+ font-weight: 600;
+}
+
+#side-bar a:hover {
+ color: white;
+ background-color: rgb(100,135,220);
+ text-decoration: none;
+}
+
+.sideBarText {
+ line-height: 1.5em;
+ margin: 0 0 1em 0;
+ padding: 0 1.5ex 0 2.5mm;
+ display: block;
+}
+
+#side-bar .sideBarText a {
+ text-decoration: underline;
+ margin: 0;
+ padding: 0;
+ display: inline;
+}
+
+#side-bar .sideBarText a:hover {
+ color: rgb(0,102,204);
+ background-color: transparent;
+ text-decoration: none;
+}
+
+.lighterBackground {
+ color: inherit;
+ background-color: white;
+}
+
+
+/* ##### Main Copy ##### */
+
+#main-copy {
+ max-width: 90em;
+ color: black;
+ background-color: white;
+ text-align: justify;
+ line-height: 1.5em;
+ margin: 0 0 0 15em;
+ padding: 0.5mm 5mm 5mm 5mm;
+ border-left: 1px solid rgb(153,153,153);
+}
+
+#main-copy p {
+ margin: 1em 1ex 2em 1ex;
+ padding: 0;
+}
+
+#main-copy a {
+ color: rgb(0,102,204);
+ background-color: transparent;
+ text-decoration: underline;
+}
+
+#main-copy a:hover {
+ text-decoration: none;
+}
+
+#main-copy h1 {
+ color: white;
+ background-color: rgb(100,135,220);
+ font-size: 100%;
+ font-weight: bold;
+ margin: 3em 0 0 0;
+ padding: 0.5ex 0 0.5ex 1ex;
+}
+
+#main-copy .topOfPage {
+ color: white;
+ background-color: transparent;
+ font-size: 91%;
+ font-weight: bold;
+ text-decoration: none;
+ margin: 2.5ex 1ex 0 0; /* For MSIE */
+ padding: 0;
+ float: right;
+}
+
+#main-copy > .topOfPage {
+ margin: 2.75ex 1ex 0 0; /* For fully standards-compliant user agents */
+}
+
+dl {
+ margin: 1em 1ex 2em 1ex;
+ padding: 0;
+}
+
+dt {
+ font-weight: bold;
+ margin: 0 0 0 0;
+ padding: 0;
+}
+
+dd {
+ margin: 0 0 2em 2em;
+ padding: 0;
+}
+
+
+/* ##### Footer ##### */
+
+#footer {
+ color: white;
+ background-color: rgb(100,135,220);
+ font-size: 91%;
+ margin: 0;
+ padding: 1em 2.5mm 2.5ex 2.5mm;
+ clear: both;
+}
+
+#footer .left {
+ line-height: 1.45em;
+ float: left;
+ clear: left;
+}
+
+#footer .right {
+ text-align: right;
+ line-height: 1.45em;
+}
+
+#footer a {
+ color: white;
+ background-color: transparent;
+ text-decoration: underline;
+}
+
+#footer a:hover {
+ text-decoration: none;
+}
+
+
+/* GENERAL */
+/* Spam */
+.spam {
+ text-align: center;
+}
+
+/* Tables */
+table {
+ border: solid 1px black;
+}
+th {
+ background-color: #abc;
+ border: solid 1px black;
+}
+td {
+ background-color: #def;
+ border: solid 1px black;
+}
+
+hr {
+ border-width: 0px 0px 0.1em 0px;
+ border-color: black;
+}
+
+.spam table, .spam th, .spam td {
+ border: none;
+}
+
+/* Code */
+pre {
+ margin-left: 2em;
+}
+
+
diff --git a/sites/pmikkelsen.com/_files/djv.tar b/sites/pmikkelsen.com/_files/djv.tar
new file mode 100644
index 0000000..5d60262
--- /dev/null
+++ b/sites/pmikkelsen.com/_files/djv.tar
Binary files differ
diff --git a/sites/pmikkelsen.com/_files/djvmono.tar b/sites/pmikkelsen.com/_files/djvmono.tar
new file mode 100644
index 0000000..1fb0c7d
--- /dev/null
+++ b/sites/pmikkelsen.com/_files/djvmono.tar
Binary files differ
diff --git a/sites/pmikkelsen.com/_werc/config b/sites/pmikkelsen.com/_werc/config
new file mode 100644
index 0000000..85a49d9
--- /dev/null
+++ b/sites/pmikkelsen.com/_werc/config
@@ -0,0 +1,3 @@
+masterSite=pmikkelsen.com
+siteTitle='Peter&#39;s website'
+siteSubTitle='random notes'
diff --git a/sites/pmikkelsen.com/_werc/lib/footer.inc b/sites/pmikkelsen.com/_werc/lib/footer.inc
new file mode 100644
index 0000000..4cabbb1
--- /dev/null
+++ b/sites/pmikkelsen.com/_werc/lib/footer.inc
@@ -0,0 +1 @@
+<a href="http://werc.cat-v.org">Powered by werc</a> © Peter Mikkelsen 2019-2020 \ No newline at end of file
diff --git a/sites/pmikkelsen.com/_werc/lib/top_bar.inc b/sites/pmikkelsen.com/_werc/lib/top_bar.inc
new file mode 100644
index 0000000..976c8b0
--- /dev/null
+++ b/sites/pmikkelsen.com/_werc/lib/top_bar.inc
@@ -0,0 +1,10 @@
+ <div class="left">
+ <a href="https://git.sr.ht/~pmikkelsen">sourcehut git</a> |
+ <a href="https://hg.sr.ht/~pmikkelsen">sourcehut hg</a> |
+ <a href="https://gitlab.com/pmikkelsen">gitlab</a> |
+ <a href="http://9front.org">9front</a>
+ </div>
+
+ <div class="right">
+ </div>
+
diff --git a/sites/pmikkelsen.com/_werc/pub/style.css b/sites/pmikkelsen.com/_werc/pub/style.css
new file mode 100644
index 0000000..10935fb
--- /dev/null
+++ b/sites/pmikkelsen.com/_werc/pub/style.css
@@ -0,0 +1,67 @@
+body { display: flex; flex-wrap: wrap; font-family: sans;}
+header { flex-basis: 100%; flex-shrink: 0; }
+article { flex-basis: 60%; padding-left: 1em; }
+footer { flex-basis: 100%; flex-shrink: 0; }
+header nav { display: flex; justify-content: space-between; }
+nav a, header a { text-decoration: none ; color: inherit; }
+header h1 span { margin-left: 1em; font-size: 50%; font-style: italic; }
+body > nav { flex-basis: content; padding-right: 1vw; min-width: 16em; }
+nav ul { display: flex; flex-direction: column; list-style-type: none; list-style-position: outside; padding-left: 0; }
+nav li ul { padding-left: 0.6em }
+footer { display: flex; justify-content: space-between; }
+
+/* cut here to leave vanity behind */
+
+body { margin:0; padding: 0; font-size: 84%; font-family: Helvetica, Verdana, Arial, 'Liberation Sans', FreeSans, sans-serif; }
+a { text-decoration: none; color: }
+a:hover { text-decoration: underline; }
+.thisPage { color: black; }
+
+/* header and top bar */
+header nav { background-color: rgb(100,135,220); color: white; padding: 0.3em; border-bottom: 2px solid black; font-size: 91%; }
+header h1 { background-color: #ff6d06; color: black; margin: 0; border-bottom: 2px solid black; font-weight: normal; padding: 0.25ex; font-size: 233%; }
+header a:hover { text-decoration: none; }
+
+/* sidebar */
+body > nav { border-right: 1px solid #ddd; padding: 0; }
+body > nav > div { border-bottom: 1px solid #ddd; }
+body > nav > div a { color: rgb(0, 102, 204); display: block; text-transform: capitalize; font-weight: bold; padding: 0.25em 1ex 0.25em 2mm; font-size: 102%}
+body > nav > div a:hover { color: white; background-color: rgb(100,135,220); border-left: black solid 0.2em; text-decoration: none; }
+body > nav > div p { font-weight: bold; margin: 0 0 0.5em 2mm; padding: 1em 0 0 0; }
+
+/* main copy */
+article { padding: 0.5ex 0 5vh 1vw; }
+article h1, article h2 { color: rgb(0,102,204); font-weight: bold; margin: 2em 0 0 0; border-bottom: 2px solid rgb(0,102,204); }
+article h3, article h4, article h5 { color: rgb(0,102,204); font-weight: bold; margin: 2em 0 0 0; }
+article h6, article h7, article h8 { color: rgb(0,102,204); font-weight: bold; margin: 2em 0 0 0; }
+article a { color: rgb(0,102,204); }
+article a:hover { color: rgb(100,135,220); }
+article pre { font-size: 1.2em; }
+
+/* footer */
+footer { color: white; background-color: rgb(100,135,220); }
+footer a { color: inherit; }
+footer div { padding: 1em; }
+
+/* tables */
+table { border: 1px solid rgba(128,128,128,0.5); padding: 0; }
+th { color: white; background-color: rgb(100,135,220); }
+tr:nth-child(odd) { background-color: rgba(128,128,128,0.1) }
+
+/* Modifications for pmikkelsen.com */
+img {
+ max-width: 100%;
+ border: 1px solid black;
+}
+
+body {
+ background-color: #ffffea;
+}
+
+header h1 {
+ background-color: #eaffea;
+}
+
+html {
+ font-size: 1.2em;
+} \ No newline at end of file
diff --git a/sites/pmikkelsen.com/contact.md b/sites/pmikkelsen.com/contact.md
new file mode 100644
index 0000000..c4dd043
--- /dev/null
+++ b/sites/pmikkelsen.com/contact.md
@@ -0,0 +1,3 @@
+You can reach me on email via **petermikkelsen10@gmail.com**
+
+On github, gitlab and most other places, my name is **pmikkelsen**. \ No newline at end of file
diff --git a/sites/pmikkelsen.com/haskell/quine.md b/sites/pmikkelsen.com/haskell/quine.md
new file mode 100644
index 0000000..09a3dcd
--- /dev/null
+++ b/sites/pmikkelsen.com/haskell/quine.md
@@ -0,0 +1,94 @@
+Today I am going to go through the process of writing a small program
+which prints it's own source when executed. These kinds of programs are
+also called quines, and I suggest that you try it out on your own if you
+haven't already done so, as it is a very fun little problem! So if you
+don't want any spoilers, please leave this page now and come back later.
+
+
+## First attempt
+
+Okay, so first of let's just write a small hello world program to get
+things going. All our code will be in a file called `quine.hs`. The code
+for hello world looks like this
+
+
+ main = putStrLn "Hello world!"
+
+
+When this is run using runhaskell quine.hs, it produces the output
+
+
+ Hello world!
+
+
+Well, not quite a quine yet but we are getting something to the screen
+at least ;)
+
+## Second attempt
+
+Now we get the idea that we copy the entire program's source into a
+string and print that string instead. To do this, the code could look
+something like this
+
+
+ main = putStrLn code
+ where code = "main = putStrLn code\n where code = ???"
+
+
+Here we run into a problem, since we can't include the entire code into
+the string. Copying everything we can into the string just increases
+the size of the string itself, which means that there is now even more
+to copy! We start by only copying all the code upto the string and see
+how it looks when executed. The code is now
+
+
+ main = putStrLn code
+ where code = "main = putStrLn code\n where code = "
+
+
+And it outputs
+
+ main = putStrLn code
+ where code =
+
+Quite good! Now if we could only include the string itself in its printed form, with the quotes and all, and not the interpreted form with `\n` shown as newlines.
+
+## Third attempt
+
+After looking through the haskell standard libraries for a bit, we find
+a function that looks promising for what we want to do. This function is
+`print`, which prints the output using the `show` function instead of
+interpreting the string. We would want to both print it as before, and
+to print it using `print`, so we change our main to be `main = putStrLn
+code >> print code`, and update our string to include the print. The
+code then becomes
+
+ main = putStrLn code >> print code
+ where code = "main = putStrLn code >> print code\n where code = "
+
+And it outputs
+
+
+ main = putStrLn code >> print code
+ where code =
+ "main = putStrLn code >> print code\n where code = "
+
+
+So close! The only problem is that the `putStrLn` append a newline after
+the output, which we don't want in this case.
+
+## Final attempt
+
+The simple fix is just to use `putStrLn`'s little brother `putStr`
+which doesn't print that newline. The final program is then
+
+
+ main = putStr code >> print code
+ where code = "main = putStr code >> print code\n where code = "
+
+And luckily it outputs exactly itself
+
+
+ main = putStr code >> print code
+ where code = "main = putStr code >> print code\n where code = "
+
diff --git a/sites/pmikkelsen.com/images/2048.gif b/sites/pmikkelsen.com/images/2048.gif
new file mode 100644
index 0000000..19dd98f
--- /dev/null
+++ b/sites/pmikkelsen.com/images/2048.gif
Binary files differ
diff --git a/sites/pmikkelsen.com/images/acme-in-action.png b/sites/pmikkelsen.com/images/acme-in-action.png
new file mode 100644
index 0000000..b74d9dc
--- /dev/null
+++ b/sites/pmikkelsen.com/images/acme-in-action.png
Binary files differ
diff --git a/sites/pmikkelsen.com/images/cdude.png b/sites/pmikkelsen.com/images/cdude.png
new file mode 100644
index 0000000..66ee18d
--- /dev/null
+++ b/sites/pmikkelsen.com/images/cdude.png
Binary files differ
diff --git a/sites/pmikkelsen.com/images/cursed.png b/sites/pmikkelsen.com/images/cursed.png
new file mode 100644
index 0000000..f92e06e
--- /dev/null
+++ b/sites/pmikkelsen.com/images/cursed.png
Binary files differ
diff --git a/sites/pmikkelsen.com/images/discordgif.gif b/sites/pmikkelsen.com/images/discordgif.gif
new file mode 100644
index 0000000..207af41
--- /dev/null
+++ b/sites/pmikkelsen.com/images/discordgif.gif
Binary files differ
diff --git a/sites/pmikkelsen.com/images/djvfonts.png b/sites/pmikkelsen.com/images/djvfonts.png
new file mode 100644
index 0000000..86d58a4
--- /dev/null
+++ b/sites/pmikkelsen.com/images/djvfonts.png
Binary files differ
diff --git a/sites/pmikkelsen.com/images/linuxreverse.gif b/sites/pmikkelsen.com/images/linuxreverse.gif
new file mode 100644
index 0000000..6684116
--- /dev/null
+++ b/sites/pmikkelsen.com/images/linuxreverse.gif
Binary files differ
diff --git a/sites/pmikkelsen.com/images/mordor.gif b/sites/pmikkelsen.com/images/mordor.gif
new file mode 100644
index 0000000..fe13c09
--- /dev/null
+++ b/sites/pmikkelsen.com/images/mordor.gif
Binary files differ
diff --git a/sites/pmikkelsen.com/index.md b/sites/pmikkelsen.com/index.md
new file mode 100644
index 0000000..2713ad4
--- /dev/null
+++ b/sites/pmikkelsen.com/index.md
@@ -0,0 +1,28 @@
+
+## This website
+
+My intention with this website it to share some ideas and thoughts on
+programming and other computer related subjects. The things I care
+about and find interesting at the moment are written below, and are
+updated sometimes. Hopefully, most of the content will be about one of
+those things.
+
+- Functional programming (Haskell, Erlang, Scheme)
+- Concurrent programming (Erlang, Go)
+- Plan 9 (both the 9front fork and plan9port)
+
+For information about how the website it setup, see
+[this](/web/the-setup-of-this-site) post.
+
+## Me
+
+My name is Peter Mikkelsen and I am a computer science student at Aalborg University, Denmark.
+
+The following list contains other facts and links
+
+- *Age*: 22
+- *Favourite programming language*: See [this](/opinions/favourite-programming-languages) post
+- *Current job*: None :)
+- *Stuff I use*: See [this](/me/stuff-i-use) post.
+- *Tabs vs spaces*: Tabs.
+- *Emacs* vs *Vi* [None of them](/me/how-i-started-using-acme)
diff --git a/sites/pmikkelsen.com/linux/eduroam.md b/sites/pmikkelsen.com/linux/eduroam.md
new file mode 100644
index 0000000..1cdc830
--- /dev/null
+++ b/sites/pmikkelsen.com/linux/eduroam.md
@@ -0,0 +1,15 @@
+# Connecting to the eduroam network on AAU
+
+Goto net.aau.dk and create a new device.
+
+Connect to the eduroam network and set the correct wifi-security settings:
+
+ security: WPA & WPA2 Enterprise
+ Authentication: PEAP
+ CA Cert: no CA certificate is required
+ PEAP Version: Automatic
+ Inner authentication: MSCHAPv2
+
+And fill out the username and password from the website.
+
+No need for the stupid script.
diff --git a/sites/pmikkelsen.com/linux/wifi.md b/sites/pmikkelsen.com/linux/wifi.md
new file mode 100644
index 0000000..be3b1cc
--- /dev/null
+++ b/sites/pmikkelsen.com/linux/wifi.md
@@ -0,0 +1,12 @@
+# Connecting to wifi on linux
+
+What I have to do:
+
+ wpa_passphrase NameOfNetwork NetworkKey >> /etc/wpa_supplicant/wpa_supplicant-wlp4s0.conf
+
+The network card name might be different.
+
+Then reboot aaaand done :)
+
+This is on void linux, so it might not be the same for you.
+
diff --git a/sites/pmikkelsen.com/me/how-i-started-using-acme.md b/sites/pmikkelsen.com/me/how-i-started-using-acme.md
new file mode 100644
index 0000000..9e37dd7
--- /dev/null
+++ b/sites/pmikkelsen.com/me/how-i-started-using-acme.md
@@ -0,0 +1,117 @@
+## The beginning
+
+My very first code editor was visual studio 2010 for windows 7. This was
+when I didn't know what programming was, and a teacher I had suggested
+that we used that, so we could start learning some C#. It was very fun and
+all, but soon after, I decided that I wanted to learn another language,
+and the whole idea of using an IDE which is specialized for a specific
+programming language or environment just seemed very odd to me. Surely
+there had to be better options, where I as a user could decide by myself
+what languages I wanted support for. I tried out multiple different
+small editors but didn't really like any of them.
+
+After some time, I decided that I wanted to try linux, and I installed
+fedora or linux mint (don't remember at this point), and I liked it very
+much. Comming from windows where it is normal just to go to a random
+website and download the tools I needed, it was a great joy to be able
+to just type a command in the terminal and watch my program get installed.
+
+## First try: vim
+
+After reading online for some time, it seemed that the editor which
+all the cool linux users used was vim, so I decided to learn that. The
+experience was very different from what I was used to, but I liked the
+keyboard shortcuts and that it looked very cool. Of course I installed
+every plugin that I had read about online and it all became a mess,
+but I stuck with it for around 3-4 years. The things I liked the most
+was that I felt it was very fast to do what I wanted, but unfortunately
+the flow with multiple files was never something I could get used to,
+and all the plugins made me sick.
+
+## Second try: emacs
+
+Just to give another very popular editor a try, I installed emacs sometime
+2-3 years ago. Comming from vim, the keyboard shortcuts in emacs sucks,
+but the editor itself is just so much more powerful, because plugins and
+customisations could be written in a real programming language. I started,
+as many other emacs users do, to use emacs for everything i could. I read
+my mails, watched PDFs, managed my system, and even used it as my window
+manager for a period. Around the same time that I started using emacs,
+I also started using OpenBSD as a secondary operating system. I noticed
+that many very good tools are available in unix, but emacs almost has
+everything implemented again in elisp, which seemed stupid to me. But hey,
+it works so I stuck with it. Until late 2019.
+
+## Now acme
+
+One day in school when one of my group mates asked if I could look at
+something for him, I saw that he was using visual studio code, which
+made it natural to use a mouse to click around in the file. Of course
+this was also possible in emacs, but since I came from vim, it was never
+something that I did. He argued that it was much faster to just click in
+the file where he wanted to edit, than to navigate using the keyboard, and
+I thought that maybe he was right, so I started digging. After some time
+looking though the internet for some editors which made much use of the
+mouse, I found acme and decided to install it. And wow it was different.
+
+[![A picture of acme in action][1]][1]
+
+Acme uses the mouse for everything, since there are very few keyboard
+shortcuts (not even copy and paste). As shown on the picture, there are no
+menus and no icons; everything is just text, and the different buttons on
+the mouse can interpret that text in different ways. For example, middle
+clicking on the text `New` wil create a new text frame, and right clicking
+on the text `example` will search the file for the word. Actually, it is
+more advanced than that since the right click will first send the selected
+text to something called the plumber, which might do something fun like
+opening the file which has that name. I suggest interrested readers to
+read the page [here](http://acme.cat-v.org/) and some of the documents
+linked from that site to learn more about acme. The video introduction
+from Russ Cox [here](https://www.youtube.com/watch?v=dP1xVpMPn8M) is
+also great.
+
+
+## What I like about acme
+
+* **The colours**: they cannot be changed without modifying the source,
+but luckily for me, I love those pale bright colours. In fact, there
+are the very reason this website has the colours that it has.
+
+* **Writing commands**: since acme allows users to control it via the 9P
+protocol (if you don't understand, read the links above), it is possible
+to write "plugins" or commands in whatever language I want. After
+just creating a program with the needed functionality, it is possible
+to write the name somewhere and middle click on it, just like it was
+always there. Not may editors makes it possible to use the environment
+as much as acme does, which was also one of my wonders about emacs back
+then. It allows me to turn it into *my* IDE using whatever tools I see fit,
+instead of depending on support directly in the editor.
+
+* **Everything as text**: this means that if I run a gdb session via
+the `win` command, it is totally possible for me to just scroll up in the
+history and delete lines which are not important, and to write notes
+as I debug. Also, since commands are just text which is clicked, it is
+possible to have a document with commands which are useful in a given
+project, which I can then open and click.
+
+* **Lack of customisation**: while this might seem strange, comming from
+a vim and emacs background, it surely is wonderful to have an editor
+which does *not* encourage the user to customise the hell out of it. I
+have spent way too much time doing this in the past and it had to stop.
+
+* **Lack of syntax highlighting and auto completion**: some people love
+it and can't live without it, but I very much prefer to learn the syntax
+of the language based on the contents, not based on some colours an
+editor throws at me. Also I feel like I learn and remember much better
+when there is no auto completion.
+
+## Other editors I use sometimes
+
+Sometimes I have to do very small editing tasks, and sometimes as root,
+in which case it is just simpler to open the file in vi. Note that this
+is the small vi, and not the "improved" vim. I also sometimes edit my
+text in sam with is another editor written by Rob Pike, and I quite like
+that one too for smaller projects. You can read more about sam
+[here](http://sam.cat-v.org/).
+
+[1]: /images/acme-in-action.png \ No newline at end of file
diff --git a/sites/pmikkelsen.com/me/stuff-i-use.md b/sites/pmikkelsen.com/me/stuff-i-use.md
new file mode 100644
index 0000000..08aa7e7
--- /dev/null
+++ b/sites/pmikkelsen.com/me/stuff-i-use.md
@@ -0,0 +1,46 @@
+* Note: Last updated on 2020/05/28
+
+# Operating system
+
+I mostly use [guix][1] but sometimes I need software that is not yet
+ported, and then I use [fedora][2]. If I could, I would use [9front][3]
+alot more, since the system is much better and different in my opinion. I
+also like [OpenBSD][4] alot.
+
+# Text editor
+
+Acme from plan9port. Sometimes I use emacs if I have to edit scheme code, since automatic closing of matching `(` and `)` makes life much easier.
+
+# Shell
+
+I use the [rc shell][5] since it works nicely in both acme and 9term from plan9port. Also I like its syntax alot more than the syntax of bash.
+
+# Web browser
+
+Mainy firefox (this page is only tested on firefox, so please let me
+know if you have problems on other browsers).
+
+# Laptop
+
+I use a Lenovo Thinkpad E495 with an AMD ryzen 3700U and 16 gigabytes
+of ram. It is a fairly good computer for the price, and OpenBSD is
+supported out of the box with the exception of wifi. For this reason, I
+have small usb wifi dongle that is constantly plugged in, which performs a
+lot worse than what some people would like, but for me it is no big deal.
+
+
+# Mouse and keyboard
+
+Since I have never been a fan of touchpads on laptops, I use an external
+mouse which is a Logitech MX Master 3. I also have a keyboard that I
+sometimes plug in if I have to do more writing than what I can comfortably
+do on my laptop. The keyboard is from the coolermaster masterkeys lite L
+bundle. Clicking in acme using the scroll wheel instead of using a true
+3-button mouse is sometimes a bit annoying, but its OK.
+
+
+[1]: https://guix.gnu.org/
+[2]: https://getfedora.org/
+[3]: http://9front.org/
+[4]: https://openbsd.org/
+[5]: https://plan9.io/sys/doc/rc.html \ No newline at end of file
diff --git a/sites/pmikkelsen.com/opinions/favourite-programming-languages.md b/sites/pmikkelsen.com/opinions/favourite-programming-languages.md
new file mode 100644
index 0000000..f80131c
--- /dev/null
+++ b/sites/pmikkelsen.com/opinions/favourite-programming-languages.md
@@ -0,0 +1 @@
+To be written \ No newline at end of file
diff --git a/sites/pmikkelsen.com/plan9/basic_9p_server.md b/sites/pmikkelsen.com/plan9/basic_9p_server.md
new file mode 100644
index 0000000..7c1c514
--- /dev/null
+++ b/sites/pmikkelsen.com/plan9/basic_9p_server.md
@@ -0,0 +1,58 @@
+## Writing a basic 9P server
+
+On plan 9 the entire system is build around the idea of namespaces
+and that _"everything is a file"_. For this reason it is very easy to write
+a new 9P fileserver in C since all the boring tasks are implemented in
+libraries. This note describes a minimal program which serves a folder to
+`/mnt/hello9p` containing a single synthetic file with the contents "Hello from 9P!".
+
+## The code
+
+ #include <u.h>
+ #include <libc.h>
+ #include <fcall.h>
+ #include <thread.h>
+ #include <9p.h>
+
+ void
+ fsread(Req *r)
+ {
+ readstr(r, "Hello from 9P!\n");
+ respond(r, nil);
+ }
+
+ Srv fs = {
+ .read = fsread,
+ };
+
+ void
+ main(void)
+ {
+ Tree *tree;
+
+ tree = alloctree(nil, nil, DMDIR|0555, nil);
+ fs.tree = tree;
+ createfile(tree->root, "hello", nil, 0555, nil);
+
+ postmountsrv(&fs, nil, "/mnt/hello9p", MREPL | MCREATE);
+ }
+
+## Explanation
+
+The global variable `fs` is a structure which contains function pointers
+to all the 9P handlers, but since I only plan on reading from the file,
+only the `read` field is set. The fsread function calls two helper functions
+from the 9p(2) library which will create a response with the given string as
+the file contents.
+
+In `main` I start by allocating a new file tree, since this 9P server deals with
+a fileserver that has a tree structure, and therefore I don't have to worry
+about how directories are handled for example. A file is added with `createfile`
+to the root of the tree.
+
+The call to `postmountsrv` will mount the 9P server under `/mnt/hello9p`.
+
+## Thats it
+
+This is not very complicated, but see the manpages at 9p(2) and 9pfile(2)
+and intro(5) for more information about the libraries and 9P itself.
diff --git a/sites/pmikkelsen.com/plan9/discord.md b/sites/pmikkelsen.com/plan9/discord.md
new file mode 100644
index 0000000..8a9815f
--- /dev/null
+++ b/sites/pmikkelsen.com/plan9/discord.md
@@ -0,0 +1,83 @@
+# Chatting on discord from 9front
+
+Sometimes you work with people who doesn't want to use the same communication
+tools as you do. A sad example is when your team members wants to use discord,
+and not something simpler like IRC. Oh well, gotta deal with it.
+
+This is the reason I wrote a "discord bot" (ugh) which can run on your 9front
+system and allow you to chat on discord anyways.
+
+## Example gif
+
+The example below just opens two chat windows with different channels in each.
+While it works both in acme and in the terminal, I like having everything
+together inside acme.
+
+[![An animated gif showing discord chats from 9front][1]][1]
+
+## The code
+
+The project is made of a few scripts and a go program. The only thing that
+actually communicates with discord is the go program, and the scripts just
+makes it easier to send and recieve on the correct channels. The code is
+available [here](https://git.sr.ht/~pmikkelsen/discordfront), and there is
+a precompiled version of the go program included (compiled for 9front amd64).
+
+## Usage
+
+To use the service, you must first create a bot on discord and get the
+access token. I personally found this step much more confusing then the
+actual coding itself, which either tells you something about my google
+skills, or about discord.. Oh, and then you must invite the bot to your
+discord server..
+
+Next, the server part of the bot must be started:
+
+ `discordsrv YOURTOKEN`
+
+This will post a pipe in /srv/discordfront, where messages can be send
+and recieved from, but you should not read from it directly. The
+`discordsrv` script does this for you as well, and it takes each line
+and dumps it in a nice readable format into
+`$home/lib/discord/logs/$serverName/$channelName`
+where the server name is the name of a discord server (or guild?), and
+the channel name is, well, the name of the channel.
+
+Now that the server is started, you can either run `discordacme` which
+opens an acme with the file `$home/lib/discord/channels` open. This file
+is not handled by any of the scripts, so it is just a handy text file to
+keep shortcuts to various channels (see the gif for an example).
+Inside `discordacme`, the `openChat` command will open a new chat window,
+using the simpler `discord` script.
+
+The `discord` script simply runs `tail -f` on the given channel's logfile
+while it reads messages from standard input and sends them to
+/srv/discordfront at the same time.
+
+## Using remotely
+
+Since this program does not fetch previous messages, it might be a good
+idea to run the server part `discordsrv` on a machine that is always online,
+so that all messages gets logged. Since this is plan9, it is almost trivial
+to still use the client on your local machine, just by using `rimport` to
+import the relevant file trees from the server.
+
+Here is what I do (in a seperate script which is run at startup):
+
+ #!/bin/rc
+
+ rfork
+ rimport -ac -p $serverhost /srv
+ rimport -c -p $serverhost /usr/glenda/lib/discord
+ discordacme
+
+I don't even notice it is not running locally.
+
+## Notes
+
+* Yes, the names of the scripts suck.
+* Yes, there is a lot of missing functionality.
+* No, this has not been tested very much, it just seems to do the job
+well enough for now.
+
+[1]: /images/discordgif.gif
diff --git a/sites/pmikkelsen.com/plan9/dns.md b/sites/pmikkelsen.com/plan9/dns.md
new file mode 100644
index 0000000..04bd7b5
--- /dev/null
+++ b/sites/pmikkelsen.com/plan9/dns.md
@@ -0,0 +1,41 @@
+# Using 9front as an authoritative DNS server
+
+This note describes the steps I took to make the 9front server
+at pmikkelsen.com the authoritative dns server for itself.
+
+First, I logged into my domain name registrar and changed the dns servers for
+my domain to `ns1.pmikkelsen.com` and `ns2.pmikkelsen.com` (It would not let me have
+just one, but I choose to live dangerously and point both of those domains at
+the same server).
+
+To let the world know _where_ `ns1` and `ns2` can be found,
+I added their ip on the registrar's website (since my own dns server cannot serve
+them for good reasons). Anyways, this might not be the same for you since you
+may not use the same provider.
+
+## Starting the dns server in "serve mode"
+
+First I added 1 line to the `/cfg/$sysname/cpurc` file to enable the dns server.
+
+ ndb/dns -s
+
+After that I added a few lines in `/lib/ndb/local` to setup all the dns records I needed:
+
+ dom=pmikkelsen.com soa=
+ ip=80.240.16.196
+ mx=vps1.pmikkelsen.com pref=1
+ txtrr="v=spf1 a mx ip:80.240.16.196 ~all"
+
+ dom=p9auth.pmikkelsen.com soa=
+ ip=80.240.16.196
+
+ dom=vps1.pmikkelsen.com soa=
+ ip=80.240.16.196
+
+ dom=_dmarc.pmikkelsen.com soa=
+ txtrr="v=DMARC1; p=none"
+
+With this done, I now have A records, mx records and txt records in place, so my website,
+mail and rcpu works as expected.
+
+Bye. \ No newline at end of file
diff --git a/sites/pmikkelsen.com/plan9/fonts.md b/sites/pmikkelsen.com/plan9/fonts.md
new file mode 100644
index 0000000..6cd6780
--- /dev/null
+++ b/sites/pmikkelsen.com/plan9/fonts.md
@@ -0,0 +1,29 @@
+# Fonts on 9front
+
+By default 9front uses a font called VGA but its too small for my eyes.
+
+The fonts I use (dejavu) look like this[![an image showing the fonts][1]][1] and are attached below:
+
+[proportional font](/_files/djv.tar)
+
+[monospace font](/_files/djvmono.tar)
+
+They can be recreated by having them installed on linux and doing the following from
+linux with plan9port installed.
+
+ fontsrv -m /tmp/fonts
+ cp -r /tmp/fonts/DejaVuSomeThing/14a/ /tmp/djv
+
+You can try a different font or font size if you want.
+
+Then connect to 9front via drawterm and run:
+
+ mkdir /lib/font/bit/djv
+ dircp /mnt/term/tmp/djv /lib/font/bit/djv
+
+Finally change your `lib/profile` to use `/lib/font/bit/djv/font` :)
+
+Note that 9front includes truetypefs which allows you to use `.ttf` files
+directly, but I find the results are better looking this way.
+
+[1]: /images/djvfonts.png \ No newline at end of file
diff --git a/sites/pmikkelsen.com/plan9/lets_encrypt.md b/sites/pmikkelsen.com/plan9/lets_encrypt.md
new file mode 100644
index 0000000..f0b1c07
--- /dev/null
+++ b/sites/pmikkelsen.com/plan9/lets_encrypt.md
@@ -0,0 +1,59 @@
+## How I get tls certificates for 9front
+
+First of all, I use linux and drawterm for this for now, but
+I would like to be able to do it all from 9front at some point.
+
+## Generate the certificate
+
+Install certbot on linux and run the following command
+
+ certbot certonly --manual -d pmikkelsen.com -d vps1.pmikkelsen.com
+
+and do the challenges, they should be easy.
+
+## Importing the cert and private key
+
+Start drawterm and login as the hostowner. After this, the filesystem of the linux
+system is available at `/mnt/term`. Run the following:
+
+ cd /sys/lib/tls/
+ cp /mnt/term/etc/letsencrypt/live/pmikkelsen.com/privkey.pem ./
+ cp /mnt/term/etc/letsencrypt/live/pmikkelsen.com/fullchain.pem ./cert
+
+Now the private key must be converted to one that can be loaded into factotum
+
+ auth/pemdecode 'PRIVATE KEY' privkey.pem | auth/asn12rsa -t 'service=tls role=client' > key
+ rm privkey.pem
+ chmod 400 key
+
+Add the following to `/cfg/$sysname/cpurc` to load the private key on boot.
+
+ cat /sys/lib/tls/key >> /mnt/factotum/ctl
+
+Done.
+
+## SMTP over TLS
+
+I have the following in `/bin/service.auth/tcp25`
+
+ #!/bin/rc
+
+ user=`{cat /dev/user}
+ exec upas/smtpd -c /sys/lib/tls/cert -n $3
+
+Notice I had to put it in the `/bin/service.auth` folder so that it could find the private key.
+
+## Https with rc-httpd
+
+I have the following in `/bin/service.auth/tcp443`
+
+ #!/bin/rc
+
+ exec tlssrv -c /sys/lib/tls/cert -l /sys/log/https /bin/service/tcp80 $*
+
+Again, in the `/bin/service.auth` folder. It simply wraps the plain http service
+in a tls wrapper which looks like this for me
+
+ #!/bin/rc
+ PLAN9=/
+ exec /rc/bin/rc-httpd/rc-httpd >>[2]/sys/log/www
diff --git a/sites/pmikkelsen.com/plan9/mounting-9p-over-drawterm.md b/sites/pmikkelsen.com/plan9/mounting-9p-over-drawterm.md
new file mode 100644
index 0000000..414b9f2
--- /dev/null
+++ b/sites/pmikkelsen.com/plan9/mounting-9p-over-drawterm.md
@@ -0,0 +1,58 @@
+# Mounting a 9P connection over drawterm
+
+I sometimes use drawterm on linux to connect to my 9front server.
+While it is possible to access the host system's files under `/mnt/term`, there is no builtin way to access the remote system's file under linux.
+Now, why would anybody want to do this?
+In my case, I often want to write some code under 9front, but for languages which aren't supported such as prolog in this case, so there are three options as I see it:
+
+* Store the files on the host machine, and access them under `/mnt/term`.
+* Store the files on the server and somehow mount the server's filesystem on the host.
+* Store the files on a third machine that both the host and server can access.
+
+Option number two seems best for me, so I asked around and it seems like the best tool to mount a 9P connection on linux is [9pfs](https://github.com/bunny351/9pfs).
+
+## How it works
+
+Before I could mount the connection, I had to serve it somehow. Normally I already serve 9P directly from my server's filesystem, but that requires some
+authentication that 9pfs does not support, so I had to serve it without authentication.
+Serving directly to the internet without authentication is of course pretty dumb since everyone can then access my files, so thanks to a hint from hiro, I figured
+out that it is actually possible to use the host's network stack on the server by binding `/mnt/term/net` over `/net`.
+
+I'll just show the final script below and explain it afterwards:
+
+ #!/bin/rc
+
+ rfork n
+ bind /mnt/term/net /net
+ aux/listen1 -t tcp!*!12335 /bin/exportfs -r / &
+ os mkdir -p /tmp/drawterm
+ os 9pfs localhost -p 12335 /tmp/drawterm
+
+So the first thing that happens is that I bind the host's network in, and from that point on, every network connection in this namespace actually goes out
+from the host instead of from the server!
+
+Then `exportfs` is started and it is serving the `/` directory over 9P at port 12335.
+The `os` command runs a command on the host, so it just creates the folder that the system will be mounted to, and then uses `9pfs` to actually mount it.
+The nice thing here is that `9pfs` just connects to localhost.
+
+## Using it
+
+As I said in the beginning, I did this to be able to edit files on the 9front server, and run compilers/interpreters on linux.
+The `os` command goes a long way, but the following script makes it even easier (I have this installed as `linux`):
+
+ #!/bin/rc
+
+ dir=/tmp/drawterm/`{pwd}
+ os -d $dir $*
+
+This means I can just go into any directory on the server, type `linux ghci` and I get a haskell repl running in the correct directory, as seen in the gif below.
+
+[![An animated gif showing ghci loading a file on the 9front server][1]][1]
+
+
+## Final notes
+
+The bind net trick still blows my mind a bit, since it is so trivial, yet so powerful. It is also fun to think about how one would do this
+on other systems than plan9, which I can't even imagine.
+
+[1]: /images/linuxreverse.gif
diff --git a/sites/pmikkelsen.com/plan9/network_booting.md b/sites/pmikkelsen.com/plan9/network_booting.md
new file mode 100644
index 0000000..3bb51b9
--- /dev/null
+++ b/sites/pmikkelsen.com/plan9/network_booting.md
@@ -0,0 +1,30 @@
+## Network booting into the server at pmikkelsen.com
+
+Sometimes it is nice to be able to connect directly to a remote server
+from my laptop and have the same root filesystem available.
+This is possible by typing `tls` at the boot prompt and then
+typing `pmikkelsen.com` for the fs and auth server.
+
+## Speed
+
+On my internet connection some things can be very slow
+(such as compiling the entire system), because of fs access now happens
+over the network. The easy fix is to just start a cpu connection to the server
+where the fs access can happen fast since it is on the same machine. This is
+done by typing
+
+ rcpu -h pmikkelsen.com
+
+Of course the server has a pretty slow cpu..
+
+## Server settings
+
+Some steps are needed to make this work:
+
+1. The user must be added to the fileserver and auth server.
+2. The fileserver must be listening for remote connections.
+3. The correct ndb entries must be set (in the beginning I forgot `fs` and I could
+only connect via tcp, not tls)
+
+More information about all those steps can be found in section 7 of [the 9front FQA](http://fqa.9front.org/fqa7.html).
+
diff --git a/sites/pmikkelsen.com/plan9/trellofs.md b/sites/pmikkelsen.com/plan9/trellofs.md
new file mode 100644
index 0000000..42e2cfd
--- /dev/null
+++ b/sites/pmikkelsen.com/plan9/trellofs.md
@@ -0,0 +1,347 @@
+## A filesystem for viewing trello
+
+Since my last note [here](http://pmikkelsen.com/plan9/basic_9p_server)
+was very simple, here comes
+a small program (around 250 lines of C), which makes it
+possible to view trello as a read-only filesystem.
+
+## Usage example
+
+First set the `trellokey` and `trellotoken` environment
+variables to the API key and token from the trello website.
+
+ term% trellokey=1298791723987937123.....
+ term% trellotoken=923748237497ab798798999999.....
+
+Then `webfs` must be started so the program can make
+requests to the trello REST api.
+
+ term% webfs
+
+Then `trellofs` itself must be started. It mounts the filesystem under `/mnt/trello`.
+
+ term% trellofs
+
+If I now go and see which boards I have, I get the following
+
+ term% cd /mnt/trello
+ term% ls
+ Beaglebone_Black
+ Fly
+ Fysik_Rapport
+ Guld_kobber
+ Ideer_til_2._produkt
+ Lektier
+ P0
+ P1
+ P2
+ Projekt_Materialer
+ Spil
+ Welcome_Board
+
+The names of the folders are almost like the original board names, except
+that that some characters have been replaced by `_`. Lets look at `P1`
+
+ term% cd P1
+ term% ls
+ Backlog
+ Done
+ Estimat
+ In-progress
+ Info
+ Master.C_Top_Down_Ting
+ Sprint_1
+ Test
+ Test_sprint_1
+ Todo
+
+Todo must have something worth looking at
+
+ term% ls
+ Færdig_rapport_aflevering
+ Korrekturlæsning
+
+Those two items are the actual trello "cards", which I think of as tasks. The first
+one means "Final report hand in", so let's have a look at that.
+
+ term% cd Færdig_rapport_aflevering
+ term% ls
+ description
+ duedate
+ url
+
+There are always three files inside each card folder, but since the information in them
+is not mandatory on trello, some of them will be empty.
+
+ term% cat description
+
+ term% cat duedate
+ 2019-12-18T13:00:00.000Z
+ term% cat url
+ https://trello.com/c/u23ZcNAm/9-f%C3%A6rdig-rapport-aflevering
+
+## Limitations
+
+The key and token are right now in environment variables, which is somewhat
+awkward. Also I would like to be able to actually create and move cards directly
+from the filesystem just by creating files, but that is not something I will implement
+right now.
+
+## The code
+
+The code for the `trellofs` program is listed below.
+
+ #include <u.h>
+ #include <libc.h>
+ #include <fcall.h>
+ #include <thread.h>
+ #include <9p.h>
+ #include <json.h>
+
+ #define MAX_RESPONSE_SIZE (1024 * 1024 * 8)
+
+ char *key;
+ char *token;
+
+ void
+ fsread(Req *r)
+ {
+ char *str;
+
+ if (r->fid->file->aux == nil) {
+ readstr(r, "");
+ } else {
+ str = malloc(strlen(r->fid->file->aux) + 2);
+ sprint(str, "%s\n", r->fid->file->aux);
+ readstr(r, str);
+ }
+ respond(r, nil);
+ }
+
+ Srv fs = {
+ .read = fsread,
+ };
+
+ JSON *
+ trelloget(char *endpoint, char *params)
+ {
+ int ctlfd, bodyfd;
+ int n;
+ int err;
+ char *buf;
+ JSON *res;
+
+ res = nil;
+ bodyfd = -1;
+
+ buf = malloc(MAX_RESPONSE_SIZE + 1);
+ if(buf == nil) {
+ perror("malloc");
+ return nil;
+ }
+
+ ctlfd = open("/mnt/web/clone", ORDWR);
+ if(ctlfd < 0) {
+ perror("open");
+ goto fail;
+ }
+
+ if(read(ctlfd, buf, 32) < 0) {
+ perror("read");
+ goto fail;
+ }
+
+ n = atoi(buf);
+
+ sprint(buf, "url https://api.trello.com/%s?key=%s&token=%s", endpoint, key, token);
+ if(params)
+ sprint(buf + strlen(buf), "\&%s\n", params);
+ else
+ sprint(buf + strlen(buf), "\n");
+
+ err = write(ctlfd, buf, strlen(buf));
+ if(err < 0){
+ perror("write");
+ goto fail;
+ }
+
+ sprint(buf, "/mnt/web/%d/body", n);
+ bodyfd = open(buf, OREAD);
+ if(bodyfd < 0){
+ perror("open");
+ goto fail;
+ }
+
+ err = readn(bodyfd, buf, MAX_RESPONSE_SIZE);
+ if (err < 0) {
+ perror("read");
+ goto fail;
+ }
+
+ res = jsonparse(buf);
+
+ fail:
+ close(ctlfd);
+ close(bodyfd);
+ free(buf);
+
+ return res;
+ }
+
+ char *
+ escapename(char *str)
+ {
+ char *new;
+ int i, len;
+
+ len = strlen(str);
+
+ new = malloc(len + 1);
+ for(i = 0; i <= len; i++){
+ switch(str[i]){
+ case '/':
+ case ' ':
+ case ',':
+ case ':':
+ case '"':
+ case '\'':
+ new[i] = '_';
+ break;
+ default:
+ new[i] = str[i];
+ }
+ }
+ return new;
+ }
+
+ void
+ addcard(File *dir, JSON *card)
+ {
+ JSON *element;
+ char *filename;
+ File *carddir;
+ char *description;
+ char *url;
+ char *duedate;
+
+ element = jsonbyname(card, "name");
+ filename = escapename(element->s);
+ carddir = createfile(dir, filename, nil, DMDIR|0555, nil);
+ if(carddir == nil){
+ perror("createfile");
+ return;
+ } else {
+ element = jsonbyname(card, "desc");
+ description = strdup(element->s);
+ element = jsonbyname(card, "url");
+ url = strdup(element->s);
+ element = jsonbyname(card, "due");
+ if (element->t == JSONString)
+ duedate = strdup(element->s);
+ else
+ duedate = nil;
+ createfile(carddir, "description", nil, 0444, description);
+ createfile(carddir, "url", nil, 0444, url);
+ createfile(carddir, "duedate", nil, 0444, duedate);
+ }
+ free(filename);
+ }
+
+ void
+ addlist(File *dir, JSON *list)
+ {
+ JSON *element;
+ char *filename;
+ JSON *cards;
+ JSONEl *card;
+ char cardsEndpoint[128];
+ File *listdir;
+
+ element = jsonbyname(list, "name");
+
+ filename = escapename(element->s);
+ listdir = createfile(dir, filename, nil, DMDIR|0555, nil);
+ if(listdir == nil){
+ perror("createfile");
+ return;
+ } else {
+ element = jsonbyname(list, "id");
+ sprint(cardsEndpoint, "1/lists/%s/cards", element->s);
+ cards = trelloget(cardsEndpoint, "fields=desc,name,url,due");
+ if(cards == nil)
+ return;
+
+ for(card = cards->first; card != nil; card = card->next){
+ addcard(listdir, card->val);
+ }
+ }
+ jsonfree(cards);
+ free(filename);
+ }
+
+ void
+ addboard(File *root, JSON *board)
+ {
+ JSON *element;
+ JSONEl *list;
+ char *filename;
+ File *boarddir;
+
+ element = jsonbyname(board, "name");
+ filename = escapename(element->s);
+ boarddir = createfile(root, filename, nil, DMDIR|0555, nil);
+ if(boarddir == nil) {
+ perror("createfile");
+ return;
+ }
+
+ element = jsonbyname(board, "lists");
+ for(list = element->first; list != nil; list = list->next){
+ addlist(boarddir, list->val);
+ }
+
+ free(filename);
+ }
+
+ void
+ trelloinit(File *root)
+ {
+ JSON *result;
+ JSONEl *board;
+
+ result = trelloget("1/members/me/boards", "fields=name,lists&lists=open");
+
+ for(board = result->first; board != nil; board = board->next){
+ addboard(root, board->val);
+ }
+ jsonfree(result);
+ }
+
+ void
+ main(void)
+ {
+ JSONfmtinstall();
+
+ key = getenv("trellokey");
+ token = getenv("trellotoken");
+
+ Tree *tree;
+
+ tree = alloctree(nil, nil, DMDIR|0555, nil);
+
+ fs.tree = tree;
+ trelloinit(tree->root);
+
+ postmountsrv(&fs, nil, "/mnt/trello", MREPL | MCREATE);
+ }
+
+The program can be compiled by hand or by using the following `mkfile`.
+
+ BIN=/usr/glenda/bin/amd64
+
+ TARG=trellofs
+
+ OFILES=\
+ main.$O\
+
+ </sys/src/cmd/mkone
diff --git a/sites/pmikkelsen.com/plan9/using_irc.md b/sites/pmikkelsen.com/plan9/using_irc.md
new file mode 100644
index 0000000..0c3622c
--- /dev/null
+++ b/sites/pmikkelsen.com/plan9/using_irc.md
@@ -0,0 +1,48 @@
+_Last updated: 2020-07-30_
+
+# Using IRC on 9front
+
+While 9front comes with an irc client called `ircrc`, it can be quite annoying that it does not
+keep a persistent connection.
+
+A different program `irc7` provides a server part which keeps a connection open to an irc server,
+and a client part which allows for connecting to the server. This works great since it makes it possible to
+get all the messages on a channel without being online all the time.
+
+## Setup
+
+Download the irc7 code via `hg`:
+
+ hg clone https://code.9front.org/hg/irc7/
+
+Build and install
+
+ cd irc7
+ mk install
+
+## Usage
+
+First the server program `ircsrv` must be started, so I would run
+
+ ircsrv -e -p VerySecretPassword pmikkelsen net!irc.freenode.net!7000
+
+to connect as `pmikkelsen` to freenode over tls and with password for my nickname.
+After this, it is possible to connect to different channels by opening new rio windows
+and running
+
+ irc -t '#cat-v' -b
+
+to connect to the cat-v channel. The `-b` option without any extra arguments
+prints the entire conversation since the ircsrv was started,
+so it might not always be desirable.
+
+To see all the messages sent directly to me, I would run
+
+ irc -t MSGS
+
+and maybe start a conversation with user 'mousehater' by running
+
+ irc -t mousehater
+
+I run this on a remote cpu server that is rarely powered off, and then I just connect via
+drawterm from linux to chat, or via rcpu from other 9front machines.
diff --git a/sites/pmikkelsen.com/web/the-setup-of-this-site.md b/sites/pmikkelsen.com/web/the-setup-of-this-site.md
new file mode 100644
index 0000000..a834d77
--- /dev/null
+++ b/sites/pmikkelsen.com/web/the-setup-of-this-site.md
@@ -0,0 +1,3 @@
+This site is hosted on a 9front VPS, using werc. I will write more about this in the future perhaps.
+
+It was created by downloading werc and adding my page. Werc + my page can be found at [https://hg.sr.ht/~pmikkelsen/website](https://hg.sr.ht/~pmikkelsen/website).
diff --git a/tpl/_debug.tpl b/tpl/_debug.tpl
new file mode 100644
index 0000000..4d650ea
--- /dev/null
+++ b/tpl/_debug.tpl
@@ -0,0 +1,29 @@
+% if(! ~ $#debug_shell 0) {
+<form method="POST" name="prompt">
+<input size="80" type="text" name="command" value="%($"post_arg_command%)">
+<input type="submit" Value="Run">
+</form>
+<script language="javascript"><!--
+document.prompt.command.focus()
+//--></script>
+
+%{
+fn evl {
+ # Buffering is probably messing this up:
+ #rc -c 'flag x +;{'^$post_arg_command'} |[2] awk ''{print ">> "$0}'''
+ rc -c 'flag s +; flag x +;'^$post_arg_command
+}
+ if(! ~ $#post_arg_command 0 && ! ~ $#post_arg_command '') {
+ echo '<hr><pre>'
+ evl | escape_html |[2] awk '{print "<b>"$0"</b>"}'
+ echo '</pre>'
+ }
+%}
+% }
+
+<hr><pre>
+% env | escape_html
+</pre><hr>
+
+% umask
+
diff --git a/tpl/_users/login.tpl b/tpl/_users/login.tpl
new file mode 100644
index 0000000..5857188
--- /dev/null
+++ b/tpl/_users/login.tpl
@@ -0,0 +1,18 @@
+<h1>User login</h1>
+<br />
+% if(check_user) {
+ You are logged in as: <b>%($logged_user%)</b>
+% }
+% if not {
+% if (~ $REQUEST_METHOD POST)
+% echo '<div class="notify_errors">Login failed!</div>'
+<form method="POST" action="" style="text-align: right; float: left;">
+<fieldset>
+ <label>User name: <input type="text" name="user_name" value="%($"post_arg_user_name%)"/></label><br>
+ <label>User password: <input type="password" name="user_password"></label><br>
+ <input name="s" type="submit" value="Login">
+</fieldset>
+</form>
+% }
+
+<br style="clear:left">
diff --git a/tpl/sitemap.tpl b/tpl/sitemap.tpl
new file mode 100644
index 0000000..9505b6d
--- /dev/null
+++ b/tpl/sitemap.tpl
@@ -0,0 +1,67 @@
+<h1>Site map</h1>
+
+%{
+tmpfile=/tmp/werc_sitemap_$pid.txt
+tmpfilex=/tmp/werc_sitemapx_$pid.txt
+saveddf=$dirfilter
+
+MON2NUM='s/Jan/01/; s/Feb/02/; s/Mar/03/; s/Apr/04/; s/May/05/; s/Jun/06/; s/Jul/07/; s/Aug/08/; s/Sep/09/; s/Oct/10/; s/Nov/11/; s/Dec/12/;'
+
+fn get_mdate {
+ t=`{mtime $1}
+ t=`{date $t(1) | sed -e $MON2NUM -e 's/ ([0-9]) / 0\1 /g'} # Make sure day of the month is two digits.
+ echo $t(6)^'-'^$t(2)^'-'^$t(3)
+}
+
+fn listDir {
+ d=$1
+ dirfilter=$saveddf
+ if(test -f $d/_werc/config)
+ . $d/_werc/config
+
+ if(~ $#perm_redir_to 0) {
+ echo '<ul class="sitemap-list">'
+
+ for(i in `{ls -dF $d^*/ $d^*.md $d^*.html $d^*.txt >[2]/dev/null | sed $dirfilter}) {
+ desc=`{get_file_title $i}
+ u=`{echo $i|sed 's!'$sitedir'!!; '$dirclean's!/index$!/!; '}
+ if(! ~ $#desc 0 && ! ~ $desc '')
+ desc=' - '$"desc
+ n=`{echo /$u|sed 's/[\-_]/ /g; s,.*/([^/]+)/?$,\1,'}
+ echo '<li><a href="'$u'">'^$"n^'</a>' $"desc '</li>'
+ echo $base_url^$u >> $tmpfile
+ echo '<url><loc>'$base_url^$u'</loc><lastmod>'^`{get_mdate $i}^'</lastmod></url>' >> $tmpfilex
+ if(test -d $i)
+ @{ listDir $i }
+ }
+ echo '</ul>'
+ }
+}
+
+
+fltr_cache listDir $sitedir/
+
+if(test -s $tmpfile) {
+ mv $tmpfile $sitedir/sitemap.txt &
+}
+if not if(test -f $tmpfile)
+ rm $tmpfile
+
+if(test -s $tmpfilex) {
+ {
+ echo '<?xml version="1.0" encoding="UTF-8"?>
+<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">'
+
+ cat $tmpfilex
+ rm $tmpfilex &
+ echo '</urlset>'
+ # TODO Enable automaic search engine update notification.
+ #hget 'http://google.com/ping?sitemap='^`{url_encode $base_url'/sitemap.gz'} > /dev/null
+
+ } | gzip > $sitedir/sitemap.gz &
+ #} > $sitedir/sitemap.xml &
+}
+if not if(test -f $tmpfilex)
+ rm $tmpfilex
+
+%}