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 '
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 @@ ++% 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} +
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.
+# 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 = "? # 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 = "" . _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 . "$list_type>\n";
+ $result;
+ }egmx;
+ }
+ else {
+ $text =~ s{
+ (?:(?<=\n\n)|\A\n?)
+ $whole_list
+ }{
+ my $list = $1;
+ my $list_type = ($3 =~ m/$marker_ul/) ? "ul" : "ol";
+ # Turn double returns into triple returns, so that we can make a
+ # paragraph for the last item in a list, if necessary:
+ $list =~ s/\n{2,}/\n\n\n/g;
+ my $result = _ProcessListItems($list, $marker_any);
+ $result = "<$list_type>\n" . $result . "$list_type>\n";
+ $result;
+ }egmx;
+ }
+
+
+ return $text;
+}
+
+
+sub _ProcessListItems {
+#
+# Process the contents of a single ordered or unordered list, splitting it
+# into individual list items.
+#
+
+ my $list_str = shift;
+ my $marker_any = shift;
+
+
+ # The $g_list_level global keeps track of when we're inside a list.
+ # Each time we enter a list, we increment it; when we leave a list,
+ # we decrement. If it's zero, we're not in a list anymore.
+ #
+ # We do this because when we're not inside a list, we want to treat
+ # something like this:
+ #
+ # I recommend upgrading to version
+ # 8. Oops, now this line is treated
+ # as a sub-list.
+ #
+ # As a single paragraph, despite the fact that the second line starts
+ # with a digit-period-space sequence.
+ #
+ # Whereas when we're inside a list (or sub-list), that line will be
+ # treated as the start of a sub-list. What a kludge, huh? This is
+ # an aspect of Markdown's syntax that's hard to parse perfectly
+ # without resorting to mind-reading. Perhaps the solution is to
+ # change the syntax rules such that sub-lists must start with a
+ # starting cardinal number; e.g. "1." or "a.".
+
+ $g_list_level++;
+
+ # trim trailing blank lines:
+ $list_str =~ s/\n{2,}\z/\n/;
+
+
+ $list_str =~ s{
+ (\n)? # leading line = $1
+ (^[ \t]*) # leading whitespace = $2
+ ($marker_any) [ \t]+ # list marker = $3
+ ((?s:.+?) # list item text = $4
+ (\n{1,2}))
+ (?= \n* (\z | \2 ($marker_any) [ \t]+))
+ }{
+ my $item = $4;
+ my $leading_line = $1;
+ my $leading_space = $2;
+
+ if ($leading_line or ($item =~ m/\n{2,}/)) {
+ $item = _RunBlockGamut(_Outdent($item));
+ }
+ else {
+ # Recursion for sub-lists:
+ $item = _DoLists(_Outdent($item));
+ chomp $item;
+ $item = _RunSpanGamut($item);
+ }
+
+ "" . $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 { '' . 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 "" tag ">");
+ else
+ oprint(text);
+ }
+ text = "";
+}
+
+BEGIN {
+ blank = 0;
+ code = 0;
+ hr = 0;
+ html = 0;
+ nl = 0;
+ nr = 0;
+ otext = "";
+ text = "";
+ par = "p";
+}
+
+# References
+!code && /^ *\[[^\]]*\]:[ ]+/ {
+ sub(/^ *\[/, "");
+ match($0, /\]/);
+ id = substr($0, 1, RSTART - 1);
+ sub(id "\\]:[ ]+", "");
+ title = "";
+ if(match($0, /\".*\"$/))
+ title = "\" title=\"" substr($0, RSTART + 1, RLENGTH - 2);
+ sub(/[ ]+\".*\"$/, "");
+ url = eschtml($0);
+ ref[id] = url title;
+
+ subref(id);
+ next;
+}
+
+# html
+!html && /^<(address|blockquote|center|dir|div|dl|fieldset|form|h[1-6r]|\
+isindex|menu|noframes|noscript|ol|p|pre|table|ul|!--)/ {
+ if(code)
+ oprint("");
+ 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("" block[nl] ">");
+ }
+}
+nnl > nl {
+ for(; nl < nnl; nl++){
+ block[nl + 1] = nblock[nl + 1];
+ oprint("<" block[nl + 1] ">");
+ if(match(block[nl + 1], /[ou]l/))
+ oprint("");
+ }
+}
+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("" block[nl] ">");
+ }
+ 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 ""hrsize($6)" "
+ print ""$7" "
+ print ""$8" "
+ print ""$9" "
+ $1="" ; $2="" ; $3="" ; $4="" ; $5="" ; $6="" ; $7="" ; $8="" ; $9=""
+ sub("^ *?", "")
+ print ""$0" "
+ print " "
+ $0=""
+}
+/^d/ {
+ print ""
+ print " "
+ print ""$7" "
+ print ""$8" "
+ print ""$9" "
+ $1="" ; $2="" ; $3="" ; $4="" ; $5="" ; $6="" ; $7="" ; $8="" ; $9=""
+ sub("^ *?", "")
+ print ""$0"/ "
+ print " "
+}'
+echo '
+
+
+'
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(" *[Bb][Oo][Dd][Yy][^>]*>.*", "") > 0 {print; body=body-1}
+ body==2 {print}
+ body==0 {buf=buf "\n" $0}
+ END {if(body<=0) {print buf}}' < $1
+}
+
+fn txt_handler {
+ # Note: Words are not broken, even if they are way beyond 82 chars long
+ echo ''
+ sed 's/\</g; 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='
By: %(`{cat $c/user}%) (%(`{cat $c/posted}%)) +
+% cat $c/body | escape_html | sed 's,$,,' +