rundoc 1.0.0 → 1.1.3

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 (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)