Author Topic: nroff daemon  (Read 1471 times)

Offline Luggage

  • Acquaintance
  • *
  • Posts: 7
    • View Profile
    • Middle of Nowhere
nroff daemon
« on: February 05, 2012, 08:50:17 PM »
I figured I'd post the code I'm working on and see if anyone might find it useful. I wouldn't consider it finished yet. I still have a few features I want to add and I need to do some refactoring. But, as they say, release early and release often.

I'd say that looking at the code should be only for the brave, but I'm interested in knowing what formatting codes you would find most useful that aren't already included. I'm using http://www.podgoretsky.com/ftp/docs/unix/unix%20unleashed%20internet/ch08.htm as the basic guide for now, selecting the ones that seem appropriate for a fixed-width text medium.

Btw, this code is freely available for anyone to do with as they like. It's not the most efficient, but I'll worry about that when I have a solid 100-200 players in my game all the time and the game is lagging because of all of the document reading. Nothing is cached or pre-compiled or saved anywhere.

The entry points are FormatAsText and FormatAsHTML.

The FormatAsText function takes an array of strings (essentially the source file exploded on newline) and an optional width to fit. It returns a list of strings suitable for passing to eventPage().

The FormatAsHTML function takes the same arguments as FormatAsText, but doesn't return anything meant for a pager. It's an array of strings that should be imploded with an empty string and displayed in a <pre/> block.

daemon/nroff.c:
Code: [Select]

#include <lib.h>

inherit LIB_DAEMON;

class nroff_state {
    string font;
    string acc;
    int filling;
    int centering;
    int ignore_next_sp;
    int make_links;
    int width;
    int array tabs;
    int pos;
    int temp_indent;
    int indent;
    string color;
    mapping macros;
    mapping strings;
    string array result;
    string control_char;
    string escape_char;
}

static string font_text(class nroff_state f);
static string font_html(class nroff_state f);
static string font_html_end(class nroff_state f);


/*
 * Given the current state object, we format the accumulated text in
 * state->acc and add the lines to the state->result array.
 *
 * The last item in the state->result array will have %^RESET%^ appended.
 *
 * We also initialize state->acc to the current font settings
 *
 * You should call this when formatting as text and you want to clear out
 * any accumulated text.
 */

static void format_text(class nroff_state state) {
  string t = state->acc;
  string tmp;
  string *bits;
  int n;

  state->acc = font_text(state);
  if(!t || t == "") return;

  tmp = "";
  n = state->temp_indent;
  state->temp_indent = 0;
  while(n--) tmp += " ";

  t = wrap(tmp + t, state->width - state->indent);
 
  if(t[<1..<1] == "\n") t = t[0..<2];
  if(t == "") return;

  tmp = "";
  n = state->indent;
  while(n--) tmp += " ";

  bits = explode(t, "\n");
  n = sizeof(bits)-1;

  bits[n] += "%^RESET%^";

  for( ; n ; n--) bits[n] = tmp + bits[n];
  bits[0] = tmp + bits[0];

  state->result += bits;
}


/*
 * strip_html(s) will remove any HTML tags from the string s
 * and convert any entity references to an underscore. This
 * is mainly used when you want to count the visible characters
 * so far.
 */
static string strip_html(string s) {
  string r;
  r = implode(map(explode(s, "<"), function(string l) {
    string *lbits;
    if(strsrch(l, ">") == -1) return l;
    lbits = explode(l, ">");
    if(l[0..0] == ">") lbits = ({ "" }) + lbits;
    if(sizeof(lbits) > 1) return lbits[1];
    return "";
  }), "");
  r = implode(map(explode(r, "&"), function(string l) {
    string *lbits;
    if(strsrch(l, ";") == -1) return l;
    lbits = explode(l, ";");
    if(l[0..0] == ";") lbits = ({ "" }) + lbits;
    if(sizeof(lbits) > 1) return "_" + lbits[1];
    return "_";
  }), "");
  return r;
}

/*
 * strip_text(s) should remove any special codes or escapes
 * for character counting
 */
static string strip_text(string s) {
    return strip_colours(s);
}

/*
 * Given the current state object, we format the accumulated text in
 * state->acc and add the lines to the state->result array.
 *
 * The last item in the state->result array will have %^RESET%^ appended.
 *
 * We also initialize state->acc to the current font settings
 *
 * You should call this when formatting as html and you want to clear out
 * any accumulated text.
 */
varargs static void format_html(class nroff_state state) {
  string tmp, *bits;
  string t = state->acc;
  int    font_stack = 0;
  int    in_bold, i;
  int    d;

  state->acc = font_html(state);
  if(!t || t == "") return;
  bits = explode(t, "%^");
  if(t[0..1] == "%^") bits = ({ "" }) + bits;
  for(i = 1; i < sizeof(bits); i += 2) {
    if(bits[i] == "BOLD") {
        if(state->font[0..0] != "B") {
            if(font_stack == 0 || state->font[0..0] != "B") {
              state->font = "B" + state->font;
              font_stack ++;
            }
            bits[i] = "";
            if(state->color) {
                bits[i] += "</span>";
            }
            bits[i] += "<strong>";
            if(state->color) {
                bits[i] += "<span style=\"color: " + state->color + ";\">";
            }
        }
        else {
            bits[i] = "";
        }
    }
    else if(bits[i] == "RESET") {
        bits[i] = "";
        if(state->color) {
            bits[i] += "</span>";
        }
        if(font_stack && member_array("B", explode(state->font[0..font_stack-1], "")) != -1) {
            bits[i] += "</strong>";
        }
        state->color = 0;
        state->font = state->font[font_stack..];
        font_stack = 0;
    }
    else {
        tmp = lower_case(bits[i]);
        bits[i] = "";
        if(state->color) bits[i] += "</span>";
        state->color = tmp;
        bits[i] += "<span style=\"color: " + state->color + ";\">";
    }
  }
  t = implode(bits, "");
  tmp = "";
  i = state->temp_indent;
  while(i--) tmp += " ";
  state->temp_indent = 0;

  t = wrap_html(tmp+t, state->width - state->indent);
  tmp = "";
  i = state->indent;
  while(i--) tmp += " ";
  t = tmp + implode(explode(t, "\n"), "\n" + tmp);
  state->result += ({ t + font_html_end(state) });
  state->acc = font_html(state);
}

static string apply_strings(string line, class nroff_state s) {
      string array names = keys(s->strings);
      string m;

      // do these from longest names to shortest names
      names = sort_array(names, (: sizeof($2) - sizeof($1) :));
      foreach(m in names) {
        line = replace_string(line, s->escape_char + m, s->strings[m]);
      }
      return line;
}

static void font_change(class nroff_state f, string s) {
    switch(s[0..0]) {
    case "B": case "I": case "R":
        f->font = s[0..0] + f->font;
        break;
    case "P":
        if(sizeof(f->font) > 1) f->font = f->font[1..];
        break;
    }
}


static string font_text(class nroff_state f) {
  string ret = "";
  switch(f->font[0..0]) {
  case "B": ret += "%^BOLD%^"; break;
  case "I": ret += "%^USCORE%^"; break;
  case "R": break;
  }
  if(f->color) ret += "%^" + upper_case(f->color) + "%^";
  return ret;
}

static string font_html(class nroff_state f) {
  switch(f->font[0..0]) {
  case "B": return "<strong>";
  case "I": return "<em>";
  case "R": return "";
  }
}

static string font_html_end(class nroff_state f) {
  switch(f->font[0..0]) {
  case "B": return "</strong>";
  case "I": return "</em>";
  case "R": return "";
  }
}


varargs string *FormatAsHTML(string *content, int x) {
  string *bits, bit;
  string line, tmp;
  class nroff_state state;
  mixed m;
  int i, n;

  state = new(class nroff_state);
  state->font = "R";
  state->acc = "";
  state->centering = 0;
  state->ignore_next_sp = 0;
  state->make_links = 0;
  state->macros = ([ ]);
  state->strings = ([
    "*(sc": "\\fISecond Contract\\fP",
  ]);
  state->result = ({ });
  state->color = 0;
  state->control_char = ".";
  state->escape_char = "\\";
  state->tabs = ({ });
  state->filling = 1;
  state->temp_indent = 0;
  state->indent = 0;

  if(!content) return state->result;
  if(!x) x = 80;

  state->width = x;

  foreach(line in content) {
    line = replace_string(line, "&", "&amp;");
    line = replace_string(line, ">", "&gt;");
    line = replace_string(line, "<", "&lt;");
    n = sizeof(line)-1;
    while(n>0 && line[n] == ' ') n--;
    line = line[0..n];
    if(line[0..0] == state->control_char) {
      bits = explode(line, " ");
// eventually, we'll remove the lower_case() - when we get multiline macros
      switch(lower_case(bits[0][1..2])) {
      case "br":
        format_html(state);
        state->result += ({ "\n" });
        state->pos = 0;
        break;

      case "sp":
        format_html(state);
        n = 1;
        if(sizeof(bits)>1) n = to_int(bits[1]);
        if(n < 1) n = 1;
        if(state->ignore_next_sp) n--;
        state->ignore_next_sp = 0;
        if(n) state->result += ({ "\n" });
        while(n--) state->result += ({ "\n" });
        state->pos = 0;
        break;
      case "fi":
        state->filling = 1;
        break;
      case "nf":
        if(state->filling) {
          format_html(state);
          state->result += ({ "\n" });
          state->pos = 0;
          state->filling = 0;
        }
        break;
      case "ec":
        if(sizeof(bits) > 1)
            state->escape_char = bits[1][0..0];
        else
            state->escape_char = "\\";
        break;
      case "sh":
        format_html(state);
        if(sizeof(state->result) > 0) state->result += ({ "\n\n" });
        state->result += ({ "<strong>" + implode(bits[1..], " ") + "</strong>\n" });
        if(lower_case(implode(bits[1..], " ")) == "see also")
          state->make_links = 1;
        else
          state->make_links = 0;
        state->ignore_next_sp = 1;
        state->pos = 0;
        break;
      case "ft":
        if(sizeof(bits) > 1) {
          state->acc += font_html_end(state);
          font_change(state, bits[1]);
          state->acc += font_html(state);
        }
        break;
      case "cc":
        if(sizeof(bits) > 1) state->control_char = bits[1][0..0];
        break;

      case "ce":
        format_html(state);
        state->centering = 1;
        if(sizeof(bits) > 1) {
          state->centering = to_int(bits[1]);
        }
        if(state->centering < 1) state->centering = 1;
        break;
      case "ds":
        if(sizeof(bits) > 2) {
          if(sizeof(bits[1]) > 1)
              state->strings["*(" + bits[1]] = implode(bits[2..], " ");
          else
              state->strings["*" + bits[1]] = implode(bits[2..], " ");
        }
        else if(sizeof(bits) > 1) {
          if(sizeof(bits[1]) > 1)
              state->strings["*(" + bits[1]] = "";
          else
              state->strings["*" + bits[1]] = "";
        }
        break;
      case "ta":
        state->tabs = ({ });
        foreach(bit in bits[1..]) {
          if(sscanf(bit, "%f%s", m, tmp) == 2) {
              if(tmp == "i") m *= 12; // 12 characters to an inch
          }
          else m = to_int(bit);
          m = to_int(m);
          if(sizeof(state->tabs) == 0 || m > state->tabs[<1]) {
            state->tabs += ({ m });
          }
        }
        break;
      case "in":
        if(sscanf(bits[1], "%f%s", m, tmp) == 2) {
            if(tmp == "i") m *= 12;
        }
        else m = to_int(bits[1]);
        m = to_int(m);
        state->indent = m;
        break;
      case "ti":
        if(sscanf(bits[1], "%f%s", m, tmp) == 2) {
            if(tmp == "i") m *= 12;
        }
        else m = to_int(bits[1]);
        m = to_int(m);
        state->temp_indent = m;
        break;
      }
    }

    else {
      // process text and add it to accumulator unless we're centering
      line = apply_strings(line, state);
      if(state->escape_char == "\\") {
          line = replace_string(line, "\\\\", "\\e");
      }
      bits = explode(line, state->escape_char);
      n = strsrch(state->acc, "\n", -1);
      if(n < 0) state->pos = sizeof(strip_html(state->acc));
      else state->pos = sizeof(strip_html(state->acc[n..]));
      if(line[0..0] == state->escape_char) bits =({ "" }) + bits;
      if(sizeof(bits)) state->pos += sizeof(strip_html(bits[0]));
      for(i = 1; i < sizeof(bits); i++) {
          switch(bits[i][0..0]){
          case "e":
              bits[i] = state->escape_char + bits[i][1..];
              break;
          case "f":
              tmp = font_html_end(state);
              font_change(state, bits[i][1..1]);
              bits[i] = tmp+font_html(state) + bits[i][2..];
              break;
          case "t":
              n = 0;
              while(n < sizeof(state->tabs) && state->tabs[n] < state->pos)
                n++;
              if(n < sizeof(state->tabs))
                  n = state->tabs[n] - state->pos;
              else
                  n = 0;
              tmp = "";
              while(n--) tmp += " ";
              bits[i] = tmp + bits[i][1..];
              break;
          case "-":
              bits[i] = "&#8722;" + bits[i][1..];
              break;
          case "(":
              tmp = "";
              switch(bits[i][1..2]) {
              case "bu": tmp = "&bull;"; break;
              case "co": tmp = "&copy;"; break;
              case "em": tmp = "&mdash;"; break;
              case "ff": tmp = "&#xfb00;"; break;
              case "fi": tmp = "&#xfb01;"; break;
              case "Fi": tmp = "Fi"; break;
              case "fl": tmp = "&#xfb02;"; break;
              case "Fl": tmp = "Fl"; break;
              }
              bits[i] = tmp + bits[i][3..];
              break;
          default:
              //bits[i] = bits[i][1..];
              break;
          }

          state->pos += sizeof(strip_html(bits[i]));
      }
      line = implode(bits, "");
      if(state->centering) {
          state->result += ({ font_html(state) + "<center>" + line + "</center>" + font_html_end(state) });
          state->centering--;
          state->pos = 0;
      }
      else {
          if(state->make_links) {
              bits = explode(line, ", ");
              if(sizeof(bits) == 1) {
                  bits = explode(line, " and ");
              }
              for(i = 0; i < sizeof(bits); i++) {
                  if(bits[i][0..3] == "and ") {
                      bits[i] = "and <a href='?topic=" + bits[i][4..] + "'>" + bits[i][4..] + "</a>";
                  }
                  else {
                      bits[i] = "<a href='?topic=" + bits[i] + "'>" + bits[i] + "</a>";
                  }
              }
              if(sizeof(bits) > 2)
                  line = implode(bits, ", ");
              else if(sizeof(bits) > 1)
                  line = implode(bits, " and ");
              else
                  line = bits[0];
          }
          if(state->filling) {
              state->acc += line + " ";
          }
          else {
              state->acc = font_html(state) + line;
              format_html(state);
              state->result += ({ "\n" });
          }
      }
    }
  }

  format_html(state);

  return state->result;
}

string *FormatAsText(string *content) {
  string *bits, bit;
  string line, tmp;
  class nroff_state state;
  mixed m;
  int n, i;

  if(!content) return ({ });

  state = new(class nroff_state);
  state->font = "R";
  state->acc = "";
  state->centering = 0;
  state->ignore_next_sp = 0;
  state->make_links = 0;
  state->macros = ([ ]);
  state->strings = ([
    "*(sc": "\\fISecond Contract\\fP",
  ]);
  state->result = ({ });
  state->color = 0;
  state->control_char = ".";
  state->escape_char = "\\";
  state->filling = 1;
  state->temp_indent = 0;
  state->indent = 0;

  if( !this_player() ) state->width = 79;
  else {
    int *tmp_n;

    tmp_n = (int *)this_player()->GetScreen();
    if( tmp_n ) state->width = (tmp_n[0] || 79);
    else state->width = 79;
  }

  foreach(line in content) {
    n = sizeof(line)-1;
    while(n>0 && line[n] == ' ') n--;
    line = line[0..n];
    if(line[0..0] == state->control_char) {
      bits = explode(line, " ");
      switch(lower_case(bits[0][1..2])) {
      case "cc":
        if(sizeof(bits) > 1) state->control_char = bits[1][0..0];
        break;
      case "br":
        format_text(state);
        break;
      case "sp":
        format_text(state);
        n = 1;
        if(sizeof(bits)>1) n = to_int(bits[1]);
        if(n < 1) n = 1;
        while(n--) state->result += ({ "" });
        break;
      case "sh":

        format_text(state);
        state->result += ({ "", "%^BOLD%^" + implode(bits[1..], " ") + "%^RESET%^" });
        break;
      case "fi":
        state->filling = 1;
        break;
      case "nf":
        if(state->filling) {
          format_text(state);
          state->filling = 0;
          state->pos = 0;
        }
        break;
      case "ec":
        if(sizeof(bits) > 1)
            state->escape_char = bits[1][0..0];
        else
            state->escape_char = "\\";
        break;
      case "ft":
        if(sizeof(bits) > 1) {
          font_change(state, bits[1]);
          state->acc += "%^RESET%^" + font_text(state);
        }
        break;
      case "ce":
        format_text(state);
        state->centering = 1;
        if(sizeof(bits) > 1) {
          state->centering = to_int(bits[1]);
        }
        if(state->centering < 1) state->centering = 1;
        break;
      case "ds":
        if(sizeof(bits) > 2) {
          if(sizeof(bits[1]) > 1)
              state->strings["*(" + bits[1]] = implode(bits[2..], " ");
          else
              state->strings["*" + bits[1]] = implode(bits[2..], " ");
        }
        else if(sizeof(bits) > 1) {
          if(sizeof(bits[1]) > 1)
              state->strings["*(" + bits[1]] = "";
          else
              state->strings["*" + bits[1]] = "";
        }
        break;

      case "ta":
        state->tabs = ({ });
        foreach(bit in bits[1..]) {
          if(sscanf(bit, "%f%s", m, tmp) == 2) {
              if(tmp == "i") m *= 12;
          }
          else m = to_int(bit);
          m = to_int(m);
          if(sizeof(state->tabs) == 0 || m > state->tabs[<1]) {
            state->tabs += ({ m });
          }
        }
        break;
      case "in":
        if(sscanf(bits[1], "%f%s", m, tmp) == 2) {
            if(tmp == "i") m *= 12;
        }
        else m = to_int(bits[1]);
        m = to_int(m);
        state->indent = m;
        break;
      case "ti":
        if(sscanf(bits[1], "%f%s", m, tmp) == 2) {
            if(tmp == "i") m *= 12;
        }
        else m = to_int(bits[1]);
        m = to_int(m);
        state->temp_indent = m;
        break;
      }
    }
    else {
      // process text and add it to accumulator unless we're centering
      line = apply_strings(line, state);
      if(state->escape_char == "\\") {
          line = replace_string(line, "\\\\", "\\e");
      }
      bits = explode(line, state->escape_char);
      if(line[0..0] == state->escape_char) bits =({ "" }) + bits;
      n = strsrch(state->acc, "\n", -1);
      if(n < 0) state->pos = sizeof(strip_text(state->acc));
      else state->pos = sizeof(strip_text(state->acc[n..]));
      if(sizeof(bits)) state->pos += sizeof(strip_text(bits[0]));
      for(i = 1; i < sizeof(bits); i++) {
          switch(bits[i][0..0]){
          case "f":
              font_change(state, bits[i][1..1]);
              bits[i] = "%^RESET%^"+font_text(state) + bits[i][2..];
              break;
          case "e":
              bits[i] = state->escape_char + bits[i][1..];
              break;

          case "t":
              n = 0;
              while(n < sizeof(state->tabs) && state->tabs[n] < state->pos)
                n++;
              if(n < sizeof(state->tabs))
                  n = state->tabs[n] - state->pos;
              else
                  n = 0;
              tmp = "";
              while(n--) tmp += " ";
              bits[i] = tmp + bits[i][1..];
              break;
          case "(":
              tmp = "";
              switch(bits[i][1..2]) {
              case "bu": tmp = "*"; break;
              case "co": tmp = "(c)"; break;
              case "em": tmp = "--"; break;
              case "ff": tmp = "ff"; break;
              case "fi": tmp = "fi"; break;
              case "Fi": tmp = "Fi"; break;
              case "fl": tmp = "fl"; break;
              case "Fl": tmp = "Fl"; break;
              }
              bits[i] = tmp + bits[i][3..];
              break;
          default:
              bits[i] = bits[i][1..];
              break;
          }
          state->pos += sizeof(strip_text(bits[i]));
      }
      line = implode(bits, "");
      if(state->centering) {
          state->result += ({ font_text(state) + center(line) + "%^RESET%^" });
          state->centering--;
      }
      else {
          if(state->filling) {
              state->acc += line + " ";
          }
          else {
              state->acc = font_text(state) + line;
              format_text(state);
          }
      }
    }
  }

  format_text(state);
  return state->result;
}

I added a couple sefuns to handle wrapping. The DS wrap function I renamed wrap_int, and used it in a new wrap() sefun that takes formatting into account (something sprintf doesn't seem to do as far as I can tell -- this seemed easier than trying to modify the sprintf efun for now):

in secure/sefun/strings.c:
Code: [Select]
static varargs string wrap_int(string str, int x) {
    if( !x ) {
        if( !this_player() ) x = 79;
        else {
            int *tmp;

            tmp = (int *)this_player()->GetScreen();
            if( tmp ) x = (tmp[0] || 79);
            else x = 79;
        }
    }
    if(sizeof(str) < 7900) return sprintf("%-=" + x + "s\n", str);
    else {
        string tmpfile = generate_tmp();
        write_file(tmpfile,str);
        str = read_bytes(tmpfile,0,7900);
        rm(tmpfile);
        str += "\n*** TRUNCATED ***\n";
        return sprintf("%-=" + x + "s\n", str);
    }
}


varargs string wrap(string t, int x) {
  string *bits, tmp, *bits2;
  mixed *words;
  int i, j;
  string begin = "", end = "";
 
  if(!t || t == "") return t;
  if(t[<1..<1] == "\n") end = "\n";
  if(t[0..0] == "\n") begin = "\n";

  if(!t || t == "") return "";

  bits = explode(t, "%^") + ({ "" });

  if(t[0..1] == "%^") bits = ({ "" }) + bits + ({ "" });
  words = ({ });
  for(i = 1; i < sizeof(bits); i += 2) {
    bits[i] = "";
  }
  tmp = implode(bits, "");
  words = explode(tmp, " ");
  if(tmp[0..0] == " ") words = ({ "" }) + words;

  if(!sizeof(words)) return begin+end;

  if(words[0] == "") i = 1;
  else i = 0;
  words = map(explode(wrap_int(implode(words, " "), x), "\n"), (:
    ($1[0..0] == " " ? ({ "" }) : ({ })) + explode($1, " ")
  :));

  bits = explode(t, " ");
  if(t[0..0] == " ") bits = ({ "" }) + bits;
  tmp = "";
  j = 0;
  for(i = 0; i < sizeof(words); i++) {
    tmp += implode(bits[j..j+sizeof(words[i])-1], " ") + "\n";
    j += sizeof(words[i]);
  }
  if(j < sizeof(bits)) {
    if(tmp != "") tmp += " ";
    tmp += implode(bits[j..], " ");
  }
  if(tmp == "") return begin+end;
  return begin + tmp + end;
}


varargs string wrap_html(string t, int x) {
  string *bits, tmp;
  mixed *words;
  int i, j;
  if(!t || t == "") return "";

  if(!x) x = 80;
 
  bits = explode(t, "<") + ({ "" });
  if(t[0..0] == "<") bits = ({ "" }) + bits;
  // bits is "text", "tag>text", "tag>text", ...
  for(i = 1; i < sizeof(bits); i += 2) {
    bits = bits[0..i-1] + explode(bits[i], ">") + bits[i+1..];
  }
  // now, bits is "text", "tag", "text", "tag", ...
  words = ({ });
  for(i = 0; i < sizeof(bits); i += 2) {
    words += explode(bits[i], " ");
  }
  if(!sizeof(words)) return "";
  words = map(words, function(string w) {
    string *bits;
    int i;
    bits = explode(w, "&");
    if(w[0..0] == "&") bits = ({ "" }) + bits;
    for(i = 1; i < sizeof(bits); i += 2) {
        if(strsrch(bits[i], ";") > 0)
            bits = bits[0..i-1] + ({ "_" + implode(explode(bits[i], ";")[1..], ";") }) + bits[i+1..];
        else
            bits = bits[0..i-1] + ({ bits[i][1..] }) + bits[i+1..];
    }
    return implode(bits, "");
  });
  words = map(explode(wrap_int(implode(words, " "), x), "\n"), (:
    ($1[0..0] == " " ? ({ "" }) : ({ })) + explode($1, " ")
  :));
  bits = explode(t, " ");
  if(t[0..0] == " ") bits = ({ "" }) + bits;
  tmp = "";
  j = 0;
  for(i = 0; i < sizeof(words); i++) {
    tmp += "<br/>" + implode(bits[j..j+sizeof(words[i])-1], " ");
    j += sizeof(words[i]);
  }
  if(j < sizeof(bits)) {
    tmp += " " + implode(bits[j..], " ");
  }
  if(tmp == "<br/>" || tmp == "") return "";
  if(sizeof(tmp) < 6) return tmp;
  return tmp[5..];
}

Finally, I have a document that describes the formatting codes and escapes supported:
Code: [Select]

.ce
NROFF Formatting

.sp
\*(sc uses a subset of the nroff/troff control language for
creating formatted pages such as help documents, books, and newspapers.
.sp
The following control sequences are understood:
.br
.ta 1.5i
.nf
.cc ,
.br\tbreak and don't fill out unfinished line
.cc \fIc\fP\tset control character
.ce [\fIn\fP]\tcenter text
.ds \fIxx\fP \fIstring\fP\tdefine string
.ec [\fIx\fP]\tset escape character
.fi\tfill text
.ft \fIf\fP\tchange font in output
.in \fIn\fP[\fIt\fP]\tset indent
.nf\tturn off filling
.sh \fIstring\fP\tsection header
.sp \fIn\fP\tleave \fIn\fP blank lines
.ta \fIn\fP[\fIt\fP] \fIm\fP[\fIt\fP]\tset tab stops
.ti \fIn\fP[\fIt\fP]\tset temporary indent
,cc .
.fi
.sp
The following escape sequences are understood:
.br
.ta 1.5i
.nf
\ee\tprints the escape character; default is the backslash(\e)
\e(\fIxx\fP\tspecifies character named \fIxx\fP
\e*\fIx\fP , \e*(\fIxx\fP\tspecifies string named \fIx\fP or \fIxx\fP
\ef\fIx\fP\trequests a font change
\et\thorizontal tab
\eX\tX, any character not listed above
.fi
.sp
The following inline requests are understood:
.br
.ta 1.5i
.nf
\e\e\tbackslash (\\)
-\thyphen (-)
\e-\tcurrent font minus sign (\-)
\e(bu\tbullet (\(bu)
\e(co\tcopyright (\(co)
\e(em\tem dash (\(em)
\e(ff\tff ligature (\(ff)
\e(fi\tfi ligature (\(fi)
\e(Fi\tFi ligature (\(Fi)
\e(fl\tfl ligature (\(fl)
\e(Fl\tFl ligature (\(Fl)
.fi
.sp
The current set of fonts are:
.ta 1i
.nf
\fBIdentifier\tFont\fP
B\tBold
I\tItalic
R\tRoman
P\tPrevious
.fi
.sp
Note that all of the typefaces are considered constant width. For purposes
of spacing and tabs, there are 12 characters to an inch.

The HTML formatted version can be seen at http://www.second-contract.com/playing/help/?index=player%20documents&topic=nroff