diff --git a/werc/README b/werc/README new file mode 100644 index 000000000..b4dc773bb --- /dev/null +++ b/werc/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/werc/apps/blagh/app.rc b/werc/apps/blagh/app.rc new file mode 100644 index 000000000..c63689d63 --- /dev/null +++ b/werc/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 'Make a new post' + } + + if(~ $req_path $blagh_uri) { + handler_body_main=blagh_body + u=$blagh_uri'index' + extraHeaders=$"extraHeaders ^ \ +' + +' + } + 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 '

'$"blogTitle'

' + + # 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 '
(RSS Feed|Atom Feed)
' + + # 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/werc/apps/blagh/atom.tpl b/werc/apps/blagh/atom.tpl new file mode 100644 index 000000000..97c665fd2 --- /dev/null +++ b/werc/apps/blagh/atom.tpl @@ -0,0 +1,58 @@ + + +%{ +# 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}'}}} +%} + + + +% if(! ~ $"conf_blog_pubsubdub_hub '') { +% echo '' +% } + + + %($base_url^$req_path%) + + + <![CDATA[%($siteTitle%)]]> + + + %($fupdated%) + + +% for(f in `{get_post_list $blagh_root$blagh_dirs}) { +% statpost $f + + +% # Maybe we should be smarter, see: http://diveintomark.org/archives/2004/05/28/howto-atom-id, example: tag:intertwingly.net,2004:2899 + %($post_uri%) + + <![CDATA[%($title%)]]> +% # + + +
+ +
+ +% # rfc3339 date when entry was last updated. +% eupdated=`{ndate -a `{date `{mtime $f | awk '{print $1}'}}} + %($eupdated%) +
+ +% } + +
+ +% exit diff --git a/werc/apps/blagh/convert.rc b/werc/apps/blagh/convert.rc new file mode 100755 index 000000000..0640805dd --- /dev/null +++ b/werc/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/werc/apps/blagh/jsonfeed.tpl b/werc/apps/blagh/jsonfeed.tpl new file mode 100644 index 000000000..fd97ed4f5 --- /dev/null +++ b/werc/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/werc/apps/blagh/new_post.tpl b/werc/apps/blagh/new_post.tpl new file mode 100644 index 000000000..bd521c411 --- /dev/null +++ b/werc/apps/blagh/new_post.tpl @@ -0,0 +1,11 @@ +
+% notices_handler +
+ Submit a new blog post +
+ + + + +
+
diff --git a/werc/apps/blagh/rss20.tpl b/werc/apps/blagh/rss20.tpl new file mode 100644 index 000000000..0cba8187c --- /dev/null +++ b/werc/apps/blagh/rss20.tpl @@ -0,0 +1,43 @@ + + +%{ +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} }} +} + +%} + + + + + <![CDATA[%($siteTitle%)]]> + %($base_url^$req_path%) + + en-us + +%{ + # uriel99+rss@gmail.com (Uriel) + # 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 ''$"lbd'' + # rfc2822 publication date for content in the channel. + pubdate=`{ndate -m} + for(f in `{get_post_list $blagh_root$blagh_dirs}){ + statpost $f +%} + + <![CDATA[%($title%)]]> + + %($post_uri%) + %($post_uri%) + %($pubdate%) + %($summary%) + +% } + + diff --git a/werc/apps/bridge/app.rc b/werc/apps/bridge/app.rc new file mode 100755 index 000000000..40477ba9b --- /dev/null +++ b/werc/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 '

To post a comment you need to login first.

' + } +} + +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/werc/apps/bridge/comments_list.tpl b/werc/apps/bridge/comments_list.tpl new file mode 100755 index 000000000..03e0ddcd8 --- /dev/null +++ b/werc/apps/bridge/comments_list.tpl @@ -0,0 +1,13 @@ +
+

Comments

+ +% for(c in `{ls $comments_dir/}) { +% if(test -s $c/body) { +
+
By: %(`{cat $c/user}%) (%(`{cat $c/posted}%)) +
+% cat $c/body | escape_html | sed 's,$,
,' +
+% } +% } + diff --git a/werc/apps/bridge/foot.tpl b/werc/apps/bridge/foot.tpl new file mode 100755 index 000000000..0dad21d7b --- /dev/null +++ b/werc/apps/bridge/foot.tpl @@ -0,0 +1,37 @@ +
+ +% notices_handler +
+ +
+ + +% if(~ $#logged_user 0) { +% if(~ $#allow_new_user_comments 1) { + + + + + +
+ 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 login before posting. +
+% } +% if not if(~ $#bridge_anon_comments 1) { + +% } +% } +
diff --git a/werc/apps/dirdir/app.rc b/werc/apps/dirdir/app.rc new file mode 100755 index 000000000..1aa9cbd4c --- /dev/null +++ b/werc/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 '$"req_path'!' +} diff --git a/werc/apps/dirdir/edit.tpl b/werc/apps/dirdir/edit.tpl new file mode 100755 index 000000000..1a5b20642 --- /dev/null +++ b/werc/apps/dirdir/edit.tpl @@ -0,0 +1,25 @@ +
+

Editing: %($req_path%)

+
+
+ +
+ + + DirDir documents are written using Markdown syntax. +
+
+ +% if(! ~ $"post_arg_dirdir_preview '') { +

Preview:

+
+% echo $post_arg_edit_text | $formatter +
+% } diff --git a/werc/apps/dirdir/sidebar_controls.tpl b/werc/apps/dirdir/sidebar_controls.tpl new file mode 100755 index 000000000..a897fc150 --- /dev/null +++ b/werc/apps/dirdir/sidebar_controls.tpl @@ -0,0 +1,3 @@ +
+ +
diff --git a/werc/apps/duckduckgo/HOWTO b/werc/apps/duckduckgo/HOWTO new file mode 100644 index 000000000..8bb952c3d --- /dev/null +++ b/werc/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/werc/apps/duckduckgo/app.rc b/werc/apps/duckduckgo/app.rc new file mode 100755 index 000000000..72dd0ecf5 --- /dev/null +++ b/werc/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 ' +

Site search

+

using DuckDuckGo

+
+ + + +
' + +} + diff --git a/werc/apps/duckduckgo/footer.inc.sample b/werc/apps/duckduckgo/footer.inc.sample new file mode 100644 index 000000000..4dd671dc5 --- /dev/null +++ b/werc/apps/duckduckgo/footer.inc.sample @@ -0,0 +1,3 @@ +
Powered by werc
+ +
diff --git a/werc/apps/hello/app.rc b/werc/apps/hello/app.rc new file mode 100755 index 000000000..e6faaa88b --- /dev/null +++ b/werc/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/werc/apps/paste/app.rc b/werc/apps/paste/app.rc new file mode 100755 index 000000000..af0c76d4d --- /dev/null +++ b/werc/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 '
+

pasted data is not publically indexed

+
+
+

+ (do not change) +
+
+ ' +} diff --git a/werc/apps/wman/app.rc b/werc/apps/wman/app.rc new file mode 100755 index 000000000..8f0a150db --- /dev/null +++ b/werc/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 ' ' '|'}^')\)!&!g' \ + | awk '/^$/ {if(n != 1) print; n=1; next} /./ {n=0; print}' \ + | col -x +} + + diff --git a/werc/apps/wman/man_page.tpl b/werc/apps/wman/man_page.tpl new file mode 100755 index 000000000..945e23af4 --- /dev/null +++ b/werc/apps/wman/man_page.tpl @@ -0,0 +1,3 @@ +
+% wman_page_gen $wman_page_file
+
diff --git a/werc/apps/wman/page_list.tpl b/werc/apps/wman/page_list.tpl new file mode 100755 index 000000000..b98600d3d --- /dev/null +++ b/werc/apps/wman/page_list.tpl @@ -0,0 +1,11 @@ +% d=`{wman_get_section_desc $wman_cat} +

Manual pages - Section %($wman_cat%): %($"d%)

+ + + diff --git a/werc/apps/wman/search.tpl b/werc/apps/wman/search.tpl new file mode 100755 index 000000000..a6c59e4ff --- /dev/null +++ b/werc/apps/wman/search.tpl @@ -0,0 +1,20 @@ +
+
+ + + + +% if(! ~ $"post_arg_wman_search '') { +% if(~ $"wman_search_results '') { + No matches found for '%($post_arg_wman_search%)'. +% } +% if not { + +% } +% } + +
+
+ diff --git a/werc/apps/wman/section_list.tpl b/werc/apps/wman/section_list.tpl new file mode 100755 index 000000000..299d613ab --- /dev/null +++ b/werc/apps/wman/section_list.tpl @@ -0,0 +1,11 @@ +

Manual Sections

+ + diff --git a/werc/bin/aux/addwuser.rc b/werc/bin/aux/addwuser.rc new file mode 100755 index 000000000..9364d39e4 --- /dev/null +++ b/werc/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/werc/bin/aux/bpst.rc b/werc/bin/aux/bpst.rc new file mode 100755 index 000000000..e60d03416 --- /dev/null +++ b/werc/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/werc/bin/aux/gensitemaptxt.rc b/werc/bin/aux/gensitemaptxt.rc new file mode 100755 index 000000000..a1b349da3 --- /dev/null +++ b/werc/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/werc/bin/aux/runtsts.rc b/werc/bin/aux/runtsts.rc new file mode 100755 index 000000000..b5b1df761 --- /dev/null +++ b/werc/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/werc/bin/cgilib.rc b/werc/bin/cgilib.rc new file mode 100755 index 000000000..4516bf976 --- /dev/null +++ b/werc/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/&/\&/g; s//\>/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/werc/bin/contrib/fix-rc-scripts b/werc/bin/contrib/fix-rc-scripts new file mode 100755 index 000000000..beb21c565 --- /dev/null +++ b/werc/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/werc/bin/contrib/hgweb.config b/werc/bin/contrib/hgweb.config new file mode 100755 index 000000000..fba802b0a --- /dev/null +++ b/werc/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/werc/bin/contrib/hgwebdir.cgi b/werc/bin/contrib/hgwebdir.cgi new file mode 100755 index 000000000..5fe4b16b2 --- /dev/null +++ b/werc/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/werc/bin/contrib/markdown.pl b/werc/bin/contrib/markdown.pl new file mode 100755 index 000000000..3758a87c0 --- /dev/null +++ b/werc/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 +# +# +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 +# and 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]* + ? # 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/"/"/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

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.: + #

+ #
+ # tags for inner block must be indented. + #
+ #
+ # + # 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 `
` and stop at the first `
`. + $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 + # 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` to `\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 + .* # 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
. 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: + + ) + [ \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 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 `` + # Must come after _DoAnchors(), because you can use < and > + # delimiters in inline links like [this](). + $text = _DoAutoLinks($text); + + $text = _EncodeAmpsAndAngles($text); + + $text = _DoItalicsAndBold($text); + + # Do hard breaks: + $text =~ s/ {2,}\n/ or 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
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 = "? # 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 = " 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/"/"/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 = "\"$alt_text\"";? # 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/"/"/g; + $title =~ s/"/"/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 = "\"$alt_text\"";" . _RunSpanGamut($1) . "\n\n"; + }egmx; + + $text =~ s{ ^(.+)[ \t]*\n-+[ \t]*\n+ }{ + "

" . _RunSpanGamut($1) . "

\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); + "" . _RunSpanGamut($2) . "\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 . "\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 . "\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); + } + + "
  • " . $item . "
  • \n"; + }egmx; + + $g_list_level--; + return $list_str; +} + + + +sub _DoCodeBlocks { +# +# Process Markdown `
    ` 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
    " . $codeblock . "\n
    \n\n"; + + $result; + }egmx; + + return $text; +} + + +sub _DoCodeSpans { +# +# * Backtick quotes are used for 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: +# +#

    Just type foo `bar` baz at the prompt.

    +# +# 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 `bar` ... +# + + my $text = shift; + + $text =~ s@ + (`+) # $1 = Opening run of ` + (.+?) # $2 = The code block + (?$c
    "; + @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/&/&/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/\$/$/g; + } + } + + + # Do the angle bracket song and dance: + s! < !<!gx; + s! > !>!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; + + # must go first: + $text =~ s{ (\*\*|__) (?=\S) (.+?[*_]*) (?<=\S) \1 } + {$2}gsx; + + $text =~ s{ (\*|_) (?=\S) (.+?) (?<=\S) \1 } + {$2}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
     content, so we need to fix that:
    +			$bq =~ s{
    +					(\s*
    .+?
    ) + }{ + my $pre = $1; + $pre =~ s/^ //mg; + $pre; + }egsx; + + "
    \n$bq\n
    \n\n"; + }egmx; + + + return $text; +} + + +sub _FormParagraphs { +# +# Params: +# $text - string to process with html

    tags +# + my $text = shift; + + # Strip leading and trailing lines: + $text =~ s/\A\n+//; + $text =~ s/\n+\z//; + + my @grafs = split(/\n{2,}/, $text); + + # + # Wrap

    tags. + # + foreach (@grafs) { + unless (defined( $g_html_blocks{$_} )) { + $_ = _RunSpanGamut($_); + s/^([ \t]*)/

    /; + $_ .= "

    "; + } + } + + # + # 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+);)/&/g; + + # Encode naked <'s + $text =~ s{<(?![a-z/?\$!])}{<}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]+)>}{
    $1}gi; + + # Email addresses: + $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.: +# +# foo +# @example.com +# +# Based on a filter by Matthew Wickline, posted to the BBEdit-Talk +# mailing list: +# + + 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{$addr}; + $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 , 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. +# +# + + 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: ) | # 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: +# +# + 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 + + +=head1 SYNOPSIS + +B [ B<--html4tags> ] [ B<--version> ] [ B<-shortversion> ] + [ I ... ] + + +=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
    and 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.: + +
    + +instead of Markdown's default XHTML style tags, e.g.: + +
    + + +=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 + +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/werc/bin/contrib/md2html.awk b/werc/bin/contrib/md2html.awk new file mode 100755 index 000000000..81d1241a7 --- /dev/null +++ b/werc/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("&", "\\&", t); + gsub("<", "\\<", 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 = "—"; + return t1 tag nextil(t2); + } + # Inline Code + if(tag == "`"){ + if(sub(/^`/, "", t2)){ + if(!match(t2, /``/)) + return t1 "”" nextil(t2); + ilcode2 = !ilcode2; + } + else if(ilcode2) + return t1 tag nextil(t2); + tag = ""; + if(ilcode){ + t1 = eschtml(t1); + tag = ""; + } + 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 "" linktext "" 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 "<" 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 "&" 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 "\""" 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 "\""" 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 "" nextil(linktext) "" 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 "" nextil(linktext) "" 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 ""); + 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(""); + for(; !text && block[nl] == "blockquote"; nl--) + oprint(""); + 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 ">)|((^
    $)")) + html = 1; + if(html && match($0, /^
    $/ || +(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(""); + 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("
  • "); +} +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(""); + 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("
  • "); + oprint(""); + } +} +nnl > nl { + for(; nl < nnl; nl++){ + block[nl + 1] = nblock[nl + 1]; + oprint("<" block[nl + 1] ">"); + if(match(block[nl + 1], /[ou]l/)) + oprint("
  • "); + } +} +hr { + oprint("
    "); + next; +} + +# Code blocks +code && /^$/ { + if(blanK) + oprint(""); + blank = 1; + next; +} +!text && sub(/^( | )/, "") { + if(blanK) + oprint(""); + blank = 0; + if(!code) + oprint("
    ");
    +	code = 1;
    +	$0 = eschtml($0);
    +	oprint($0);
    +	next;
    +}
    +code {
    +	oprint("
    "); + 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(""); + code = 0; + } + printp(par); + for(; nl > 0; nl--){ + if(match(block[nl], /[ou]l/)) + oprint("
  • "); + oprint(""); + } + gsub(/<<[^\"]*/, "", otext); + print(otext); +} diff --git a/werc/bin/contrib/rc-httpd/handlers/authorize b/werc/bin/contrib/rc-httpd/handlers/authorize new file mode 100755 index 000000000..ea4db3ef7 --- /dev/null +++ b/werc/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/werc/bin/contrib/rc-httpd/handlers/cgi b/werc/bin/contrib/rc-httpd/handlers/cgi new file mode 100755 index 000000000..2c9a9b9b8 --- /dev/null +++ b/werc/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/werc/bin/contrib/rc-httpd/handlers/dir-index b/werc/bin/contrib/rc-httpd/handlers/dir-index new file mode 100755 index 000000000..00ff8ce22 --- /dev/null +++ b/werc/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 ' + +'^$title^' + + +' +echo '

    '^$title^'

    ' +if(! ~ $PATH_INFO /) + echo 'Parent directory' +echo '
    ' +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 "" + print "" + print "" + print "" + print "" + $1="" ; $2="" ; $3="" ; $4="" ; $5="" ; $6="" ; $7="" ; $8="" ; $9="" + sub("^ *?", "") + print "" + print "" + $0="" +} +/^d/ { + print "" + print "" + print "" + print "" + print "" + $1="" ; $2="" ; $3="" ; $4="" ; $5="" ; $6="" ; $7="" ; $8="" ; $9="" + sub("^ *?", "") + print "" + print "" +}' +echo '
    "hrsize($6)""$7""$8""$9""$0"
    "$7""$8""$9""$0"/
    + + +' diff --git a/werc/bin/contrib/rc-httpd/handlers/error b/werc/bin/contrib/rc-httpd/handlers/error new file mode 100755 index 000000000..282d8706c --- /dev/null +++ b/werc/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 ' + +'^$1^' + + +

    '^$1^'

    ' + echo $2 + echo '

    rc-httpd at' $SERVER_NAME '' + echo ' + + + ' +} + +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/werc/bin/contrib/rc-httpd/handlers/redirect b/werc/bin/contrib/rc-httpd/handlers/redirect new file mode 100755 index 000000000..e223091eb --- /dev/null +++ b/werc/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 '' +if(~ $#3 0) + echo 'Browser did not accept redirect.' +if not + echo $3 +echo 'Click here' +echo '' diff --git a/werc/bin/contrib/rc-httpd/handlers/serve-static b/werc/bin/contrib/rc-httpd/handlers/serve-static new file mode 100755 index 000000000..00cc70a72 --- /dev/null +++ b/werc/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/werc/bin/contrib/rc-httpd/handlers/static-or-cgi b/werc/bin/contrib/rc-httpd/handlers/static-or-cgi new file mode 100755 index 000000000..4d8a2d44a --- /dev/null +++ b/werc/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/werc/bin/contrib/rc-httpd/handlers/static-or-index b/werc/bin/contrib/rc-httpd/handlers/static-or-index new file mode 100755 index 000000000..f0904f8a9 --- /dev/null +++ b/werc/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/werc/bin/contrib/rc-httpd/lib/urldecode.awk b/werc/bin/contrib/rc-httpd/lib/urldecode.awk new file mode 100755 index 000000000..1dadd0068 --- /dev/null +++ b/werc/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/werc/bin/contrib/rc-httpd/rc-httpd b/werc/bin/contrib/rc-httpd/rc-httpd new file mode 100755 index 000000000..776892f43 --- /dev/null +++ b/werc/bin/contrib/rc-httpd/rc-httpd @@ -0,0 +1,106 @@ +#!/bin/rc +rc_httpd_dir=/werc/bin/contrib/rc-httpd +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{ read -c $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;\?.*;; + 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) +} +switch($SERVER_NAME){ + case */* .. + error 400 + exit +} +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/werc/bin/contrib/rc-httpd/select-handler b/werc/bin/contrib/rc-httpd/select-handler new file mode 100755 index 000000000..e60c216d2 --- /dev/null +++ b/werc/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=/werc/sites/$SERVER_NAME + exec static-or-cgi /werc/bin/werc.rc +} +if not + error 503 diff --git a/werc/bin/contrib/tcp80 b/werc/bin/contrib/tcp80 new file mode 100755 index 000000000..ae111a02a --- /dev/null +++ b/werc/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/werc/bin/contrib/urldecode.awk b/werc/bin/contrib/urldecode.awk new file mode 100755 index 000000000..bd791e3c0 --- /dev/null +++ b/werc/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/werc/bin/contrib/urlencode.awk b/werc/bin/contrib/urlencode.awk new file mode 100755 index 000000000..d4d354d67 --- /dev/null +++ b/werc/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/werc/bin/contrib/webserver.rc b/werc/bin/contrib/webserver.rc new file mode 100755 index 000000000..804456515 --- /dev/null +++ b/werc/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/werc/bin/corehandlers.rc b/werc/bin/corehandlers.rc new file mode 100755 index 000000000..294b1a7e5 --- /dev/null +++ b/werc/bin/corehandlers.rc @@ -0,0 +1,152 @@ +# Werc builtin handlers + +fn nav_tree { + if(! ~ $#sideBarNavTitle 0) + echo '

    '$"sideBarNavTitle':

    ' + # 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 "
      " } + { + d = "" + if(match($0, "/$")) + d = "/" + sub("/$", "") # Strip trailing / for dirs so NF is consistent + + p(NF, lNF, "
      • ") + p(lNF, NF, "
    • ") + 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 "
    • » " bname "
    • " + else + print "
    • › " bname "
    • " + } + END { p(lNF, 2, "
    "); print "" }' +} + +fn link_bar { + if(~ $1 -t) { + echo '

    '$2'

    ' + shift; shift + } + echo '
      ' + while(! ~ $#* 0) { + echo '
    • - '$1'
    • ' + shift; shift + } + echo '
    ' +} + +fn md_handler { $formatter $1 } + +fn tpl_handler { template $* } + +fn html_handler { + # body states: 0 = no found, 2 = after , 1 = after , -1 = after + awk 'gsub(".*<[Bb][Oo][Dd][Yy][^>]*>", "") > 0 {body=2} + gsub("]*>.*", "") > 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 '
    '
    +    sed 's//\>/g' < $1 | fmt -l 82 -j
    +    echo '
    ' +} + +fn dir_listing_handler { + d=`{basename -d $1} + if(~ $#d 0) + d='/' + echo $d|sed 's,.*//,,g; s,/$,,; s,/, / ,g; s,.*,

    &

      ,' + # 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,.*/([^/]+/?)$,
    • \1
    • ,' + echo '
    ' +} + +fn notices_handler { + for(type in notify_errors notify_notes notify_success) + for(n in $$type) + echo '
    '$"n'
    ' +} + +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/werc/bin/fltr_cache.rc b/werc/bin/fltr_cache.rc new file mode 100755 index 000000000..9394724a3 --- /dev/null +++ b/werc/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/werc/bin/template.awk b/werc/bin/template.awk new file mode 100755 index 000000000..8f02ebbfd --- /dev/null +++ b/werc/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/werc/bin/werc.rc b/werc/bin/werc.rc new file mode 100755 index 000000000..0d006a304 --- /dev/null +++ b/werc/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/ +# +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='' +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/werc/bin/werc_errlog_wrap.rc b/werc/bin/werc_errlog_wrap.rc new file mode 100755 index 000000000..94bd18f3b --- /dev/null +++ b/werc/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/werc/bin/wercconf.rc b/werc/bin/wercconf.rc new file mode 100755 index 000000000..bb3422d44 --- /dev/null +++ b/werc/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/werc/bin/werclib.rc b/werc/bin/werclib.rc new file mode 100755 index 000000000..bcebf9180 --- /dev/null +++ b/werc/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/werc/etc/initrc b/werc/etc/initrc new file mode 100644 index 000000000..e223647f1 --- /dev/null +++ b/werc/etc/initrc @@ -0,0 +1,40 @@ +# 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) +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/werc/etc/users/GROUP_AND_USER_ACCOUNTS b/werc/etc/users/GROUP_AND_USER_ACCOUNTS new file mode 100644 index 000000000..f563306d4 --- /dev/null +++ b/werc/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/werc/lib/404.tpl b/werc/lib/404.tpl new file mode 100644 index 000000000..a106f6906 --- /dev/null +++ b/werc/lib/404.tpl @@ -0,0 +1,4 @@ +

    The requested document at '%($"req_path%)' doesn't exist

    +404 error: coffee not found +

    maybe it is in the sitemap?

    +
    diff --git a/werc/lib/default_master.tpl b/werc/lib/default_master.tpl new file mode 100644 index 000000000..6742c49c6 --- /dev/null +++ b/werc/lib/default_master.tpl @@ -0,0 +1,26 @@ +
    + +

    %($"siteTitle%) %($"siteSubTitle%)

    +
    + +% if(! ~ $#handlers_bar_left 0) { + +% } + +
    +% run_handlers $handlers_body_head +% run_handler $handler_body_main +% run_handlers $handlers_body_foot +
    + +
    +% cat `{ get_lib_file footer.inc } +
    diff --git a/werc/lib/footer.inc b/werc/lib/footer.inc new file mode 100644 index 000000000..1a2b48e6a --- /dev/null +++ b/werc/lib/footer.inc @@ -0,0 +1,2 @@ + + diff --git a/werc/lib/headers.tpl b/werc/lib/headers.tpl new file mode 100644 index 000000000..0835cf849 --- /dev/null +++ b/werc/lib/headers.tpl @@ -0,0 +1,31 @@ + + + + + %($pageTitle%) + + + +% if(test -f $sitedir/_werc/pub/style.css) +% echo ' ' + + +% # Legacy charset declaration for backards compatibility with non-html5 browsers. + + + + +% if(! ~ $#meta_description 0) +% echo ' ' +% if(! ~ $#meta_keywords 0) +% echo ' ' + +% h = `{get_lib_file headers.inc} +% if(! ~ $#h 0) +% cat $h + + %($"extraHeaders%) + + + + diff --git a/werc/lib/top_bar.inc b/werc/lib/top_bar.inc new file mode 100644 index 000000000..ff3aa199c --- /dev/null +++ b/werc/lib/top_bar.inc @@ -0,0 +1,8 @@ +
    escape | +withdrawal +
    + + diff --git a/werc/pub/404coffee.png b/werc/pub/404coffee.png new file mode 100644 index 000000000..a6c6051eb Binary files /dev/null and b/werc/pub/404coffee.png differ diff --git a/werc/pub/default_favicon.ico b/werc/pub/default_favicon.ico new file mode 100755 index 000000000..817f5fa22 Binary files /dev/null and b/werc/pub/default_favicon.ico differ diff --git a/werc/pub/style/imgs/sgl.png b/werc/pub/style/imgs/sgl.png new file mode 100755 index 000000000..d68580e1c Binary files /dev/null and b/werc/pub/style/imgs/sgl.png differ diff --git a/werc/pub/style/sinorca-screen-alt.css b/werc/pub/style/sinorca-screen-alt.css new file mode 100755 index 000000000..d11e9ad80 --- /dev/null +++ b/werc/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/werc/pub/style/style.css b/werc/pub/style/style.css new file mode 100644 index 000000000..2d74b6484 --- /dev/null +++ b/werc/pub/style/style.css @@ -0,0 +1,19 @@ +body { display: flex; flex-wrap: wrap; font-family: Helvetica, Verdana, Arial, 'Liberation Sans', FreeSans, sans-serif; } +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; } +nav a:hover { text-decoration: underline; } +h1, h2, h3, h4, h5 { font-weight: normal; } +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 } +img { width: 100%; } +footer { display: flex; justify-content: space-between; } +@media screen and (max-width: 600px) { + textarea { + width: 100%; + } +} diff --git a/werc/pub/style/style.werc140.css b/werc/pub/style/style.werc140.css new file mode 100755 index 000000000..e3261e93a --- /dev/null +++ b/werc/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/werc/pub/style/style_old.css b/werc/pub/style/style_old.css new file mode 100755 index 000000000..e4a41fe36 --- /dev/null +++ b/werc/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/werc/sites/CREATE-SITE-DIRECTORIES-HERE b/werc/sites/CREATE-SITE-DIRECTORIES-HERE new file mode 100644 index 000000000..e69de29bb diff --git a/werc/tpl/_debug.tpl b/werc/tpl/_debug.tpl new file mode 100644 index 000000000..4d650ea74 --- /dev/null +++ b/werc/tpl/_debug.tpl @@ -0,0 +1,29 @@ +% if(! ~ $#debug_shell 0) { +
    + + +
    + + +%{ +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 '
    '
    +        evl | escape_html |[2] awk '{print ""$0""}' 
    +        echo '
    ' + } +%} +% } + +
    +% env | escape_html
    +

    + +% umask + diff --git a/werc/tpl/_users/login.tpl b/werc/tpl/_users/login.tpl new file mode 100644 index 000000000..5857188cc --- /dev/null +++ b/werc/tpl/_users/login.tpl @@ -0,0 +1,18 @@ +

    User login

    +
    +% if(check_user) { + You are logged in as: %($logged_user%) +% } +% if not { +% if (~ $REQUEST_METHOD POST) +% echo '
    Login failed!
    ' +
    +
    +
    +
    + +
    +
    +% } + +
    diff --git a/werc/tpl/sitemap.tpl b/werc/tpl/sitemap.tpl new file mode 100644 index 000000000..9505b6d0a --- /dev/null +++ b/werc/tpl/sitemap.tpl @@ -0,0 +1,67 @@ +

    Site map

    + +%{ +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 '
      ' + + 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 '
    • '^$"n^'' $"desc '
    • ' + echo $base_url^$u >> $tmpfile + echo ''$base_url^$u''^`{get_mdate $i}^'' >> $tmpfilex + if(test -d $i) + @{ listDir $i } + } + echo '
    ' + } +} + + +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 ' +' + + cat $tmpfilex + rm $tmpfilex & + echo '' + # 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 + +%}