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.
- 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
|
+
}();
|