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.
- checksums.yaml +4 -4
- data/.github/workflows/check_changelog.yml +13 -0
- data/.gitignore +7 -0
- data/CHANGELOG.md +27 -0
- data/Dockerfile +24 -0
- data/README.md +221 -100
- data/bin/rundoc +4 -68
- data/lib/rundoc.rb +1 -0
- data/lib/rundoc/cli.rb +84 -0
- data/lib/rundoc/code_command.rb +3 -2
- data/lib/rundoc/code_command/background/process_spawn.rb +30 -4
- data/lib/rundoc/code_command/background/start.rb +2 -0
- data/lib/rundoc/code_command/bash.rb +3 -2
- data/lib/rundoc/code_command/bash/cd.rb +20 -2
- data/lib/rundoc/code_command/no_such_command.rb +4 -0
- data/lib/rundoc/code_command/pipe.rb +17 -4
- data/lib/rundoc/code_command/rundoc/depend_on.rb +37 -0
- data/lib/rundoc/code_command/rundoc/require.rb +41 -0
- data/lib/rundoc/code_command/rundoc_command.rb +4 -1
- data/lib/rundoc/code_command/website.rb +7 -0
- data/lib/rundoc/code_command/website/driver.rb +111 -0
- data/lib/rundoc/code_command/website/navigate.rb +29 -0
- data/lib/rundoc/code_command/website/screenshot.rb +28 -0
- data/lib/rundoc/code_command/website/visit.rb +47 -0
- data/lib/rundoc/code_section.rb +13 -4
- data/lib/rundoc/parser.rb +4 -3
- data/lib/rundoc/peg_parser.rb +1 -1
- data/lib/rundoc/version.rb +1 -1
- data/rundoc.gemspec +7 -2
- data/test/fixtures/build_logs/rundoc.md +56 -0
- data/test/fixtures/depend_on/dependency/rundoc.md +5 -0
- data/test/fixtures/depend_on/main/rundoc.md +10 -0
- data/test/fixtures/java/rundoc.md +9 -0
- data/test/fixtures/rails_4/rundoc.md +8 -5
- data/test/fixtures/rails_5/rundoc.md +17 -7
- data/test/fixtures/{rails_5_beta → rails_6}/rundoc.md +97 -88
- data/test/fixtures/require/dependency/rundoc.md +5 -0
- data/test/fixtures/require/main/rundoc.md +10 -0
- data/test/fixtures/screenshot/rundoc.md +10 -0
- data/test/rundoc/code_commands/background_test.rb +26 -0
- data/test/rundoc/code_commands/bash_test.rb +7 -0
- data/test/rundoc/code_commands/pipe_test.rb +11 -1
- data/test/rundoc/parser_test.rb +0 -1
- data/test/rundoc/peg_parser_test.rb +43 -0
- 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
|
18
|
+
class RundocThorCLI < Thor
|
19
19
|
|
20
20
|
def initialize(*args)
|
21
21
|
super
|
22
|
-
@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
|
-
|
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
|
-
|
34
|
+
RundocThorCLI.start(ARGV)
|
data/lib/rundoc.rb
CHANGED
data/lib/rundoc/cli.rb
ADDED
@@ -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
|
data/lib/rundoc/code_command.rb
CHANGED
@@ -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
|
-
@
|
51
|
+
@log_reference = log # https://twitter.com/schneems/status/1285289971083907075
|
27
52
|
|
28
|
-
@log = Pathname.new(
|
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(
|
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
|
-
|
15
|
-
|
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
|
@@ -6,10 +6,7 @@ module Rundoc
|
|
6
6
|
# ::: | tail -n 2
|
7
7
|
# => "test\ntmp.file\n"
|
8
8
|
def initialize(line)
|
9
|
-
|
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)
|