question_compiler 0.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 6ba34bc5b728df960376e88dca9625b8f9b600e6
4
+ data.tar.gz: b40716c4e48cef949c7b6c60af5c1963246917c6
5
+ SHA512:
6
+ metadata.gz: 8d538f7526618e9820f0b9286e71aa71251ef6447d1b95bcbec6db7d5c40c65993ac833b1531cb6a44e3e59d837883159fa82e194dd66b304f95652fc45fcf34
7
+ data.tar.gz: 77b116b976c4ce7c026d984977565dfaf22435d9f12ccee73365503010a2f5ffe0020846b9d524495ea56e11f489acbddfb59fcf59727f184dee0cd24b1a217f
data/bin/qc ADDED
@@ -0,0 +1,23 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # When developing, run this script like so:
4
+ # DEV=1 ~/gems/question_compiler/bin/qc
5
+
6
+ if ENV['DEV'] == '1'
7
+ puts "\n"
8
+ puts '======================================'
9
+ puts 'Question compiler running in dev mode.'
10
+ puts '======================================'
11
+ puts "\n"
12
+
13
+ $:.unshift File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib'))
14
+
15
+ dev = true
16
+ else
17
+ dev = false
18
+ end
19
+
20
+ require 'question_compiler'
21
+ require 'question_compiler/command'
22
+
23
+ QuestionCompiler::Command.new(ARGV.dup, dev).run
@@ -0,0 +1,5 @@
1
+ require 'nokogiri'
2
+
3
+ require 'question_compiler/version'
4
+ require 'question_compiler/compiler'
5
+ require 'question_compiler/server'
@@ -0,0 +1,52 @@
1
+ require 'fileutils'
2
+ require 'logger'
3
+ require 'slop'
4
+ require 'zip'
5
+ require 'question_compiler/local'
6
+ require 'question_compiler/zipper'
7
+
8
+ module QuestionCompiler
9
+ class Command
10
+ def initialize(args, dev = false)
11
+ @args = args
12
+ @dev = dev
13
+ end
14
+
15
+ def run
16
+ opts = Slop.parse(@args) do |o|
17
+ o.bool '-z', '--zip', 'Builds zip files for all questions and contexts. Invoke from root folder.'
18
+ o.array '-c', '--compile', 'Builds the given questions Invoke from root folder. Usage: qc -c user1/qst1 user1/qst2 user2/qst3'
19
+ o.bool '-s', '--server', 'Starts a development webserver. Invoke from context or question folder.'
20
+ o.integer '-p', '--port', 'Webserver port.', default: 3000
21
+ o.on '-v', '--version', 'Prints the gem version.'
22
+ o.bool '-h', '--help', 'Prints this message.'
23
+ end
24
+
25
+ if opts.help?
26
+ puts opts
27
+ elsif opts.version?
28
+ puts QuestionCompiler::VERSION
29
+ elsif opts.compile?
30
+ log = Logger.new(STDOUT)
31
+ log.level = Logger::DEBUG if @dev
32
+ opts[:compile].each do |question_path|
33
+ username, _ = question_path.split('/')
34
+ FileUtils.mkdir_p File.join('compiled', question_path)
35
+ FileUtils.cp_r File.join('questions', question_path), File.join('compiled', username)
36
+
37
+ QuestionCompiler::Compiler.new(log).compile(
38
+ File.read(File.join('questions', question_path, 'question.html')),
39
+ QuestionCompiler::LocalContextReader.new('contexts'),
40
+ QuestionCompiler::LocalQuestionWriter.new('compiled', question_path)
41
+ )
42
+ end
43
+ elsif opts.zip?
44
+ QuestionCompiler::Zipper.new.zip_all Dir.getwd
45
+ elsif opts.server?
46
+ QuestionCompiler::Server.start opts[:port]
47
+ else
48
+ puts opts
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,86 @@
1
+ module QuestionCompiler
2
+ class Compiler
3
+ attr_reader :context_path
4
+
5
+ def copy_dependencies(context_reader, question_writer, directory)
6
+ @log.debug "Copying dependencies in #{directory}"
7
+ context_reader.glob(
8
+ File.join(directory, '/**/*')
9
+ ).reject do |dep_path|
10
+ !context_reader.file?(dep_path) or
11
+ File.basename(dep_path).start_with?('.')
12
+ end.each do |dep_path|
13
+ dep_path = dep_path.to_s
14
+ if dep_src = context_reader.read(dep_path)
15
+ question_writer.write dep_path, dep_src
16
+ end
17
+ end
18
+ end
19
+
20
+ def copy_nodes(nodes_to_copy, dst_node, before)
21
+ @log.debug "Copying nodes [#{nodes_to_copy.map(&:name).join(', ')}] to #{dst_node.name}"
22
+ before = dst_node.at_css(before)
23
+ nodes_to_copy.each do |node|
24
+ if before
25
+ before.add_previous_sibling node
26
+ else
27
+ dst_node << node
28
+ end
29
+ end
30
+ end
31
+
32
+ # Note that `QuestionCompiler::Server` doesn't call this. Instead, it calls
33
+ # `#transform_question` directly.
34
+ def compile(question_src, context_reader, question_writer)
35
+ context_reader.log = @log
36
+ question_writer.log = @log
37
+ question_writer.open do
38
+ question_doc = Nokogiri.HTML question_src
39
+ question_doc = transform_question(question_doc, context_reader) do |context_path|
40
+ copy_dependencies context_reader, question_writer, 'assets'
41
+ end
42
+
43
+ question_writer.write 'question.html', question_doc.to_html
44
+ end
45
+ end
46
+
47
+ def initialize(log)
48
+ @log = log
49
+ end
50
+
51
+ # `QuestionCompiler::Server` calls this directly, bypassing `#compile`.
52
+ def transform_question(question_doc, context_reader)
53
+ context_reader.log = @log
54
+ question_head = question_doc.at_css('head')
55
+ @log.debug 'Looking for an embedded context'
56
+ if embed_div = question_doc.css('div[data-embed-context]').first
57
+ @context_path = embed_div['data-embed-context']
58
+ @log.debug "Found embedded context: #{@context_path}"
59
+
60
+ context_reader.open(@context_path) do
61
+ if context_src = context_reader.read('context.html')
62
+ context_doc = Nokogiri.HTML context_src
63
+
64
+ if question_head
65
+ copy_nodes context_doc.css('head link'), question_head, 'link'
66
+ copy_nodes context_doc.css('head style'), question_head, 'style'
67
+ copy_nodes context_doc.css('head script'), question_head, 'script'
68
+ end
69
+
70
+ if context_body = context_doc.at_css('body')
71
+ embed_div.inner_html = ''
72
+ embed_div << context_body.inner_html
73
+ end
74
+
75
+ if block_given?
76
+ yield @context_path
77
+ end
78
+ else
79
+ @log.warn "Could not read context.html in #{@context_path}"
80
+ end
81
+ end
82
+ end
83
+ question_doc
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,89 @@
1
+ require 'fileutils'
2
+
3
+ module QuestionCompiler
4
+ class LocalContextReader
5
+ # Given a context path such as `jarrett/example` and a glob such as `assets/*.css`,
6
+ # returns an array of matching paths. The returned paths are relative to the
7
+ # context folder.
8
+ def glob(glob_str)
9
+ Dir.glob(File.join(@root, @context_path, glob_str)).select do |sub_path|
10
+ File.file? sub_path
11
+ end.map do |sub_path|
12
+ sub_path.sub(File.join('contexts', @context_path), '')
13
+ end
14
+ end
15
+
16
+ def file?(sub_path)
17
+ File.file? File.join(@root, @context_path, sub_path)
18
+ end
19
+
20
+ # Pass the path to the contexts folder. I.e. the folder whose children are usernames.
21
+ def initialize(root)
22
+ @root = root
23
+ end
24
+
25
+ attr_accessor :log
26
+
27
+ # Pass a path such as `jarrett/example`.
28
+ def open(context_path)
29
+ @context_path = context_path
30
+ # This is basically a no-op. Some other context reader implementations may need to
31
+ # open and close the question as a whole.
32
+ yield
33
+ end
34
+
35
+ # Given a path to a file within the context such as `assets/context.css`, returns the
36
+ # file data as a string. Returns nil if the file doesn't exist or is unreadable.
37
+ def read(sub_path)
38
+ path = File.expand_path(File.join(@root, @context_path, sub_path))
39
+ begin
40
+ data = File.read path
41
+ @log.debug "R: #{path} (#{data.length} bytes)"
42
+ data
43
+ rescue Exception
44
+ @log.warn "Could not read #{path}"
45
+ nil
46
+ end
47
+ end
48
+ end
49
+
50
+ class LocalQuestionWriter
51
+ # Pass the path to the compiled folder. I.e. the folder whose children are question names.
52
+ def initialize(root, question_path)
53
+ @root = root
54
+ @question_path = question_path
55
+ end
56
+
57
+ attr_accessor :log
58
+
59
+ def open
60
+ # This is basically a no-op. Some other question writer implementations may need to
61
+ # open and close the question as a whole.
62
+ yield
63
+ end
64
+
65
+ # Given a path within the question such as `assets/style.css` and the data, writes
66
+ # the file. Returns true if the file was written successfully. Returns false is the
67
+ # file was unwritable.
68
+ def write(sub_path, data)
69
+ path = File.expand_path(File.join(@root, @question_path, sub_path))
70
+ @log.debug "W: #{path} (#{data.length} bytes)"
71
+ begin
72
+ FileUtils.mkdir_p File.dirname(path)
73
+ File.open(path, 'w') do |f|
74
+ f << data
75
+ end
76
+ true
77
+ rescue Exception
78
+ fail_write path
79
+ end
80
+ end
81
+
82
+ private
83
+
84
+ def fail_write(path)
85
+ @log.warn "Could not write #{path}"
86
+ false
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,81 @@
1
+ module QuestionCompiler
2
+ module Server
3
+ def self.start(port)
4
+ require 'rack'
5
+ app = QuestionHandler.new Dir.getwd
6
+ begin
7
+ Rack::Handler::WEBrick.run app, Port: port
8
+ ensure
9
+ app.cleanup
10
+ end
11
+ end
12
+
13
+ class QuestionHandler
14
+ def call(env)
15
+ req = Rack::Request.new env
16
+
17
+ if req.path == '/question.html'
18
+ serve_question
19
+ else
20
+ maybe_serve_context_file env, req
21
+ end
22
+ end
23
+
24
+ def cleanup
25
+ path = File.join @root, '.context-path'
26
+ if File.exists? path
27
+ File.delete path
28
+ end
29
+ end
30
+
31
+ def initialize(root)
32
+ @root = root
33
+ @context_reader = LocalContextReader.new File.join(@root, '../../../contexts')
34
+ end
35
+
36
+ def read_context_path
37
+ path = File.join @root, '.context-path'
38
+ if File.exists? path
39
+ File.read path
40
+ else
41
+ nil
42
+ end
43
+ end
44
+
45
+ def maybe_serve_context_file(env, req)
46
+ if context_path = read_context_path
47
+ context_root = File.expand_path(
48
+ File.join(@root, '../../../contexts', context_path)
49
+ )
50
+ file_path = File.join(context_root, req.path)
51
+ if File.exists? file_path
52
+ Rack::File.new(context_root).call env
53
+ else
54
+ Rack::File.new(@root).call env
55
+ end
56
+ else
57
+ serve_generic env
58
+ end
59
+ end
60
+
61
+ def serve_generic(env)
62
+ Rack::File.new(@root).call env
63
+ end
64
+
65
+ def serve_question
66
+ compiler = QuestionCompiler::Compiler.new(Logger.new(STDOUT))
67
+ question_src = File.read 'question.html'
68
+ question_doc = Nokogiri.HTML question_src
69
+ question_doc = compiler.transform_question question_doc, @context_reader
70
+ write_context_path compiler.context_path
71
+ [200, {'Content-Type' => 'text/html'}, [question_doc.to_html]]
72
+ end
73
+
74
+ def write_context_path(context_path)
75
+ File.open('.context-path', 'w') do |f|
76
+ f << context_path
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,3 @@
1
+ module QuestionCompiler
2
+ VERSION = '0.0.0'
3
+ end
@@ -0,0 +1,97 @@
1
+ require 'fileutils'
2
+
3
+ module QuestionCompiler
4
+ class Zipper
5
+ # Zips each question and each context, placing the results into the `zip` subfolder.
6
+ # Creates `zip` if it doesn't exist.
7
+ #
8
+ # Pass the path to the root folder. I.e. the folder that contains the subfolders
9
+ # `questions` and `contexts`.
10
+ def zip_all(dir)
11
+ FileUtils.mkdir_p File.join(dir, 'zip')
12
+
13
+ # Zip each question.
14
+ Dir.glob(File.join(dir, 'questions/*/*')).each do |question_path|
15
+ if File.directory? question_path
16
+ question_name = File.basename(question_path).sub('/', '-')
17
+ zip_folder(
18
+ question_path,
19
+ File.join(dir, 'zip', question_name + '.qst.zip')
20
+ )
21
+ end
22
+ end
23
+
24
+ # Zip each context.
25
+ Dir.glob(File.join(dir, 'contexts/*/*')).each do |context_path|
26
+ if File.directory? context_path
27
+ context_name = File.basename(context_path).sub('/', '-')
28
+ zip_folder(
29
+ context_path,
30
+ File.join(dir, 'zip', context_name + '.ctx.zip')
31
+ )
32
+ end
33
+ end
34
+ end
35
+
36
+ private
37
+
38
+ # Zips up `src_path` and writes the zip file to `dst_path`.
39
+ def zip_folder(src_path, dst_path)
40
+ FileUtils.rm_f dst_path
41
+ Zip::File.open(dst_path, Zip::File::CREATE) do |zip|
42
+ # Nest everything inside one folder.
43
+ zip_path_prefix = File.basename(src_path)
44
+ zip.mkdir zip_path_prefix
45
+ zip_folder_recursively(
46
+ zip_path_prefix, # Zip path prefix.
47
+ File.expand_path(src_path), # Disk path prefix.
48
+ '', # Folder disk path.
49
+ zip
50
+ )
51
+ end
52
+ end
53
+
54
+ # Helper method for `#zip_folder`.
55
+ #
56
+ # In this method, we document each path variable with reference to the following
57
+ # example filesystem layout, where root is the folder being zipped:
58
+ # root
59
+ # apples.txt
60
+ # dir-1
61
+ # dir-2
62
+ # bananas.txt
63
+ def zip_folder_recursively(
64
+ zip_path_prefix, # E.g. 'root'
65
+ disk_path_prefix, # E.g. '/home/username/documents/root'
66
+ folder_path, # E.g. 'dir-1/dir-2'
67
+ zip
68
+ )
69
+ # E.g. ['apples.txt', 'subfolder']
70
+ entries = Dir.entries(File.join(disk_path_prefix, folder_path)) - ['.', '..']
71
+
72
+ entries.each do |entry|
73
+ # E.g. '/home/username/documents/root/dir-1/dir-2/bananas.txt'
74
+ abs_disk_path = File.join disk_path_prefix, folder_path, entry
75
+
76
+ if File.directory? abs_disk_path
77
+ # E.g. 'root/dir-1/dir-2'
78
+ zip.mkdir File.join(zip_path_prefix, folder_path, entry)
79
+
80
+ zip_folder_recursively(
81
+ zip_path_prefix, # E.g. 'root'
82
+ disk_path_prefix, # E.g. '/home/username/documents/root'
83
+ File.join(folder_path, entry), # E.g. 'dir-1/dir-2'
84
+ zip
85
+ )
86
+ else
87
+ zip.add(
88
+ # E.g. 'root/dir-1/dir-2/bananas.txt'
89
+ File.join(zip_path_prefix, folder_path, entry),
90
+ # E.g. '/home/username/documents/root/dir-1/dir-2/bananas.txt'
91
+ File.join(disk_path_prefix, folder_path, entry)
92
+ )
93
+ end
94
+ end
95
+ end
96
+ end
97
+ end
metadata ADDED
@@ -0,0 +1,107 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: question_compiler
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Jarrett Colby
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2016-05-10 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: nokogiri
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1'
27
+ - !ruby/object:Gem::Dependency
28
+ name: slop
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '4'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '4'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rubyzip
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '1'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '1'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rack
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '1'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '1'
69
+ description:
70
+ email: jarrett@uchicago.edu
71
+ executables:
72
+ - qc
73
+ extensions: []
74
+ extra_rdoc_files: []
75
+ files:
76
+ - bin/qc
77
+ - lib/question_compiler.rb
78
+ - lib/question_compiler/command.rb
79
+ - lib/question_compiler/compiler.rb
80
+ - lib/question_compiler/local.rb
81
+ - lib/question_compiler/server.rb
82
+ - lib/question_compiler/version.rb
83
+ - lib/question_compiler/zipper.rb
84
+ homepage:
85
+ licenses: []
86
+ metadata: {}
87
+ post_install_message:
88
+ rdoc_options: []
89
+ require_paths:
90
+ - lib
91
+ required_ruby_version: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - ">="
94
+ - !ruby/object:Gem::Version
95
+ version: '0'
96
+ required_rubygems_version: !ruby/object:Gem::Requirement
97
+ requirements:
98
+ - - ">="
99
+ - !ruby/object:Gem::Version
100
+ version: '0'
101
+ requirements: []
102
+ rubyforge_project:
103
+ rubygems_version: 2.2.2
104
+ signing_key:
105
+ specification_version: 4
106
+ summary: Compiler for Number Stories questions
107
+ test_files: []