rundoc 1.0.0 → 1.1.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/check_changelog.yml +13 -0
  3. data/.gitignore +7 -0
  4. data/CHANGELOG.md +27 -0
  5. data/Dockerfile +24 -0
  6. data/README.md +221 -100
  7. data/bin/rundoc +4 -68
  8. data/lib/rundoc.rb +1 -0
  9. data/lib/rundoc/cli.rb +84 -0
  10. data/lib/rundoc/code_command.rb +3 -2
  11. data/lib/rundoc/code_command/background/process_spawn.rb +30 -4
  12. data/lib/rundoc/code_command/background/start.rb +2 -0
  13. data/lib/rundoc/code_command/bash.rb +3 -2
  14. data/lib/rundoc/code_command/bash/cd.rb +20 -2
  15. data/lib/rundoc/code_command/no_such_command.rb +4 -0
  16. data/lib/rundoc/code_command/pipe.rb +17 -4
  17. data/lib/rundoc/code_command/rundoc/depend_on.rb +37 -0
  18. data/lib/rundoc/code_command/rundoc/require.rb +41 -0
  19. data/lib/rundoc/code_command/rundoc_command.rb +4 -1
  20. data/lib/rundoc/code_command/website.rb +7 -0
  21. data/lib/rundoc/code_command/website/driver.rb +111 -0
  22. data/lib/rundoc/code_command/website/navigate.rb +29 -0
  23. data/lib/rundoc/code_command/website/screenshot.rb +28 -0
  24. data/lib/rundoc/code_command/website/visit.rb +47 -0
  25. data/lib/rundoc/code_section.rb +13 -4
  26. data/lib/rundoc/parser.rb +4 -3
  27. data/lib/rundoc/peg_parser.rb +1 -1
  28. data/lib/rundoc/version.rb +1 -1
  29. data/rundoc.gemspec +7 -2
  30. data/test/fixtures/build_logs/rundoc.md +56 -0
  31. data/test/fixtures/depend_on/dependency/rundoc.md +5 -0
  32. data/test/fixtures/depend_on/main/rundoc.md +10 -0
  33. data/test/fixtures/java/rundoc.md +9 -0
  34. data/test/fixtures/rails_4/rundoc.md +8 -5
  35. data/test/fixtures/rails_5/rundoc.md +17 -7
  36. data/test/fixtures/{rails_5_beta → rails_6}/rundoc.md +97 -88
  37. data/test/fixtures/require/dependency/rundoc.md +5 -0
  38. data/test/fixtures/require/main/rundoc.md +10 -0
  39. data/test/fixtures/screenshot/rundoc.md +10 -0
  40. data/test/rundoc/code_commands/background_test.rb +26 -0
  41. data/test/rundoc/code_commands/bash_test.rb +7 -0
  42. data/test/rundoc/code_commands/pipe_test.rb +11 -1
  43. data/test/rundoc/parser_test.rb +0 -1
  44. data/test/rundoc/peg_parser_test.rb +43 -0
  45. metadata +90 -11
data/bin/rundoc CHANGED
@@ -15,12 +15,11 @@ $: << File.expand_path(File.dirname(File.realpath(__FILE__)) + '/../lib')
15
15
  require 'rundoc'
16
16
  require 'thor'
17
17
 
18
- class RundocCLI < Thor
18
+ class RundocThorCLI < Thor
19
19
 
20
20
  def initialize(*args)
21
21
  super
22
- @path = options[:path]
23
- @working_dir = Pathname.new(File.expand_path("../", @path))
22
+ @path = options[:path]
24
23
  end
25
24
 
26
25
  default_task :help
@@ -28,71 +27,8 @@ class RundocCLI < Thor
28
27
  desc "build", "turns rundoc file into docs and a project"
29
28
  class_option :path, banner: "path/to/file.md", optional: true, default: 'rundoc.md'
30
29
  def build
31
- raise "#{@path} does not exist" unless File.exist?(@path)
32
- raise "Expecting #{@path} to be a rundoc markdown file" unless File.file?(@path)
33
- source_contents = File.read(@path)
34
- tmp_dir = @working_dir.join("tmp")
35
-
36
- FileUtils.remove_entry_secure(tmp_dir) if tmp_dir.exist?
37
- tmp_dir.mkdir
38
- banner = <<~HEREDOC
39
- <!-- STOP
40
- This file was generated by a rundoc script, do not modify it.
41
-
42
- Instead modify the rundoc script and re-run it.
43
-
44
- Command: #{ $0 } #{$*.join(' ')}
45
- STOP -->
46
- HEREDOC
47
-
48
- puts "== Running your docs"
49
- Dir.chdir(tmp_dir) do
50
- @output = Rundoc::Parser.new(source_contents, Rundoc.parser_options).to_md
51
- Rundoc.sanitize(@output)
52
- @output = "#{banner}\n#{@output}"
53
- end
54
-
55
- puts "== Done, run was successful"
56
- project_name = if Rundoc.project_root
57
- Rundoc.project_root.split('/').last
58
- else
59
- 'project'
60
- end
61
-
62
- project_dir = @working_dir.join(project_name)
63
-
64
- FileUtils.remove_entry_secure(project_dir) if project_dir.exist?
65
-
66
- cp_root = if Rundoc.project_root
67
- tmp_dir.join(Rundoc.project_root, ".")
68
- else
69
- tmp_dir.join(".")
70
- end
71
-
72
- FileUtils.cp_r(cp_root, project_dir)
73
-
74
- FileUtils.remove_entry_secure(tmp_dir) if tmp_dir.exist?
75
-
76
- source_path = project_dir.join("README.md")
77
- puts "== Done, writing original source to #{source_path}"
78
- File.open(source_path, "w") { |f| f.write @output }
79
-
80
- puts "== Copying source"
81
- source_path = project_dir.join("coppied-#{@path.split('/').last}")
82
- File.open(source_path, "w") { |f| f.write source_contents }
83
-
84
- Dir.chdir(project_dir) do
85
- Rundoc.run_after_build
86
- end
87
-
88
- ensure
89
- Rundoc::CodeCommand::Background::ProcessSpawn.tasks.each do |name, task|
90
- next unless task.alive?
91
-
92
- puts "Warning background task is still running, cleaning up: name: #{name}"
93
- task.stop
94
- end
30
+ Rundoc::CLI.new.build(path: @path)
95
31
  end
96
32
  end
97
33
 
98
- RundocCLI.start(ARGV)
34
+ RundocThorCLI.start(ARGV)
@@ -87,3 +87,4 @@ require 'rundoc/parser'
87
87
  require 'rundoc/code_section'
88
88
  require 'rundoc/code_command'
89
89
  require 'rundoc/peg_parser'
90
+ require 'rundoc/cli'
@@ -0,0 +1,84 @@
1
+ module Rundoc
2
+ class CLI
3
+ def build(path: )
4
+ @path = Pathname.new(path).expand_path
5
+ raise "#{@path} does not exist" unless File.exist?(@path)
6
+ raise "Expecting #{@path} to be a rundoc markdown file" unless File.file?(@path)
7
+ @working_dir = Pathname.new(File.expand_path("../", @path))
8
+
9
+
10
+ dot_env_path = File.expand_path("../.env", @path)
11
+ if File.exist?(dot_env_path)
12
+ require 'dotenv'
13
+ Dotenv.load(dot_env_path)
14
+ ENV['AWS_ACCESS_KEY_ID'] ||= ENV['BUCKETEER_AWS_ACCESS_KEY_ID']
15
+ ENV['AWS_REGION'] ||= ENV['BUCKETEER_AWS_REGION']
16
+ ENV['AWS_SECRET_ACCESS_KEY'] ||= ENV['BUCKETEER_AWS_SECRET_ACCESS_KEY']
17
+ ENV['AWS_BUCKET_NAME'] ||= ENV['BUCKETEER_BUCKET_NAME']
18
+ end
19
+
20
+ source_contents = File.read(@path)
21
+ tmp_dir = @working_dir.join("tmp")
22
+
23
+ FileUtils.remove_entry_secure(tmp_dir) if tmp_dir.exist?
24
+ tmp_dir.mkdir
25
+ banner = <<~HEREDOC
26
+ <!-- STOP
27
+ This file was generated by a rundoc script, do not modify it.
28
+
29
+ Instead modify the rundoc script and re-run it.
30
+
31
+ Command: #{ $0 } #{$*.join(' ')}
32
+ STOP -->
33
+ HEREDOC
34
+
35
+ puts "== Running your docs"
36
+ Dir.chdir(tmp_dir) do
37
+ @output = Rundoc::Parser.new(source_contents, document_path: @path).to_md
38
+ Rundoc.sanitize(@output)
39
+ @output = "#{banner}\n#{@output}"
40
+ end
41
+
42
+ puts "== Done, run was successful"
43
+ project_name = if Rundoc.project_root
44
+ Rundoc.project_root.split('/').last
45
+ else
46
+ 'project'
47
+ end
48
+
49
+ project_dir = @working_dir.join(project_name)
50
+
51
+ FileUtils.remove_entry_secure(project_dir) if project_dir.exist?
52
+
53
+ cp_root = if Rundoc.project_root
54
+ tmp_dir.join(Rundoc.project_root, ".")
55
+ else
56
+ tmp_dir.join(".")
57
+ end
58
+
59
+ FileUtils.cp_r(cp_root, project_dir)
60
+
61
+ FileUtils.remove_entry_secure(tmp_dir) if tmp_dir.exist?
62
+
63
+ source_path = project_dir.join("README.md")
64
+ puts "== Done, writing original source to #{source_path}"
65
+ File.open(source_path, "w") { |f| f.write @output }
66
+
67
+ puts "== Copying source"
68
+ source_path = project_dir.join("coppied-#{@path.to_s.split('/').last}")
69
+ File.open(source_path, "w") { |f| f.write source_contents }
70
+
71
+ Dir.chdir(project_dir) do
72
+ Rundoc.run_after_build
73
+ end
74
+
75
+ ensure
76
+ Rundoc::CodeCommand::Background::ProcessSpawn.tasks.each do |name, task|
77
+ next unless task.alive?
78
+
79
+ puts "Warning background task is still running, cleaning up: name: #{name}"
80
+ task.stop
81
+ end
82
+ end
83
+ end
84
+ end
@@ -29,12 +29,12 @@ module Rundoc
29
29
  # Executes command to build project
30
30
  # Is expected to return the result of the command
31
31
  def call(env = {})
32
- raise "not implemented"
32
+ raise "not implemented on #{self.inspect}"
33
33
  end
34
34
 
35
35
  # the output of the command, i.e. `$ cat foo.txt`
36
36
  def to_md(env = {})
37
- raise "not implemented"
37
+ raise "not implemented on #{self.inspect}"
38
38
  end
39
39
  end
40
40
  end
@@ -48,3 +48,4 @@ require 'rundoc/code_command/rundoc_command'
48
48
  require 'rundoc/code_command/no_such_command'
49
49
  require 'rundoc/code_command/raw'
50
50
  require 'rundoc/code_command/background'
51
+ require 'rundoc/code_command/website'
@@ -1,7 +1,32 @@
1
1
  require 'shellwords'
2
2
  require 'timeout'
3
+ require 'fileutils'
3
4
 
4
5
  class Rundoc::CodeCommand::Background
6
+ # This class is responsible for running processes in the background
7
+ #
8
+ # By default it logs output to a file. This can be used to "wait" for a
9
+ # specific output before continuing:
10
+ #
11
+ # server = ProcessSpawn("rails server")
12
+ # server.wait("Use Ctrl-C to stop")
13
+ #
14
+ # The process can be queried for it's status to check if it is still booted or not.
15
+ # the process can also be manually stopped:
16
+ #
17
+ # server = ProcessSpawn("rails server")
18
+ # server.alive? # => true
19
+ # server.stop
20
+ # server.alive? # => false
21
+ #
22
+ #
23
+ # There are class level methods that can be used to "name" and record
24
+ # background processes. They can be used like this:
25
+ #
26
+ # server = ProcessSpawn("rails server")
27
+ # ProcessSpawn.add("muh_server", server)
28
+ # ProcessSpawn.find("muh_server") # => <# ProcessSpawn instance >
29
+ # ProcessSpawn.find("foo") # => RuntimeError "Could not find task with name 'foo', ..."
5
30
  class ProcessSpawn
6
31
  def self.tasks
7
32
  @tasks
@@ -23,10 +48,11 @@ class Rundoc::CodeCommand::Background
23
48
  def initialize(command , timeout: 5, log: Tempfile.new("log"), out: "2>&1")
24
49
  @command = command
25
50
  @timeout_value = timeout
26
- @log = log
51
+ @log_reference = log # https://twitter.com/schneems/status/1285289971083907075
27
52
 
28
- @log = Pathname.new(@log)
53
+ @log = Pathname.new(log)
29
54
  @log.dirname.mkpath
55
+ FileUtils.touch(@log)
30
56
 
31
57
  @command = "/usr/bin/env bash -c #{@command.shellescape} >> #{@log} #{out}"
32
58
  @pid = nil
@@ -55,7 +81,7 @@ class Rundoc::CodeCommand::Background
55
81
 
56
82
  def stop
57
83
  return unless alive?
58
- Process.kill('TERM', @pid)
84
+ Process.kill('TERM', -Process.getpgid(@pid))
59
85
  Process.wait(@pid)
60
86
  end
61
87
 
@@ -64,7 +90,7 @@ class Rundoc::CodeCommand::Background
64
90
  end
65
91
 
66
92
  private def call
67
- @pid ||= Process.spawn(@command)
93
+ @pid ||= Process.spawn(@command, pgroup: true)
68
94
  end
69
95
  end
70
96
  end
@@ -7,6 +7,7 @@ class Rundoc::CodeCommand::Background
7
7
  @name = name
8
8
  @wait = wait
9
9
  @allow_fail = allow_fail
10
+ FileUtils.touch(log)
10
11
 
11
12
  @spawn = ProcessSpawn.new(
12
13
  @command,
@@ -24,6 +25,7 @@ class Rundoc::CodeCommand::Background
24
25
  def call(env = {})
25
26
  @spawn.wait(@wait)
26
27
  @spawn.check_alive! unless @allow_fail
28
+
27
29
  @spawn.log.read
28
30
  end
29
31
 
@@ -32,12 +32,13 @@ class Rundoc::CodeCommand::Bash < Rundoc::CodeCommand
32
32
  end
33
33
 
34
34
  def shell(cmd, stdin = nil)
35
+ cmd = "(#{cmd}) 2>&1"
35
36
  msg = "Running: $ '#{cmd}'"
36
37
  msg << " with stdin: '#{stdin.inspect}'" if stdin && !stdin.empty?
37
38
  puts msg
38
39
 
39
40
  result = ""
40
- IO.popen("#{cmd} 2>&1", "w+") do |io|
41
+ IO.popen(cmd, "w+") do |io|
41
42
  io << stdin if stdin
42
43
  io.close_write
43
44
  result = sanitize_escape_chars io.read
@@ -54,4 +55,4 @@ Rundoc.register_code_command(:bash, Rundoc::CodeCommand::Bash)
54
55
  Rundoc.register_code_command(:'$', Rundoc::CodeCommand::Bash)
55
56
  Rundoc.register_code_command(:'fail.$', Rundoc::CodeCommand::Bash)
56
57
 
57
- require 'rundoc/code_command/bash/cd'
58
+ require 'rundoc/code_command/bash/cd'
@@ -8,11 +8,29 @@ class Rundoc::CodeCommand::Bash
8
8
  @line = line
9
9
  end
10
10
 
11
+ # Ignore duplicate chdir warnings "warning: conflicting chdir during another chdir block"
12
+ def supress_chdir_warning
13
+ old_stderr = $stderr
14
+ capture_stderr = StringIO.new
15
+ $stderr = capture_stderr
16
+ return yield
17
+ ensure
18
+ if old_stderr
19
+ $stderr = old_stderr
20
+ capture_string = capture_stderr.string
21
+ $stderr.puts capture_string if capture_string.each_line.count > 1 || !capture_string.include?("conflicting chdir")
22
+ end
23
+ end
24
+
11
25
  def call(env)
12
26
  line = @line.sub('cd', '').strip
13
27
  puts "running $ cd #{line}"
14
- Dir.chdir(line)
15
- nil
28
+
29
+ supress_chdir_warning do
30
+ Dir.chdir(line)
31
+ end
32
+
33
+ return nil
16
34
  end
17
35
  end
18
36
  end
@@ -1,6 +1,10 @@
1
1
  module Rundoc
2
2
  class CodeCommand
3
3
  class NoSuchCommand < Rundoc::CodeCommand
4
+
5
+ def call(env = {})
6
+ raise "No such command registered with rundoc: #{@keyword.inspect} for '#{@keyword} #{@original_args}'"
7
+ end
4
8
  end
5
9
  end
6
10
  end
@@ -6,10 +6,7 @@ module Rundoc
6
6
  # ::: | tail -n 2
7
7
  # => "test\ntmp.file\n"
8
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)
9
+ @delegate = parse(line)
13
10
  end
14
11
 
15
12
  # before: "",
@@ -27,6 +24,22 @@ module Rundoc
27
24
  def to_md(env = {})
28
25
  ""
29
26
  end
27
+
28
+ private def parse(code)
29
+ parser = Rundoc::PegParser.new.method_call
30
+ tree = parser.parse(code)
31
+ actual = Rundoc::PegTransformer.new.apply(tree)
32
+
33
+ actual = actual.first if actual.is_a?(Array)
34
+
35
+ actual = Rundoc::CodeCommand::Bash.new(code) if actual.is_a?(Rundoc::CodeCommand::NoSuchCommand)
36
+ return actual
37
+
38
+ # Since `| tail -n 2` does not start with a `$` assume any "naked" commands
39
+ # are bash
40
+ rescue Parslet::ParseFailed
41
+ Rundoc::CodeCommand::Bash.new(code)
42
+ end
30
43
  end
31
44
  end
32
45
  end
@@ -0,0 +1,37 @@
1
+ class ::Rundoc::CodeCommand
2
+ class RundocCommand
3
+ class DependOn < ::Rundoc::CodeCommand
4
+
5
+ # Pass in the relative path of another rundoc document in order to
6
+ # run all of it's commands (but not to )
7
+ def initialize(path)
8
+ raise "Path must be relative (i.e. start with `.` or `..`. #{path.inspect} does not" unless path.start_with?(".")
9
+ @path = Pathname.new(path)
10
+ end
11
+
12
+ def to_md(env = {})
13
+ ""
14
+ end
15
+
16
+ def call(env = {})
17
+ current_path = Pathname.new(env[:document_path]).dirname
18
+ document_path = @path.expand_path(current_path)
19
+ # Run the commands, but do not
20
+ puts "rundoc.depend_on: Start executing #{@path.to_s.inspect}"
21
+ output = Rundoc::Parser.new(document_path.read, document_path: document_path.to_s).to_md
22
+ puts "rundoc.depend_on: Done executing #{@path.to_s.inspect}, discarding intermediate document"
23
+ output
24
+ end
25
+
26
+ def hidden?
27
+ true
28
+ end
29
+
30
+ def not_hidden?
31
+ !hidden?
32
+ end
33
+ end
34
+ end
35
+ end
36
+
37
+ Rundoc.register_code_command(:"rundoc.depend_on", ::Rundoc::CodeCommand::RundocCommand::DependOn)
@@ -0,0 +1,41 @@
1
+ class ::Rundoc::CodeCommand
2
+ class RundocCommand
3
+ class Require < ::Rundoc::CodeCommand
4
+
5
+ # Pass in the relative path of another rundoc document in order to
6
+ # run all of it's commands. Resulting contents will be displayed
7
+ # in current document
8
+ def initialize(path)
9
+ raise "Path must be relative (i.e. start with `.` or `..`. #{path.inspect} does not" unless path.start_with?(".")
10
+ @path = Pathname.new(path)
11
+ end
12
+
13
+ def to_md(env = {})
14
+ ""
15
+ end
16
+
17
+ def call(env = {})
18
+ env[:replace] ||= String.new
19
+ current_path = Pathname.new(env[:document_path]).dirname
20
+ document_path = @path.expand_path(current_path)
21
+
22
+ puts "rundoc.require: Start executing #{@path.to_s.inspect}"
23
+ output = Rundoc::Parser.new(document_path.read, document_path: document_path.to_s).to_md
24
+ puts "rundoc.require: Done executing #{@path.to_s.inspect}, putting contents into document"
25
+
26
+ env[:replace] << output
27
+ return ""
28
+ end
29
+
30
+ def hidden?
31
+ true
32
+ end
33
+
34
+ def not_hidden?
35
+ !hidden?
36
+ end
37
+ end
38
+ end
39
+ end
40
+
41
+ Rundoc.register_code_command(:"rundoc.require", ::Rundoc::CodeCommand::RundocCommand::Require)