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(/ /gm, '<angular/>');
+ text = new Showdown.converter().makeHtml(text.replace(/^#/gm, '###'));
+
+ while (match = text.match(R_LINK)) {
+ text = text.replace(match[0], '' +
+ (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(/
+
+
+
+
+