From 4f22d6866c052fb5b770ce4f377cecacacd9e6d8 Mon Sep 17 00:00:00 2001 From: Misko Hevery Date: Thu, 23 Dec 2010 00:44:27 +0100 Subject: complete rewrite of documentation generation - romeved mustache.js - unified templates - improved testability of the code --- docs/src/callback.js | 69 ++++ docs/src/dom.js | 123 +++++++ docs/src/gen-docs.js | 42 +++ docs/src/ignore.words | 0 docs/src/ngdoc.js | 614 ++++++++++++++++++++++++++++++++++ docs/src/reader.js | 91 +++++ docs/src/templates/doc_widgets.css | 35 ++ docs/src/templates/doc_widgets.js | 71 ++++ docs/src/templates/docs-scenario.html | 10 + docs/src/templates/docs-scenario.js | 9 + docs/src/templates/docs.css | 262 +++++++++++++++ docs/src/templates/docs.js | 47 +++ docs/src/templates/index.html | 45 +++ docs/src/writer.js | 61 ++++ 14 files changed, 1479 insertions(+) create mode 100644 docs/src/callback.js create mode 100644 docs/src/dom.js create mode 100644 docs/src/gen-docs.js create mode 100644 docs/src/ignore.words create mode 100644 docs/src/ngdoc.js create mode 100644 docs/src/reader.js create mode 100644 docs/src/templates/doc_widgets.css create mode 100644 docs/src/templates/doc_widgets.js create mode 100644 docs/src/templates/docs-scenario.html create mode 100644 docs/src/templates/docs-scenario.js create mode 100644 docs/src/templates/docs.css create mode 100644 docs/src/templates/docs.js create mode 100644 docs/src/templates/index.html create mode 100644 docs/src/writer.js (limited to 'docs/src') diff --git a/docs/src/callback.js b/docs/src/callback.js new file mode 100644 index 00000000..aaf69cde --- /dev/null +++ b/docs/src/callback.js @@ -0,0 +1,69 @@ +function noop(){} + +function chain(delegateFn, explicitDone){ + var onDoneFn = noop; + var onErrorFn = function(e){ + console.error(e.stack || e); + process.exit(-1); + }; + var waitForCount = 1; + delegateFn = delegateFn || noop; + var stackError = new Error('capture stack'); + + function decrementWaitFor() { + waitForCount--; + if (waitForCount == 0) + onDoneFn(); + } + + function self(){ + try { + return delegateFn.apply(self, arguments); + } catch (error) { + self.error(error); + } finally { + if (!explicitDone) + decrementWaitFor(); + } + }; + self.onDone = function(callback){ + onDoneFn = callback; + return self; + }; + self.onError = function(callback){ + onErrorFn = callback; + return self; + }; + self.waitFor = function(callback){ + if (waitForCount == 0) + throw new Error("Can not wait on already called callback."); + waitForCount++; + return chain(callback).onDone(decrementWaitFor).onError(self.error); + }; + + self.waitMany = function(callback){ + if (waitForCount == 0) + throw new Error("Can not wait on already called callback."); + waitForCount++; + return chain(callback, true).onDone(decrementWaitFor).onError(self.error); + }; + + self.done = function(callback){ + decrementWaitFor(); + }; + + self.error = function(error) { + var stack = stackError.stack.split(/\n\r?/).splice(2); + var nakedStack = []; + stack.forEach(function(frame){ + if (!frame.match(/callback\.js:\d+:\d+\)$/)) + nakedStack.push(frame); + }); + error.stack = error.stack + '\nCalled from:\n' + nakedStack.join('\n'); + onErrorFn(error); + }; + + return self; +} + +exports.chain = chain; diff --git a/docs/src/dom.js b/docs/src/dom.js new file mode 100644 index 00000000..e6dd09e6 --- /dev/null +++ b/docs/src/dom.js @@ -0,0 +1,123 @@ +/** + * DOM generation class + */ + +exports.DOM = DOM; + +////////////////////////////////////////////////////////// + +function DOM(){ + this.out = []; + this.headingDepth = 1; +} + +var INLINE_TAGS = { + i: true, + b: true +}; + +DOM.prototype = { + toString: function() { + return this.out.join(''); + }, + + text: function(content) { + if (typeof content == "string") { + this.out.push(content.replace(/&/g, '&').replace(//g, '>')); + } else if (typeof content == 'function') { + content.call(this, this); + } else if (content instanceof Array) { + this.ul(content); + } + }, + + html: function(html) { + if (html) { + this.out.push(html); + } + }, + + tag: function(name, attr, text) { + if (!text) { + text = attr; + attr = {}; + if (name == 'code') + attr['ng:non-bindable'] = ''; + } + this.out.push('<' + name); + for(var key in attr) { + this.out.push(" " + key + '="' + attr[key] + '"'); + } + this.out.push('>'); + this.text(text); + this.out.push('' + name + '>'); + if (!INLINE_TAGS[name]) + this.out.push('\n'); + }, + + code: function(text) { + this.tag('div', {'ng:non-bindable':''}, function(){ + this.tag('pre', {'class':"brush: js; html-script: true;"}, text); + }); + }, + + example: function(source, scenario) { + if (source || scenario) { + this.h('Example', function(){ + if (scenario === false) { + this.code(source); + } else { + this.tag('doc:example', function(){ + if (source) this.tag('doc:source', source); + if (scenario) this.tag('doc:scenario', scenario); + }); + } + }); + } + }, + + h: function(heading, content, fn){ + if (content==undefined || content && content.legth == 0) return; + this.tag('h' + this.headingDepth, heading); + this.headingDepth++; + if (content instanceof Array) { + this.ul(content, {'class': heading.toLowerCase()}, fn); + } else if (fn) { + fn.call(this, content); + } else { + this.text(content); + } + this.headingDepth--; + }, + + h1: function(attr, text) { + this.tag('h1', attr, text); + }, + + h2: function(attr, text) { + this.tag('h2', attr, text); + }, + + h3: function(attr, text) { + this.tag('h3', attr, text); + }, + + p: function(attr, text) { + this.tag('p', attr, text); + }, + + ul: function(list, attr, fn) { + if (typeof attr == 'function') { + fn = attr; + attr = {}; + } + this.tag('ul', attr, function(dom){ + list.forEach(function(item){ + dom.out.push('
[\s\S]*?<\/pre>)/),
+      match;
+
+  parts.forEach(function(text, i){
+    if (text.match(/^/)) {
+      text = text.
+        replace(/^/, '').
+        replace(/<\/pre>/, '
');
+    } else {
+      text = text.replace(/' +
+                                        (match[4] || match[1]) +
+                                      '');
+      }
+    }
+    parts[i] = text;
+  });
+  return parts.join('');
+};
+var R_LINK = /{@link ([^\s}]+)((\s|\n)+(.+?))?\s*}/m;
+            //       1       123     3 4     42
+function markdownNoP(text) {
+  var lines = markdown(text).split(NEW_LINE);
+  var last = lines.length - 1;
+  lines[0] = lines[0].replace(/^/, '');
+  lines[last] = lines[last].replace(/<\/p>$/, '');
+  return lines.join('\n');
+}
+
+
+//////////////////////////////////////////////////////////
+function scenarios(docs){
+  var specs = [];
+  docs.forEach(function(doc){
+    if (doc.scenario) {
+      specs.push('describe("');
+      specs.push(doc.name);
+      specs.push('", function(){\n');
+      specs.push('  beforeEach(function(){\n');
+      specs.push('    browser().navigateTo("index.html#!' + doc.name + '");');
+      specs.push('  });\n\n');
+      specs.push(doc.scenario);
+      specs.push('\n});\n\n');
+    }
+  });
+  return specs;
+}
+
+
+//////////////////////////////////////////////////////////
+function metadata(docs){
+  var words = [];
+  docs.forEach(function(doc){
+    words.push({
+      name:doc.name,
+      type: doc.ngdoc,
+      keywords:doc.keywords()
+    });
+  });
+  words.sort(keywordSort);
+  return words;
+}
+
+function keywordSort(a,b){
+  // supper ugly comparator that orders all utility methods and objects before all the other stuff
+  // like widgets, directives, services, etc.
+  // Mother of all beautiful code please forgive me for the sin that this code certainly is.
+
+  if (a.name === b.name) return 0;
+  if (a.name === 'angular') return -1;
+  if (b.name === 'angular') return 1;
+
+  function namespacedName(page) {
+    return (page.name.match(/\./g).length === 1 && page.type !== 'overview' ? '0' : '1') + page.name;
+  }
+
+  var namespacedA = namespacedName(a),
+      namespacedB = namespacedName(b);
+
+  return namespacedA < namespacedB ? -1 : 1;
+}
+
+
+//////////////////////////////////////////////////////////
+function trim(text) {
+  var MAX = 9999;
+  var empty = RegExp.prototype.test.bind(/^\s*$/);
+  var lines = text.split('\n');
+  var minIndent = MAX;
+  lines.forEach(function(line){
+    minIndent = Math.min(minIndent, indent(line));
+  });
+  for ( var i = 0; i < lines.length; i++) {
+    lines[i] = lines[i].substring(minIndent);
+  }
+  // remove leading lines
+  while (empty(lines[0])) {
+    lines.shift();
+  }
+  // remove trailing
+  while (empty(lines[lines.length - 1])) {
+    lines.pop();
+  }
+  return lines.join('\n');
+  
+  function indent(line) {
+    for(var i = 0; i < line.length; i++) {
+      if (line.charAt(i) != ' ')  {
+        return i;
+      }
+    }
+    return MAX;
+  }
+}
+
+//////////////////////////////////////////////////////////
+function merge(docs){
+  var byName = {};
+  docs.forEach(function(doc){
+    byName[doc.name] = doc;
+  });
+  for(var i=0; i b.name ? 1 : 0);
+  }
+}
+//////////////////////////////////////////////////////////
+
+function property(name) {
+  return function(value){ 
+    return value[name];
+  };
+} 
\ No newline at end of file
diff --git a/docs/src/reader.js b/docs/src/reader.js
new file mode 100644
index 00000000..8f9f22c3
--- /dev/null
+++ b/docs/src/reader.js
@@ -0,0 +1,91 @@
+/**
+ * All reading related code here. This is so that we can separate the async code from sync code
+ * for testability
+ */
+require.paths.push(__dirname);
+var fs       = require('fs'),
+    callback = require('callback');
+
+var NEW_LINE = /\n\r?/;
+
+function collect(callback){
+  findJsFiles('src', callback.waitMany(function(file) {
+    //console.log('reading', file, '...');
+    findNgDocInJsFile(file, callback.waitMany(function(doc, line) {
+      callback(doc, file, line);
+    }));
+  }));
+  findNgDocInDir('docs/', callback.waitMany(callback));
+  callback.done();
+}
+
+function findJsFiles(dir, callback){
+  fs.readdir(dir, callback.waitFor(function(err, files){
+    if (err) return this.error(err);
+    files.forEach(function(file){
+      var path = dir + '/' + file;
+      fs.lstat(path, callback.waitFor(function(err, stat){
+        if (err) return this.error(err);
+        if (stat.isDirectory())
+          findJsFiles(path, callback.waitMany(callback));
+        else if (/\.js$/.test(path))
+          callback(path);
+      }));
+    });
+    callback.done();
+  }));
+}
+
+function findNgDocInDir(directory, docNotify) {
+  fs.readdir(directory, docNotify.waitFor(function(err, files){
+    if (err) return this.error(err);
+    files.forEach(function(file){
+      //console.log('reading', directory + file, '...');
+      if (!file.match(/\.ngdoc$/)) return;
+      fs.readFile(directory + file, docNotify.waitFor(function(err, content){
+        if (err) return this.error(err);
+        docNotify(content.toString(), directory + file, 1);
+      }));
+    });
+    docNotify.done();
+  }));
+}
+
+function findNgDocInJsFile(file, callback) {
+  fs.readFile(file, callback.waitFor(function(err, content){
+    var lines = content.toString().split(NEW_LINE);
+    var text;
+    var startingLine ;
+    var match;
+    var inDoc = false;
+    lines.forEach(function(line, lineNumber){
+      lineNumber++;
+      // is the comment starting?
+      if (!inDoc && (match = line.match(/^\s*\/\*\*\s*(.*)$/))) {
+        line = match[1];
+        inDoc = true;
+        text = [];
+        startingLine = lineNumber;
+      }
+      // are we done?
+      if (inDoc && line.match(/\*\//)) {
+        text = text.join('\n');
+        text = text.replace(/^\n/, '');
+        if (text.match(/@ngdoc/)){
+          callback(text, startingLine);
+        }
+        doc = null;
+        inDoc = false;
+      }
+      // is the comment add text
+      if (inDoc){
+        text.push(line.replace(/^\s*\*\s?/, ''));
+      }
+    });
+    callback.done();
+  }));
+}
+
+
+
+exports.collect = collect;
\ No newline at end of file
diff --git a/docs/src/templates/doc_widgets.css b/docs/src/templates/doc_widgets.css
new file mode 100644
index 00000000..8361f105
--- /dev/null
+++ b/docs/src/templates/doc_widgets.css
@@ -0,0 +1,35 @@
+@namespace doc url("http://docs.angularjs.org/");
+
+doc\:example {
+  display: none;
+}
+
+ul.doc-example {
+  list-style-type: none;
+  position: relative;
+  font-size: 14px;
+}
+
+ul.doc-example > li {
+  border: 2px solid gray;
+  border-radius: 5px;
+  -moz-border-radius: 5px;
+  -webkit-border-radius: 5px;
+  background-color: white;
+  margin-bottom: 20px;
+}
+
+ul.doc-example > li.doc-example-heading {
+  border: none;
+  border-radius: none;
+  margin-bottom: -10px;
+}
+
+li.doc-example-live {
+  padding: 10px;
+  font-size: 1.2em;
+}
+
+div.syntaxhighlighter {
+  padding-bottom: 1px !important; /* fix to remove unnecessary scrollbars http://is.gd/gSMgC */
+}
\ No newline at end of file
diff --git a/docs/src/templates/doc_widgets.js b/docs/src/templates/doc_widgets.js
new file mode 100644
index 00000000..b119e326
--- /dev/null
+++ b/docs/src/templates/doc_widgets.js
@@ -0,0 +1,71 @@
+(function(){
+  
+  var angularJsUrl;
+  var scripts = document.getElementsByTagName("script");
+  var filename = /(.*\/)angular([^\/]*)/;
+  for(var j = 0; j < scripts.length; j++) {
+    var src = scripts[j].src;
+    if (src && src.match(filename)) {
+      angularJsUrl = src;
+    }
+  }
+
+  
+  var HTML_TEMPLATE =
+  '\n' +
+  '\n' +
+  ' \n' +
+  ' \n' +
+  '_HTML_SOURCE_\n' +
+  ' \n' +
+  '';
+
+  angular.widget('doc:example', function(element){
+    this.descend(true); //compile the example code
+    element.hide();
+
+    var example = element.find('doc\\:source').eq(0),
+        exampleSrc = example.text(),
+        scenario = element.find('doc\\:scenario').eq(0);
+
+    var code = indent(exampleSrc);
+    var tabs = angular.element(
+        '' +
+          '- Source
' +
+          '- ' + 
+            '' +
+          '
- Live Preview
' +
+          '- ' + exampleSrc +'' +
+          '
- Scenario Test
' +
+          '- ' + scenario.text() + ' 
' +
+        '
');
+    
+    tabs.find('li.doc-example-source > pre').text(HTML_TEMPLATE.replace('_HTML_SOURCE_', code.html));
+
+    element.html('');
+    element.append(tabs);
+    element.show();
+
+    var script = (exampleSrc.match(/
+  
+
+
+
+