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 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
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+ # typed: true
3
+ # frozen_string_literal: true
4
+
5
+ require 'mdtoc'
6
+ Mdtoc.main(ARGV)
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
@@ -0,0 +1,6 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module Mdtoc
5
+ VERSION = '0.1.3'
6
+ end
@@ -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
@@ -0,0 +1,2 @@
1
+ # c 1
2
+ ## c 2
@@ -0,0 +1 @@
1
+ ## f 2
@@ -0,0 +1 @@
1
+ # e 1
@@ -0,0 +1 @@
1
+ # README 1 for g
@@ -0,0 +1 @@
1
+ # h 1
@@ -0,0 +1,4 @@
1
+ # readme 1
2
+ ## readme 2
3
+ ### readme 3
4
+ #### readme 4
@@ -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: []