rundoc 0.0.1

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.
@@ -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