rundoc 0.0.1 → 1.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (63) hide show
  1. checksums.yaml +5 -5
  2. data/.github/workflows/check_changelog.yml +13 -0
  3. data/.gitignore +9 -0
  4. data/.travis.yml +8 -0
  5. data/CHANGELOG.md +22 -0
  6. data/Dockerfile +24 -0
  7. data/Gemfile +1 -0
  8. data/README.md +276 -74
  9. data/bin/rundoc +4 -52
  10. data/lib/rundoc.rb +15 -1
  11. data/lib/rundoc/cli.rb +84 -0
  12. data/lib/rundoc/code_command.rb +25 -6
  13. data/lib/rundoc/code_command/background.rb +9 -0
  14. data/lib/rundoc/code_command/background/log/clear.rb +17 -0
  15. data/lib/rundoc/code_command/background/log/read.rb +16 -0
  16. data/lib/rundoc/code_command/background/process_spawn.rb +96 -0
  17. data/lib/rundoc/code_command/background/start.rb +38 -0
  18. data/lib/rundoc/code_command/background/stop.rb +17 -0
  19. data/lib/rundoc/code_command/background/wait.rb +19 -0
  20. data/lib/rundoc/code_command/bash.rb +1 -1
  21. data/lib/rundoc/code_command/bash/cd.rb +21 -3
  22. data/lib/rundoc/code_command/file_command/append.rb +16 -11
  23. data/lib/rundoc/code_command/file_command/remove.rb +6 -5
  24. data/lib/rundoc/code_command/no_such_command.rb +4 -0
  25. data/lib/rundoc/code_command/pipe.rb +18 -5
  26. data/lib/rundoc/code_command/raw.rb +18 -0
  27. data/lib/rundoc/code_command/rundoc/depend_on.rb +37 -0
  28. data/lib/rundoc/code_command/rundoc/require.rb +41 -0
  29. data/lib/rundoc/code_command/rundoc_command.rb +6 -2
  30. data/lib/rundoc/code_command/website.rb +7 -0
  31. data/lib/rundoc/code_command/website/driver.rb +111 -0
  32. data/lib/rundoc/code_command/website/navigate.rb +29 -0
  33. data/lib/rundoc/code_command/website/screenshot.rb +28 -0
  34. data/lib/rundoc/code_command/website/visit.rb +47 -0
  35. data/lib/rundoc/code_command/write.rb +22 -7
  36. data/lib/rundoc/code_section.rb +41 -66
  37. data/lib/rundoc/parser.rb +5 -4
  38. data/lib/rundoc/peg_parser.rb +282 -0
  39. data/lib/rundoc/version.rb +2 -2
  40. data/rundoc.gemspec +9 -3
  41. data/test/fixtures/build_logs/rundoc.md +56 -0
  42. data/test/fixtures/depend_on/dependency/rundoc.md +5 -0
  43. data/test/fixtures/depend_on/main/rundoc.md +10 -0
  44. data/test/fixtures/java/rundoc.md +9 -0
  45. data/test/fixtures/rails_4/rundoc.md +151 -188
  46. data/test/fixtures/rails_5/rundoc.md +445 -0
  47. data/test/fixtures/rails_6/rundoc.md +451 -0
  48. data/test/fixtures/require/dependency/rundoc.md +5 -0
  49. data/test/fixtures/require/main/rundoc.md +10 -0
  50. data/test/fixtures/screenshot/rundoc.md +10 -0
  51. data/test/rundoc/code_commands/append_file_test.rb +33 -6
  52. data/test/rundoc/code_commands/background_test.rb +69 -0
  53. data/test/rundoc/code_commands/bash_test.rb +1 -1
  54. data/test/rundoc/code_commands/pipe_test.rb +1 -1
  55. data/test/rundoc/code_commands/remove_contents_test.rb +3 -4
  56. data/test/rundoc/code_section_test.rb +95 -2
  57. data/test/rundoc/parser_test.rb +7 -13
  58. data/test/rundoc/peg_parser_test.rb +381 -0
  59. data/test/rundoc/regex_test.rb +6 -6
  60. data/test/rundoc/test_parse_java.rb +1 -1
  61. data/test/test_helper.rb +1 -3
  62. metadata +143 -18
  63. data/Gemfile.lock +0 -38
@@ -0,0 +1,28 @@
1
+ class Rundoc::CodeCommand::Website
2
+ class Screenshot < Rundoc::CodeCommand
3
+ def initialize(name: , upload: false)
4
+ @driver = Rundoc::CodeCommand::Website::Driver.find(name)
5
+ @upload = upload
6
+ end
7
+
8
+ def to_md(env = {})
9
+ ""
10
+ end
11
+
12
+ def call(env = {})
13
+ puts "Taking screenshot: #{@driver.current_url}"
14
+ filename = @driver.screenshot(upload: @upload)
15
+ env[:replace] = "![Screenshot of #{@driver.current_url}](#{filename})"
16
+ ""
17
+ end
18
+
19
+ # def hidden?
20
+ # true
21
+ # end
22
+
23
+ # def not_hidden?
24
+ # !hidden?
25
+ # end
26
+ end
27
+ end
28
+ Rundoc.register_code_command(:"website.screenshot", Rundoc::CodeCommand::Website::Screenshot)
@@ -0,0 +1,47 @@
1
+ class Rundoc::CodeCommand::Website
2
+ class Visit < Rundoc::CodeCommand
3
+ def initialize(name: , url: nil, scroll: nil, height: 720, width: 1024, visible: false)
4
+ @name = name
5
+ @url = url
6
+ @scroll = scroll
7
+ @driver = Driver.new(
8
+ name: name,
9
+ url: url,
10
+ height: height,
11
+ width: width,
12
+ visible: visible
13
+ )
14
+ Driver.add(@name, @driver)
15
+ end
16
+
17
+ def to_md(env = {})
18
+ ""
19
+ end
20
+
21
+ def call(env = {})
22
+ message = String.new("Visting: #{@url}")
23
+ message << "and executing:\n#{contents}" unless contents.nil? || contents.empty?
24
+
25
+ puts message
26
+
27
+ @driver.visit(@url) if @url
28
+ @driver.scroll(@scroll) if @scroll
29
+
30
+
31
+ return "" if contents.nil? || contents.empty?
32
+ @driver.safe_eval(contents)
33
+
34
+ return ""
35
+ end
36
+
37
+ def hidden?
38
+ true
39
+ end
40
+
41
+ def not_hidden?
42
+ !hidden?
43
+ end
44
+ end
45
+ end
46
+
47
+ Rundoc.register_code_command(:"website.visit", Rundoc::CodeCommand::Website::Visit)
@@ -1,6 +1,23 @@
1
1
  module Rundoc
2
2
  class CodeCommand
3
+ module FileUtil
4
+ def filename
5
+ files = Dir.glob(@filename)
6
+ if files.length > 1
7
+ raise "Filename glob #{@filename.inspect} matched more than one file. Be more specific to only match one file. Matches:\n" + files.join(" \n")
8
+ end
9
+ files.first || @filename
10
+ end
11
+
12
+ def mkdir_p
13
+ dir = File.expand_path("../", filename)
14
+ FileUtils.mkdir_p(dir)
15
+ end
16
+ end
17
+
3
18
  class Write < Rundoc::CodeCommand
19
+ include FileUtil
20
+
4
21
  def initialize(filename)
5
22
  @filename = filename
6
23
  end
@@ -8,16 +25,14 @@ module Rundoc
8
25
  def to_md(env)
9
26
  raise "must call write in its own code section" unless env[:commands].empty?
10
27
  before = env[:before]
11
- env[:before] = "In file `#{@filename}` write:\n\n#{before}"
28
+ env[:before] = "In file `#{filename}` write:\n\n#{before}"
12
29
  nil
13
30
  end
14
31
 
15
32
  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|
33
+ puts "Writing to: '#{filename}'"
34
+ mkdir_p
35
+ File.open(filename, "w") do |f|
21
36
  f.write(contents)
22
37
  end
23
38
  contents
@@ -31,4 +46,4 @@ Rundoc.register_code_command(:write, Rundoc::CodeCommand::Write)
31
46
  Rundoc.register_code_command(:'file.write', Rundoc::CodeCommand::Write)
32
47
 
33
48
  require 'rundoc/code_command/file_command/append'
34
- require 'rundoc/code_command/file_command/remove'
49
+ require 'rundoc/code_command/file_command/remove'
@@ -32,6 +32,7 @@ module Rundoc
32
32
  @commands = []
33
33
  @stack = []
34
34
  @keyword = options[:keyword] or raise "keyword is required"
35
+ @document_path = options[:document_path]
35
36
  @fence = match[:fence]
36
37
  @lang = match[:lang]
37
38
  @code = match[:contents]
@@ -40,10 +41,11 @@ module Rundoc
40
41
 
41
42
  def render
42
43
  result = []
43
- env = {}
44
+ env = {}
44
45
  env[:commands] = []
45
- env[:before] = "#{fence}#{lang}"
46
- env[:after] = "#{fence}"
46
+ env[:before] = String.new("#{fence}#{lang}")
47
+ env[:after] = String.new(fence)
48
+ env[:document_path] = @document_path
47
49
 
48
50
  @stack.each do |s|
49
51
  unless s.respond_to?(:call)
@@ -55,19 +57,27 @@ module Rundoc
55
57
  code_output = code_command.call(env) || ""
56
58
  code_line = code_command.to_md(env) || ""
57
59
 
58
- env[:commands] << { object: code_command, output: code_output, command: code_line}
60
+ env[:commands] << { object: code_command, output: code_output, command: code_line }
59
61
 
60
- if code_command.render_result?
61
- result << [code_line, code_output]
62
- else
63
- result << code_line unless code_command.hidden?
64
- end
62
+ tmp_result = []
63
+ tmp_result << code_line if code_command.render_command?
64
+ tmp_result << code_output if code_command.render_result?
65
+
66
+ result << tmp_result unless code_command.hidden?
67
+ result
65
68
  end
66
69
 
70
+ return env[:replace] if env[:replace]
71
+
67
72
  return "" if hidden?
68
73
 
69
74
  array = [env[:before], result, env[:after]]
70
- return array.flatten.compact.map(&:rstrip).reject(&:empty?).join("\n") << "\n"
75
+ array.flatten!
76
+ array.compact!
77
+ array.map!(&:rstrip)
78
+ array.reject!(&:empty?)
79
+
80
+ return array.join("\n") << "\n"
71
81
  end
72
82
 
73
83
  # all of the commands are hidden
@@ -81,65 +91,30 @@ module Rundoc
81
91
  commands.map(&:not_hidden?).detect {|c| c }
82
92
  end
83
93
 
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
94
  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
95
+ parser = Rundoc::PegParser.new.code_block
96
+ tree = parser.parse(@code)
97
+ actual = Rundoc::PegTransformer.new.apply(tree)
98
+ actual = [actual] unless actual.is_a?(Array)
99
+ actual.each do |code_command|
100
+ @stack << "\n" if commands.last.is_a?(Rundoc::CodeCommand)
101
+ @stack << code_command
102
+ commands << code_command
125
103
  end
126
-
127
- @stack << "\n" if commands.last.is_a?(Rundoc::CodeCommand)
128
- @stack << code_command
129
- commands << code_command
130
- code_command
104
+ rescue ::Parslet::ParseFailed => e
105
+ raise "Could not compile code:\n#{@code}\nReason: #{e.message}"
131
106
  end
132
107
 
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
108
+ # def check_parse_error(command, code_block)
109
+ # return unless code_command = @stack.last
110
+ # return unless code_command.is_a?(Rundoc::CodeCommand::NoSuchCommand)
111
+ # @original.lines.each_with_index do |line, index|
112
+ # next unless line == command
113
+ # raise ParseError.new(keyword: code_command.keyword,
114
+ # block: code_block,
115
+ # command: command,
116
+ # line_number: index.next)
117
+ # end
118
+ # end
144
119
  end
145
120
  end
@@ -5,15 +5,16 @@ module Rundoc
5
5
  GITHUB_BLOCK = '^(?<fence>(?<fence_char>~|`){3,})\s*?(?<lang>\w+)?\s*?\n(?<contents>.*?)^\g<fence>\g<fence_char>*\s*?\n'
6
6
  CODEBLOCK_REGEX = /(#{GITHUB_BLOCK})/m
7
7
  COMMAND_REGEX = ->(keyword) {
8
- /^#{keyword}(?<tag>(\s|=|-)?)\s*(?<command>(\S)+)\s+(?<statement>.*)$/
8
+ /^#{keyword}(?<tag>(\s|=|-|>)?(=|-|>)?)\s*(?<command>(\S)+)\s+(?<statement>.*)$/
9
9
  }
10
10
 
11
11
  attr_reader :contents, :keyword, :stack
12
12
 
13
- def initialize(contents, options = {})
13
+ def initialize(contents, keyword: DEFAULT_KEYWORD, document_path: nil)
14
+ @document_path = document_path
14
15
  @contents = contents
15
16
  @original = contents.dup
16
- @keyword = options[:keyword] || DEFAULT_KEYWORD
17
+ @keyword = keyword
17
18
  @stack = []
18
19
  partition
19
20
  end
@@ -42,7 +43,7 @@ module Rundoc
42
43
  @stack << head unless head.empty?
43
44
  unless code.empty?
44
45
  match = code.match(CODEBLOCK_REGEX)
45
- @stack << CodeSection.new(match, keyword: keyword)
46
+ @stack << CodeSection.new(match, keyword: keyword, document_path: @document_path)
46
47
  end
47
48
  @contents = tail
48
49
  end
@@ -0,0 +1,282 @@
1
+ require 'parslet'
2
+
3
+ module Rundoc
4
+ class PegParser < Parslet::Parser
5
+ rule(:spaces) { match('\s').repeat(1) }
6
+ rule(:spaces?) { spaces.maybe }
7
+ rule(:comma) { spaces? >> str(',') >> spaces? }
8
+ rule(:digit) { match('[0-9]') }
9
+ rule(:lparen) { str('(') >> spaces? }
10
+ rule(:rparen) { str(')') }
11
+ rule(:newline) { str("\r").maybe >> str("\n") }
12
+
13
+ rule(:singlequote_string) {
14
+ str("'") >> (
15
+ str("'").absnt? >> any
16
+ ).repeat.as(:string) >>
17
+ str("'") >> spaces?
18
+ }
19
+ rule(:doublequote_string) {
20
+ str('"') >> (
21
+ str('"').absnt? >> any
22
+ ).repeat.as(:string) >>
23
+ str('"') >> spaces?
24
+ }
25
+ rule(:string) { doublequote_string | singlequote_string }
26
+
27
+ rule(:number) {
28
+ (
29
+ str('-').maybe >> (
30
+ str('0') | (match('[1-9]') >> digit.repeat)
31
+ ) >> (
32
+ str('.') >> digit.repeat(1)
33
+ ).maybe >> (
34
+ match('[eE]') >> (str('+') | str('-')).maybe >> digit.repeat(1)
35
+ ).maybe
36
+ ).as(:number)
37
+ }
38
+
39
+ rule(:value) {
40
+ string |
41
+ number |
42
+ str('true').as(:true) |
43
+ str('false').as(:false) |
44
+ str('nil').as(:nil)
45
+ }
46
+
47
+ rule(:key) {
48
+ spaces? >> (
49
+ str(':').absent? >> match('\s').absent? >> any
50
+ ).repeat.as(:key) >> str(':') >> spaces?
51
+ }
52
+
53
+ rule(:key_value) {
54
+ (
55
+ key >> value.as(:val)
56
+ ).as(:key_value) >> spaces?
57
+ }
58
+
59
+ rule(:named_args) {
60
+ spaces? >> (
61
+ key_value >> (comma >> key_value).repeat
62
+ ).as(:named_args) >>
63
+ spaces?
64
+ }
65
+
66
+ rule(:unquoted_string) {
67
+ (newline.absent? >> any).repeat.as(:string) #>> newline
68
+ }
69
+
70
+ rule(:positional_args) {
71
+ spaces? >> value.as(:val) >> (comma >> value.as(:val)).repeat >>
72
+ comma.maybe >> named_args.maybe
73
+ }
74
+
75
+ rule(:args) {
76
+ (positional_args | named_args | string | unquoted_string)
77
+ }
78
+
79
+ rule(:funcall) {
80
+ spaces? >> (newline.absent? >> match('[^ \(\)]')).repeat(1).as(:funcall)
81
+ }
82
+
83
+ rule(:parens_method) {
84
+ funcall >> lparen >>
85
+ args.as(:args) >>
86
+ rparen
87
+ }
88
+
89
+ rule(:seattle_method) {
90
+ funcall >> spaces >>
91
+ (args).as(:args)
92
+ }
93
+
94
+ rule(:no_args_method) {
95
+ spaces? >> ( lparen.absent? >> rparen.absent? >> spaces.absent? >> any).repeat(1)
96
+ }
97
+
98
+ rule(:method_call) {
99
+ (parens_method | seattle_method | no_args_method).as(:method_call)
100
+ }
101
+
102
+ # >>
103
+ # >-
104
+ # ->
105
+ # --
106
+ rule(:visability) {
107
+ (
108
+ match('>|-').maybe.as(:vis_command) >> match('>|-').maybe.as(:vis_result)
109
+ ).as(:visability)
110
+ }
111
+
112
+ # :::
113
+ rule(:start_command) {
114
+ match(/\A:/) >> str('::')
115
+ }
116
+
117
+ # :::>> $ cat foo.rb
118
+ rule(:command) {
119
+ (
120
+ start_command >>
121
+ visability.as(:cmd_visability) >> spaces? >>
122
+ method_call.as(:cmd_method_call) >> newline.maybe #>> match(/\z/)
123
+ ).as(:command)
124
+ }
125
+
126
+ # :::>> file.write hello.txt
127
+ # world
128
+ rule(:command_with_stdin) {
129
+ command >>
130
+ (
131
+ start_command.absent? >>
132
+ code_fence.absent? >>
133
+ any
134
+ ).repeat(1).as(:stdin) |
135
+ command
136
+ }
137
+
138
+
139
+ # :::>> file.write hello.txt
140
+ # world
141
+ # :::>> file.write foo.txt
142
+ # bar
143
+ rule(:multiple_commands) {
144
+ (command_with_stdin | command).repeat
145
+ }
146
+
147
+ rule(:code_fence) {
148
+ match(/\A`/) >> str("``")
149
+ }
150
+
151
+ rule(:fenced_commands) {
152
+ code_fence >>
153
+ match('\S').repeat >>
154
+ newline >>
155
+ multiple_commands >>
156
+ code_fence >> newline
157
+ }
158
+
159
+ rule(:raw_code) {
160
+ (start_command.absent? >> command.absent? >> any).repeat(1).as(:raw_code) >>
161
+ multiple_commands.maybe
162
+ }
163
+
164
+ rule(:code_block) {
165
+ raw_code | multiple_commands
166
+ }
167
+ end
168
+ end
169
+
170
+
171
+ module Rundoc
172
+ class PegTransformer < Parslet::Transform
173
+ rule(nill: simple(:nu)) { nil }
174
+ rule(true: simple(:tr)) { true }
175
+ rule(false: simple(:fa)) { false }
176
+ rule(string: simple(:st)) { st.to_s }
177
+
178
+ rule(:number => simple(:nb)) {
179
+ nb.match(/[eE\.]/) ? Float(nb) : Integer(nb)
180
+ }
181
+
182
+ def self.convert_named_args(na)
183
+ (na.is_a?(Array) ? na : [ na ]).each_with_object({}) { |element, hash|
184
+ key = element[:key_value][:key].to_sym
185
+ val = element[:key_value][:val]
186
+ hash[key] = val
187
+ }
188
+ end
189
+
190
+ rule(:named_args => subtree(:na)) {
191
+ PegTransformer.convert_named_args(na)
192
+ }
193
+
194
+ rule(val: simple(:val)) {
195
+ val
196
+ }
197
+
198
+ # Handle the case where there is only one value
199
+ rule(val: simple(:val), named_args: subtree(:na)) {
200
+ [val, PegTransformer.convert_named_args(na)]
201
+ }
202
+
203
+ rule(method_call: subtree(:mc)) {
204
+ if mc.is_a?(::Parslet::Slice)
205
+ keyword = mc.to_sym
206
+ args = nil
207
+ else
208
+ keyword = mc[:funcall].to_sym
209
+ args = mc[:args]
210
+ end
211
+
212
+ Rundoc.code_command_from_keyword(keyword, args)
213
+ }
214
+
215
+ class Visability
216
+ attr_reader :command, :result
217
+ alias :command? :command
218
+ alias :result? :result
219
+ def initialize(command:, result:)
220
+ @command = command
221
+ @result = result
222
+ end
223
+ end
224
+
225
+ class TransformError < ::StandardError
226
+ attr_reader :line_and_column
227
+ def initialize(message: , line_and_column:)
228
+ @line_and_column = line_and_column || [1, 1]
229
+ super message
230
+ end
231
+ end
232
+
233
+ rule(:visability => {
234
+ vis_command: simple(:command),
235
+ vis_result: simple(:result)
236
+ }) {
237
+ if result.nil? || command.nil?
238
+ line_and_column = command&.line_and_column
239
+ line_and_column ||= result&.line_and_column
240
+
241
+ message = +""
242
+ message << "You attempted to use a command that does not begin with two visibility indicators. Please replace: "
243
+ message << "`:::#{command}#{result}` with `:::#{command || '-'}#{result || '-'}`"
244
+ raise TransformError.new(message: message, line_and_column: line_and_column)
245
+ end
246
+ Visability.new(
247
+ command: command.to_s == '>'.freeze,
248
+ result: result.to_s == '>'.freeze
249
+ )
250
+ }
251
+
252
+ rule(
253
+ cmd_visability: simple(:cmd_vis), # Visibility#new
254
+ cmd_method_call: simple(:code_command) # Rundoc::CodeCommand#new
255
+ ) {
256
+ code_command.render_command = cmd_vis.command?
257
+ code_command.render_result = cmd_vis.result?
258
+ code_command
259
+ }
260
+
261
+ rule(command: simple(:code_command)) {
262
+ code_command
263
+ }
264
+
265
+ rule(command: simple(:code_command), stdin: simple(:str)) {
266
+ code_command << str
267
+ code_command
268
+ }
269
+
270
+ # The lines before a CodeCommand are rendered
271
+ # without running any code
272
+ rule(raw_code: simple(:raw_code)) {
273
+ CodeCommand::Raw.new(raw_code)
274
+ }
275
+
276
+ # Sometimes
277
+ rule(raw_code: sequence(:raw_code)) {
278
+ hidden = raw_code.nil? || raw_code.empty?
279
+ CodeCommand::Raw.new(raw_code, visible: !hidden)
280
+ }
281
+ end
282
+ end