crazy_ivan 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +7 -0
- data/LICENSE +20 -0
- data/README.rdoc +94 -0
- data/Rakefile +92 -0
- data/TODO +33 -0
- data/VERSION +1 -0
- data/bin/crazy_ivan +114 -0
- data/crazy_ivan.gemspec +182 -0
- data/lib/crazy_ivan.rb +5 -0
- data/lib/html_asset_crush.rb +56 -0
- data/lib/report_assembler.rb +78 -0
- data/lib/test_runner.rb +71 -0
- data/templates/css/ci.css +11 -0
- data/templates/index.html +105 -0
- data/templates/javascript/json-template.js +544 -0
- data/templates/javascript/prototype.js +4917 -0
- data/test/crazy_ivan_test.rb +4 -0
- data/test/test_helper.rb +9 -0
- data/vendor/json-1.1.7/CHANGES +119 -0
- data/vendor/json-1.1.7/GPL +340 -0
- data/vendor/json-1.1.7/README +78 -0
- data/vendor/json-1.1.7/RUBY +58 -0
- data/vendor/json-1.1.7/Rakefile +270 -0
- data/vendor/json-1.1.7/TODO +1 -0
- data/vendor/json-1.1.7/VERSION +1 -0
- data/vendor/json-1.1.7/benchmarks/data-p4-3GHz-ruby18/GeneratorBenchmarkComparison.log +52 -0
- data/vendor/json-1.1.7/benchmarks/data-p4-3GHz-ruby18/GeneratorBenchmarkExt#generator_fast-autocorrelation.dat +1000 -0
- data/vendor/json-1.1.7/benchmarks/data-p4-3GHz-ruby18/GeneratorBenchmarkExt#generator_fast.dat +1001 -0
- data/vendor/json-1.1.7/benchmarks/data-p4-3GHz-ruby18/GeneratorBenchmarkExt#generator_pretty-autocorrelation.dat +900 -0
- data/vendor/json-1.1.7/benchmarks/data-p4-3GHz-ruby18/GeneratorBenchmarkExt#generator_pretty.dat +901 -0
- data/vendor/json-1.1.7/benchmarks/data-p4-3GHz-ruby18/GeneratorBenchmarkExt#generator_safe-autocorrelation.dat +1000 -0
- data/vendor/json-1.1.7/benchmarks/data-p4-3GHz-ruby18/GeneratorBenchmarkExt#generator_safe.dat +1001 -0
- data/vendor/json-1.1.7/benchmarks/data-p4-3GHz-ruby18/GeneratorBenchmarkExt.log +261 -0
- data/vendor/json-1.1.7/benchmarks/data-p4-3GHz-ruby18/GeneratorBenchmarkPure#generator_fast-autocorrelation.dat +1000 -0
- data/vendor/json-1.1.7/benchmarks/data-p4-3GHz-ruby18/GeneratorBenchmarkPure#generator_fast.dat +1001 -0
- data/vendor/json-1.1.7/benchmarks/data-p4-3GHz-ruby18/GeneratorBenchmarkPure#generator_pretty-autocorrelation.dat +1000 -0
- data/vendor/json-1.1.7/benchmarks/data-p4-3GHz-ruby18/GeneratorBenchmarkPure#generator_pretty.dat +1001 -0
- data/vendor/json-1.1.7/benchmarks/data-p4-3GHz-ruby18/GeneratorBenchmarkPure#generator_safe-autocorrelation.dat +1000 -0
- data/vendor/json-1.1.7/benchmarks/data-p4-3GHz-ruby18/GeneratorBenchmarkPure#generator_safe.dat +1001 -0
- data/vendor/json-1.1.7/benchmarks/data-p4-3GHz-ruby18/GeneratorBenchmarkPure.log +262 -0
- data/vendor/json-1.1.7/benchmarks/data-p4-3GHz-ruby18/GeneratorBenchmarkRails#generator-autocorrelation.dat +1000 -0
- data/vendor/json-1.1.7/benchmarks/data-p4-3GHz-ruby18/GeneratorBenchmarkRails#generator.dat +1001 -0
- data/vendor/json-1.1.7/benchmarks/data-p4-3GHz-ruby18/GeneratorBenchmarkRails.log +82 -0
- data/vendor/json-1.1.7/benchmarks/data-p4-3GHz-ruby18/ParserBenchmarkComparison.log +34 -0
- data/vendor/json-1.1.7/benchmarks/data-p4-3GHz-ruby18/ParserBenchmarkExt#parser-autocorrelation.dat +900 -0
- data/vendor/json-1.1.7/benchmarks/data-p4-3GHz-ruby18/ParserBenchmarkExt#parser.dat +901 -0
- data/vendor/json-1.1.7/benchmarks/data-p4-3GHz-ruby18/ParserBenchmarkExt.log +81 -0
- data/vendor/json-1.1.7/benchmarks/data-p4-3GHz-ruby18/ParserBenchmarkPure#parser-autocorrelation.dat +1000 -0
- data/vendor/json-1.1.7/benchmarks/data-p4-3GHz-ruby18/ParserBenchmarkPure#parser.dat +1001 -0
- data/vendor/json-1.1.7/benchmarks/data-p4-3GHz-ruby18/ParserBenchmarkPure.log +82 -0
- data/vendor/json-1.1.7/benchmarks/data-p4-3GHz-ruby18/ParserBenchmarkRails#parser-autocorrelation.dat +1000 -0
- data/vendor/json-1.1.7/benchmarks/data-p4-3GHz-ruby18/ParserBenchmarkRails#parser.dat +1001 -0
- data/vendor/json-1.1.7/benchmarks/data-p4-3GHz-ruby18/ParserBenchmarkRails.log +82 -0
- data/vendor/json-1.1.7/benchmarks/data-p4-3GHz-ruby18/ParserBenchmarkYAML#parser-autocorrelation.dat +1000 -0
- data/vendor/json-1.1.7/benchmarks/data-p4-3GHz-ruby18/ParserBenchmarkYAML#parser.dat +1001 -0
- data/vendor/json-1.1.7/benchmarks/data-p4-3GHz-ruby18/ParserBenchmarkYAML.log +82 -0
- data/vendor/json-1.1.7/benchmarks/generator_benchmark.rb +165 -0
- data/vendor/json-1.1.7/benchmarks/parser_benchmark.rb +197 -0
- data/vendor/json-1.1.7/bin/edit_json.rb +9 -0
- data/vendor/json-1.1.7/bin/prettify_json.rb +75 -0
- data/vendor/json-1.1.7/data/example.json +1 -0
- data/vendor/json-1.1.7/data/index.html +38 -0
- data/vendor/json-1.1.7/data/prototype.js +4184 -0
- data/vendor/json-1.1.7/doc-templates/main.txt +283 -0
- data/vendor/json-1.1.7/ext/json/ext/generator/extconf.rb +11 -0
- data/vendor/json-1.1.7/ext/json/ext/generator/generator.c +919 -0
- data/vendor/json-1.1.7/ext/json/ext/generator/unicode.c +182 -0
- data/vendor/json-1.1.7/ext/json/ext/generator/unicode.h +53 -0
- data/vendor/json-1.1.7/ext/json/ext/parser/extconf.rb +11 -0
- data/vendor/json-1.1.7/ext/json/ext/parser/parser.c +1829 -0
- data/vendor/json-1.1.7/ext/json/ext/parser/parser.rl +686 -0
- data/vendor/json-1.1.7/ext/json/ext/parser/unicode.c +154 -0
- data/vendor/json-1.1.7/ext/json/ext/parser/unicode.h +58 -0
- data/vendor/json-1.1.7/install.rb +26 -0
- data/vendor/json-1.1.7/lib/json.rb +10 -0
- data/vendor/json-1.1.7/lib/json/Array.xpm +21 -0
- data/vendor/json-1.1.7/lib/json/FalseClass.xpm +21 -0
- data/vendor/json-1.1.7/lib/json/Hash.xpm +21 -0
- data/vendor/json-1.1.7/lib/json/Key.xpm +73 -0
- data/vendor/json-1.1.7/lib/json/NilClass.xpm +21 -0
- data/vendor/json-1.1.7/lib/json/Numeric.xpm +28 -0
- data/vendor/json-1.1.7/lib/json/String.xpm +96 -0
- data/vendor/json-1.1.7/lib/json/TrueClass.xpm +21 -0
- data/vendor/json-1.1.7/lib/json/add/core.rb +135 -0
- data/vendor/json-1.1.7/lib/json/add/rails.rb +58 -0
- data/vendor/json-1.1.7/lib/json/common.rb +354 -0
- data/vendor/json-1.1.7/lib/json/editor.rb +1371 -0
- data/vendor/json-1.1.7/lib/json/ext.rb +15 -0
- data/vendor/json-1.1.7/lib/json/json.xpm +1499 -0
- data/vendor/json-1.1.7/lib/json/pure.rb +77 -0
- data/vendor/json-1.1.7/lib/json/pure/generator.rb +430 -0
- data/vendor/json-1.1.7/lib/json/pure/parser.rb +269 -0
- data/vendor/json-1.1.7/lib/json/version.rb +8 -0
- data/vendor/json-1.1.7/tests/fixtures/fail1.json +1 -0
- data/vendor/json-1.1.7/tests/fixtures/fail10.json +1 -0
- data/vendor/json-1.1.7/tests/fixtures/fail11.json +1 -0
- data/vendor/json-1.1.7/tests/fixtures/fail12.json +1 -0
- data/vendor/json-1.1.7/tests/fixtures/fail13.json +1 -0
- data/vendor/json-1.1.7/tests/fixtures/fail14.json +1 -0
- data/vendor/json-1.1.7/tests/fixtures/fail18.json +1 -0
- data/vendor/json-1.1.7/tests/fixtures/fail19.json +1 -0
- data/vendor/json-1.1.7/tests/fixtures/fail2.json +1 -0
- data/vendor/json-1.1.7/tests/fixtures/fail20.json +1 -0
- data/vendor/json-1.1.7/tests/fixtures/fail21.json +1 -0
- data/vendor/json-1.1.7/tests/fixtures/fail22.json +1 -0
- data/vendor/json-1.1.7/tests/fixtures/fail23.json +1 -0
- data/vendor/json-1.1.7/tests/fixtures/fail24.json +1 -0
- data/vendor/json-1.1.7/tests/fixtures/fail25.json +1 -0
- data/vendor/json-1.1.7/tests/fixtures/fail27.json +2 -0
- data/vendor/json-1.1.7/tests/fixtures/fail28.json +2 -0
- data/vendor/json-1.1.7/tests/fixtures/fail3.json +1 -0
- data/vendor/json-1.1.7/tests/fixtures/fail4.json +1 -0
- data/vendor/json-1.1.7/tests/fixtures/fail5.json +1 -0
- data/vendor/json-1.1.7/tests/fixtures/fail6.json +1 -0
- data/vendor/json-1.1.7/tests/fixtures/fail7.json +1 -0
- data/vendor/json-1.1.7/tests/fixtures/fail8.json +1 -0
- data/vendor/json-1.1.7/tests/fixtures/fail9.json +1 -0
- data/vendor/json-1.1.7/tests/fixtures/pass1.json +56 -0
- data/vendor/json-1.1.7/tests/fixtures/pass15.json +1 -0
- data/vendor/json-1.1.7/tests/fixtures/pass16.json +1 -0
- data/vendor/json-1.1.7/tests/fixtures/pass17.json +1 -0
- data/vendor/json-1.1.7/tests/fixtures/pass2.json +1 -0
- data/vendor/json-1.1.7/tests/fixtures/pass26.json +1 -0
- data/vendor/json-1.1.7/tests/fixtures/pass3.json +6 -0
- data/vendor/json-1.1.7/tests/test_json.rb +312 -0
- data/vendor/json-1.1.7/tests/test_json_addition.rb +164 -0
- data/vendor/json-1.1.7/tests/test_json_fixtures.rb +34 -0
- data/vendor/json-1.1.7/tests/test_json_generate.rb +106 -0
- data/vendor/json-1.1.7/tests/test_json_rails.rb +146 -0
- data/vendor/json-1.1.7/tests/test_json_unicode.rb +62 -0
- data/vendor/json-1.1.7/tools/fuzz.rb +139 -0
- data/vendor/json-1.1.7/tools/server.rb +61 -0
- metadata +196 -0
data/lib/crazy_ivan.rb
ADDED
@@ -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
|
data/lib/test_runner.rb
ADDED
@@ -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,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,'&').
|
59
|
+
replace(/>/g,'>').
|
60
|
+
replace(/</g,'<');
|
61
|
+
}
|
62
|
+
|
63
|
+
function HtmlTagEscape(s) {
|
64
|
+
return s.replace(/&/g,'&').
|
65
|
+
replace(/>/g,'>').
|
66
|
+
replace(/</g,'<').
|
67
|
+
replace(/"/g,'"');
|
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
|
+
}();
|