#!/bin/sh
##############################################################################
# WikiSH - wikiwiki engine for /bin/sh - 2005 Sebastian Misch
#
# $Id: wiki,v 1.32 2006/02/01 16:30:46 smisch Exp $
#
##############################################################################
## Defaults

BASEURL=http://localhost
SCRIPTNAME=/edit/wiki
DATADIR=..
STYLESHEET=$BASEURL/wiki.css
HEADER=header
FOOTER=footer
AWK=awk
MAXUPLOAD=100000

[ -f wiki.conf ] && . wiki.conf # Create wiki.conf to override defaults

##############################################################################
## Internal vars

WD=`pwd`
REMOTE_USER=${REMOTE_USER:=anonymous}
VERSION=`echo '$Id: wiki,v 1.32 2006/02/01 16:30:46 smisch Exp $'|cut -d' ' -f3`

##############################################################################
## Edit

edit() {
  echo "<H2>Edit <EM>$FILE</EM> as user <EM>$REMOTE_USER</EM></H2>"
  if [ -f $DATADIR/RCS/$FILE,v ] ; then
    echo '<FORM ACTION="?save&'$FILE'" METHOD="POST">
          <P><TEXTAREA NAME="POST" COLS="80" ROWS="30">'|sed 's/^ *//g'
    cd $DATADIR
    co -l -q $FILE 2>/dev/null
    $AWK 's>0{print};/^$/{s=1}' $FILE
    cd $WD
  else
    echo '<FORM ACTION="?add&'$FILE'" METHOD="POST">
          <P><TEXTAREA NAME="POST" COLS="80" ROWS="30">'|sed 's/^ *//g'
    echo Describe $FILE ...
  fi
  echo '</TEXTAREA><BR>
        <INPUT TYPE="SUBMIT" VALUE="Submit">
        Options:
        <A CLASS="wikishmenuitem" HREF="?rebuildAll">Rebuild all files</A>
        <A CLASS="wikishmenuitem" TARGET="_new"
          HREF="http://wikish.do.homeunix.org/WikiFormattingGuide.html">Help</A>
        </FORM>'|sed 's/^ *//g'
}

add() {
  save $FILE
  WIKIES=`ls -1p $DATADIR/RCS/*,v|sed 's/\/RCS\//\//g;s/,v//'|\
          grep -v $FILE|xargs grep -l "$FILE"|sed 's/:.*$//;s/^.*\\///'`
  rebuild
  echo '<P><A CLASS="wikishmenuitem" TARGET="_top" HREF="'$BASEURL'">Home</A>'
}

save() {
  echo "<P>Saving <EM>$FILE</EM> as user <EM>$REMOTE_USER</EM>..."
  cd $DATADIR
  rm -f $FILE 2>/dev/null
  co -q -l -w"$REMOTE_USER" $FILE 2>/dev/null
  touch $FILE
  chmod 660 $FILE
  echo -e "\$""Id: $FILE \$\n">$FILE
  dd ibs=1 count=$CONTENT_LENGTH 2>/dev/null|\
    sed 's/^.*=//'| sed 's/+/ /g'| sed 's/\%0[dD]//g' |\
    $AWK '/%/{while(match($0,/\%[0-9a-fA-F][0-9a-fA-F]/))\
       {$0=substr($0,1,RSTART-1)sprintf("%c",0+("0x"substr(\
       $0,RSTART+1,2)))substr($0,RSTART+3);}}{print}'\
     >>$FILE 2>/dev/null
  ci -q -u -w"$REMOTE_USER" -m"Changed via WikiSH" $FILE 2>/dev/null 
  echo 'finished.<P>'
  cd $WD
  WIKIES=$FILE
  rebuild
  echo '<P>See <A CLASS="wikishinternal" HREF="'$BASEURL/$FILE'.html">'$FILE'.html</A>'
}

rebuild() {
  echo '<P>Updating HTML file...'
  for FILE in $WIKIES ; do
    echo "<EM>$FILE...</EM>"
    ID=`head -1 $DATADIR/$FILE`
    REVISION=`echo $ID|cut -d ' ' -f 3`
    TITLE=`echo $FILE|sed 's/\([a-z0-9]\)\([A-Z]\)/\1 \2/g'`
    WORDS=`$AWK 'BEGIN{FS="[^a-zA-Z0-9-]"}{for(i=0;i++<length;){\
           if(length($i)>6){print tolower($i)}}}' $DATADIR/$FILE|sort|\
           uniq -c|sort -r|head -10|$AWK '{printf("%s,",$2)}'`
    TOC=`$AWK 'BEGIN{print ""}\
         $0!~/^--? /{next}\
         /^- /{s=0; c++}/^-- /{s++;}\
         {$1="";gsub(/^ /,"");printf\
         ("<SPAN STYLE=float:left;min-width:50px;>%s%s.%s%s</SPAN>"\
          "<A CLASS=\"wikishtocitem\" HREF=#%s.%s>%s</A><BR>\n",\
         (s==0?"<B>":""),c,(s==0?"":s),(s==0?"</B>":""),\
         c,s,(s==0?"<B>":"")$0(s==0?"</B>":""))}' $DATADIR/$FILE`

    cat $HEADER|meta>$DATADIR/$FILE.html
    $AWK 'n>0{print}/^___PARSER/{n++}' $0|\
      $AWK -f - -v toc="$TOC" -v datadir="$DATADIR" \
        -v scriptname="$SCRIPTNAME" -v baseurl="$BASEURL" -v wiki="$FILE" \
        $DATADIR/$FILE >> $DATADIR/$FILE.html
    cat $FOOTER|meta>>$DATADIR/$FILE.html
    chmod 644 $DATADIR/$FILE.html &
  done
  echo 'finished.'
}

rebuildAll() {
  WIKIES=`ls -1p $DATADIR/RCS/*,v|sed 's/^.*\/RCS\///;s/,v//'`
  rebuild
  echo '<P><A CLASS="wikishmenuitem" TARGET="_top" HREF="'$BASEURL'">Home</A>'
}

meta() {
  $AWK -v bu="$BASEURL"\
       -v sn="$SCRIPTNAME"\
       -v cs="$STYLESHEET"\
       -v pg="$FILE"\
       -v tt="$TITLE"\
       -v rv="$REVISION"\
       -v wd="$WORDS"\
       -v id="$ID"\
       -v vs="$VERSION" "\
       gsub(/<!--Baseurl-->/, bu)\
       gsub(/<!--Wiki-->/, sn)\
       gsub(/<!--Stylesheet-->/, cs)\
       gsub(/<!--Page-->/, pg)\
       gsub(/<!--Filename-->/, pg \".html\")\
       gsub(/<!--Title-->/, tt)\
       gsub(/<!--Revision-->/, rv)\
       gsub(/<!--Words-->/, wd)\
       gsub(/<!--Id-->/, id)\
       gsub(/<!--WikishVersion-->/, vs)\
       {print}"
}

##############################################################################
## HTML

header() {
  echo 'Content-Type: text/html

  <HTML>
  <HEAD>'
  test "$ACTION" = "save" &&\
    echo '<META HTTP-EQUIV="refresh" CONTENT="1;URL='$BASEURL/$FILE'.html">'
  echo '<LINK REL="stylesheet" HREF="'$STYLESHEET'">
    <TITLE>WikiSH</TITLE>
  </HEAD>
  <BODY>
  <H1>WikiSH</H1>
  <P>
  <SMALL>Version '$VERSION' - <A HREF="http://wikish.do.homeunix.org"
    CLASS="wikishexternal">http://wikish.do.homeunix.org</A></SMALL><P>'
}

footer() {
  echo '</BODY>
        </HTML>'
}

##############################################################################
## Some security checks

test "0${#QUERY_STRING}" -gt 256 && exit 1
test "0$CONTENT_LENGTH" -gt "0$MAXUPLOAD" && exit 1
echo "$QUERY_STRING" | egrep '[^\&a-zA-Z0-9-]' && exit 1

##############################################################################
## Main

ACTION=`echo "$QUERY_STRING"|cut -d '&' -f1`
FILE=`echo "$QUERY_STRING"|cut -d '&' -f2|sed 's/[^A-Za-z0-9]//g'`

header | sed 's/^ *//g'

(echo "$ACTION"|egrep 'rebuildAll|add|edit|save'>/dev/null 2>&1 && "$ACTION")||\
  echo "<P>Action '$ACTION' is not implemented. Go away."

footer | sed 's/^ *//g'
exit

___PARSER
##############################################################################
# This code is based on the parser.awk AwkiAwki 2002 by Oliver Tonnhofer
# It was changed for use with wikish 2005 by Sebastian Misch. Some patches
# where also applied to support tables and internationalization.
##############################################################################

BEGIN {
  for (i=33; i<128; i++) {
    CTAB[sprintf("%c", i)] = "&#" (i) ";"
  }
  list["OL"] = 0
  list["UL"] = 0
  FS = "[ ]"
  cmd = "ls " datadir
  while ((cmd | getline ls_out) >0) {
    if (match(ls_out, /[a-z]+/) && substr(ls_out, RSTART + RLENGTH) !~ /,v/) {
      page = substr(ls_out, RSTART, RLENGTH)
      pages[page] = 1
    }
  }
  close(cmd)
}

##############################################################################
## Start parsing in second line
l < 1 {l++; if (sh == 0) { next }}

##############################################################################
## Implementation

# HTML entities for <, >, &, and [[, ]]
/[&<>]|\[\[|\]\]/ {
  gsub(/&/, "\\&amp;")
  gsub(/</, "\\&lt;")
  gsub(/>/, "\\&gt;")
  gsub(/\]\]/, "\\&#93;")
  gsub(/\[\[/, "\\&#91;")
}

# In premode do no parsing
pre == 1 {
  if (/^_/) {
    printPre() ;
    next;
  } else {
    pre=0;
    print "</PRE>" ; FS="[ ]"
  }
}


# Goto premode?
/^_/ {
  print "<PRE>"
  printPre()
  blankline = 0
  pre = 1
  FS="[\n]"
  next;
}


# register blanklines
/^$/ {
  blankline = 1;
  next;
}


/\[.*:TOC\]/ {
  close_tags();
  match($0, /\[(.*):TOC\]/)
  print "<H2>" substr($0, RSTART+1, RLENGTH-6) \
        "</H2><SPAN CLASS=\"wikishtoc\"><P>" toc "</SPAN><BR><P>"
  sub(/\[.*:TOC\]/, "")
}


# generate links
/[A-Z][a-z]+[A-Z][A-Za-z]*/ || /(https?|ftp|gopher|news):/ {
  tmpline = ""
  for(i=1;i<=NF;i++) {
    field = $i
    # generate HTML img tag for .jpg,.jpeg,.gif,png URLs
    if (field ~ /((l|r|c)-)?https?:\/\/[^\t]*\.(jpg|jpeg|gif|png)/) {
      if (field ~ /^c-/) { class = "CLASS=\"imgcenter\"" }
      if (field ~ /^r-/) { class = "CLASS=\"imgright\"" }
      if (field ~ /^l-/) { class = "CLASS=\"imgleft\"" }
      sub(/^((l|r|c)-)/, "", field)
      sub(/https?:\/\/[^\t]*\.(jpg|jpeg|gif|png)/, \
          "<IMG " class " SRC=\"&\">",field)
    } else if (field ~ /((https?|ftp|gopher):\/\/|(mailto|news):)[^\t]*/) {
      # links for news and http, ftp and gopher URLs
      sub(/((https?|ftp|gopher):\/\/|(news):)[^\t]*[^.,?;:'")\t]/, \
        "<A TARGET=\"_new\" CLASS=\"wikishexternal\" HREF=\"&\">&</A>",field)
    } else if ((sh == 0) \
            && field ~ /(^|[[,.?;:'"\(\t])[A-Z][a-z]+[A-Z][A-Za-z]*/ && field !~ /''''''/) {
      # links for wikipages
      match(field, /[a-zA-Z0-9]+/)
      preText = substr(field, 1, RSTART-1)
      text = substr(field, RSTART, RLENGTH - 1 + (RSTART > 1 ? RSTART - 1: RSTART))
      postText = substr(field, RSTART+RLENGTH)
      filename = text
      match(text, /[a-z][A-Z]/)
      while (RSTART > 0) {
        first=substr(text, 1, RSTART)
        last=substr(text, RSTART + 1)
        text = first" "last
        match(text, /[a-z][A-Z]/)
      }
      if (system("[ -f \"" datadir "/"filename".html\" ]") == 0 ) {
        field = preText "<A CLASS=\"wikishinternal\" HREF=\""\
                baseurl"/"filename".html?"wiki"\">"text"</A>" postText
      } else {
        # New page
        field = preText "<A CLASS=\"wikisherror\" HREF=\""\
                scriptname"?edit&"filename"\">"text"</A>" postText
      }
    }
    tmpline = tmpline field OFS
  }
  # return tmpline to $0 and remove last OFS (whitespace)
  $0 = substr(tmpline, 1, length(tmpline)-1)
}


## EMail Adresses (Secure them not to be snaked by a robot)
/mailto:[a-zA-Z0-9\.-_]+\@[a-zA-Z0-9\.-]+/ {
  tmpline = ""
  for(i=1;i<=NF;i++) {
    field = $i
    if (field ~ /^mailto:[a-zA-Z0-9\.-_]+\@[a-zA-Z0-9\.-]+$/) {
      sub(/mailto:/, "", field)
      e = ""; eT = ""
      n = split(field, chars, "")
      for (i=1; i<=n; ++i) {
        e = e CTAB[chars[i]]
        eT = eT (chars[i] == "@" ? " <SUP>AT</SUP> " : CTAB[chars[i]])
      }
      field = "<A\nCLASS=\"wikishexternal\" HREF=\"mailto:"e"\">"eT"</A>"
    }
    tmpline = tmpline field OFS
  }
  # return tmpline to $0 and remove last OFS (whitespace)
  $0 = substr(tmpline, 1, length(tmpline)-1)
}


# remove six single quotes (Wiki''''''Links)
/''''''/ {
  gsub(/''''''/,"");
}


# bold and emphasize text in single-quotes
/'''/ {
  gsub(/'''('?'?[^'])*'''/, "<STRONG>&</STRONG>");
  gsub(/'''/,"");
}
/''/ {
  gsub(/''('?[^'])*''/, "<EM>&</EM>");
  gsub(/''/,"");
}

/\[/ {
  fnc++
  gsub(/\[([^\[])*\]/, "<SUP>"fnc"</SUP><SPAN CLASS=\"wikishfootnote\"><SUP>"fnc"</SUP>: &</SPAN>");
  gsub(/\]|\[/,"");
}

# headings
/^-[^-]/ {
  print "<A NAME=\"" ++chapter ".0\"></A>"
  section = 0
  $0 = "<H2>" substr($0, 2) "</H2><P>";
  close_tags();
  print;
  next;
}
/^--[^-]/ {
  print "<A NAME=\"" chapter "." ++section "\"></A>"
  $0 = "<H3>" substr($0, 3) "</H3><P>";
  close_tags();
  print;
  next;
}
/^---[^-]/ {
  $0 = "<H4>" substr($0, 4) "</H4><P>";
  close_tags();
  print;
  next;
}


# horizontal line
/^----+/ {
  sub(/^----+/, "<HR>");
  blankline = 1;
  close_tags();
  print;
  next;
}


# lists
/^(  )+[*]/ {
  close_tags("list");
  parse_list("UL", "OL");
  print;
  next;
}
/^(  )+[1]/ {
  close_tags("list");
  parse_list("OL", "UL");
  print;
  next;
}


# paragraphs
NR == 1 { print "<P>"; }
# TABLE
/^\|.*\|/ { close_tags("TABLE"); parse_table(); print; next; }

{
  close_tags();
  # print paragraph when blankline registered
  if (blankline==1) {
    print "<P>";
    blankline=0;
  }
  print;
}

##############################################################################
END {
  $0 = ""
  close_tags();
}

##############################################################################
# Helpers

function printPre() {
  sub(/^_/, "") ;
  print
}

function close_tags(not) {
  # if list is parsed this line print it
  if (not !~ "list") {
    if (list["OL"] > 0) {
      parse_list("OL", "UL")
    } else if (list["UL"] > 0) {
      parse_list("UL", "OL")
    }
  }
  # close table
  if (not !~ "TABLE") {
    if (table == 1) {
      parse_table()
      print "</TABLE></CENTER>"
      table = 0
    }
  }
}

function parse_list(this, other) {
  thislist = list[this]
  otherlist = list[other]
  tabcount = 0
  while(/^(  )+[1*]/) {
    sub(/^  /,"")
    tabcount++
  }
  # close foreign tags
  if (otherlist > 0) {
    while(otherlist-- > 0) {
      print "</" other ">"
    }
  }
  # if we are needing more tags we open new
  if (thislist < tabcount) {
    while(thislist++ < tabcount) {
      print "<" this ">"
    }
  # if we are needing less tags we close some
  } else if (thislist > tabcount) {
    while(thislist-- != tabcount) {
      print "</" this ">"
    }
  }
  if (tabcount) {
    sub(/^[1*]/,"")
    $0 = "\t<LI>" $0
  }
  list[other] = 0
  list[this] = tabcount
}

function parse_table() {
  if (table != 1) {
    print "<CENTER><TABLE WIDTH=\"80%\" BORDER=\"1\">"
    table = 1;
    gsub(/^\|/,"<TR><TH>");
    gsub(/\|[ ]*$/,"</TH></TR>");
    gsub(/\|/,"</TH><TH>");
    return ;
  }
  if (table == 1) {
    gsub(/^\|/,"<TR><TD>");
    gsub(/\|[ ]*$/,"</TD></TR>");
    gsub(/\|/,"</TD><TD>");
  }
}

