Windows: add build step that generates license.rtf from LICENSE
authorBert Belder <bertbelder@gmail.com>
Wed, 4 Apr 2012 16:06:00 +0000 (18:06 +0200)
committerisaacs <i@izs.me>
Fri, 6 Apr 2012 23:15:51 +0000 (16:15 -0700)
tools/license2rtf.js [new file with mode: 0644]
vcbuild.bat

diff --git a/tools/license2rtf.js b/tools/license2rtf.js
new file mode 100644 (file)
index 0000000..9bf7de4
--- /dev/null
@@ -0,0 +1,327 @@
+
+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.
+ */
+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++;
+      }
+    }
+
+    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);
index f6d0fef..bb53210 100644 (file)
@@ -22,6 +22,7 @@ set nosnapshot=
 set test=
 set test_args=
 set msi=
+set licensertf=
 set upload=
 
 :next-arg
@@ -36,6 +37,7 @@ if /i "%1"=="noprojgen"     set noprojgen=1&goto arg-ok
 if /i "%1"=="nobuild"       set nobuild=1&goto arg-ok
 if /i "%1"=="nosign"        set nosign=1&goto arg-ok
 if /i "%1"=="nosnapshot"    set nosnapshot=1&goto arg-ok
+if /i "%1"=="licensertf"    set licensertf=1&goto arg-ok
 if /i "%1"=="test-uv"       set test=test-uv&goto arg-ok
 if /i "%1"=="test-internet" set test=test-internet&goto arg-ok
 if /i "%1"=="test-pummel"   set test=test-pummel&goto arg-ok
@@ -43,7 +45,7 @@ if /i "%1"=="test-simple"   set test=test-simple&goto arg-ok
 if /i "%1"=="test-message"  set test=test-message&goto arg-ok
 if /i "%1"=="test-all"      set test=test-all&goto arg-ok
 if /i "%1"=="test"          set test=test&goto arg-ok
-if /i "%1"=="msi"           set msi=1&goto arg-ok
+if /i "%1"=="msi"           set msi=1&set licensertf=1&goto arg-ok
 if /i "%1"=="upload"        set upload=1&goto arg-ok
 
 echo Warning: ignoring invalid command line option `%1`.
@@ -75,7 +77,7 @@ echo Project files generated.
 
 :msbuild
 @rem Skip project generation if requested.
-if defined nobuild goto msi
+if defined nobuild goto sign
 
 @rem Bail out early if not running in VS build env.
 if defined VCINSTALLDIR goto msbuild-found
@@ -94,9 +96,19 @@ goto run
 msbuild node.sln /t:%target% /p:Configuration=%config% /clp:NoSummary;NoItemAndPropertyList;Verbosity=minimal /nologo
 if errorlevel 1 goto exit
 
-if defined nosign goto msi
+:sign
+@rem Skip signing if the `nosign` option was specified.
+if defined nosign goto licensertf
+
 signtool sign /a Release\node.exe
 
+:licensertf
+@rem Skip license.rtf generation if not requested.
+if not defined licensertf goto msi
+
+%config%\node tools\license2rtf.js < LICENSE > %config%\license.rtf
+if errorlevel 1 echo Failed to generate license.rtf&goto exit
+
 :msi
 @rem Skip msi generation if not requested
 if not defined msi goto run