question_compiler 0.0.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.
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: []