mdtoc 0.1.3

Sign up to get free protection for your applications and to get access to all the features.
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: []