crazy_ivan 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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
+ }();