mdtoc 0.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 +7 -0
- data/bin/mdtoc +6 -0
- data/lib/mdtoc/cli.rb +48 -0
- data/lib/mdtoc/markdown.rb +78 -0
- data/lib/mdtoc/node.rb +67 -0
- data/lib/mdtoc/version.rb +6 -0
- data/lib/mdtoc/writer.rb +68 -0
- data/lib/mdtoc.rb +40 -0
- data/test/samples/README.md +18 -0
- data/test/samples/a/c.md +2 -0
- data/test/samples/a/d/f.md +1 -0
- data/test/samples/a/e.md +1 -0
- data/test/samples/a/g/README.md +1 -0
- data/test/samples/a/g/h.md +1 -0
- data/test/samples/a/readme.md +4 -0
- data/test/test_markdown.rb +127 -0
- data/test/test_node.rb +50 -0
- metadata +171 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: ae0f7d7fa8f96a2d68073f0fbc5a0b92f8ac8182b82fbd2a7bcabb074449d525
|
4
|
+
data.tar.gz: a0af9901b92c93dc3eea21ddae16fa349f7be653945f51e9867d2f69efc1073a
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 350566fff5655c9ab17502f02992c23d04e23d844cd94a6c25788d9c745142135f3cc2a1d41e7b7cc6a691caf209edfb9f689816d2b3a1379a75f061a756fddc
|
7
|
+
data.tar.gz: 33af4aeb021e9b61bb111ab2c20904f1af90afb4bf5c62361fea8e08d3bb21fcde68a9dc45b1863035f321db4a23a367898b9648f270931394c76e3437562140
|
data/bin/mdtoc
ADDED
data/lib/mdtoc/cli.rb
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
# typed: true
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'optparse'
|
5
|
+
require 'sorbet-runtime'
|
6
|
+
require 'tempfile'
|
7
|
+
require_relative 'markdown'
|
8
|
+
require_relative 'node'
|
9
|
+
require_relative 'writer'
|
10
|
+
|
11
|
+
module Mdtoc
|
12
|
+
module CLI
|
13
|
+
class Options < T::Struct
|
14
|
+
extend T::Sig
|
15
|
+
|
16
|
+
prop :append, T::Boolean, default: false
|
17
|
+
prop :create, T::Boolean, default: false
|
18
|
+
prop :output, T.nilable(String)
|
19
|
+
prop :paths, T::Array[String], default: []
|
20
|
+
|
21
|
+
def []=(key, val)
|
22
|
+
send("#{key}=", val)
|
23
|
+
end
|
24
|
+
|
25
|
+
sig { params(args: T::Array[String]).returns(Options) }
|
26
|
+
def self.for_args(args)
|
27
|
+
parser = OptionParser.new do |parser_|
|
28
|
+
parser_.banner = "Usage: #{parser_.program_name} [options] files or directories..."
|
29
|
+
parser_.on('-h', '--help', 'Show this message') do
|
30
|
+
puts parser_
|
31
|
+
exit
|
32
|
+
end
|
33
|
+
parser_.on('-o', '--output PATH', 'Update a table of contents in the file at PATH')
|
34
|
+
parser_.on('-a', '--[no-]append', 'Append to the --output file if a <!-- mdtoc --> tag isn\'t found')
|
35
|
+
parser_.on('-c', '--[no-]create', 'Create the --output file if it does not exist')
|
36
|
+
end
|
37
|
+
|
38
|
+
options = Options.new
|
39
|
+
options.paths = parser.parse(args, into: options)
|
40
|
+
if options.paths.empty?
|
41
|
+
warn('Specify at least one file or directory to read')
|
42
|
+
exit(1)
|
43
|
+
end
|
44
|
+
options
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
# typed: true
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'sorbet-runtime'
|
5
|
+
|
6
|
+
module Mdtoc
|
7
|
+
module Markdown
|
8
|
+
class Header
|
9
|
+
extend T::Sig
|
10
|
+
|
11
|
+
sig { params(depth: Integer, label: String, url: String).void }
|
12
|
+
def initialize(depth, label, url)
|
13
|
+
if depth < 0
|
14
|
+
raise ArgumentError, "Header depth must be >= 0, but was #{depth}"
|
15
|
+
end
|
16
|
+
@depth = depth
|
17
|
+
@label = label.strip.gsub(/\s+/, ' ')
|
18
|
+
@url = url
|
19
|
+
end
|
20
|
+
|
21
|
+
sig { params(relative_to_depth: Integer).returns(T::Boolean) }
|
22
|
+
def top_level?(relative_to_depth)
|
23
|
+
@depth == relative_to_depth
|
24
|
+
end
|
25
|
+
|
26
|
+
sig { returns(String) }
|
27
|
+
def to_s
|
28
|
+
prefix = ' ' * 2 * @depth
|
29
|
+
"#{prefix}* [#{@label}](#{@url})"
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
class HeaderWithFragment < Header
|
34
|
+
sig { params(depth: Integer, label: String, url: String).void }
|
35
|
+
def initialize(depth, label, url)
|
36
|
+
url = "#{url}##{label.downcase.strip.gsub(/ /, '-').gsub(/[^\w\-_ ]/, '')}"
|
37
|
+
super
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
class Parser
|
42
|
+
extend T::Sig
|
43
|
+
|
44
|
+
sig { params(depth: Integer, url: String).void }
|
45
|
+
def initialize(depth, url)
|
46
|
+
@depth = depth
|
47
|
+
@url = url
|
48
|
+
end
|
49
|
+
|
50
|
+
sig { params(lines: T::Enumerable[String]).returns(T::Array[Header]) }
|
51
|
+
def headers(lines)
|
52
|
+
# TODO: Skip headers within multi-line comments.
|
53
|
+
# TODO: Handle --- and === style headers.
|
54
|
+
skip = T.let(false, T::Boolean)
|
55
|
+
lines.filter_map do |line|
|
56
|
+
# Skip code blocks.
|
57
|
+
if line.start_with?('```') && !T.must(line[3..]).strip.end_with?('```')
|
58
|
+
skip = !skip
|
59
|
+
end
|
60
|
+
next if skip || !line.start_with?('#')
|
61
|
+
|
62
|
+
header(line)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
private
|
67
|
+
|
68
|
+
sig { params(line: String).returns(Header) }
|
69
|
+
def header(line)
|
70
|
+
m = T.must(line.strip.match(/^(#+)\s*(.*)$/))
|
71
|
+
num_hashes = m[1]&.count('#') || 1
|
72
|
+
depth = @depth + num_hashes - 1
|
73
|
+
label = m[2] || ''
|
74
|
+
HeaderWithFragment.new(depth, label, @url)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
data/lib/mdtoc/node.rb
ADDED
@@ -0,0 +1,67 @@
|
|
1
|
+
# typed: true
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'pathname'
|
5
|
+
require 'sorbet-runtime'
|
6
|
+
require_relative 'markdown'
|
7
|
+
|
8
|
+
module Mdtoc
|
9
|
+
class Node
|
10
|
+
extend T::Helpers
|
11
|
+
extend T::Sig
|
12
|
+
abstract!
|
13
|
+
|
14
|
+
sig { params(path: String, depth: Integer).returns(Node) }
|
15
|
+
def self.for_path(path, depth = 0)
|
16
|
+
# Ensure that `path` is a relative path, so that all links are relative and therefore portable.
|
17
|
+
path = Pathname.new(path)
|
18
|
+
path = path.relative_path_from(Dir.pwd) if path.absolute?
|
19
|
+
path = path.to_s
|
20
|
+
File.directory?(path) ? DirNode.new(path, depth) : FileNode.new(path, depth)
|
21
|
+
end
|
22
|
+
|
23
|
+
sig { params(path: String, depth: Integer).void }
|
24
|
+
def initialize(path, depth)
|
25
|
+
@path = path
|
26
|
+
@depth = depth
|
27
|
+
end
|
28
|
+
|
29
|
+
sig { returns(String) }
|
30
|
+
def label
|
31
|
+
File.basename(@path, File.extname(@path)).gsub(/_+/, ' ').gsub(/\s+/, ' ').capitalize
|
32
|
+
end
|
33
|
+
|
34
|
+
sig { abstract.returns(T::Array[Mdtoc::Markdown::Header]) }
|
35
|
+
def headers; end
|
36
|
+
end
|
37
|
+
|
38
|
+
class DirNode < Node
|
39
|
+
sig { override.returns(T::Array[Mdtoc::Markdown::Header]) }
|
40
|
+
def headers
|
41
|
+
readme_path = T.let(nil, T.nilable(String))
|
42
|
+
child_headers = Dir
|
43
|
+
.each_child(@path)
|
44
|
+
.map { |path| File.join(@path, path) }
|
45
|
+
.reject { |path| readme_path = path if File.basename(path).downcase == 'readme.md' }
|
46
|
+
.sort!
|
47
|
+
.map { |path| Node.for_path(path, @depth + 1).headers }
|
48
|
+
.flatten(1)
|
49
|
+
return child_headers unless readme_path
|
50
|
+
|
51
|
+
# Include the headers from the README at the beginning.
|
52
|
+
readme_headers = FileNode.new(readme_path, @depth).headers
|
53
|
+
readme_headers.push(*child_headers)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
class FileNode < Node
|
58
|
+
sig { override.returns(T::Array[Mdtoc::Markdown::Header]) }
|
59
|
+
def headers
|
60
|
+
parser = Markdown::Parser.new(@depth, @path)
|
61
|
+
headers = parser.headers(File.foreach(@path))
|
62
|
+
return headers if headers[0]&.top_level?(@depth)
|
63
|
+
|
64
|
+
headers.unshift(Mdtoc::Markdown::Header.new(@depth, label, @path))
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
data/lib/mdtoc/writer.rb
ADDED
@@ -0,0 +1,68 @@
|
|
1
|
+
# typed: true
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module Mdtoc
|
5
|
+
module Writer
|
6
|
+
COMMENT_BEGIN = '<!-- mdtoc -->'
|
7
|
+
COMMENT_END = '<!-- mdtoc-end -->'
|
8
|
+
|
9
|
+
class << self
|
10
|
+
extend T::Sig
|
11
|
+
|
12
|
+
sig { params(toc: String, path: String, append: T::Boolean, create: T::Boolean).void }
|
13
|
+
def write(toc, path, append, create)
|
14
|
+
validate_path(path, create)
|
15
|
+
new_content = content(toc, path, append)
|
16
|
+
File.open(path, 'w') do |f|
|
17
|
+
f.write(new_content)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
sig { params(toc: String, path: String, append: T::Boolean).returns(String) }
|
24
|
+
def content(toc, path, append)
|
25
|
+
toc = "#{COMMENT_BEGIN}\n#{toc}\n#{COMMENT_END}"
|
26
|
+
|
27
|
+
begin
|
28
|
+
f = File.open(path)
|
29
|
+
rescue
|
30
|
+
# If File.open failed because the file didn't exist, then we know that --create
|
31
|
+
# was specified due to the validation in self.validate_path.
|
32
|
+
return "#{toc}\n"
|
33
|
+
end
|
34
|
+
begin
|
35
|
+
old_content = T.must(f.read)
|
36
|
+
ensure
|
37
|
+
f.close
|
38
|
+
end
|
39
|
+
|
40
|
+
if Regexp.new(Regexp.escape(COMMENT_BEGIN), Regexp::IGNORECASE).match?(old_content)
|
41
|
+
return old_content.gsub(
|
42
|
+
/#{Regexp.escape(COMMENT_BEGIN)}(.*#{Regexp.escape(COMMENT_END)})?/im, toc
|
43
|
+
)
|
44
|
+
elsif append
|
45
|
+
return "#{old_content}\n#{toc}\n"
|
46
|
+
end
|
47
|
+
|
48
|
+
warn("Could not update #{path}, because the target HTML tag \"#{COMMENT_BEGIN}\" was not found")
|
49
|
+
exit(1)
|
50
|
+
end
|
51
|
+
|
52
|
+
sig { params(path: String, create: T::Boolean).void }
|
53
|
+
def validate_path(path, create)
|
54
|
+
if path
|
55
|
+
if File.exist?(path)
|
56
|
+
unless File.file?(path)
|
57
|
+
warn("--output PATH \"#{path}\" is not a regular file")
|
58
|
+
exit
|
59
|
+
end
|
60
|
+
elsif !create
|
61
|
+
warn("--output PATH \"#{path}\" does not exist. Specify --create to create it.")
|
62
|
+
exit
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
data/lib/mdtoc.rb
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
# typed: true
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'optparse'
|
5
|
+
require 'tempfile'
|
6
|
+
require_relative 'mdtoc/cli'
|
7
|
+
require_relative 'mdtoc/markdown'
|
8
|
+
require_relative 'mdtoc/node'
|
9
|
+
require_relative 'mdtoc/writer'
|
10
|
+
|
11
|
+
module Mdtoc
|
12
|
+
class << self
|
13
|
+
extend T::Sig
|
14
|
+
|
15
|
+
sig { params(args: T::Array[String]).void }
|
16
|
+
def main(args)
|
17
|
+
options = Mdtoc::CLI::Options.for_args(args)
|
18
|
+
toc = render_toc(options.paths)
|
19
|
+
unless options.output
|
20
|
+
puts toc
|
21
|
+
return
|
22
|
+
end
|
23
|
+
|
24
|
+
Mdtoc::Writer.write(toc, T.must(options.output), options.append, options.create)
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
sig { params(paths: T::Array[String]).returns(String) }
|
30
|
+
def render_toc(paths)
|
31
|
+
paths
|
32
|
+
.map { |path| Mdtoc::Node.for_path(path).headers }
|
33
|
+
.flatten(1)
|
34
|
+
.map(&:to_s)
|
35
|
+
.join("\n")
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
Mdtoc.main(ARGV) if __FILE__ == $PROGRAM_NAME
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# Title
|
2
|
+
intro text
|
3
|
+
# Ignore this non-title
|
4
|
+
# ignore this non title
|
5
|
+
```
|
6
|
+
ignore this multi-line code block
|
7
|
+
```
|
8
|
+
```ignore this inline block```
|
9
|
+
<!-- ignore this comment -->
|
10
|
+
## 2
|
11
|
+
### 3
|
12
|
+
|
13
|
+
## 2
|
14
|
+
text
|
15
|
+
|
16
|
+
#### 4
|
17
|
+
text
|
18
|
+
## 2
|
data/test/samples/a/c.md
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
## f 2
|
data/test/samples/a/e.md
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
# e 1
|
@@ -0,0 +1 @@
|
|
1
|
+
# README 1 for g
|
@@ -0,0 +1 @@
|
|
1
|
+
# h 1
|
@@ -0,0 +1,127 @@
|
|
1
|
+
# typed: false
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "minitest/autorun"
|
5
|
+
require "mdtoc/markdown"
|
6
|
+
|
7
|
+
class TestHeader < Minitest::Test
|
8
|
+
def test_fragment_normalization
|
9
|
+
sample = [
|
10
|
+
'Spaces 1 space',
|
11
|
+
'Spaces 2 spaces',
|
12
|
+
"Spaces\t1 tab",
|
13
|
+
"Spaces\t\t2 tabs",
|
14
|
+
' Spaces leading and trailing ',
|
15
|
+
'Numbers 1234567890',
|
16
|
+
'Symbols -=~!@#$%^&',
|
17
|
+
'Symbols *()_+',
|
18
|
+
'Symbols <>?:"{}|[]\;\',./',
|
19
|
+
]
|
20
|
+
expecteds = [
|
21
|
+
'/a#spaces-1-space',
|
22
|
+
'/a#spaces--2-spaces',
|
23
|
+
'/a#spaces1-tab',
|
24
|
+
'/a#spaces2-tabs',
|
25
|
+
'/a#spaces-leading-and-trailing',
|
26
|
+
'/a#numbers-1234567890',
|
27
|
+
'/a#symbols--',
|
28
|
+
'/a#symbols-_',
|
29
|
+
'/a#symbols-',
|
30
|
+
]
|
31
|
+
actuals = sample.map do |label|
|
32
|
+
Mdtoc::Markdown::HeaderWithFragment.new(1, label, '/a').instance_variable_get(:@url)
|
33
|
+
end
|
34
|
+
|
35
|
+
expecteds.zip(actuals).each { |expected, actual| assert_equal(expected, actual) }
|
36
|
+
end
|
37
|
+
|
38
|
+
def test_invalid_depth
|
39
|
+
assert_raises(ArgumentError) do
|
40
|
+
Mdtoc::Markdown::Header.new(-1, 'a', '/a')
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def test_label_normalization
|
45
|
+
sample = [
|
46
|
+
' strip ',
|
47
|
+
"squeeze internal \t spaces",
|
48
|
+
'Don\'t change "#1?|!@#$%^&*()+ 2--',
|
49
|
+
]
|
50
|
+
expecteds = [
|
51
|
+
'strip',
|
52
|
+
"squeeze internal spaces",
|
53
|
+
'Don\'t change "#1?|!@#$%^&*()+ 2--',
|
54
|
+
]
|
55
|
+
actuals = sample.map { |label| Mdtoc::Markdown::Header.new(0, label, '/a').instance_variable_get(:@label) }
|
56
|
+
|
57
|
+
expecteds.zip(actuals).each { |expected, actual| assert_equal(expected, actual) }
|
58
|
+
end
|
59
|
+
|
60
|
+
def test_to_s_prefix
|
61
|
+
str = Mdtoc::Markdown::Header.new(3, 'a', '/a').to_s
|
62
|
+
|
63
|
+
assert_equal(' * [a](/a)', str)
|
64
|
+
end
|
65
|
+
|
66
|
+
def test_to_s_with_fragment
|
67
|
+
str = Mdtoc::Markdown::HeaderWithFragment.new(0, 'a', '/a').to_s
|
68
|
+
|
69
|
+
assert_equal('* [a](/a#a)', str)
|
70
|
+
end
|
71
|
+
|
72
|
+
def test_to_s_without_fragment
|
73
|
+
str = Mdtoc::Markdown::Header.new(0, 'a', '/a').to_s
|
74
|
+
|
75
|
+
assert_equal('* [a](/a)', str)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
class TestParser < Minitest::Test
|
80
|
+
def test_skips_multiline_code_blocks
|
81
|
+
parser = Mdtoc::Markdown::Parser.new(0, '/')
|
82
|
+
sample = <<~END
|
83
|
+
# title
|
84
|
+
```
|
85
|
+
code
|
86
|
+
# code
|
87
|
+
```
|
88
|
+
END
|
89
|
+
|
90
|
+
headers = parser.headers(sample.each_line)
|
91
|
+
|
92
|
+
assert_equal(1, headers.size)
|
93
|
+
assert_equal('title', headers[0].instance_variable_get(:@label))
|
94
|
+
end
|
95
|
+
|
96
|
+
def test_skips_inline_code_blocks
|
97
|
+
parser = Mdtoc::Markdown::Parser.new(0, '/')
|
98
|
+
sample = <<~END
|
99
|
+
```code #```
|
100
|
+
# Title
|
101
|
+
```# code```
|
102
|
+
```code```#
|
103
|
+
END
|
104
|
+
|
105
|
+
headers = parser.headers(sample.each_line)
|
106
|
+
|
107
|
+
assert_equal(1, headers.size)
|
108
|
+
assert_equal(headers[0].instance_variable_get(:@label), 'Title')
|
109
|
+
end
|
110
|
+
|
111
|
+
def test_depth
|
112
|
+
parser = Mdtoc::Markdown::Parser.new(10, '/')
|
113
|
+
sample = <<~END
|
114
|
+
# 1
|
115
|
+
## 2
|
116
|
+
### 3
|
117
|
+
#### 4
|
118
|
+
END
|
119
|
+
|
120
|
+
headers = parser.headers(sample.each_line)
|
121
|
+
|
122
|
+
assert_equal(4, headers.size)
|
123
|
+
(0..3).each do |i|
|
124
|
+
assert_equal(10 + i, headers[i].instance_variable_get(:@depth))
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
data/test/test_node.rb
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
# typed: false
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "minitest/autorun"
|
5
|
+
require "mdtoc/node"
|
6
|
+
|
7
|
+
class TestNode < Minitest::Test
|
8
|
+
SAMPLE_DIR = File.join(File.dirname(__FILE__), 'samples')
|
9
|
+
|
10
|
+
def test_dir
|
11
|
+
expected = <<~END
|
12
|
+
* [readme 1](test/samples/a/readme.md#readme-1)
|
13
|
+
* [readme 2](test/samples/a/readme.md#readme-2)
|
14
|
+
* [readme 3](test/samples/a/readme.md#readme-3)
|
15
|
+
* [readme 4](test/samples/a/readme.md#readme-4)
|
16
|
+
* [c 1](test/samples/a/c.md#c-1)
|
17
|
+
* [c 2](test/samples/a/c.md#c-2)
|
18
|
+
* [F](test/samples/a/d/f.md)
|
19
|
+
* [f 2](test/samples/a/d/f.md#f-2)
|
20
|
+
* [e 1](test/samples/a/e.md#e-1)
|
21
|
+
* [README 1 for g](test/samples/a/g/README.md#readme-1-for-g)
|
22
|
+
* [h 1](test/samples/a/g/h.md#h-1)
|
23
|
+
END
|
24
|
+
node = Mdtoc::Node.for_path(sample_path('a'))
|
25
|
+
actual = node.headers.join("\n") + "\n"
|
26
|
+
|
27
|
+
assert_equal(expected, actual)
|
28
|
+
end
|
29
|
+
|
30
|
+
def test_file
|
31
|
+
expected = <<~END
|
32
|
+
* [Title](test/samples/README.md#title)
|
33
|
+
* [2](test/samples/README.md#2)
|
34
|
+
* [3](test/samples/README.md#3)
|
35
|
+
* [2](test/samples/README.md#2)
|
36
|
+
* [4](test/samples/README.md#4)
|
37
|
+
* [2](test/samples/README.md#2)
|
38
|
+
END
|
39
|
+
node = Mdtoc::Node.for_path(sample_path('README.md'))
|
40
|
+
actual = node.headers.join("\n") + "\n"
|
41
|
+
|
42
|
+
assert_equal(expected, actual)
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
def sample_path(path)
|
48
|
+
File.join('test/samples', path)
|
49
|
+
end
|
50
|
+
end
|
metadata
ADDED
@@ -0,0 +1,171 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: mdtoc
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.3
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- andornaut
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2020-11-02 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: minitest
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '5'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '5'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: sorbet
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rubocop
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0.86'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0.86'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rubocop-shopify
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: 1.0.4
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: 1.0.4
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: rubocop-sorbet
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0.5'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0.5'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: unparser
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - "~>"
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: 0.4.9
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - "~>"
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: 0.4.9
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: sorbet-runtime
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - ">="
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0'
|
118
|
+
type: :runtime
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - ">="
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '0'
|
125
|
+
description:
|
126
|
+
email:
|
127
|
+
executables:
|
128
|
+
- mdtoc
|
129
|
+
extensions: []
|
130
|
+
extra_rdoc_files: []
|
131
|
+
files:
|
132
|
+
- bin/mdtoc
|
133
|
+
- lib/mdtoc.rb
|
134
|
+
- lib/mdtoc/cli.rb
|
135
|
+
- lib/mdtoc/markdown.rb
|
136
|
+
- lib/mdtoc/node.rb
|
137
|
+
- lib/mdtoc/version.rb
|
138
|
+
- lib/mdtoc/writer.rb
|
139
|
+
- test/samples/README.md
|
140
|
+
- test/samples/a/c.md
|
141
|
+
- test/samples/a/d/f.md
|
142
|
+
- test/samples/a/e.md
|
143
|
+
- test/samples/a/g/README.md
|
144
|
+
- test/samples/a/g/h.md
|
145
|
+
- test/samples/a/readme.md
|
146
|
+
- test/test_markdown.rb
|
147
|
+
- test/test_node.rb
|
148
|
+
homepage: https://github.com/andornaut/mdtoc
|
149
|
+
licenses:
|
150
|
+
- MIT
|
151
|
+
metadata: {}
|
152
|
+
post_install_message:
|
153
|
+
rdoc_options: []
|
154
|
+
require_paths:
|
155
|
+
- lib
|
156
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
157
|
+
requirements:
|
158
|
+
- - ">="
|
159
|
+
- !ruby/object:Gem::Version
|
160
|
+
version: 2.7.2
|
161
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
162
|
+
requirements:
|
163
|
+
- - ">="
|
164
|
+
- !ruby/object:Gem::Version
|
165
|
+
version: '0'
|
166
|
+
requirements: []
|
167
|
+
rubygems_version: 3.1.4
|
168
|
+
signing_key:
|
169
|
+
specification_version: 4
|
170
|
+
summary: Read Markdown files and output a table of contents
|
171
|
+
test_files: []
|