adhearsion 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. data/LICENSE +339 -0
  2. data/Rakefile +108 -0
  3. data/ahn +195 -0
  4. data/lib/adhearsion.rb +402 -0
  5. data/lib/constants.rb +20 -0
  6. data/lib/core_extensions.rb +157 -0
  7. data/lib/database_functions.rb +76 -0
  8. data/lib/rami.rb +822 -0
  9. data/lib/servlet_container.rb +146 -0
  10. data/new_projects/Rakefile +100 -0
  11. data/new_projects/config/adhearsion.sqlite3 +0 -0
  12. data/new_projects/config/adhearsion.yml +11 -0
  13. data/new_projects/config/database.rb +50 -0
  14. data/new_projects/config/database.yml +10 -0
  15. data/new_projects/config/helpers/drb_server.yml +43 -0
  16. data/new_projects/config/helpers/factorial.alien.c.yml +1 -0
  17. data/new_projects/config/helpers/manager_proxy.yml +7 -0
  18. data/new_projects/config/helpers/micromenus.yml +1 -0
  19. data/new_projects/config/helpers/micromenus/collab.rb +55 -0
  20. data/new_projects/config/helpers/micromenus/images/tux.bmp +0 -0
  21. data/new_projects/config/helpers/micromenus/javascripts/builder.js +131 -0
  22. data/new_projects/config/helpers/micromenus/javascripts/controls.js +834 -0
  23. data/new_projects/config/helpers/micromenus/javascripts/dragdrop.js +944 -0
  24. data/new_projects/config/helpers/micromenus/javascripts/effects.js +956 -0
  25. data/new_projects/config/helpers/micromenus/javascripts/prototype.js +2319 -0
  26. data/new_projects/config/helpers/micromenus/javascripts/scriptaculous.js +51 -0
  27. data/new_projects/config/helpers/micromenus/javascripts/slider.js +278 -0
  28. data/new_projects/config/helpers/micromenus/javascripts/unittest.js +557 -0
  29. data/new_projects/config/helpers/micromenus/stylesheets/firefox.css +10 -0
  30. data/new_projects/config/helpers/micromenus/stylesheets/firefox.xul.css +44 -0
  31. data/new_projects/config/helpers/weather.yml +1 -0
  32. data/new_projects/config/helpers/xbmc.yml +1 -0
  33. data/new_projects/config/migration.rb +53 -0
  34. data/new_projects/extensions.rb +56 -0
  35. data/new_projects/helpers/drb_server.rb +32 -0
  36. data/new_projects/helpers/factorial.alien.c +32 -0
  37. data/new_projects/helpers/manager_proxy.rb +43 -0
  38. data/new_projects/helpers/micromenus.rb +374 -0
  39. data/new_projects/helpers/oscar_wilde_quotes.rb +197 -0
  40. data/new_projects/helpers/weather.rb +85 -0
  41. data/new_projects/helpers/xbmc.rb +12 -0
  42. data/new_projects/logs/database.log +0 -0
  43. data/test/core_extensions_test.rb +26 -0
  44. data/test/dial_test.rb +43 -0
  45. data/test/stress_tests/test.rb +13 -0
  46. data/test/stress_tests/test.yml +13 -0
  47. data/test/test_micromenus.rb +0 -0
  48. metadata +131 -0
@@ -0,0 +1,146 @@
1
+ # Adhearsion, open source technology integrator
2
+ # Copyright 2006 Jay Phillips
3
+ #
4
+ # This program is free software; you can redistribute it and/or
5
+ # modify it under the terms of the GNU General Public License
6
+ # as published by the Free Software Foundation; either version 2
7
+ # of the License, or (at your option) any later version.
8
+ #
9
+ # This program is distributed in the hope that it will be useful,
10
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ # GNU General Public License for more details.
13
+ #
14
+ # You should have received a copy of the GNU General Public License
15
+ # along with this program; if not, write to the Free Software
16
+ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
+
18
+ class ServletContainer
19
+
20
+ # TODO: Port Mongrel server here.
21
+ class NativeServer;end
22
+
23
+ require 'gserver'
24
+ class RubyServer < GServer
25
+ def initialize(port, *args)
26
+ @stdlog = $STDOUT
27
+ log "Starting server on port #{port}"
28
+ audit = true
29
+ super(port, *args)
30
+ end
31
+
32
+ def read_variables io
33
+ # When debugging, this method could actually not do any IO operations and just
34
+ # return a development Hash of call variables
35
+ call_variables = {}
36
+ while(line = io.gets.chomp)
37
+ break if line.empty? # Empty lines signify no more variables
38
+ variable = line.split(/:\s*/)
39
+ new_name, new_value = variable.first[4..-1].downcase, variable.last
40
+ call_variables["#{new_name}"] = new_value =~ /^\d+$/ ? Integer(new_value) : new_value
41
+ end
42
+ call_variables
43
+ end
44
+
45
+ # GServer allows all functionality to be packed into
46
+ def serve io
47
+ io.sync = true
48
+ Thread.current[:io] = io
49
+ begin
50
+ call_variables = read_variables io
51
+ Thread.current[:VARS] = call_variables
52
+ Thread.current[:container] = Contexts::Container.new
53
+
54
+ if call_variables.extension == 'h'
55
+ # We've received a notification that a channel's hung up!
56
+ Thread.list.each do |thread|
57
+ if thread[:VARS] && thread[:VARS]['uniqueid'] == call_variables['uniqueid']
58
+ thread[:hungup?] = true
59
+ # Could kill() the Thread here but it may need to finish. We're merciful people.
60
+ log "Received notification that channel #{call_variables['channel']} has been hung up."
61
+ return
62
+ end
63
+ end
64
+ elsif ['t', 'failed'].include? call_variables['extension']
65
+ # Timeout notifications. Not implemented yet...
66
+ io.close
67
+ return
68
+ end
69
+
70
+ log "Executing call with variables: " + call_variables.inspect
71
+
72
+ # TODO Will perform cache checking here.
73
+
74
+ call_variables.each do |k,v|
75
+ Thread.current[:container].run_inside do
76
+ meta_def k do v end
77
+ end
78
+ end
79
+
80
+ # Execute all before_call hooks
81
+ [$BEFORE_CALl_HIGH, $BEFORE_CALL, $BEFORE_CALL_LOW].flatten.compact.each &:call
82
+ +lambda { answer if CONFIG.answer_before_call }
83
+
84
+ # A call hook may decree that the call shouldn't be processed further (e.g. if
85
+ # it processed the call itself). This is done be rewriting the context variable.
86
+ unless Thread.current[:VARS]['context'] == :interrupted
87
+
88
+ begin
89
+ Contexts.new.instance_eval do
90
+ # Interpret the extensions.rb file!
91
+ eval File.read(File.join(Dir.pwd, "extensions.rb"))
92
+ end
93
+ rescue => detail
94
+ log "Exception raised in extensions.rb! " << detail.message
95
+ # TODO: Make error reports more intutive. Use notifications DSL?
96
+ detail.backtrace.each do |msg| log " "*8 << msg end
97
+ end
98
+
99
+ log "Parsing of extensions.rb complete"
100
+ begin
101
+ target_context = call_variables['context']
102
+ if target_context
103
+ Thread.current[:container].run_inside do
104
+ begin
105
+ +send(target_context.to_s.to_sym)
106
+ rescue => e
107
+ STDERR.puts e.inspect
108
+ +lambda {
109
+ play 'were-sorry'
110
+ hangup
111
+ }
112
+ end
113
+ end
114
+ else
115
+ io.close
116
+ log "ASTERISK REQUESTED ROUTING FOR A CONTEXT UNDEFINED IN extensions.rb!!! REQUEST IGNORED!!!"
117
+ return
118
+ end
119
+ rescue => detail
120
+ log "ERROR: #{detail.class.name} => #{detail.inspect}"
121
+ detail.backtrace.each do |msg| log " "*8 << msg end
122
+ end
123
+ log "Call routing complete"
124
+ end
125
+ rescue => detail
126
+ log "Call thread raised an exception! #{detail.message}"
127
+ detail.backtrace.each do |msg| log " "*8 << msg end
128
+ end
129
+
130
+ [$AFTER_CALl_HIGH, $AFTER_CALL, $AFTER_CALL_LOW].flatten.compact.each &:call
131
+ +lambda { hangup if CONFIG.hangup_after_call }
132
+ end
133
+ end
134
+
135
+ def initialize(port=4573, native=false)
136
+ @server = (native ? NativeServer : RubyServer).new port, '0.0.0.0'
137
+ @server.start
138
+ end
139
+
140
+
141
+ def shutdown
142
+ @server.shutdown
143
+ end
144
+
145
+ attr_reader :server
146
+ end
@@ -0,0 +1,100 @@
1
+ require 'rubygems'
2
+ require 'rake/testtask'
3
+ require 'rake/rdoctask'
4
+ require 'rake/gempackagetask'
5
+
6
+ ADHEARSION_VERSION = '0.7.0'
7
+ Summary = 'Adhearsion is a professional integration system for
8
+ integrating anything and everything.'
9
+
10
+ #task :default => [:test]
11
+
12
+ #desc "Run the Adhearsion unit tests"
13
+ #task :test do
14
+ # Dir['test/*.rb'].each do |f| require f end
15
+ #end
16
+
17
+ gem_spec = Gem::Specification.new do |s|
18
+
19
+ s.name = 'adhearsion'
20
+ s.rubyforge_project = 'adhearsion'
21
+ s.author = 'Jay Phillips'
22
+ s.email = 'admin -at- jicksta dot com'
23
+ s.version = ADHEARSION_VERSION
24
+ s.summary = Summary
25
+ s.homepage = 'http://adhearsion.com'
26
+
27
+ s.add_dependency 'activerecord', '>= 1.14.4'
28
+ s.add_dependency 'activesupport', '>= 1.3.1'
29
+ s.add_dependency 'rake', '>= 0.7.1'
30
+
31
+ s.platform = Gem::Platform::RUBY
32
+ s.require_path = 'lib'
33
+ s.executables = 'start_adhearsion' # ['start_adhearsion', 'ahn']
34
+ s.bindir = '.'
35
+ #s.extra_rdoc_files = ['LICENSE']
36
+
37
+ include = Dir['**/*']
38
+ exclude = Dir['{log,pkg}/**/*']
39
+
40
+ s.files = include - exclude
41
+
42
+ # test_files = Dir['tests/*.rb'] # Will be added when not so buggy
43
+ end
44
+
45
+ Rake::GemPackageTask.new gem_spec do |pkg|
46
+ pkg.need_zip = false
47
+ pkg.need_tar = false
48
+ end
49
+
50
+ #desc "Generate documentation for Adhearsion"
51
+ #Rake::RDocTask.new do |rdoc|
52
+ # rdoc.rdoc_dir 'doc'
53
+ #end
54
+
55
+ desc "Pulls down the entire wiki in HTML format"
56
+ task :wiki => [:rm_wiki] do
57
+ require 'open-uri'
58
+ File.open "wiki.zip",'a' do |f|
59
+ f.write open('http://adhearsion.stikipad.com/codex/export_html').read
60
+ end
61
+ Dir.mkdir 'docs' unless File.exists? 'docs'
62
+ `unzip -d docs/wiki wiki.zip`
63
+ File.delete 'wiki.zip'
64
+ puts `find docs/wiki`
65
+ end
66
+
67
+ desc "Removes all cached compiled RubyInline shared objects"
68
+ task :purge_objects do
69
+ `rm -rf ~/.ruby_inline/*`
70
+ end
71
+
72
+ desc "Removes the local copy of the wiki"
73
+ task :rm_wiki do
74
+ `rm -rf wiki.zip docs/wiki/`
75
+ end
76
+
77
+ desc "Prepares Adhearsion for a new release"
78
+ task :prepare_release do
79
+ # Remove log files
80
+ Dir['log/*.log'].each do |f|
81
+ puts "Removing file #{f}"
82
+ File.delete f
83
+ end
84
+
85
+ # Check for unversioned files
86
+ unversioned_files = `svn st | grep '^\?' | awk '{ print $2 }'`
87
+ puts "WARNING: These files are not under version control:\n#{unversioned_files}" unless unversioned_files.empty?
88
+ end
89
+
90
+ desc "Simply prints the Adhearsion version."
91
+ task :version do puts ADHEARSION_VERSION end
92
+
93
+ desc 'Create sample databases per the config/migration.rb and database.yml files.'
94
+ task :migrate do
95
+ require 'config/migration'
96
+ Module.constants.each do |c|
97
+ c = Module.const_get(c)
98
+ c.up if c.respond_to?(:superclass) && c.superclass == ActiveRecord::Migration
99
+ end
100
+ end
@@ -0,0 +1,11 @@
1
+ answer_before_call: true
2
+ hangup_after_call: true
3
+
4
+ info:
5
+ name: Codemecca LLC
6
+ main_number: 409/767-2813
7
+ street_address:
8
+ city:
9
+ state:
10
+ zip:
11
+ country: United States
@@ -0,0 +1,50 @@
1
+ # Place any database customizations here. There are several ways you may desire enabling
2
+ # database connectivity:
3
+ #
4
+ # * Daemons (MySQL/PostgreSQL/etc)
5
+ # |
6
+ # | If you're running sophisticated applications for your VoIP server
7
+ # | and desire performance or integration, you'll want a daemon database
8
+ # | management system. MySQL is recommended.
9
+ # '____________________________________________________________
10
+ # * File-based (Sqlite/Sqlite3)
11
+ # |
12
+ # | If you have little desire to integrate your VoIP application's
13
+ # | user and group data with other apps, sqlite/3 is a very
14
+ # | easy solution to get running.
15
+ # '____________________________________________________________
16
+ # * No database
17
+ # |
18
+ # | If you simply have no use for keeping any information about
19
+ # | users, groups, or anything else, you can leave this entire
20
+ # | file commented. You don't even need ActiveRecord installed.
21
+ # '____________________________________________________________
22
+ #
23
+ ### SETTING UP YOUR DATABASE WITH A SAMPLE SCHEMA
24
+ #
25
+ # If you would like run
26
+ #
27
+ # ActiveRecord resources:
28
+ # * http://slash7.com/cheats/activerecord_cheatsheet.pdf
29
+ #
30
+ #
31
+ # Uncomment the =begin/=end blocks to enable database access.
32
+ #=begin
33
+
34
+ require 'active_record'
35
+ ActiveRecord::Base.logger = Logger.new 'logs/database.log', 10, 1.megabyte
36
+ ActiveRecord::Base.establish_connection YAML.load_file('config/database.yml')
37
+
38
+ # When Adhearsion first encounters this Group class, it will automatically associate it
39
+ # to the "groups" database.
40
+ class Group < ActiveRecord::Base
41
+ has_many :user
42
+ end
43
+
44
+ class User < ActiveRecord::Base
45
+ validates_uniqueness_of :name
46
+ validates_presence_of :name
47
+ belongs_to :group
48
+ end
49
+
50
+ #=end
@@ -0,0 +1,10 @@
1
+ adapter: sqlite3 # can also be "sqlite" for sqlite v2
2
+ dbfile: config/adhearsion.sqlite3
3
+
4
+ #### If you want to use MySQL, use these settings instead:
5
+ # adapter: mysql
6
+ # host: localhost
7
+ # database: adhearsion
8
+ # username: root
9
+ # password:
10
+ # # socket: /var/run/mysqld/mysqld.sock # May not be required
@@ -0,0 +1,43 @@
1
+ enabled: false
2
+ host: 127.0.0.1
3
+ port: 9050
4
+
5
+ deny: all
6
+ allow:
7
+ - 127.0.0.1
8
+ - 192.168.1.*
9
+
10
+ # Set your access control permissions above.
11
+ # Values for deny and allow can be several
12
+ # things:
13
+ #
14
+ # * The 'all' keyword can be used to match
15
+ # everything.
16
+ # * A single IP can be given right after
17
+ # the colon.
18
+ # * A single IP with wildcards can be given
19
+ # * Or, combining all of these, a YAML list
20
+ # can be used to specify many policies.
21
+ # See the comments below for more examples.
22
+ #
23
+ # Also note, the "host" field above may also
24
+ # affect the ability for DRb clients to connect
25
+ # to the server. If you wish you receive
26
+ # connections from the 192.168.1.*, listen on
27
+ # the IP associated with this machine on that
28
+ # subnet.
29
+ #
30
+ #
31
+ # USING YAML LISTS
32
+ #
33
+ # YAML allows lists of items to be created by
34
+ # prepending a hyphen to each list element.
35
+ # These lists can be used to refine your
36
+ # access control list better. Example:
37
+ #
38
+ # deny: all
39
+ # allow:
40
+ # - 192.168.1.123
41
+ # - 192.168.1.99
42
+ # - 66.199.34.44
43
+
@@ -0,0 +1 @@
1
+ enabled: false
@@ -0,0 +1,7 @@
1
+ enabled: false
2
+ host: 192.168.1.2
3
+ username: jicksta
4
+ secret: jicksta
5
+ #port: 5038
6
+ #console: 0
7
+ #event_cache: 100
@@ -0,0 +1 @@
1
+ port: 1337
@@ -0,0 +1,55 @@
1
+ heading "Adhearsion Micromenus Home"
2
+
3
+ # A simple example of a helper used in a Micromenu.
4
+ item oscar_wilde_quote + ' - Oscar Wilde'
5
+
6
+ # Use dial plan logic here in the Micromenu!
7
+ call "Check your voicemail!" do
8
+ check_voicemail
9
+ end
10
+
11
+ # If you have Adhearsion's Asterisk Manager Interface configured
12
+ # properly, you can use the guess_sip_user feature.
13
+ item "My User!" do
14
+ guess = guess_sip_user
15
+ item guess ? guess : "Sorry, you're behind a NAT or on a PC."
16
+ end
17
+
18
+ item "Employee Collaboration" do
19
+ item "View SIP users" do
20
+ sip_users = PBX.sip_users
21
+ items sip_users.collect(&:ip)
22
+ end
23
+
24
+ item "Add someone to the conference" do
25
+ # Originate a call into the user
26
+ end
27
+ item "Have Tweedledum call Tweedledee" do
28
+ x = PBX.rami_client.originate 'Context' => 'internal', 'Exten' => '11', 'Priority' => '1', 'Channel' => 'SIP/tweedledum'
29
+ item x.inspect
30
+ end
31
+ end
32
+
33
+ item 'Adhearsion Server Statistics' do
34
+ item 'View Registered SIP Users' do
35
+ PBX.sip_users.each do |u|
36
+ item %(SIP user "#{u.username}" on IP #{u.ip})
37
+ end
38
+ end
39
+ item 'View System Uptime' do
40
+ item `uptime`
41
+ end
42
+ item 'Network' do
43
+ heading 'Network Interface Info'
44
+ `ifconfig eth1`.each_line do |line|
45
+ item line
46
+ end
47
+ end
48
+ end
49
+
50
+ item 'View Users' do
51
+ item 'Select a user to call below.'
52
+ User.find(:all).each { |user| call user.ivr_extension, user.name }
53
+ end
54
+
55
+ image 'tux'
@@ -0,0 +1,131 @@
1
+ // script.aculo.us builder.js v1.6.5, Wed Nov 08 14:17:49 CET 2006
2
+
3
+ // Copyright (c) 2005, 2006 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
4
+ //
5
+ // script.aculo.us is freely distributable under the terms of an MIT-style license.
6
+ // For details, see the script.aculo.us web site: http://script.aculo.us/
7
+
8
+ var Builder = {
9
+ NODEMAP: {
10
+ AREA: 'map',
11
+ CAPTION: 'table',
12
+ COL: 'table',
13
+ COLGROUP: 'table',
14
+ LEGEND: 'fieldset',
15
+ OPTGROUP: 'select',
16
+ OPTION: 'select',
17
+ PARAM: 'object',
18
+ TBODY: 'table',
19
+ TD: 'table',
20
+ TFOOT: 'table',
21
+ TH: 'table',
22
+ THEAD: 'table',
23
+ TR: 'table'
24
+ },
25
+ // note: For Firefox < 1.5, OPTION and OPTGROUP tags are currently broken,
26
+ // due to a Firefox bug
27
+ node: function(elementName) {
28
+ elementName = elementName.toUpperCase();
29
+
30
+ // try innerHTML approach
31
+ var parentTag = this.NODEMAP[elementName] || 'div';
32
+ var parentElement = document.createElement(parentTag);
33
+ try { // prevent IE "feature": http://dev.rubyonrails.org/ticket/2707
34
+ parentElement.innerHTML = "<" + elementName + "></" + elementName + ">";
35
+ } catch(e) {}
36
+ var element = parentElement.firstChild || null;
37
+
38
+ // see if browser added wrapping tags
39
+ if(element && (element.tagName != elementName))
40
+ element = element.getElementsByTagName(elementName)[0];
41
+
42
+ // fallback to createElement approach
43
+ if(!element) element = document.createElement(elementName);
44
+
45
+ // abort if nothing could be created
46
+ if(!element) return;
47
+
48
+ // attributes (or text)
49
+ if(arguments[1])
50
+ if(this._isStringOrNumber(arguments[1]) ||
51
+ (arguments[1] instanceof Array)) {
52
+ this._children(element, arguments[1]);
53
+ } else {
54
+ var attrs = this._attributes(arguments[1]);
55
+ if(attrs.length) {
56
+ try { // prevent IE "feature": http://dev.rubyonrails.org/ticket/2707
57
+ parentElement.innerHTML = "<" +elementName + " " +
58
+ attrs + "></" + elementName + ">";
59
+ } catch(e) {}
60
+ element = parentElement.firstChild || null;
61
+ // workaround firefox 1.0.X bug
62
+ if(!element) {
63
+ element = document.createElement(elementName);
64
+ for(attr in arguments[1])
65
+ element[attr == 'class' ? 'className' : attr] = arguments[1][attr];
66
+ }
67
+ if(element.tagName != elementName)
68
+ element = parentElement.getElementsByTagName(elementName)[0];
69
+ }
70
+ }
71
+
72
+ // text, or array of children
73
+ if(arguments[2])
74
+ this._children(element, arguments[2]);
75
+
76
+ return element;
77
+ },
78
+ _text: function(text) {
79
+ return document.createTextNode(text);
80
+ },
81
+
82
+ ATTR_MAP: {
83
+ 'className': 'class',
84
+ 'htmlFor': 'for'
85
+ },
86
+
87
+ _attributes: function(attributes) {
88
+ var attrs = [];
89
+ for(attribute in attributes)
90
+ attrs.push((attribute in this.ATTR_MAP ? this.ATTR_MAP[attribute] : attribute) +
91
+ '="' + attributes[attribute].toString().escapeHTML() + '"');
92
+ return attrs.join(" ");
93
+ },
94
+ _children: function(element, children) {
95
+ if(typeof children=='object') { // array can hold nodes and text
96
+ children.flatten().each( function(e) {
97
+ if(typeof e=='object')
98
+ element.appendChild(e)
99
+ else
100
+ if(Builder._isStringOrNumber(e))
101
+ element.appendChild(Builder._text(e));
102
+ });
103
+ } else
104
+ if(Builder._isStringOrNumber(children))
105
+ element.appendChild(Builder._text(children));
106
+ },
107
+ _isStringOrNumber: function(param) {
108
+ return(typeof param=='string' || typeof param=='number');
109
+ },
110
+ build: function(html) {
111
+ var element = this.node('div');
112
+ $(element).update(html.strip());
113
+ return element.down();
114
+ },
115
+ dump: function(scope) {
116
+ if(typeof scope != 'object' && typeof scope != 'function') scope = window; //global scope
117
+
118
+ var tags = ("A ABBR ACRONYM ADDRESS APPLET AREA B BASE BASEFONT BDO BIG BLOCKQUOTE BODY " +
119
+ "BR BUTTON CAPTION CENTER CITE CODE COL COLGROUP DD DEL DFN DIR DIV DL DT EM FIELDSET " +
120
+ "FONT FORM FRAME FRAMESET H1 H2 H3 H4 H5 H6 HEAD HR HTML I IFRAME IMG INPUT INS ISINDEX "+
121
+ "KBD LABEL LEGEND LI LINK MAP MENU META NOFRAMES NOSCRIPT OBJECT OL OPTGROUP OPTION P "+
122
+ "PARAM PRE Q S SAMP SCRIPT SELECT SMALL SPAN STRIKE STRONG STYLE SUB SUP TABLE TBODY TD "+
123
+ "TEXTAREA TFOOT TH THEAD TITLE TR TT U UL VAR").split(/\s+/);
124
+
125
+ tags.each( function(tag){
126
+ scope[tag] = function() {
127
+ return Builder.node.apply(Builder, [tag].concat($A(arguments)));
128
+ }
129
+ });
130
+ }
131
+ }