rundoc 0.0.1 → 1.1.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.
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