crazy_ivan 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (133) hide show
  1. data/.gitignore +7 -0
  2. data/LICENSE +20 -0
  3. data/README.rdoc +94 -0
  4. data/Rakefile +92 -0
  5. data/TODO +33 -0
  6. data/VERSION +1 -0
  7. data/bin/crazy_ivan +114 -0
  8. data/crazy_ivan.gemspec +182 -0
  9. data/lib/crazy_ivan.rb +5 -0
  10. data/lib/html_asset_crush.rb +56 -0
  11. data/lib/report_assembler.rb +78 -0
  12. data/lib/test_runner.rb +71 -0
  13. data/templates/css/ci.css +11 -0
  14. data/templates/index.html +105 -0
  15. data/templates/javascript/json-template.js +544 -0
  16. data/templates/javascript/prototype.js +4917 -0
  17. data/test/crazy_ivan_test.rb +4 -0
  18. data/test/test_helper.rb +9 -0
  19. data/vendor/json-1.1.7/CHANGES +119 -0
  20. data/vendor/json-1.1.7/GPL +340 -0
  21. data/vendor/json-1.1.7/README +78 -0
  22. data/vendor/json-1.1.7/RUBY +58 -0
  23. data/vendor/json-1.1.7/Rakefile +270 -0
  24. data/vendor/json-1.1.7/TODO +1 -0
  25. data/vendor/json-1.1.7/VERSION +1 -0
  26. data/vendor/json-1.1.7/benchmarks/data-p4-3GHz-ruby18/GeneratorBenchmarkComparison.log +52 -0
  27. data/vendor/json-1.1.7/benchmarks/data-p4-3GHz-ruby18/GeneratorBenchmarkExt#generator_fast-autocorrelation.dat +1000 -0
  28. data/vendor/json-1.1.7/benchmarks/data-p4-3GHz-ruby18/GeneratorBenchmarkExt#generator_fast.dat +1001 -0
  29. data/vendor/json-1.1.7/benchmarks/data-p4-3GHz-ruby18/GeneratorBenchmarkExt#generator_pretty-autocorrelation.dat +900 -0
  30. data/vendor/json-1.1.7/benchmarks/data-p4-3GHz-ruby18/GeneratorBenchmarkExt#generator_pretty.dat +901 -0
  31. data/vendor/json-1.1.7/benchmarks/data-p4-3GHz-ruby18/GeneratorBenchmarkExt#generator_safe-autocorrelation.dat +1000 -0
  32. data/vendor/json-1.1.7/benchmarks/data-p4-3GHz-ruby18/GeneratorBenchmarkExt#generator_safe.dat +1001 -0
  33. data/vendor/json-1.1.7/benchmarks/data-p4-3GHz-ruby18/GeneratorBenchmarkExt.log +261 -0
  34. data/vendor/json-1.1.7/benchmarks/data-p4-3GHz-ruby18/GeneratorBenchmarkPure#generator_fast-autocorrelation.dat +1000 -0
  35. data/vendor/json-1.1.7/benchmarks/data-p4-3GHz-ruby18/GeneratorBenchmarkPure#generator_fast.dat +1001 -0
  36. data/vendor/json-1.1.7/benchmarks/data-p4-3GHz-ruby18/GeneratorBenchmarkPure#generator_pretty-autocorrelation.dat +1000 -0
  37. data/vendor/json-1.1.7/benchmarks/data-p4-3GHz-ruby18/GeneratorBenchmarkPure#generator_pretty.dat +1001 -0
  38. data/vendor/json-1.1.7/benchmarks/data-p4-3GHz-ruby18/GeneratorBenchmarkPure#generator_safe-autocorrelation.dat +1000 -0
  39. data/vendor/json-1.1.7/benchmarks/data-p4-3GHz-ruby18/GeneratorBenchmarkPure#generator_safe.dat +1001 -0
  40. data/vendor/json-1.1.7/benchmarks/data-p4-3GHz-ruby18/GeneratorBenchmarkPure.log +262 -0
  41. data/vendor/json-1.1.7/benchmarks/data-p4-3GHz-ruby18/GeneratorBenchmarkRails#generator-autocorrelation.dat +1000 -0
  42. data/vendor/json-1.1.7/benchmarks/data-p4-3GHz-ruby18/GeneratorBenchmarkRails#generator.dat +1001 -0
  43. data/vendor/json-1.1.7/benchmarks/data-p4-3GHz-ruby18/GeneratorBenchmarkRails.log +82 -0
  44. data/vendor/json-1.1.7/benchmarks/data-p4-3GHz-ruby18/ParserBenchmarkComparison.log +34 -0
  45. data/vendor/json-1.1.7/benchmarks/data-p4-3GHz-ruby18/ParserBenchmarkExt#parser-autocorrelation.dat +900 -0
  46. data/vendor/json-1.1.7/benchmarks/data-p4-3GHz-ruby18/ParserBenchmarkExt#parser.dat +901 -0
  47. data/vendor/json-1.1.7/benchmarks/data-p4-3GHz-ruby18/ParserBenchmarkExt.log +81 -0
  48. data/vendor/json-1.1.7/benchmarks/data-p4-3GHz-ruby18/ParserBenchmarkPure#parser-autocorrelation.dat +1000 -0
  49. data/vendor/json-1.1.7/benchmarks/data-p4-3GHz-ruby18/ParserBenchmarkPure#parser.dat +1001 -0
  50. data/vendor/json-1.1.7/benchmarks/data-p4-3GHz-ruby18/ParserBenchmarkPure.log +82 -0
  51. data/vendor/json-1.1.7/benchmarks/data-p4-3GHz-ruby18/ParserBenchmarkRails#parser-autocorrelation.dat +1000 -0
  52. data/vendor/json-1.1.7/benchmarks/data-p4-3GHz-ruby18/ParserBenchmarkRails#parser.dat +1001 -0
  53. data/vendor/json-1.1.7/benchmarks/data-p4-3GHz-ruby18/ParserBenchmarkRails.log +82 -0
  54. data/vendor/json-1.1.7/benchmarks/data-p4-3GHz-ruby18/ParserBenchmarkYAML#parser-autocorrelation.dat +1000 -0
  55. data/vendor/json-1.1.7/benchmarks/data-p4-3GHz-ruby18/ParserBenchmarkYAML#parser.dat +1001 -0
  56. data/vendor/json-1.1.7/benchmarks/data-p4-3GHz-ruby18/ParserBenchmarkYAML.log +82 -0
  57. data/vendor/json-1.1.7/benchmarks/generator_benchmark.rb +165 -0
  58. data/vendor/json-1.1.7/benchmarks/parser_benchmark.rb +197 -0
  59. data/vendor/json-1.1.7/bin/edit_json.rb +9 -0
  60. data/vendor/json-1.1.7/bin/prettify_json.rb +75 -0
  61. data/vendor/json-1.1.7/data/example.json +1 -0
  62. data/vendor/json-1.1.7/data/index.html +38 -0
  63. data/vendor/json-1.1.7/data/prototype.js +4184 -0
  64. data/vendor/json-1.1.7/doc-templates/main.txt +283 -0
  65. data/vendor/json-1.1.7/ext/json/ext/generator/extconf.rb +11 -0
  66. data/vendor/json-1.1.7/ext/json/ext/generator/generator.c +919 -0
  67. data/vendor/json-1.1.7/ext/json/ext/generator/unicode.c +182 -0
  68. data/vendor/json-1.1.7/ext/json/ext/generator/unicode.h +53 -0
  69. data/vendor/json-1.1.7/ext/json/ext/parser/extconf.rb +11 -0
  70. data/vendor/json-1.1.7/ext/json/ext/parser/parser.c +1829 -0
  71. data/vendor/json-1.1.7/ext/json/ext/parser/parser.rl +686 -0
  72. data/vendor/json-1.1.7/ext/json/ext/parser/unicode.c +154 -0
  73. data/vendor/json-1.1.7/ext/json/ext/parser/unicode.h +58 -0
  74. data/vendor/json-1.1.7/install.rb +26 -0
  75. data/vendor/json-1.1.7/lib/json.rb +10 -0
  76. data/vendor/json-1.1.7/lib/json/Array.xpm +21 -0
  77. data/vendor/json-1.1.7/lib/json/FalseClass.xpm +21 -0
  78. data/vendor/json-1.1.7/lib/json/Hash.xpm +21 -0
  79. data/vendor/json-1.1.7/lib/json/Key.xpm +73 -0
  80. data/vendor/json-1.1.7/lib/json/NilClass.xpm +21 -0
  81. data/vendor/json-1.1.7/lib/json/Numeric.xpm +28 -0
  82. data/vendor/json-1.1.7/lib/json/String.xpm +96 -0
  83. data/vendor/json-1.1.7/lib/json/TrueClass.xpm +21 -0
  84. data/vendor/json-1.1.7/lib/json/add/core.rb +135 -0
  85. data/vendor/json-1.1.7/lib/json/add/rails.rb +58 -0
  86. data/vendor/json-1.1.7/lib/json/common.rb +354 -0
  87. data/vendor/json-1.1.7/lib/json/editor.rb +1371 -0
  88. data/vendor/json-1.1.7/lib/json/ext.rb +15 -0
  89. data/vendor/json-1.1.7/lib/json/json.xpm +1499 -0
  90. data/vendor/json-1.1.7/lib/json/pure.rb +77 -0
  91. data/vendor/json-1.1.7/lib/json/pure/generator.rb +430 -0
  92. data/vendor/json-1.1.7/lib/json/pure/parser.rb +269 -0
  93. data/vendor/json-1.1.7/lib/json/version.rb +8 -0
  94. data/vendor/json-1.1.7/tests/fixtures/fail1.json +1 -0
  95. data/vendor/json-1.1.7/tests/fixtures/fail10.json +1 -0
  96. data/vendor/json-1.1.7/tests/fixtures/fail11.json +1 -0
  97. data/vendor/json-1.1.7/tests/fixtures/fail12.json +1 -0
  98. data/vendor/json-1.1.7/tests/fixtures/fail13.json +1 -0
  99. data/vendor/json-1.1.7/tests/fixtures/fail14.json +1 -0
  100. data/vendor/json-1.1.7/tests/fixtures/fail18.json +1 -0
  101. data/vendor/json-1.1.7/tests/fixtures/fail19.json +1 -0
  102. data/vendor/json-1.1.7/tests/fixtures/fail2.json +1 -0
  103. data/vendor/json-1.1.7/tests/fixtures/fail20.json +1 -0
  104. data/vendor/json-1.1.7/tests/fixtures/fail21.json +1 -0
  105. data/vendor/json-1.1.7/tests/fixtures/fail22.json +1 -0
  106. data/vendor/json-1.1.7/tests/fixtures/fail23.json +1 -0
  107. data/vendor/json-1.1.7/tests/fixtures/fail24.json +1 -0
  108. data/vendor/json-1.1.7/tests/fixtures/fail25.json +1 -0
  109. data/vendor/json-1.1.7/tests/fixtures/fail27.json +2 -0
  110. data/vendor/json-1.1.7/tests/fixtures/fail28.json +2 -0
  111. data/vendor/json-1.1.7/tests/fixtures/fail3.json +1 -0
  112. data/vendor/json-1.1.7/tests/fixtures/fail4.json +1 -0
  113. data/vendor/json-1.1.7/tests/fixtures/fail5.json +1 -0
  114. data/vendor/json-1.1.7/tests/fixtures/fail6.json +1 -0
  115. data/vendor/json-1.1.7/tests/fixtures/fail7.json +1 -0
  116. data/vendor/json-1.1.7/tests/fixtures/fail8.json +1 -0
  117. data/vendor/json-1.1.7/tests/fixtures/fail9.json +1 -0
  118. data/vendor/json-1.1.7/tests/fixtures/pass1.json +56 -0
  119. data/vendor/json-1.1.7/tests/fixtures/pass15.json +1 -0
  120. data/vendor/json-1.1.7/tests/fixtures/pass16.json +1 -0
  121. data/vendor/json-1.1.7/tests/fixtures/pass17.json +1 -0
  122. data/vendor/json-1.1.7/tests/fixtures/pass2.json +1 -0
  123. data/vendor/json-1.1.7/tests/fixtures/pass26.json +1 -0
  124. data/vendor/json-1.1.7/tests/fixtures/pass3.json +6 -0
  125. data/vendor/json-1.1.7/tests/test_json.rb +312 -0
  126. data/vendor/json-1.1.7/tests/test_json_addition.rb +164 -0
  127. data/vendor/json-1.1.7/tests/test_json_fixtures.rb +34 -0
  128. data/vendor/json-1.1.7/tests/test_json_generate.rb +106 -0
  129. data/vendor/json-1.1.7/tests/test_json_rails.rb +146 -0
  130. data/vendor/json-1.1.7/tests/test_json_unicode.rb +62 -0
  131. data/vendor/json-1.1.7/tools/fuzz.rb +139 -0
  132. data/vendor/json-1.1.7/tools/server.rb +61 -0
  133. metadata +196 -0
data/lib/crazy_ivan.rb ADDED
@@ -0,0 +1,5 @@
1
+ require 'fileutils'
2
+ require 'yaml'
3
+ require File.join(File.dirname(__FILE__), 'report_assembler')
4
+ require File.join(File.dirname(__FILE__), 'test_runner')
5
+ require File.join(File.dirname(__FILE__), 'html_asset_crush')
@@ -0,0 +1,56 @@
1
+ # Parses an html file's <head> section,
2
+ # looks for <script ... > and <link rel="stylesheet" ... >
3
+ # and crunches them all together into one file
4
+ #
5
+ # Warning: this script is super-ghetto and probably not robust.
6
+ # If you're concerned, use a real lexer/parser.
7
+
8
+ require 'strscan'
9
+
10
+ module HtmlAssetCrush
11
+ def self.source_for(asset_path)
12
+ case asset_path
13
+ when /js$/
14
+ <<-JS
15
+ <script type="text/javascript" charset="utf-8">
16
+ #{File.open(asset_path).read}
17
+ </script>
18
+ JS
19
+ when /css$/
20
+ <<-CSS
21
+ <style type="text/css">
22
+ #{File.open(asset_path).read}
23
+ </style>
24
+ CSS
25
+ end
26
+ rescue Errno::ENOENT
27
+ raise "Could not find #{asset_path} to bring in"
28
+ end
29
+
30
+ def self.crush(html_filepath)
31
+ Dir.chdir(File.dirname(html_filepath)) do
32
+ html = File.open(html_filepath).read
33
+ crushed_html = ""
34
+
35
+ s = StringScanner.new(html)
36
+
37
+ js = /<script.+? src=['"](.+)['"].+?\/script>/
38
+ css = /<link .+? href=['"](.+?)['"].+?>/
39
+ asset = Regexp.union(js, css)
40
+
41
+ while result = s.scan_until(asset) do
42
+ asset_path = s[2] || s[1]
43
+
44
+ # Weird that pre_match doesn't do this
45
+ # crushed_html << s.pre_match
46
+ crushed_html << result[0...(-s.matched_size)]
47
+
48
+ crushed_html << source_for(asset_path) + "\n"
49
+ end
50
+
51
+ crushed_html << s.rest
52
+
53
+ return crushed_html
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,78 @@
1
+ class ReportAssembler
2
+ MAXIMUM_RECENTS = 10
3
+ ROOT_PATH = File.expand_path(File.dirname(__FILE__))
4
+ TEMPLATES_PATH = File.join(ROOT_PATH, *%w[.. templates])
5
+
6
+ attr_accessor :test_results
7
+
8
+ def initialize(output_directory)
9
+ @test_results = []
10
+ @output_directory = output_directory
11
+ end
12
+
13
+ def generate
14
+ Dir.chdir(@output_directory) do
15
+ @test_results.each do |result|
16
+ update_project(result)
17
+ end
18
+
19
+ update_projects
20
+ update_index
21
+ end
22
+ end
23
+
24
+ def version(string)
25
+ string[0..240]
26
+ end
27
+
28
+ def update_project(result)
29
+ FileUtils.mkdir_p(result.project_name)
30
+ Dir.chdir(result.project_name) do
31
+ File.open("#{version(result.version_output)}.json", 'w+') do |f|
32
+ f.puts({
33
+ "version" => result.version_output,
34
+ "timestamp" => result.timestamp,
35
+ "update" => result.update_output,
36
+ "update_error" => result.update_error,
37
+ "test" => result.test_output,
38
+ "test_error" => result.test_error
39
+ }.to_json)
40
+ end
41
+
42
+ update_recent(result)
43
+ end
44
+ end
45
+
46
+ def update_recent(result)
47
+ recent_versions_json = File.open('recent.json', File::RDWR|File::CREAT).read
48
+
49
+ recent_versions = []
50
+
51
+ if !recent_versions_json.empty?
52
+ recent_versions = JSON.parse(recent_versions_json)["recent_versions"]
53
+ end
54
+
55
+ recent_versions << version(result.version_output)
56
+ recent_versions.shift if recent_versions.size > MAXIMUM_RECENTS
57
+
58
+ File.open('recent.json', 'w+') do |f|
59
+ f.print "{\"recent_versions\": [#{recent_versions.map {|v| "\"#{v}\""}.join(', ')}]}"
60
+ end
61
+ end
62
+
63
+ def update_projects
64
+ projects = @test_results.map {|r| "\"#{r.project_name}\""}
65
+
66
+ File.open('projects.json', 'w+') do |f|
67
+ f.print "{\"projects\": [#{projects.join(', ')}]}"
68
+ end
69
+ end
70
+
71
+ def update_index
72
+ index_template = HtmlAssetCrush.crush(File.join(TEMPLATES_PATH, "index.html"))
73
+
74
+ File.open('index.html', 'w+') do |f|
75
+ f.print index_template
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,71 @@
1
+ require 'open3'
2
+
3
+ class TestRunner
4
+
5
+ class Result < Struct.new(:project_name, :update_output, :version_output, :test_output, :update_error, :test_error, :timestamp)
6
+ end
7
+
8
+ def initialize(project_path)
9
+ @project_path = project_path
10
+ end
11
+
12
+ def valid?
13
+ check_script('update')
14
+ check_script('version')
15
+ check_script('test')
16
+ return true
17
+ end
18
+
19
+ def script_path(name)
20
+ script_path = File.join('.ci', name)
21
+ end
22
+
23
+ def check_script(name)
24
+ script_path = script_path(name)
25
+
26
+ Dir.chdir(@project_path) do
27
+ if File.exists?(script_path)
28
+ if !File.stat(script_path).executable?
29
+ abort "#{@project_path}/.ci/#{name} script not executable"
30
+ elsif File.open(script_path).read.empty?
31
+ abort "#{@project_path}/.ci/#{name} script empty"
32
+ end
33
+ else
34
+ abort "#{@project_path}/.ci/#{name} script missing"
35
+ end
36
+ end
37
+ end
38
+
39
+ def run_script(name)
40
+ output = ''
41
+ error = ''
42
+
43
+ Dir.chdir(@project_path) do
44
+ Open3.popen3(script_path(name)) do |stdin, stdout, stderr|
45
+ stdin.close # Close to prevent hanging if the script wants input
46
+ output = stdout.read
47
+ error = stderr.read
48
+ end
49
+ end
50
+
51
+ return output.chomp, error.chomp
52
+ end
53
+
54
+ def invoke
55
+ if valid?
56
+ project_name = @project_path.split(File::SEPARATOR).last
57
+ results = Result.new(project_name)
58
+
59
+ results.version_output = run_script('version').join
60
+ results.update_output, results.update_error = run_script('update')
61
+
62
+ if results.update_error.empty?
63
+ results.test_output, results.test_error = run_script('test')
64
+ end
65
+
66
+ results.timestamp = Time.now
67
+
68
+ return results
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,11 @@
1
+ body {
2
+ margin: 2.5em 3em;
3
+ padding: 0;
4
+ background: #fff;
5
+ color: #333;
6
+ font: 100%/1.5 Calibri, "Helvetica Neue", Helvetica, Arial, sans-serif;
7
+ }
8
+
9
+ td {
10
+ vertical-align: top;
11
+ }
@@ -0,0 +1,105 @@
1
+ <html>
2
+ <head>
3
+ <meta http-equiv="Content-type" content="text/html; charset=utf-8">
4
+ <title>Crazy Ivan: CI straight up.</title>
5
+
6
+ <link rel="stylesheet" href="css/ci.css" type="text/css" charset="utf-8">
7
+ <script type="text/javascript" src="javascript/prototype.js"></script>
8
+ <script type="text/javascript" src="javascript/json-template.js"></script>
9
+ <script type="text/javascript">
10
+ var init = function() {
11
+ render('index.jsont', projects());
12
+ }
13
+
14
+ var projects = function() {
15
+ var projects = [];
16
+ var project_names = [];
17
+
18
+ new Ajax.Request('projects.json', {
19
+ asynchronous: false,
20
+ onSuccess: function(transport) {
21
+ project_names = transport.responseText.evalJSON().projects;
22
+ }
23
+ });
24
+
25
+ project_names.each(function(project_name) {
26
+ var reports = [];
27
+
28
+ reports = recent_versions(project_name).map(function(version) {
29
+ return version_test_report(project_name, version);
30
+ })
31
+
32
+ project = {name: project_name, reports: reports};
33
+ projects.push(project);
34
+ })
35
+
36
+ return {"projects": projects};
37
+ }
38
+
39
+ var recent_versions = function(project) {
40
+ var recent_versions = [];
41
+ new Ajax.Request(project + '/recent.json', {
42
+ asynchronous: false,
43
+ onSuccess: function(transport) {
44
+ recent_versions = transport.responseText.evalJSON().recent_versions;
45
+ }
46
+ });
47
+ return recent_versions;
48
+ }
49
+
50
+ var version_test_report = function(project, version) {
51
+ var report = {};
52
+ url = project + '/' + version + '.json';
53
+
54
+ new Ajax.Request(url, {
55
+ asynchronous: false,
56
+ onSuccess: function(transport) {
57
+ report = transport.responseText.evalJSON();
58
+ }
59
+ });
60
+ return report;
61
+ }
62
+
63
+ var render = function(template_name, json) {
64
+ var template = jsontemplate.Template(" \
65
+ {.section projects} \
66
+ {.repeated section @} \
67
+ <h2>{name}</h2> \
68
+ {.section reports} \
69
+ <table> \
70
+ <tr> \
71
+ <th>Version</th> \
72
+ <th>Update Result</th> \
73
+ <th>Update Errors</th> \
74
+ <th>Test Result</th> \
75
+ <th>Test Errors</th> \
76
+ </tr> \
77
+ {.repeated section @} \
78
+ <tr> \
79
+ <td>{timestamp}</td> \
80
+ <td>{version}</td> \
81
+ <td>{update}</td> \
82
+ <td>{update_error}</td> \
83
+ <td>{test}</td> \
84
+ <td>{test_error}</td> \
85
+ </tr> \
86
+ {.end} \
87
+ </table> \
88
+ {.or} \
89
+ <p>No test reports found. Please run the `ci` executable.</p> \
90
+ {.end} \
91
+ {.end} \
92
+ {.or} \
93
+ <p>No projects found.</p> \
94
+ {.end} \
95
+ ");
96
+
97
+ var html = template.expand(json);
98
+ $("replace").update(html);
99
+ }
100
+ </script>
101
+ </head>
102
+ <body onload="init();">
103
+ <div id="replace"></div>
104
+ </body>
105
+ </html>
@@ -0,0 +1,544 @@
1
+ // Copyright (C) 2009 Andy Chu
2
+ //
3
+ // Licensed under the Apache License, Version 2.0 (the "License");
4
+ // you may not use this file except in compliance with the License.
5
+ // You may obtain a copy of the License at
6
+ //
7
+ // http://www.apache.org/licenses/LICENSE-2.0
8
+ //
9
+ // Unless required by applicable law or agreed to in writing, software
10
+ // distributed under the License is distributed on an "AS IS" BASIS,
11
+ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ // See the License for the specific language governing permissions and
13
+ // limitations under the License.
14
+
15
+ // $Id: json-template.js 271 2009-05-31 19:35:43Z andy@chubot.org $
16
+
17
+ //
18
+ // JavaScript implementation of json-template.
19
+ //
20
+
21
+ // This is predefined in tests, shouldn't be defined anywhere else. TODO: Do
22
+ // something nicer.
23
+ var log = log || function() {};
24
+ var repr = repr || function() {};
25
+
26
+
27
+ // The "module" exported by this script is called "jsontemplate":
28
+
29
+ var jsontemplate = function() {
30
+
31
+
32
+ // Regex escaping for common metacharacters (note that JavaScript needs 2 \\ --
33
+ // no raw strings!
34
+ var META_ESCAPE = {
35
+ '{': '\\{',
36
+ '}': '\\}',
37
+ '{{': '\\{\\{',
38
+ '}}': '\\}\\}',
39
+ '[': '\\[',
40
+ ']': '\\]'
41
+ };
42
+
43
+ function _MakeTokenRegex(meta_left, meta_right) {
44
+ // TODO: check errors
45
+ return new RegExp(
46
+ '(' +
47
+ META_ESCAPE[meta_left] +
48
+ '.+?' +
49
+ META_ESCAPE[meta_right] +
50
+ '\n?)', 'g'); // global for use with .exec()
51
+ }
52
+
53
+ //
54
+ // Formatters
55
+ //
56
+
57
+ function HtmlEscape(s) {
58
+ return s.replace(/&/g,'&amp;').
59
+ replace(/>/g,'&gt;').
60
+ replace(/</g,'&lt;');
61
+ }
62
+
63
+ function HtmlTagEscape(s) {
64
+ return s.replace(/&/g,'&amp;').
65
+ replace(/>/g,'&gt;').
66
+ replace(/</g,'&lt;').
67
+ replace(/"/g,'&quot;');
68
+ }
69
+
70
+ // Default ToString can be changed
71
+ function ToString(s) {
72
+ return s.toString();
73
+ }
74
+
75
+ var DEFAULT_FORMATTERS = {
76
+ 'html': HtmlEscape,
77
+ 'htmltag': HtmlTagEscape,
78
+ 'html-attr-value': HtmlTagEscape,
79
+ 'str': ToString,
80
+ 'raw': function(x) {return x;}
81
+ };
82
+
83
+
84
+ //
85
+ // Template implementation
86
+ //
87
+
88
+ function _ScopedContext(context, undefined_str) {
89
+ // The stack contains:
90
+ // The current context (an object).
91
+ // An iteration index. -1 means we're NOT iterating.
92
+ var stack = [{context: context, index: -1}];
93
+
94
+ return {
95
+ PushSection: function(name) {
96
+ log('PushSection '+name);
97
+ if (name === undefined || name === null) {
98
+ return null;
99
+ }
100
+ var new_context = stack[stack.length-1].context[name] || null;
101
+ stack.push({context: new_context, index: -1});
102
+ return new_context;
103
+ },
104
+
105
+ Pop: function() {
106
+ stack.pop();
107
+ },
108
+
109
+ next: function() {
110
+ var stacktop = stack[stack.length-1];
111
+
112
+ // Now we're iterating -- push a new mutable object onto the stack
113
+ if (stacktop.index == -1) {
114
+ stacktop = {context: null, index: 0};
115
+ stack.push(stacktop);
116
+ }
117
+
118
+ // The thing we're iterating over
119
+ var context_array = stack[stack.length - 2].context;
120
+
121
+ // We're already done
122
+ if (stacktop.index == context_array.length) {
123
+ stack.pop();
124
+ log('next: null');
125
+ return null; // sentinel to say that we're done
126
+ }
127
+
128
+ log('next: ' + stacktop.index);
129
+
130
+ stacktop.context = context_array[stacktop.index++];
131
+
132
+ log('next: true');
133
+ return true; // OK, we mutated the stack
134
+ },
135
+
136
+ CursorValue: function() {
137
+ return stack[stack.length - 1].context;
138
+ },
139
+
140
+ _Undefined: function(name) {
141
+ if (undefined_str === undefined) {
142
+ throw {
143
+ name: 'UndefinedVariable', message: name + ' is not defined'
144
+ };
145
+ } else {
146
+ return undefined_str;
147
+ }
148
+ },
149
+
150
+ _LookUpStack: function(name) {
151
+ var i = stack.length - 1;
152
+ while (true) {
153
+ var context = stack[i].context;
154
+ log('context '+repr(context));
155
+
156
+ if (typeof context !== 'object') {
157
+ i--;
158
+ } else {
159
+ var value = context[name];
160
+ if (value === undefined || value === null) {
161
+ i--;
162
+ } else {
163
+ return value;
164
+ }
165
+ }
166
+ if (i <= -1) {
167
+ return this._Undefined(name);
168
+ }
169
+ }
170
+ },
171
+
172
+ Lookup: function(name) {
173
+ var parts = name.split('.');
174
+ var value = this._LookUpStack(parts[0]);
175
+ if (parts.length > 1) {
176
+ for (var i=1; i<parts.length; i++) {
177
+ value = value[parts[i]];
178
+ if (value === undefined) {
179
+ return this._Undefined(parts[i]);
180
+ }
181
+ }
182
+ }
183
+ return value;
184
+ }
185
+
186
+ };
187
+ }
188
+
189
+
190
+ function _Section(section_name) {
191
+ var current_clause = [];
192
+ var statements = {'default': current_clause};
193
+
194
+ return {
195
+ section_name: section_name, // public attribute
196
+
197
+ Statements: function(clause) {
198
+ clause = clause || 'default';
199
+ return statements[clause] || [];
200
+ },
201
+
202
+ NewClause: function(clause_name) {
203
+ var new_clause = [];
204
+ statements[clause_name] = new_clause;
205
+ current_clause = new_clause;
206
+ },
207
+
208
+ Append: function(statement) {
209
+ current_clause.push(statement);
210
+ }
211
+ };
212
+ }
213
+
214
+
215
+ function _Execute(statements, context, callback) {
216
+ var i;
217
+ for (i=0; i<statements.length; i++) {
218
+ statement = statements[i];
219
+
220
+ //log('Executing ' + statement);
221
+
222
+ if (typeof(statement) == 'string') {
223
+ callback(statement);
224
+ } else {
225
+ var func = statement[0];
226
+ var args = statement[1];
227
+ func(args, context, callback);
228
+ }
229
+ }
230
+ }
231
+
232
+
233
+ function _DoSubstitute(statement, context, callback) {
234
+ log('Substituting: '+ statement.name);
235
+ var value;
236
+ if (statement.name == '@') {
237
+ value = context.CursorValue();
238
+ } else {
239
+ value = context.Lookup(statement.name);
240
+ }
241
+
242
+ // Format values
243
+ for (i=0; i<statement.formatters.length; i++) {
244
+ value = statement.formatters[i](value);
245
+ }
246
+
247
+ callback(value);
248
+ }
249
+
250
+
251
+ // for [section foo]
252
+ function _DoSection(args, context, callback) {
253
+
254
+ var block = args;
255
+ var value = context.PushSection(block.section_name);
256
+ var do_section = false;
257
+
258
+ // "truthy" values should have their sections executed.
259
+ if (value) {
260
+ do_section = true;
261
+ }
262
+ // Except: if the value is a zero-length array (which is "truthy")
263
+ if (value && value.length === 0) {
264
+ do_section = false;
265
+ }
266
+
267
+ if (do_section) {
268
+ _Execute(block.Statements(), context, callback);
269
+ context.Pop();
270
+ } else { // Empty list, None, False, etc.
271
+ context.Pop();
272
+ _Execute(block.Statements('or'), context, callback);
273
+ }
274
+ }
275
+
276
+
277
+ function _DoRepeatedSection(args, context, callback) {
278
+ var block = args;
279
+ var pushed;
280
+
281
+ if (block.section_name == '@') {
282
+ // If the name is @, we stay in the enclosing context, but assume it's a
283
+ // list, and repeat this block many times.
284
+ items = context.CursorValue();
285
+ // TODO: check that items is an array; apparently this is hard in JavaScript
286
+ //if type(items) is not list:
287
+ // raise EvaluationError('Expected a list; got %s' % type(items))
288
+ pushed = false;
289
+ } else {
290
+ items = context.PushSection(block.section_name);
291
+ pushed = true;
292
+ }
293
+
294
+ //log('ITEMS: '+showArray(items));
295
+ if (items && items.length > 0) {
296
+ // Execute the statements in the block for every item in the list.
297
+ // Execute the alternate block on every iteration except the last. Each
298
+ // item could be an atom (string, integer, etc.) or a dictionary.
299
+
300
+ var last_index = items.length - 1;
301
+ var statements = block.Statements();
302
+ var alt_statements = block.Statements('alternate');
303
+
304
+ for (var i=0; context.next() !== null; i++) {
305
+ log('_DoRepeatedSection i: ' +i);
306
+ _Execute(statements, context, callback);
307
+ if (i != last_index) {
308
+ log('ALTERNATE');
309
+ _Execute(alt_statements, context, callback);
310
+ }
311
+ }
312
+ } else {
313
+ log('OR: '+block.Statements('or'));
314
+ _Execute(block.Statements('or'), context, callback);
315
+ }
316
+
317
+ if (pushed) {
318
+ context.Pop();
319
+ }
320
+ }
321
+
322
+
323
+ var _SECTION_RE = /(repeated)?\s*(section)\s+(\S+)?/;
324
+
325
+
326
+ // TODO: The compile function could be in a different module, in case we want to
327
+ // compile on the server side.
328
+ function _Compile(template_str, options) {
329
+ var more_formatters = options.more_formatters ||
330
+ function (x) { return null; };
331
+
332
+ // We want to allow an explicit null value for default_formatter, which means
333
+ // that an error is raised if no formatter is specified.
334
+ var default_formatter;
335
+ if (options.default_formatter === undefined) {
336
+ default_formatter = 'str';
337
+ } else {
338
+ default_formatter = options.default_formatter;
339
+ }
340
+
341
+ function GetFormatter(format_str) {
342
+ var formatter = more_formatters(format_str) ||
343
+ DEFAULT_FORMATTERS[format_str];
344
+ if (formatter === undefined) {
345
+ throw {
346
+ name: 'BadFormatter',
347
+ message: format_str + ' is not a valid formatter'
348
+ };
349
+ }
350
+ return formatter;
351
+ }
352
+
353
+ var format_char = options.format_char || '|';
354
+ if (format_char != ':' && format_char != '|') {
355
+ throw {
356
+ name: 'ConfigurationError',
357
+ message: 'Only format characters : and | are accepted'
358
+ };
359
+ }
360
+
361
+ var meta = options.meta || '{}';
362
+ var n = meta.length;
363
+ if (n % 2 == 1) {
364
+ throw {
365
+ name: 'ConfigurationError',
366
+ message: meta + ' has an odd number of metacharacters'
367
+ };
368
+ }
369
+ var meta_left = meta.substring(0, n/2);
370
+ var meta_right = meta.substring(n/2, n);
371
+
372
+ var token_re = _MakeTokenRegex(meta_left, meta_right);
373
+ var current_block = _Section();
374
+ var stack = [current_block];
375
+
376
+ var strip_num = meta_left.length; // assume they're the same length
377
+
378
+ var token_match;
379
+ var last_index = 0;
380
+
381
+ while (true) {
382
+ token_match = token_re.exec(template_str);
383
+ log('match:', token_match);
384
+ if (token_match === null) {
385
+ break;
386
+ } else {
387
+ var token = token_match[0];
388
+ }
389
+ log('last_index: '+ last_index);
390
+ log('token_match.index: '+ token_match.index);
391
+
392
+ // Add the previous literal to the program
393
+ if (token_match.index > last_index) {
394
+ var tok = template_str.slice(last_index, token_match.index);
395
+ current_block.Append(tok);
396
+ log('tok: "'+ tok+'"');
397
+ }
398
+ last_index = token_re.lastIndex;
399
+
400
+ log('token0: "'+ token+'"');
401
+
402
+ var had_newline = false;
403
+ if (token.slice(-1) == '\n') {
404
+ token = token.slice(null, -1);
405
+ had_newline = true;
406
+ }
407
+
408
+ token = token.slice(strip_num, -strip_num);
409
+
410
+ if (token.charAt(0) == '#') {
411
+ continue; // comment
412
+ }
413
+
414
+ if (token.charAt(0) == '.') { // Keyword
415
+ token = token.substring(1, token.length);
416
+
417
+ var literal = {
418
+ 'meta-left': meta_left,
419
+ 'meta-right': meta_right,
420
+ 'space': ' ',
421
+ 'tab': '\t',
422
+ 'newline': '\n'
423
+ }[token];
424
+
425
+ if (literal !== undefined) {
426
+ current_block.Append(literal);
427
+ continue;
428
+ }
429
+
430
+ var section_match = token.match(_SECTION_RE);
431
+
432
+ if (section_match) {
433
+ var repeated = section_match[1];
434
+ var section_name = section_match[3];
435
+ var func = repeated ? _DoRepeatedSection : _DoSection;
436
+ log('repeated ' + repeated + ' section_name ' + section_name);
437
+
438
+ var new_block = _Section(section_name);
439
+ current_block.Append([func, new_block]);
440
+ stack.push(new_block);
441
+ current_block = new_block;
442
+ continue;
443
+ }
444
+
445
+ if (token == 'alternates with') {
446
+ current_block.NewClause('alternate');
447
+ continue;
448
+ }
449
+
450
+ if (token == 'or') {
451
+ current_block.NewClause('or');
452
+ continue;
453
+ }
454
+
455
+ if (token == 'end') {
456
+ // End the block
457
+ stack.pop();
458
+ if (stack.length > 0) {
459
+ current_block = stack[stack.length-1];
460
+ } else {
461
+ throw {
462
+ name: 'TemplateSyntaxError',
463
+ message: 'Got too many {end} statements'
464
+ };
465
+ }
466
+ continue;
467
+ }
468
+ }
469
+
470
+ // A variable substitution
471
+ var parts = token.split(format_char);
472
+ var formatters;
473
+ var name;
474
+ if (parts.length == 1) {
475
+ if (default_formatter === null) {
476
+ throw {
477
+ name: 'MissingFormatter',
478
+ message: 'This template requires explicit formatters.'
479
+ };
480
+ }
481
+ // If no formatter is specified, the default is the 'str' formatter,
482
+ // which the user can define however they desire.
483
+ formatters = [GetFormatter(default_formatter)];
484
+ name = token;
485
+ } else {
486
+ formatters = [];
487
+ for (var j=1; j<parts.length; j++) {
488
+ formatters.push(GetFormatter(parts[j]));
489
+ }
490
+ name = parts[0];
491
+ }
492
+ current_block.Append(
493
+ [_DoSubstitute, { name: name, formatters: formatters}]);
494
+ if (had_newline) {
495
+ current_block.Append('\n');
496
+ }
497
+ }
498
+
499
+ // Add the trailing literal
500
+ current_block.Append(template_str.slice(last_index));
501
+
502
+ if (stack.length !== 1) {
503
+ throw {
504
+ name: 'TemplateSyntaxError',
505
+ message: 'Got too few {end} statements'
506
+ };
507
+ }
508
+ return current_block;
509
+ }
510
+
511
+ // The Template class is defined in the traditional style so that users can add
512
+ // methods by mutating the prototype attribute. TODO: Need a good idiom for
513
+ // inheritance without mutating globals.
514
+
515
+ function Template(template_str, options) {
516
+
517
+ // Add 'new' if we were not called with 'new', so prototyping works.
518
+ if(!(this instanceof Template)) {
519
+ return new Template(template_str, options);
520
+ }
521
+
522
+ this._options = options || {};
523
+ this._program = _Compile(template_str, this._options);
524
+ }
525
+
526
+ Template.prototype.render = function(data_dict, callback) {
527
+ // options.undefined_str can either be a string or undefined
528
+ var context = _ScopedContext(data_dict, this._options.undefined_str);
529
+ _Execute(this._program.Statements(), context, callback);
530
+ };
531
+
532
+ Template.prototype.expand = function(data_dict) {
533
+ var tokens = [];
534
+ this.render(data_dict, function(x) { tokens.push(x); });
535
+ return tokens.join('');
536
+ };
537
+
538
+
539
+ // We just export one name for now, the Template "class".
540
+ // We need HtmlEscape in the browser tests, so might as well export it.
541
+
542
+ return {Template: Template, HtmlEscape: HtmlEscape};
543
+
544
+ }();