rundoc 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,76 @@
1
+ require 'fileutils'
2
+ require 'pathname'
3
+ require 'rundoc/version'
4
+
5
+ module Rundoc
6
+ extend self
7
+
8
+ def code_command_from_keyword(keyword, args)
9
+ klass = code_command(keyword.to_sym) || Rundoc::CodeCommand::NoSuchCommand
10
+ cc = klass.new(args)
11
+ cc.keyword = keyword
12
+ cc
13
+ end
14
+
15
+ def parser_options
16
+ @parser_options ||= {}
17
+ end
18
+
19
+ def code_lookup
20
+ @code_lookup ||= {}
21
+ end
22
+
23
+ def code_command(keyword)
24
+ code_lookup[:"#{keyword}"]
25
+ end
26
+
27
+ def known_commands
28
+ code_lookup.keys
29
+ end
30
+
31
+ def register_code_command(keyword, klass)
32
+ code_lookup[keyword] = klass
33
+ end
34
+
35
+ def configure(&block)
36
+ yield self
37
+ end
38
+
39
+ def run_after_build
40
+ @after_build_block ||= []
41
+ @after_build_block.each(&:call)
42
+ end
43
+
44
+ def after_build(&block)
45
+ @after_build_block ||= []
46
+ @after_build_block << block
47
+ end
48
+
49
+ def config
50
+ yield self
51
+ end
52
+
53
+ def register_repl(*args, &block)
54
+ ReplRunner.register_commands(*args, &block)
55
+ end
56
+
57
+ def filter_sensitive(sensitive)
58
+ raise "Expecting #{sensitive} to be a hash" unless sensitive.is_a?(Hash)
59
+ @sensitive ||= {}
60
+ @sensitive.merge!(sensitive)
61
+ end
62
+
63
+ def sanitize(doc)
64
+ return doc if @sensitive.nil?
65
+ @sensitive.each do |sensitive, replace|
66
+ doc.gsub!(sensitive.to_s, replace)
67
+ end
68
+ return doc
69
+ end
70
+
71
+ attr_accessor :project_root
72
+ end
73
+
74
+ require 'rundoc/parser'
75
+ require 'rundoc/code_section'
76
+ require 'rundoc/code_command'
@@ -0,0 +1,32 @@
1
+ module Rundoc
2
+ class CodeCommand
3
+ attr_accessor :hidden, :render_result, :command, :contents, :keyword
4
+ alias :hidden? :hidden
5
+ alias :render_result? :render_result
6
+
7
+ def initialize(arg)
8
+ end
9
+
10
+ def not_hidden?
11
+ !hidden?
12
+ end
13
+
14
+ def push(contents)
15
+ @contents ||= ""
16
+ @contents << contents
17
+ end
18
+ alias :<< :push
19
+
20
+ # executes command to build project
21
+ def call(env = {})
22
+ raise "not implemented"
23
+ end
24
+ end
25
+ end
26
+
27
+ require 'rundoc/code_command/bash'
28
+ require 'rundoc/code_command/pipe'
29
+ require 'rundoc/code_command/write'
30
+ require 'rundoc/code_command/repl'
31
+ require 'rundoc/code_command/rundoc_command'
32
+ require 'rundoc/code_command/no_such_command'
@@ -0,0 +1,57 @@
1
+ class Rundoc::CodeCommand::Bash < Rundoc::CodeCommand
2
+
3
+ # line = "cd ..""
4
+ # line = "pwd"
5
+ # line = "ls"
6
+ def initialize(line)
7
+ @line = line
8
+ @contents = ""
9
+ @delegate = case @line.split(' ').first.downcase
10
+ when 'cd'
11
+ Cd.new(@line)
12
+ else
13
+ false
14
+ end
15
+ end
16
+
17
+ def to_md(env = {})
18
+ return @delegate.to_md(env) if @delegate
19
+
20
+ "$ #{@line}"
21
+ end
22
+
23
+ def call(env = {})
24
+ return @delegate.call(env) if @delegate
25
+
26
+ shell(@line, @contents)
27
+ end
28
+
29
+ # markdown doesn't understand bash color codes
30
+ def sanitize_escape_chars(input)
31
+ input.gsub(/\e\[(\d+)m/, '')
32
+ end
33
+
34
+ def shell(cmd, stdin = nil)
35
+ msg = "Running: $ '#{cmd}'"
36
+ msg << " with stdin: '#{stdin.inspect}'" if stdin && !stdin.empty?
37
+ puts msg
38
+
39
+ result = ""
40
+ IO.popen("#{cmd} 2>&1", "w+") do |io|
41
+ io << stdin if stdin
42
+ io.close_write
43
+ result = sanitize_escape_chars io.read
44
+ end
45
+ unless $?.success?
46
+ raise "Command `#{@line}` exited with non zero status: #{result}" unless keyword.include?("fail")
47
+ end
48
+ return result
49
+ end
50
+ end
51
+
52
+
53
+ Rundoc.register_code_command(:bash, Rundoc::CodeCommand::Bash)
54
+ Rundoc.register_code_command(:'$', Rundoc::CodeCommand::Bash)
55
+ Rundoc.register_code_command(:'fail.$', Rundoc::CodeCommand::Bash)
56
+
57
+ require 'rundoc/code_command/bash/cd'
@@ -0,0 +1,18 @@
1
+ class Rundoc::CodeCommand::Bash
2
+ # special purpose class to persist cd behavior across the entire program
3
+ # we change the directory of the parent program (rundoc) rather than
4
+ # changing the directory of a spawned child (via exec, ``, system, etc.)
5
+ class Cd < Rundoc::CodeCommand::Bash
6
+
7
+ def initialize(line)
8
+ @line = line
9
+ end
10
+
11
+ def call(env)
12
+ line = @line.sub('cd', '').strip
13
+ puts "running $ cd #{line}"
14
+ Dir.chdir(line)
15
+ nil
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,74 @@
1
+ class Rundoc::CodeCommand::FileCommand
2
+ class Append < Rundoc::CodeCommand
3
+
4
+ def initialize(filename)
5
+ @filename, line = filename.split('#')
6
+ @line_number = Integer(line) if line
7
+ end
8
+
9
+ def to_md(env)
10
+ raise "must call write in its own code section" unless env[:commands].empty?
11
+ before = env[:before]
12
+ if @line_number
13
+ env[:before] = "In file `#{@filename}`, on line #{@line_number} add:\n\n#{before}"
14
+ else
15
+ env[:before] = "At the end of `#{@filename}` add:\n\n#{before}"
16
+ end
17
+ nil
18
+ end
19
+
20
+ def last_char_of(string)
21
+ string[-1,1]
22
+ end
23
+
24
+ def ends_in_newline?(string)
25
+ last_char_of(string) == "\n"
26
+ end
27
+
28
+ def concat_with_newline(str1, str2)
29
+ result = ""
30
+ result << str1
31
+ result << "\n" unless ends_in_newline?(result)
32
+ result << str2
33
+ result << "\n" unless ends_in_newline?(result)
34
+ result
35
+ end
36
+
37
+ def insert_contents_into_at_line(doc)
38
+ lines = doc.lines
39
+ raise "Expected #{@filename} to have at least #{@line_number} but only has #{lines.count}" if lines.count < @line_number
40
+ result = []
41
+ lines.each_with_index do |line, index|
42
+ line_number = index.next
43
+ if line_number == @line_number
44
+ result << contents
45
+ result << "\n" unless ends_in_newline?(contents)
46
+ end
47
+ result << line
48
+ end
49
+ doc = result.flatten.join("")
50
+ end
51
+
52
+ def call(env = {})
53
+ dir = File.expand_path("../", @filename)
54
+ FileUtils.mkdir_p(dir)
55
+
56
+ doc = File.read(@filename)
57
+ if @line_number
58
+ puts "Writing to: '#{@filename}' line #{@line_number} with: #{contents.inspect}"
59
+ doc = insert_contents_into_at_line(doc)
60
+ else
61
+ puts "Appending to file: '#{@filename}' with: #{contents.inspect}"
62
+ doc = concat_with_newline(doc, contents)
63
+ end
64
+
65
+ File.open(@filename, "w") do |f|
66
+ f.write(doc)
67
+ end
68
+ contents
69
+ end
70
+ end
71
+ end
72
+
73
+
74
+ Rundoc.register_code_command(:'file.append', Rundoc::CodeCommand::FileCommand::Append)
@@ -0,0 +1,32 @@
1
+ class Rundoc::CodeCommand::FileCommand
2
+ class Remove < Rundoc::CodeCommand
3
+
4
+ def initialize(filename)
5
+ @filename = filename
6
+ end
7
+
8
+ def to_md(env)
9
+ raise "must call write in its own code section" unless env[:commands].empty?
10
+ before = env[:before]
11
+ env[:before] = "In file `#{@filename}` remove:\n\n#{before}"
12
+ nil
13
+ end
14
+
15
+ def call(env = {})
16
+ puts "Deleting '#{contents.strip}' from #{@filename}"
17
+ raise "#{@filename} does not exist" unless File.exist?(@filename)
18
+
19
+ regex = /^\s*#{Regexp.quote(contents)}/
20
+ doc = File.read(@filename)
21
+ doc.sub!(regex, '')
22
+
23
+ File.open(@filename, "w") do |f|
24
+ f.write(doc)
25
+ end
26
+ contents
27
+ end
28
+ end
29
+ end
30
+
31
+
32
+ Rundoc.register_code_command(:'file.remove', Rundoc::CodeCommand::FileCommand::Remove)
@@ -0,0 +1,6 @@
1
+ module Rundoc
2
+ class CodeCommand
3
+ class NoSuchCommand < Rundoc::CodeCommand
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,37 @@
1
+ module Rundoc
2
+ class CodeCommand
3
+ class Pipe < Rundoc::CodeCommand
4
+
5
+ # ::: ls
6
+ # ::: | tail -n 2
7
+ # => "test\ntmp.file\n"
8
+ def initialize(line)
9
+ line_array = line.split(" ")
10
+ @first = line_array.shift.strip
11
+ @delegate = Rundoc.code_command_from_keyword(@first, line_array.join(" "))
12
+ @delegate = Rundoc::CodeCommand::Bash.new(line) if @delegate.kind_of?(Rundoc::CodeCommand::NoSuchCommand)
13
+ end
14
+
15
+ # before: "",
16
+ # after: "",
17
+ # commands:
18
+ # [[cmd, output], [cmd, output]]
19
+ def call(env = {})
20
+ last_command = env[:commands].last
21
+ puts "Piping: results of '#{last_command[:command]}' to '#{@delegate}'"
22
+
23
+ @delegate.push(last_command[:output])
24
+ @delegate.call(env)
25
+ end
26
+
27
+ def to_md(env = {})
28
+ ""
29
+ end
30
+ end
31
+ end
32
+ end
33
+
34
+
35
+ Rundoc.register_code_command(:pipe, Rundoc::CodeCommand::Pipe)
36
+ Rundoc.register_code_command(:|, Rundoc::CodeCommand::Pipe)
37
+
@@ -0,0 +1,37 @@
1
+ require 'repl_runner'
2
+
3
+ module Rundoc
4
+ class CodeCommand
5
+ class Repl < Rundoc::CodeCommand
6
+ def initialize(command)
7
+ @command = command
8
+ @contents = ""
9
+ end
10
+
11
+ def keyword=(keyword)
12
+ @keyword = keyword
13
+ if keyword.to_s == "repl"
14
+ command_array = @command.split(" ")
15
+ @keyword = command_array.first
16
+ else
17
+ @command = "#{keyword} #{@command}"
18
+ end
19
+ end
20
+
21
+ def call(env = {})
22
+ puts "Running '#{@command}'' with repl: #{keyword}"
23
+ repl = ReplRunner.new(:"#{keyword}", @command)
24
+ @result = repl.zip(contents.strip).flatten.join("\n")
25
+ return @result
26
+ end
27
+
28
+ def to_md(env = {})
29
+ return "$ #{@command}"
30
+ end
31
+ end
32
+ end
33
+ end
34
+
35
+
36
+ Rundoc.register_code_command(:repl, Rundoc::CodeCommand::Repl)
37
+ Rundoc.register_code_command(:irb, Rundoc::CodeCommand::Repl)
@@ -0,0 +1,22 @@
1
+ module ::Rundoc
2
+ class CodeCommand
3
+ class ::RundocCommand < ::Rundoc::CodeCommand
4
+
5
+ def initialize(line)
6
+ end
7
+
8
+ def to_md(env = {})
9
+ ""
10
+ end
11
+
12
+ def call(env = {})
13
+ puts "Running: #{contents}"
14
+ eval(contents)
15
+ ""
16
+ end
17
+ end
18
+ end
19
+ end
20
+
21
+
22
+ Rundoc.register_code_command(:rundoc, ::Rundoc::CodeCommand::RundocCommand)
@@ -0,0 +1,34 @@
1
+ module Rundoc
2
+ class CodeCommand
3
+ class Write < Rundoc::CodeCommand
4
+ def initialize(filename)
5
+ @filename = filename
6
+ end
7
+
8
+ def to_md(env)
9
+ raise "must call write in its own code section" unless env[:commands].empty?
10
+ before = env[:before]
11
+ env[:before] = "In file `#{@filename}` write:\n\n#{before}"
12
+ nil
13
+ end
14
+
15
+ def call(env = {})
16
+ puts "Writing to: '#{@filename}'"
17
+
18
+ dir = File.expand_path("../", @filename)
19
+ FileUtils.mkdir_p(dir)
20
+ File.open(@filename, "w") do |f|
21
+ f.write(contents)
22
+ end
23
+ contents
24
+ end
25
+ end
26
+ end
27
+ end
28
+
29
+
30
+ Rundoc.register_code_command(:write, Rundoc::CodeCommand::Write)
31
+ Rundoc.register_code_command(:'file.write', Rundoc::CodeCommand::Write)
32
+
33
+ require 'rundoc/code_command/file_command/append'
34
+ require 'rundoc/code_command/file_command/remove'
@@ -0,0 +1,145 @@
1
+ module Rundoc
2
+ # holds code, parses and creates CodeCommand
3
+ class CodeSection
4
+ class ParseError < StandardError
5
+ def initialize(options = {})
6
+ keyword = options[:keyword]
7
+ command = options[:command]
8
+ line_number = options[:line_number]
9
+ block = options[:block].lines.map do |line|
10
+ if line == command
11
+ " > #{line}"
12
+ else
13
+ " #{line}"
14
+ end
15
+ end.join("")
16
+
17
+ msg = "Error parsing (line:#{line_number}):\n"
18
+ msg << "> '#{command.strip}'\n"
19
+ msg << "No such registered command: '#{keyword}'\n"
20
+ msg << "registered commands: #{Rundoc.known_commands.inspect}\n\n"
21
+ msg << block
22
+ msg << "\n"
23
+ super msg
24
+ end
25
+ end
26
+
27
+ COMMAND_REGEX = Rundoc::Parser::COMMAND_REGEX # todo: move whole thing
28
+ attr_accessor :original, :fence, :lang, :code, :commands, :keyword
29
+
30
+ def initialize(match, options = {})
31
+ @original = match.to_s
32
+ @commands = []
33
+ @stack = []
34
+ @keyword = options[:keyword] or raise "keyword is required"
35
+ @fence = match[:fence]
36
+ @lang = match[:lang]
37
+ @code = match[:contents]
38
+ parse_code_command
39
+ end
40
+
41
+ def render
42
+ result = []
43
+ env = {}
44
+ env[:commands] = []
45
+ env[:before] = "#{fence}#{lang}"
46
+ env[:after] = "#{fence}"
47
+
48
+ @stack.each do |s|
49
+ unless s.respond_to?(:call)
50
+ result << s
51
+ next
52
+ end
53
+
54
+ code_command = s
55
+ code_output = code_command.call(env) || ""
56
+ code_line = code_command.to_md(env) || ""
57
+
58
+ env[:commands] << { object: code_command, output: code_output, command: code_line}
59
+
60
+ if code_command.render_result?
61
+ result << [code_line, code_output]
62
+ else
63
+ result << code_line unless code_command.hidden?
64
+ end
65
+ end
66
+
67
+ return "" if hidden?
68
+
69
+ array = [env[:before], result, env[:after]]
70
+ return array.flatten.compact.map(&:rstrip).reject(&:empty?).join("\n") << "\n"
71
+ end
72
+
73
+ # all of the commands are hidden
74
+ def hidden?
75
+ !not_hidden?
76
+ end
77
+
78
+ # one or more of the commands are not hidden
79
+ def not_hidden?
80
+ return true if commands.empty?
81
+ commands.map(&:not_hidden?).detect {|c| c }
82
+ end
83
+
84
+ def command_regex
85
+ COMMAND_REGEX.call(keyword)
86
+ end
87
+
88
+ def add_code(match, line)
89
+ add_match_to_code_command(match, commands)
90
+ check_parse_error(line, code)
91
+ end
92
+
93
+ def add_contents(line)
94
+ if commands.empty?
95
+ @stack << line
96
+ else
97
+ commands.last << line
98
+ end
99
+ end
100
+
101
+ def parse_code_command
102
+ code.lines.each do |line|
103
+ if match = line.match(command_regex)
104
+ add_code(match, line)
105
+ else
106
+ add_contents(line)
107
+ end
108
+ end
109
+ end
110
+
111
+ def add_match_to_code_command(match, commands)
112
+ command = match[:command]
113
+ tag = match[:tag]
114
+ statement = match[:statement]
115
+
116
+ code_command = Rundoc.code_command_from_keyword(command, statement)
117
+
118
+ case tag
119
+ when /\-/
120
+ code_command.hidden = true
121
+ when /\=/
122
+ code_command.render_result = true
123
+ when /\s/
124
+ # default do nothing
125
+ end
126
+
127
+ @stack << "\n" if commands.last.is_a?(Rundoc::CodeCommand)
128
+ @stack << code_command
129
+ commands << code_command
130
+ code_command
131
+ end
132
+
133
+ def check_parse_error(command, code_block)
134
+ return unless code_command = @stack.last
135
+ return unless code_command.is_a?(Rundoc::CodeCommand::NoSuchCommand)
136
+ @original.lines.each_with_index do |line, index|
137
+ next unless line == command
138
+ raise ParseError.new(keyword: code_command.keyword,
139
+ block: code_block,
140
+ command: command,
141
+ line_number: index.next)
142
+ end
143
+ end
144
+ end
145
+ end