From cc749760fd59418433de5a055d1ca401f7500444 Mon Sep 17 00:00:00 2001
From: Vojta Jina
Date: Mon, 15 Nov 2010 01:10:16 +0000
Subject: Added basic Services, which support @memberOf and @methodOf
---
docs/collect.js | 105 ++++++++++---
docs/docs.css | 5 +
docs/service.template | 37 +++++
docs/spec/collectSpec.js | 141 +++++++++++++++++
docs/specs.js | 2 +-
src/Angular.js | 48 +++++-
src/AngularPublic.js | 8 +
src/Browser.js | 155 +++++++++++++++---
src/services.js | 402 ++++++++++++++++++++++++++++++++++++++++++++---
9 files changed, 838 insertions(+), 65 deletions(-)
create mode 100644 docs/service.template
diff --git a/docs/collect.js b/docs/collect.js
index 4e845f5b..7ad1630b 100644
--- a/docs/collect.js
+++ b/docs/collect.js
@@ -7,7 +7,8 @@ var fs = require('fs'),
Showdown = require('showdown').Showdown;
var documentation = {
- pages:[]
+ pages:[],
+ byName: {}
};
var keywordPages = [];
@@ -23,18 +24,7 @@ var work = callback.chain(function () {
//console.log('reading', file, '...');
findNgDoc(file, work.waitMany(function(doc) {
parseNgDoc(doc);
- if (doc.ngdoc) {
- keywordPages.push({
- name:doc.name,
- keywords:keywords(doc.raw.text)
- }
- );
- documentation.pages.push(doc);
- console.log('Found:', doc.ngdoc + ':' + doc.shortName);
- mergeTemplate(
- doc.ngdoc + '.template',
- doc.name + '.html', doc, work.waitFor());
- }
+ processNgDoc(documentation, doc);
}));
}));
}));
@@ -42,6 +32,7 @@ var work = callback.chain(function () {
console.log('ERROR:', err.stack || err);
}).onDone(function(){
keywordPages.sort(function(a,b){ return a.name == b.name ? 0:(a.name < b.name ? -1 : 1);});
+ writeDoc(documentation.pages);
mergeTemplate('docs-data.js', 'docs-data.js', {JSON:JSON.stringify(keywordPages)}, callback.chain());
mergeTemplate('docs-scenario.js', 'docs-scenario.js', documentation, callback.chain());
copy('docs-scenario.html', callback.chain());
@@ -156,11 +147,17 @@ function markdownTag(doc, name, value) {
}
function markdown(text) {
- text = text.replace(/
[\s\S]*<\/pre>)/);
+ parts.forEach(function(text, i){
+ if (!text.match(/^/)) {
+ text = text.replace(/ /gm, '<angular/>');
+ text = new Showdown.converter().makeHtml(text);
+ text = text.replace(/(angular\.[\$\w\._\-:]+)/gm, '$1');
+ text = text.replace(/(`(ng:[\w\._\-]+)`)/gm, '$1');
+ parts[i] = text;
+ }
+ });
+ return parts.join('');
}
function markdownNoP(text) {
@@ -171,6 +168,43 @@ function markdownNoP(text) {
return lines.join('\n');
}
+function requiresTag(doc, name, value) {
+ doc.requires = doc.requires || [];
+ doc.requires.push({name: value});
+}
+
+function propertyTag(doc, name, value) {
+ doc[name] = doc[name] || [];
+ var match = value.match(/^({(\S+)}\s*)?(\S+)(\s+(.*))?/);
+
+ if (match) {
+ var tag = {
+ type: match[2],
+ name: match[3],
+ description: match[5] || false
+ };
+ } else {
+ throw "[" + doc.raw.file + ":" + doc.raw.line +
+ "]: @" + name + " must be in format '{type} name description' got: " + value;
+ }
+ return doc[name].push(tag);
+}
+
+function returnsTag(doc, name, value) {
+ var match = value.match(/^({(\S+)}\s*)?(.*)?/);
+
+ if (match) {
+ var tag = {
+ type: match[2],
+ description: match[3] || false
+ };
+ } else {
+ throw "[" + doc.raw.file + ":" + doc.raw.line +
+ "]: @" + name + " must be in format '{type} description' got: " + value;
+ }
+ return doc[name] = tag;
+}
+
var TAG = {
ngdoc: valueTag,
example: escapedHtmlTag,
@@ -186,6 +220,7 @@ var TAG = {
paramDescription: markdownTag,
exampleDescription: markdownTag,
element: valueTag,
+ methodOf: valueTag,
name: function(doc, name, value) {
doc.name = value;
doc.shortName = value.split(/\./).pop();
@@ -212,7 +247,10 @@ var TAG = {
throw "[" + doc.raw.file + ":" + doc.raw.line +
"]: @param must be in format '{type} name=value description' got: " + value;
}
- }
+ },
+ property: propertyTag,
+ requires: requiresTag,
+ returns: returnsTag
};
function parseNgDoc(doc){
@@ -291,3 +329,32 @@ function findJsFiles(dir, callback){
callback.done();
}));
}
+
+function processNgDoc(documentation, doc) {
+ if (!doc.ngdoc) return;
+ console.log('Found:', doc.ngdoc + ':' + doc.name);
+
+ documentation.byName[doc.name] = doc;
+
+ if (doc.methodOf) {
+ if (parent = documentation.byName[doc.methodOf]) {
+ (parent.method = parent.method || []).push(doc);
+ } else {
+ throw 'Owner "' + doc.methodOf + '" is not defined.';
+ }
+ } else {
+ documentation.pages.push(doc);
+ keywordPages.push({
+ name:doc.name,
+ keywords:keywords(doc.raw.text)
+ });
+ }
+}
+
+function writeDoc(pages) {
+ pages.forEach(function(doc) {
+ mergeTemplate(
+ doc.ngdoc + '.template',
+ doc.name + '.html', doc, callback.chain());
+ });
+}
diff --git a/docs/docs.css b/docs/docs.css
index 25a8ffea..664328d0 100644
--- a/docs/docs.css
+++ b/docs/docs.css
@@ -125,3 +125,8 @@ a {
margin-left: 2em;
font-family: monospace;
}
+
+#sidebar ul li.level-3 {
+ margin-left: 3em;
+ font-family: monospace;
+}
diff --git a/docs/service.template b/docs/service.template
new file mode 100644
index 00000000..2774cad2
--- /dev/null
+++ b/docs/service.template
@@ -0,0 +1,37 @@
+{{name}}
+Description
+{{{description}}}
+
+Dependencies
+<angular/>
'); }); + + it('should not replace anything in', function(){
+ expect(collect.markdown('angular.x\n\nangular.k\n
\nangular.x')).
+ toEqual(
+ '' +
+ '\nangular.k\n
' +
+ '');
+ });
+ });
+
+ describe('processNgDoc', function() {
+ var processNgDoc = collect.processNgDoc,
+ documentation;
+
+ beforeEach(function() {
+ documentation = {
+ pages: [],
+ byName: {}
+ };
+ });
+
+ it('should store references to docs by name', function() {
+ var doc = {ngdoc: 'section', name: 'fake', raw: {text:''}};
+ processNgDoc(documentation, doc);
+ expect(documentation.byName.fake).toBe(doc);
+ });
+
+ it('should connect doc to owner (specified by @methodOf)', function() {
+ var parentDoc = {ngdoc: 'section', name: 'parent', raw: {text:''}};
+ var doc = {ngdoc: 'section', name: 'child', methodOf: 'parent', raw: {text:''}};
+ processNgDoc(documentation, parentDoc);
+ processNgDoc(documentation, doc);
+ expect(documentation.byName.parent.method).toBeDefined();
+ expect(documentation.byName.parent.method[0]).toBe(doc);
+ });
+
+ it('should not add doc to sections if @memberOf specified', function() {
+ var parentDoc = {ngdoc: 'parent', name: 'parent', raw: {text:''}};
+ var doc = {ngdoc: 'child', name: 'child', methodOf: 'parent', raw: {text:''}};
+ processNgDoc(documentation, parentDoc);
+ processNgDoc(documentation, doc);
+ expect(documentation.pages.child).not.toBeDefined();
+ });
+
+ it('should throw exception if owner does not exist', function() {
+ expect(function() {
+ processNgDoc(documentation, {ngdoc: 'section', methodOf: 'not.exist', raw: {text:''}});
+ }).toThrow('Owner "not.exist" is not defined.');
+ });
+
+ it('should ignore non-ng docs', function() {
+ var doc = {name: 'anything'};
+ expect(function() {
+ processNgDoc(documentation, doc);
+ }).not.toThrow();
+ expect(documentation.pages).not.toContain(doc);
+ });
});
describe('TAG', function(){
@@ -41,6 +98,90 @@ describe('collect', function(){
});
});
+ describe('@requires', function() {
+ it('should parse more @requires tag into array', function() {
+ TAG.requires(doc, 'requires', '$service');
+ TAG.requires(doc, 'requires', '$another');
+
+ expect(doc.requires).toEqual([
+ {name: '$service'},
+ {name: '$another'}
+ ]);
+ });
+ });
+
+ describe('@property', function() {
+ it('should parse @property tags into array', function() {
+ TAG.property(doc, 'property', '{type} name1 desc');
+ TAG.property(doc, 'property', '{type} name2 desc');
+ expect(doc.property.length).toEqual(2);
+ });
+
+ it('should parse @property with only name', function() {
+ TAG.property(doc, 'property', 'fake');
+ expect(doc.property[0].name).toEqual('fake');
+ });
+
+ it('should parse @property with optional type', function() {
+ TAG.property(doc, 'property', '{string} name');
+ expect(doc.property[0].name).toEqual('name');
+ expect(doc.property[0].type).toEqual('string');
+ });
+
+ it('should parse @property with optional description', function() {
+ TAG.property(doc, 'property', 'name desc rip tion');
+ expect(doc.property[0].name).toEqual('name');
+ expect(doc.property[0].description).toEqual('desc rip tion');
+ });
+
+ it('should parse @property with type and description both', function() {
+ TAG.property(doc, 'property', '{bool} name desc rip tion');
+ expect(doc.property[0].name).toEqual('name');
+ expect(doc.property[0].type).toEqual('bool');
+ expect(doc.property[0].description).toEqual('desc rip tion');
+ });
+
+ /**
+ * If property description is undefined, this variable is not set in the template,
+ * so the whole @description tag is used instead
+ */
+ it('should set undefined description to "false"', function() {
+ TAG.property(doc, 'property', 'name');
+ expect(doc.property[0].description).toBe(false);
+ });
+ });
+
+ describe('@methodOf', function() {
+ it('should parse @methodOf tag', function() {
+ expect(function() {
+ TAG.methodOf(doc, 'methodOf', 'parentName');
+ }).not.toThrow();
+ expect(doc.methodOf).toEqual('parentName');
+ });
+ });
+
+ describe('@returns', function() {
+ it('should parse @returns', function() {
+ expect(function() {TAG.returns(doc, 'returns', '');})
+ .not.toThrow();
+ });
+
+ it('should parse @returns with type', function() {
+ TAG.returns(doc, 'returns', '{string}');
+ expect(doc.returns.type).toEqual('string');
+ });
+
+ it('should parse @returns with description', function() {
+ TAG.returns(doc, 'returns', 'descrip tion');
+ expect(doc.returns.description).toEqual('descrip tion');
+ });
+
+ it('should parse @returns with type and description', function() {
+ TAG.returns(doc, 'returns', '{string} description');
+ expect(doc.returns).toEqual({type: 'string', description: 'description'});
+ });
+ });
+
describe('@describe', function(){
it('should support pre blocks', function(){
TAG.description(doc, 'description', 'abc
');
diff --git a/docs/specs.js b/docs/specs.js
index 5f1cd153..d564b045 100644
--- a/docs/specs.js
+++ b/docs/specs.js
@@ -1,6 +1,6 @@
require.paths.push("./lib");
var jasmine = require('jasmine-1.0.1');
-var sys = require('sys');
+var sys = require('util');
for(var key in jasmine) {
global[key] = jasmine[key];
diff --git a/src/Angular.js b/src/Angular.js
index e917b3ee..5a5a4ab3 100644
--- a/src/Angular.js
+++ b/src/Angular.js
@@ -315,7 +315,6 @@ var _undefined = undefined,
*/
angularValidator = extensionMap(angular, 'validator'),
-
/**
* @ngdoc overview
* @name angular.filter
@@ -473,6 +472,53 @@ var _undefined = undefined,
* });
*/
angularFormatter = extensionMap(angular, 'formatter'),
+
+ /**
+ * @ngdoc overview
+ * @name angular.service
+ *
+ * @description
+ * # Overview
+ * Services are substituable objects, which are wired together using dependency injection.
+ * Each service could have dependencies (other services), which are passed in constructor.
+ * Because JS is dynamicaly typed language, dependency injection can not use static types
+ * to satisfy these dependencies, so each service must explicitely define its dependencies.
+ * This is done by $inject property.
+ *
+ * For now, life time of all services is the same as the life time of page.
+ *
+ *
+ * # Standard services
+ * The Angular framework provides a standard set of services for common operations.
+ * You can write your own services and rewrite these standard services as well.
+ * Like other core angular variables, standard services always start with $.
+ *
+ * * `angular.service.$window`
+ * * `angular.service.$document`
+ * * `angular.service.$location`
+ * * `angular.service.$log`
+ * * `angular.service.$exceptionHandler`
+ * * `angular.service.$hover`
+ * * `angular.service.$invalidWidgets`
+ * * `angular.service.$route`
+ * * `angular.service.$xhr`
+ * * `angular.service.$xhr.error`
+ * * `angular.service.$xhr.bulk`
+ * * `angular.service.$xhr.cache`
+ * * `angular.service.$resource`
+ * * `angular.service.$cookies`
+ * * `angular.service.$cookieStore`
+ *
+ * # Writing your own services
+ *
+ * angular.service('notify', function(location) {
+ * this.one = function() {
+ * }
+ * }, {$inject: ['$location']});
+ *
+ *
+ * # Using services in controller
+ */
angularService = extensionMap(angular, 'service'),
angularCallbacks = extensionMap(angular, 'callbacks'),
nodeName,
diff --git a/src/AngularPublic.js b/src/AngularPublic.js
index 2b5d4fbc..c241238c 100644
--- a/src/AngularPublic.js
+++ b/src/AngularPublic.js
@@ -1,4 +1,12 @@
var browserSingleton;
+/**
+ * @ngdoc service
+ * @name angular.service.$browser
+ * @requires $log
+ *
+ * @description
+ * Represents the browser.
+ */
angularService('$browser', function($log){
if (!browserSingleton) {
browserSingleton = new Browser(
diff --git a/src/Browser.js b/src/Browser.js
index bf9b5483..2608f160 100644
--- a/src/Browser.js
+++ b/src/Browser.js
@@ -19,7 +19,20 @@ function Browser(location, document, head, XHR, $log) {
var outstandingRequestCount = 0;
var outstandingRequestCallbacks = [];
- self.xhr = function(method, url, post, callback){
+ /**
+ * @ngdoc method
+ * @name angular.service.$browser#xhr
+ * @methodOf angular.service.$browser
+ *
+ * @param {string} method Requested method (get|post|put|delete|head|json)
+ * @param {string} url Requested url
+ * @param {string=} post Post data to send
+ * @param {function(number, string)} callback Function that will be called on response
+ *
+ * @description
+ * Send ajax request
+ */
+ self.xhr = function(method, url, post, callback) {
if (isFunction(post)) {
callback = post;
post = _null;
@@ -63,7 +76,14 @@ function Browser(location, document, head, XHR, $log) {
}
};
- self.notifyWhenNoOutstandingRequests = function(callback){
+ /**
+ * @ngdoc method
+ * @name angular.service.$browser#notifyWhenNoOutstandingRequests
+ * @methodOf angular.service.$browser
+ *
+ * @param {function} callback Function that will be called when no outstanding request
+ */
+ self.notifyWhenNoOutstandingRequests = function(callback) {
if (outstandingRequestCount === 0) {
callback();
} else {
@@ -75,27 +95,48 @@ function Browser(location, document, head, XHR, $log) {
// Poll Watcher API
//////////////////////////////////////////////////////////////
var pollFns = [];
- function poll(){
+
+ /**
+ * @ngdoc method
+ * @name angular.service.$browser#poll
+ * @methodOf angular.service.$browser
+ */
+ self.poll = function() {
foreach(pollFns, function(pollFn){ pollFn(); });
- }
- self.poll = poll;
+ };
/**
+ * @ngdoc method
+ * @name angular.service.$browser#addPollFn
+ * @methodOf angular.service.$browser
+ *
+ * @param {function} fn Poll function to add
+ *
+ * @description
* Adds a function to the list of functions that poller periodically executes
- * @return {Function} the added function
+ *
+ * @returns {function} the added function
*/
- self.addPollFn = function(/**Function*/fn){
+ self.addPollFn = function(fn) {
pollFns.push(fn);
return fn;
};
/**
- * Configures the poller to run in the specified intervals, using the specified setTimeout fn and
- * kicks it off.
+ * @ngdoc method
+ * @name angular.service.$browser#startPoller
+ * @methodOf angular.service.$browser
+ *
+ * @param {number} interval How often should browser call poll functions (ms)
+ * @param {function} setTimeout
+ *
+ * @description
+ * Configures the poller to run in the specified intervals, using the specified
+ * setTimeout fn and kicks it off.
*/
- self.startPoller = function(/**number*/interval, /**Function*/setTimeout){
+ self.startPoller = function(interval, setTimeout) {
(function check(){
- poll();
+ self.poll();
setTimeout(check, interval);
})();
};
@@ -103,15 +144,37 @@ function Browser(location, document, head, XHR, $log) {
//////////////////////////////////////////////////////////////
// URL API
//////////////////////////////////////////////////////////////
+
+ /**
+ * @ngdoc method
+ * @name angular.service.$browser#setUrl
+ * @methodOf angular.service.$browser
+ *
+ * @param {string} url New url
+ *
+ * @description
+ * Sets browser's url
+ */
self.setUrl = function(url) {
var existingURL = location.href;
if (!existingURL.match(/#/)) existingURL += '#';
if (!url.match(/#/)) url += '#';
location.href = url;
};
- self.getUrl = function() {
+
+ /**
+ * @ngdoc method
+ * @name angular.service.$browser#getUrl
+ * @methodOf angular.service.$browser
+ *
+ * @description
+ * Get current browser's url
+ *
+ * @returns {string} Browser's url
+ */
+ self.getUrl = function() {
return location.href;
- };
+ };
//////////////////////////////////////////////////////////////
// Cookies API
@@ -121,19 +184,27 @@ function Browser(location, document, head, XHR, $log) {
var lastCookieString = '';
/**
- * The cookies method provides a 'private' low level access to browser cookies. It is not meant to
- * be used directly, use the $cookie service instead.
+ * @ngdoc method
+ * @name angular.service.$browser#cookies
+ * @methodOf angular.service.$browser
+ *
+ * @param {string=} name Cookie name
+ * @param {string=} value Cokkie value
+ *
+ * @description
+ * The cookies method provides a 'private' low level access to browser cookies.
+ * It is not meant to be used directly, use the $cookie service instead.
*
* The return values vary depending on the arguments that the method was called with as follows:
- * $location = {{$location}}
+ */
angularServiceInject("$location", function(browser) {
var scope = this,
location = {toString:toString, update:update, updateHash: updateHash},
@@ -39,6 +92,11 @@ angularServiceInject("$location", function(browser) {
// PUBLIC METHODS
/**
+ * @ngdoc method
+ * @name angular.service.$location#update
+ * @methodOf angular.service.$location
+ *
+ * @description
* Update location object
* Does not immediately update the browser
* Browser is updated at the end of $eval()
@@ -69,7 +127,12 @@ angularServiceInject("$location", function(browser) {
}
/**
- * Update location hash
+ * @ngdoc method
+ * @name angular.service.$location#updateHash
+ * @methodOf angular.service.$location
+ *
+ * @description
+ * Update location hash part
* @see update()
*
* @example
@@ -99,9 +162,12 @@ angularServiceInject("$location", function(browser) {
}
/**
+ * @ngdoc method
+ * @name angular.service.$location#toString
+ * @methodOf angular.service.$location
+ *
+ * @description
* Returns string representation - href
- *
- * @return {string} Location's href property
*/
function toString() {
updateLocation();
@@ -224,23 +290,105 @@ angularServiceInject("$location", function(browser) {
}, ['$browser'], EAGER_PUBLISHED);
+/**
+ * @ngdoc service
+ * @name angular.service.$log
+ * @requires $window
+ *
+ * @description
+ * Is simple service for logging. Default implementation writes the message
+ * into the browser's console (if present).
+ *
+ * This is useful for debugging.
+ *
+ * @example
+ Reload this page with open console, enter text and hit the log button...
+ Message: + + + + + + */ angularServiceInject("$log", function($window){ var console = $window.console || {log: noop, warn: noop, info: noop, error: noop}, log = console.log || noop; return { + /** + * @ngdoc method + * @name angular.service.$log#log + * @methodOf angular.service.$log + * + * @description + * Write a log message + */ log: bind(console, log), + + /** + * @ngdoc method + * @name angular.service.$log#warn + * @methodOf angular.service.$log + * + * @description + * Write a warning message + */ warn: bind(console, console.warn || log), + + /** + * @ngdoc method + * @name angular.service.$log#info + * @methodOf angular.service.$log + * + * @description + * Write an information message + */ info: bind(console, console.info || log), + + /** + * @ngdoc method + * @name angular.service.$log#error + * @methodOf angular.service.$log + * + * @description + * Write an error message + */ error: bind(console, console.error || log) }; }, ['$window'], EAGER_PUBLISHED); +/** + * @ngdoc service + * @name angular.service.$exceptionHandler + * @requires $log + * + * @description + * Any uncaught exception in