tools/license2rtf.js

Summary

Maintainability
D
2 days
Test Coverage

var assert = require('assert'),
    Stream = require('stream'),
    inherits = require('util').inherits;


/*
 * This filter consumes a stream of characters and emits one string per line.
 */
function LineSplitter() {
  var self = this,
      buffer = "";

  Stream.call(this);
  this.writable = true;

  this.write = function(data) {
    var lines = (buffer + data).split(/\r\n|\n\r|\n|\r/);
    for (var i = 0; i < lines.length - 1; i++) {
      self.emit('data', lines[i]);
    }
    buffer = lines[lines.length - 1];
    return true;
  };

  this.end = function(data) {
    this.write(data || '');
    if (buffer) {
      self.emit('data', buffer);
    }
    self.emit('end');
  };
}
inherits(LineSplitter, Stream);


/*
 * This filter consumes lines and emits paragraph objects.
 */
function ParagraphParser() {
  var self = this,
      block_is_license_block = false,
      block_has_c_style_comment,
      is_first_line_in_paragraph,
      paragraph_line_indent,
      paragraph;

   Stream.call(this);
   this.writable = true;

   resetBlock(false);

   this.write = function(data) {
     parseLine(data + '');
     return true;
   };

   this.end = function(data) {
     if (data) {
       parseLine(data + '');
     }
     flushParagraph();
     self.emit('end');
   };

  function resetParagraph() {
    is_first_line_in_paragraph = true;
    paragraph_line_indent = -1;

    paragraph = {
      li: '',
      in_license_block: block_is_license_block,
      lines: []
    };
  }

  function resetBlock(is_license_block) {
    block_is_license_block = is_license_block;
    block_has_c_style_comment = false;
    resetParagraph();
  }

  function flushParagraph() {
    if (paragraph.lines.length || paragraph.li) {
      self.emit('data', paragraph);
    }
    resetParagraph();
  }

  function parseLine(line) {
    // Strip trailing whitespace
    line = line.replace(/\s*$/, '');

    // Detect block separator
    if (/^\s*(=|"){3,}\s*$/.test(line)) {
      flushParagraph();
      resetBlock(!block_is_license_block);
      return;
    }

    // Strip comments around block
    if (block_is_license_block) {
      if (!block_has_c_style_comment)
        block_has_c_style_comment = /^\s*(\/\*)/.test(line);
      if (block_has_c_style_comment) {
        var prev = line;
        line = line.replace(/^(\s*?)(?:\s?\*\/|\/\*\s|\s\*\s?)/, '$1');
        if (prev == line)
          line = line.replace(/^\s{2}/, '');
        if (/\*\//.test(prev))
          block_has_c_style_comment = false;
      } else {
        // Strip C++ and perl style comments.
        line = line.replace(/^(\s*)(?:\/\/\s?|#\s?)/, '$1');
      }
    }

    // Detect blank line (paragraph separator)
    if (!/\S/.test(line)) {
      flushParagraph();
      return;
    }

    // Detect separator "lines" within a block. These mark a paragraph break
    // and are stripped from the output.
    if (/^\s*[=*\-]{5,}\s*$/.test(line)) {
      flushParagraph();
      return;
    }

    // Find out indentation level and the start of a lied or numbered list;
    var result = /^(\s*)(\d+\.|\*|-)?\s*/.exec(line);
    assert.ok(result);
    // The number of characters that will be stripped from the beginning of
    // the line.
    var line_strip_length = result[0].length;
    // The indentation size that will be used to detect indentation jumps.
    // Fudge by 1 space.
    var line_indent = Math.floor(result[0].length / 2) * 2;
    // The indentation level that will be exported
    var level = Math.floor(result[1].length / 2);
    // The list indicator that precedes the actual content, if any.
    var line_li = result[2];

    // Flush the paragraph when there is a li or an indentation jump
    if (line_li || (line_indent != paragraph_line_indent &&
                    paragraph_line_indent != -1)) {
      flushParagraph();
      paragraph.li = line_li;
    }

    // Set the paragraph indent that we use to detect indentation jumps. When
    // we just detected a list indicator, wait
    // for the next line to arrive before setting this.
    if (!line_li && paragraph_line_indent != -1) {
      paragraph_line_indent = line_indent;
    }

    // Set the output indent level if it has not been set yet.
    if (paragraph.level === undefined)
      paragraph.level = level;

    // Strip leading whitespace and li.
    line = line.slice(line_strip_length);

    if (line)
      paragraph.lines.push(line);

    is_first_line_in_paragraph = false;
  }
}
inherits(ParagraphParser, Stream);


/*
 * This filter consumes paragraph objects and emits modified paragraph objects.
 * The lines within the paragraph are unwrapped where appropriate. It also
 * replaces multiple consecutive whitespace characters by a single one.
 */
function Unwrapper() {
  var self = this;

  Stream.call(this);
  this.writable = true;

  this.write = function(paragraph) {
    var lines = paragraph.lines,
        break_after = [],
        i;

    for (i = 0; i < lines.length - 1; i++) {
      var line = lines[i];

      // When a line is really short, the line was probably kept separate for a
      // reason.
      if (line.length < 50)  {
        // If the first word on the next line really didn't fit after the line,
        // it probably was just ordinary wrapping after all.
        var next_first_word_length = lines[i + 1].replace(/\s.*$/, '').length;
        if (line.length + next_first_word_length < 60) {
          break_after[i] = true;
        }
      }
    }

    for (i = 0; i < lines.length - 1; ) {
      if (!break_after[i]) {
        lines[i] += ' ' + lines.splice(i + 1, 1)[0];
      } else {
        i++;
      }
    }

    for (i = 0; i < lines.length; i++) {
      // Replace multiple whitespace characters by a single one, and strip
      // trailing whitespace.
      lines[i] = lines[i].replace(/\s+/g, ' ').replace(/\s+$/, '');
    }

    self.emit('data', paragraph);
  };

  this.end = function(data) {
    if (data)
      self.write(data);
    self.emit('end');
  };
}
inherits(Unwrapper, Stream);


/*
 * This filter generates an rtf document from a stream of paragraph objects.
 */
function RtfGenerator() {
  var self = this,
      did_write_anything = false;

  Stream.call(this);
  this.writable = true;

  this.write = function(paragraph) {
    if (!did_write_anything) {
      emitHeader();
      did_write_anything = true;
    }

    var li = paragraph.li,
        level = paragraph.level + (li ? 1 : 0),
        lic = paragraph.in_license_block;

    var rtf = "\\pard";
    rtf += '\\sa150\\sl300\\slmult1';
    if (level > 0)
      rtf += '\\li' + (level * 240);
    if (li) {
      rtf += '\\tx' + (level) * 240;
      rtf += '\\fi-240';
    }
    if (lic)
      rtf += '\\ri240';
    if (!lic)
      rtf += '\\b';
    if (li)
      rtf += ' ' + li + '\\tab';
    rtf += ' ';
    rtf += paragraph.lines.map(rtfEscape).join('\\line ');
    if (!lic)
      rtf += '\\b0';
    rtf += '\\par\n';

    self.emit('data', rtf);
  };

  this.end = function(data) {
    if (data)
      self.write(data);
    if (did_write_anything)
      emitFooter();
    self.emit('end');
  };

  function toHex(number, length) {
    var hex = (~~number).toString(16);
    while (hex.length < length)
      hex = '0' + hex;
    return hex;
  }

  function rtfEscape(string) {
    return string
      .replace(/[\\\{\}]/g, function(m) {
       return '\\' + m;
      })
      .replace(/\t/g, function() {
        return '\\tab ';
      })
      .replace(/[\x00-\x1f\x7f-\xff]/g, function(m) {
        return '\\\'' + toHex(m.charCodeAt(0), 2);
      })
      .replace(/\ufeff/g, '')
      .replace(/[\u0100-\uffff]/g, function(m) {
        return '\\u' + toHex(m.charCodeAt(0), 4) + '?';
     });
  }

  function emitHeader() {
    self.emit('data', '{\\rtf1\\ansi\\ansicpg1252\\uc1\\deff0\\deflang1033' +
                      '{\\fonttbl{\\f0\\fswiss\\fcharset0 Tahoma;}}\\fs20\n' +
                      '{\\*\\generator txt2rtf 0.0.1;}\n');
  }

  function emitFooter() {
    self.emit('data', '}');
  }
}
inherits(RtfGenerator, Stream);


var stdin = process.stdin,
    stdout = process.stdout,
    line_splitter = new LineSplitter(),
    paragraph_parser = new ParagraphParser(),
    unwrapper = new Unwrapper(),
    rtf_generator = new RtfGenerator();

stdin.setEncoding('utf-8');
stdin.resume();

stdin.pipe(line_splitter);
line_splitter.pipe(paragraph_parser);
paragraph_parser.pipe(unwrapper);
unwrapper.pipe(rtf_generator);
rtf_generator.pipe(stdout);