mdtoc 0.2.0 → 0.3.1
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/LICENSE +21 -0
- data/README.md +112 -0
- data/bin/setup +37 -0
- data/lib/mdtoc/cli.rb +8 -8
- data/lib/mdtoc/markdown/fragment_generator.rb +50 -0
- data/lib/mdtoc/markdown/header.rb +17 -11
- data/lib/mdtoc/markdown/parser.rb +75 -16
- data/lib/mdtoc/node.rb +29 -20
- data/lib/mdtoc/version.rb +1 -1
- data/lib/mdtoc/writer.rb +18 -13
- data/lib/mdtoc.rb +5 -5
- metadata +23 -29
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: dd651a744c265508fe1b78eae797f688f83a417d0710ed70fdff838a05e8e2b9
|
|
4
|
+
data.tar.gz: bfd727b632d7d5076ef17e309e1a2715f5de971a313e74190b9e1facadcade7a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2282ca2fb424b8dc093c856c83d4c13373bc10206d0a0f6c80c05c83329d057d1b3ce2c8dcf1a68ca2a7f303df22b95fccf3a71c1ead0f4c4e6da8049477005e
|
|
7
|
+
data.tar.gz: 714e58db5f76da82a910398f308698ed049bd56c9632b85d7131465d04d247ac57a0648b718f28b37dfa86be275535b0aee31a0cf5362a4f86523ced23037151
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2020 andornaut
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# mdtoc - Markdown Table of Contents
|
|
2
|
+
|
|
3
|
+
Read Markdown files and output a table of contents.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
Requirements:
|
|
8
|
+
|
|
9
|
+
* [Ruby](https://www.ruby-lang.org/en/) (see [.ruby-version](./.ruby-version))
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
gem install mdtoc
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Usage
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
$ mdtoc --help
|
|
19
|
+
Usage: mdtoc [options] files or directories...
|
|
20
|
+
-h, --help Show this message
|
|
21
|
+
-o, --output PATH Update a table of contents in the file at PATH
|
|
22
|
+
-a, --[no-]append Append to the --output file if a <!-- mdtoc --> tag isn't found
|
|
23
|
+
-c, --[no-]create Create the --output file if it does not exist
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
1. Add a `<!-- mdtoc -->` tag to a Markdown file.
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
echo '<!-- mdtoc -->' >> README.md
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
2. Run `mdtoc` and specify input files or directories (eg. the "test/samples" directory) and an output file (eg. "README.md").
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
mdtoc -aco README.md test/samples
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Example Rakefile
|
|
39
|
+
|
|
40
|
+
Create a `Rakefile` with the contents below, then run
|
|
41
|
+
[`rake`](https://github.com/ruby/rake) to:
|
|
42
|
+
|
|
43
|
+
* `git pullgem push pkg/mdtoc-0.2.0.gem`
|
|
44
|
+
* `git add` any *.md files
|
|
45
|
+
* Run `mdtoc` to update the generated table of contents in the ./README.md file
|
|
46
|
+
* Git commit and push any changes
|
|
47
|
+
|
|
48
|
+
```ruby
|
|
49
|
+
task default: %w[mdtoc]
|
|
50
|
+
|
|
51
|
+
desc 'Update Markdown table of contents and push changes to the git repository'
|
|
52
|
+
task :mdtoc do
|
|
53
|
+
command = <<~CMD
|
|
54
|
+
set -e
|
|
55
|
+
if [ -n "$(git diff --name-only --diff-filter=U)" ]; then
|
|
56
|
+
echo 'Error: conflicts exist' >&2
|
|
57
|
+
exit 1
|
|
58
|
+
fi
|
|
59
|
+
mdtoc --append --create --output README.md docs/
|
|
60
|
+
git add *.md **/*.md
|
|
61
|
+
git commit -qm 'Update TOC' || true
|
|
62
|
+
git pull
|
|
63
|
+
git push
|
|
64
|
+
CMD
|
|
65
|
+
sh command, verbose: false do |ok, status|
|
|
66
|
+
unless ok
|
|
67
|
+
fail "Failed with status: #{status.exitstatus}"
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
See [andornaut/til](https://github.com/andornaut/til/blob/master/Rakefile) for an example.
|
|
74
|
+
|
|
75
|
+
## Development
|
|
76
|
+
|
|
77
|
+
### Setup
|
|
78
|
+
|
|
79
|
+
Requirements:
|
|
80
|
+
|
|
81
|
+
* [Bundler](https://bundler.io/)
|
|
82
|
+
* [chruby](https://github.com/postmodern/chruby) (recommended)
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
# Setup development environment
|
|
86
|
+
bin/setup
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Tasks
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
# List rake tasks
|
|
93
|
+
$ rake -T
|
|
94
|
+
rake build # Build gem into the pkg directory
|
|
95
|
+
rake default # Run the build, rubocop, sorbet and test tasks
|
|
96
|
+
rake install # Build and install gem into system gems
|
|
97
|
+
rake rubocop # Run RuboCop
|
|
98
|
+
rake sorbet # Run the Sorbet type checker
|
|
99
|
+
rake test # Run tests
|
|
100
|
+
```
|
|
101
|
+
# Run mdtoc with test inputs
|
|
102
|
+
$ ruby -Ilib bin/mdtoc test/samples
|
|
103
|
+
|
|
104
|
+
# Run mdtoc with test inputs, and write to a newly created output file
|
|
105
|
+
$ f=$(mktemp) && ruby -Ilib bin/mdtoc -aco ${f} test/samples ; cat ${f}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Publishing
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
rake release
|
|
112
|
+
```
|
data/bin/setup
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require 'fileutils'
|
|
5
|
+
|
|
6
|
+
# path to your application root.
|
|
7
|
+
APP_ROOT = File.expand_path('..', __dir__)
|
|
8
|
+
|
|
9
|
+
def system!(*args)
|
|
10
|
+
system(*args) || abort("
|
|
11
|
+
== Command #{args} failed ==")
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
FileUtils.chdir APP_ROOT do
|
|
15
|
+
# This script is a way to set up a development environment or update your current one.
|
|
16
|
+
# You can run it with `bin/setup`.
|
|
17
|
+
|
|
18
|
+
puts '== Installing dependencies =='
|
|
19
|
+
system! 'gem install bundler --conservative'
|
|
20
|
+
system('bundle check') || system!('bundle install')
|
|
21
|
+
|
|
22
|
+
puts "
|
|
23
|
+
== Removing old RBIs =="
|
|
24
|
+
FileUtils.rm_rf('sorbet/rbi/gems')
|
|
25
|
+
FileUtils.rm_rf('sorbet/rbi/todo.rbi')
|
|
26
|
+
|
|
27
|
+
puts "
|
|
28
|
+
== Generating RBIs =="
|
|
29
|
+
system! 'bundle exec tapioca gems'
|
|
30
|
+
|
|
31
|
+
puts "
|
|
32
|
+
== Initializing Sorbet =="
|
|
33
|
+
system! 'bundle exec srb tc'
|
|
34
|
+
|
|
35
|
+
puts "
|
|
36
|
+
== Setup complete! =="
|
|
37
|
+
end
|
data/lib/mdtoc/cli.rb
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# typed: strict
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
|
-
require
|
|
5
|
-
require
|
|
6
|
-
require
|
|
4
|
+
require 'optparse'
|
|
5
|
+
require 'sorbet-runtime'
|
|
6
|
+
require 'tempfile'
|
|
7
7
|
|
|
8
8
|
module Mdtoc
|
|
9
9
|
module CLI
|
|
@@ -14,19 +14,19 @@ module Mdtoc
|
|
|
14
14
|
def parse(args)
|
|
15
15
|
parser = OptionParser.new do |parser_|
|
|
16
16
|
parser_.banner = "Usage: #{parser_.program_name} [options] files or directories..."
|
|
17
|
-
parser_.on(
|
|
17
|
+
parser_.on('-h', '--help', 'Show this message') do
|
|
18
18
|
puts parser_
|
|
19
19
|
exit
|
|
20
20
|
end
|
|
21
|
-
parser_.on(
|
|
22
|
-
parser_.on(
|
|
23
|
-
parser_.on(
|
|
21
|
+
parser_.on('-o', '--output PATH', 'Update a table of contents in the file at PATH')
|
|
22
|
+
parser_.on('-a', '--[no-]append', "Append to the --output file if a <!-- mdtoc --> tag isn't found")
|
|
23
|
+
parser_.on('-c', '--[no-]create', 'Create the --output file if it does not exist')
|
|
24
24
|
end
|
|
25
25
|
|
|
26
26
|
options = Options.new
|
|
27
27
|
options.paths = parser.parse(args, into: options)
|
|
28
28
|
if options.paths.empty?
|
|
29
|
-
warn(
|
|
29
|
+
warn('Specify at least one file or directory to read')
|
|
30
30
|
exit(1)
|
|
31
31
|
end
|
|
32
32
|
options
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require 'sorbet-runtime'
|
|
5
|
+
|
|
6
|
+
module Mdtoc
|
|
7
|
+
module Markdown
|
|
8
|
+
module FragmentGenerator
|
|
9
|
+
extend T::Sig
|
|
10
|
+
extend T::Helpers
|
|
11
|
+
|
|
12
|
+
interface!
|
|
13
|
+
|
|
14
|
+
sig { abstract.params(label: String).returns(String) }
|
|
15
|
+
def generate(label); end
|
|
16
|
+
|
|
17
|
+
class GitHub
|
|
18
|
+
extend T::Sig
|
|
19
|
+
include FragmentGenerator
|
|
20
|
+
|
|
21
|
+
sig { void }
|
|
22
|
+
def initialize
|
|
23
|
+
@counts = T.let(Hash.new(0), T::Hash[String, Integer])
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
sig { override.params(label: String).returns(String) }
|
|
27
|
+
def generate(label)
|
|
28
|
+
# GitHub's fragment generation:
|
|
29
|
+
# 1. Downcase
|
|
30
|
+
# 2. Replace spaces with dashes
|
|
31
|
+
# 3. Remove non-alphanumeric characters (keeping dashes, dots and underscores)
|
|
32
|
+
# 4. Collapse multiple dashes
|
|
33
|
+
# 5. Remove leading/trailing dashes and dots (common in many implementations)
|
|
34
|
+
fragment = label.downcase.tr(' ', '-').gsub(/[^\w.-]/, '')
|
|
35
|
+
fragment = fragment.gsub(/-+/, '-')
|
|
36
|
+
fragment = fragment.gsub(/^[.-]+|[.-]+$/, '')
|
|
37
|
+
|
|
38
|
+
count = @counts[fragment]
|
|
39
|
+
@counts[fragment] += 1
|
|
40
|
+
|
|
41
|
+
if count.positive?
|
|
42
|
+
"#{fragment}-#{count}"
|
|
43
|
+
else
|
|
44
|
+
fragment
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
# typed: true
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
|
-
require
|
|
4
|
+
require 'sorbet-runtime'
|
|
5
|
+
require_relative 'fragment_generator'
|
|
5
6
|
|
|
6
7
|
module Mdtoc
|
|
7
8
|
module Markdown
|
|
@@ -10,9 +11,7 @@ module Mdtoc
|
|
|
10
11
|
|
|
11
12
|
sig { params(depth: Integer, label: String, url: String).void }
|
|
12
13
|
def initialize(depth, label, url)
|
|
13
|
-
|
|
14
|
-
raise ArgumentError, "Header depth must be >= 0, but was #{depth}"
|
|
15
|
-
end
|
|
14
|
+
raise ArgumentError, "Header depth must be >= 0, but was #{depth}" if depth.negative?
|
|
16
15
|
|
|
17
16
|
@depth = depth
|
|
18
17
|
@label = normalize_label(label)
|
|
@@ -21,7 +20,7 @@ module Mdtoc
|
|
|
21
20
|
|
|
22
21
|
sig { returns(String) }
|
|
23
22
|
def to_s
|
|
24
|
-
prefix =
|
|
23
|
+
prefix = ' ' * 2 * @depth
|
|
25
24
|
"#{prefix}* [#{@label}](#{@url})"
|
|
26
25
|
end
|
|
27
26
|
|
|
@@ -33,16 +32,23 @@ module Mdtoc
|
|
|
33
32
|
private
|
|
34
33
|
|
|
35
34
|
def normalize_label(label)
|
|
36
|
-
label = label.strip.tr("\t\n\r",
|
|
37
|
-
label.gsub(/\[(
|
|
35
|
+
label = label.strip.tr("\t\n\r", '') # Remove whitespace characters other than spaces.
|
|
36
|
+
label.gsub(/\[(.*?)\]\(.*?\)/, '\1') # Remove links
|
|
38
37
|
end
|
|
39
38
|
end
|
|
40
39
|
|
|
41
40
|
class HeaderWithFragment < Header
|
|
42
|
-
sig
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
41
|
+
sig do
|
|
42
|
+
params(
|
|
43
|
+
depth: Integer,
|
|
44
|
+
label: String,
|
|
45
|
+
url: String,
|
|
46
|
+
generator: FragmentGenerator
|
|
47
|
+
).void
|
|
48
|
+
end
|
|
49
|
+
def initialize(depth, label, url, generator:)
|
|
50
|
+
super(depth, label, url)
|
|
51
|
+
@url += "##{generator.generate(@label)}"
|
|
46
52
|
end
|
|
47
53
|
end
|
|
48
54
|
end
|
|
@@ -1,45 +1,104 @@
|
|
|
1
1
|
# typed: strict
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
|
-
require
|
|
5
|
-
require_relative
|
|
4
|
+
require 'sorbet-runtime'
|
|
5
|
+
require_relative 'header'
|
|
6
|
+
require_relative 'fragment_generator'
|
|
6
7
|
|
|
7
8
|
module Mdtoc
|
|
8
9
|
module Markdown
|
|
9
10
|
class Parser
|
|
10
11
|
extend T::Sig
|
|
11
12
|
|
|
12
|
-
sig
|
|
13
|
-
|
|
13
|
+
sig do
|
|
14
|
+
params(
|
|
15
|
+
depth: Integer,
|
|
16
|
+
url: String,
|
|
17
|
+
generator: FragmentGenerator
|
|
18
|
+
).void
|
|
19
|
+
end
|
|
20
|
+
def initialize(depth, url, generator: FragmentGenerator::GitHub.new)
|
|
14
21
|
@depth = depth
|
|
15
22
|
@url = url
|
|
23
|
+
@generator = generator
|
|
24
|
+
@in_code_block = T.let(false, T::Boolean)
|
|
25
|
+
@in_html_comment = T.let(false, T::Boolean)
|
|
16
26
|
end
|
|
17
27
|
|
|
18
28
|
sig { params(lines: T::Enumerable[String]).returns(T::Array[Header]) }
|
|
19
29
|
def headers(lines)
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
30
|
+
@in_code_block = false
|
|
31
|
+
@in_html_comment = false
|
|
32
|
+
|
|
33
|
+
headers = T.let([], T::Array[Header])
|
|
34
|
+
prev_line = T.let(nil, T.nilable(String))
|
|
35
|
+
|
|
36
|
+
lines.each do |line|
|
|
37
|
+
stripped = line.strip
|
|
38
|
+
|
|
39
|
+
if skip_line?(line, stripped)
|
|
40
|
+
prev_line = line
|
|
41
|
+
next
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
if line.start_with?('#')
|
|
45
|
+
headers << header(line)
|
|
46
|
+
elsif (h = process_setext_header(stripped, prev_line))
|
|
47
|
+
headers << h
|
|
27
48
|
end
|
|
28
|
-
next if skip || !line.start_with?("#")
|
|
29
49
|
|
|
30
|
-
|
|
50
|
+
prev_line = line
|
|
31
51
|
end
|
|
52
|
+
|
|
53
|
+
headers
|
|
32
54
|
end
|
|
33
55
|
|
|
34
56
|
private
|
|
35
57
|
|
|
58
|
+
sig { params(line: String, stripped: String).returns(T::Boolean) }
|
|
59
|
+
def skip_line?(line, stripped)
|
|
60
|
+
html_comment?(stripped) || code_block?(line)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
sig { params(stripped: String).returns(T::Boolean) }
|
|
64
|
+
def html_comment?(stripped)
|
|
65
|
+
if stripped.start_with?('<!--')
|
|
66
|
+
@in_html_comment = true unless stripped.end_with?('-->')
|
|
67
|
+
return true
|
|
68
|
+
elsif @in_html_comment && stripped.end_with?('-->')
|
|
69
|
+
@in_html_comment = false
|
|
70
|
+
return true
|
|
71
|
+
end
|
|
72
|
+
@in_html_comment
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
sig { params(line: String).returns(T::Boolean) }
|
|
76
|
+
def code_block?(line)
|
|
77
|
+
if line.start_with?('```') && !T.must(line[3..]).strip.end_with?('```')
|
|
78
|
+
@in_code_block = !@in_code_block
|
|
79
|
+
return true
|
|
80
|
+
end
|
|
81
|
+
@in_code_block
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
sig { params(stripped: String, prev_line: T.nilable(String)).returns(T.nilable(Header)) }
|
|
85
|
+
def process_setext_header(stripped, prev_line)
|
|
86
|
+
return nil unless prev_line && !prev_line.strip.empty?
|
|
87
|
+
|
|
88
|
+
if stripped.match?(/^=+$/)
|
|
89
|
+
HeaderWithFragment.new(@depth, prev_line.strip, @url, generator: @generator)
|
|
90
|
+
elsif stripped.match?(/^-+$/)
|
|
91
|
+
HeaderWithFragment.new(@depth + 1, prev_line.strip, @url, generator: @generator)
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
36
95
|
sig { params(line: String).returns(HeaderWithFragment) }
|
|
37
96
|
def header(line)
|
|
38
97
|
m = T.must(line.strip.match(/^(#+)\s*(.*)$/))
|
|
39
|
-
num_hashes = m[1]&.count(
|
|
98
|
+
num_hashes = m[1]&.count('#') || 1
|
|
40
99
|
depth = @depth + num_hashes - 1
|
|
41
|
-
label = m[2] ||
|
|
42
|
-
HeaderWithFragment.new(depth, label, @url)
|
|
100
|
+
label = m[2] || ''
|
|
101
|
+
HeaderWithFragment.new(depth, label, @url, generator: @generator)
|
|
43
102
|
end
|
|
44
103
|
end
|
|
45
104
|
end
|
data/lib/mdtoc/node.rb
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
# typed: strict
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
|
-
require
|
|
5
|
-
require
|
|
6
|
-
require_relative
|
|
7
|
-
require_relative
|
|
4
|
+
require 'pathname'
|
|
5
|
+
require 'sorbet-runtime'
|
|
6
|
+
require_relative 'markdown/header'
|
|
7
|
+
require_relative 'markdown/parser'
|
|
8
8
|
|
|
9
9
|
module Mdtoc
|
|
10
10
|
class Node
|
|
11
11
|
extend T::Helpers
|
|
12
12
|
extend T::Sig
|
|
13
|
+
|
|
13
14
|
abstract!
|
|
14
15
|
|
|
15
16
|
class << self
|
|
@@ -18,10 +19,9 @@ module Mdtoc
|
|
|
18
19
|
sig { params(path: String, depth: Integer).returns(Node) }
|
|
19
20
|
def for_path(path, depth = 0)
|
|
20
21
|
# Ensure that `path` is a relative path, so that all links are relative and therefore portable.
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
File.directory?(path) ? DirNode.new(path, depth) : FileNode.new(path, depth)
|
|
22
|
+
pathname = Pathname.new(path)
|
|
23
|
+
pathname = pathname.relative_path_from(Dir.pwd) if pathname.absolute?
|
|
24
|
+
pathname.directory? ? DirNode.new(pathname, depth) : FileNode.new(pathname, depth)
|
|
25
25
|
end
|
|
26
26
|
|
|
27
27
|
sig { params(paths: T::Array[String]).returns(String) }
|
|
@@ -32,7 +32,7 @@ module Mdtoc
|
|
|
32
32
|
end
|
|
33
33
|
end
|
|
34
34
|
|
|
35
|
-
sig { params(path:
|
|
35
|
+
sig { params(path: Pathname, depth: Integer).void }
|
|
36
36
|
def initialize(path, depth)
|
|
37
37
|
@path = path
|
|
38
38
|
@depth = depth
|
|
@@ -43,22 +43,30 @@ module Mdtoc
|
|
|
43
43
|
|
|
44
44
|
sig { returns(String) }
|
|
45
45
|
def label
|
|
46
|
-
|
|
46
|
+
@path.basename(@path.extname).to_s.gsub(/_+/, ' ').gsub(/\s+/, ' ').capitalize
|
|
47
47
|
end
|
|
48
48
|
|
|
49
49
|
class DirNode < Node
|
|
50
50
|
sig { override.returns(T::Array[Mdtoc::Markdown::Header]) }
|
|
51
51
|
def headers
|
|
52
|
-
readme_path = T.let(nil, T.nilable(
|
|
53
|
-
|
|
54
|
-
.
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
52
|
+
readme_path = T.let(nil, T.nilable(Pathname))
|
|
53
|
+
children = @path.children.reject do |child|
|
|
54
|
+
if child.basename.to_s.casecmp?('readme.md')
|
|
55
|
+
readme_path = child
|
|
56
|
+
true
|
|
57
|
+
else
|
|
58
|
+
false
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
child_headers = children.sort.flat_map do |child|
|
|
63
|
+
Node.for_path(child.to_s, @depth + 1).headers
|
|
64
|
+
end
|
|
65
|
+
|
|
58
66
|
return child_headers unless readme_path
|
|
59
67
|
|
|
60
68
|
# Include the headers from the README at the beginning.
|
|
61
|
-
readme_headers = FileNode.new(readme_path, @depth).headers
|
|
69
|
+
readme_headers = FileNode.new(T.must(readme_path), @depth).headers
|
|
62
70
|
readme_headers + child_headers
|
|
63
71
|
end
|
|
64
72
|
end
|
|
@@ -66,11 +74,12 @@ module Mdtoc
|
|
|
66
74
|
class FileNode < Node
|
|
67
75
|
sig { override.returns(T::Array[Mdtoc::Markdown::Header]) }
|
|
68
76
|
def headers
|
|
69
|
-
|
|
70
|
-
|
|
77
|
+
path_s = @path.to_s
|
|
78
|
+
parser = Markdown::Parser.new(@depth, path_s)
|
|
79
|
+
headers = parser.headers(@path.each_line)
|
|
71
80
|
return headers if headers[0]&.top_level?(@depth)
|
|
72
81
|
|
|
73
|
-
headers.unshift(Mdtoc::Markdown::Header.new(@depth, label,
|
|
82
|
+
headers.unshift(Mdtoc::Markdown::Header.new(@depth, label, path_s))
|
|
74
83
|
end
|
|
75
84
|
end
|
|
76
85
|
end
|
data/lib/mdtoc/version.rb
CHANGED
data/lib/mdtoc/writer.rb
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
|
-
# typed:
|
|
1
|
+
# typed: true
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
|
+
require 'fileutils'
|
|
5
|
+
require 'tempfile'
|
|
6
|
+
|
|
4
7
|
module Mdtoc
|
|
5
8
|
module Writer
|
|
6
|
-
COMMENT_BEGIN =
|
|
7
|
-
COMMENT_END =
|
|
9
|
+
COMMENT_BEGIN = '<!-- mdtoc -->'
|
|
10
|
+
COMMENT_END = '<!-- mdtoc-end -->'
|
|
8
11
|
|
|
9
12
|
class << self
|
|
10
13
|
extend T::Sig
|
|
@@ -13,8 +16,15 @@ module Mdtoc
|
|
|
13
16
|
def write(toc, path, append, create)
|
|
14
17
|
validate_path(path, create)
|
|
15
18
|
new_content = content(toc, path, append)
|
|
16
|
-
|
|
17
|
-
|
|
19
|
+
|
|
20
|
+
# Write to a temporary file and rename it to the target path to ensure atomic writing.
|
|
21
|
+
temp = Tempfile.new(File.basename(path), File.dirname(path))
|
|
22
|
+
begin
|
|
23
|
+
temp.write(new_content)
|
|
24
|
+
temp.close
|
|
25
|
+
FileUtils.mv(temp.path, path)
|
|
26
|
+
ensure
|
|
27
|
+
temp.close!
|
|
18
28
|
end
|
|
19
29
|
end
|
|
20
30
|
|
|
@@ -25,17 +35,12 @@ module Mdtoc
|
|
|
25
35
|
toc = "#{COMMENT_BEGIN}\n#{toc}\n#{COMMENT_END}"
|
|
26
36
|
|
|
27
37
|
begin
|
|
28
|
-
|
|
29
|
-
rescue
|
|
30
|
-
# If File.
|
|
38
|
+
old_content = File.read(path)
|
|
39
|
+
rescue StandardError
|
|
40
|
+
# If File.read failed because the file didn't exist, then we know that --create
|
|
31
41
|
# was specified due to the validation in validate_path.
|
|
32
42
|
return "#{toc}\n"
|
|
33
43
|
end
|
|
34
|
-
begin
|
|
35
|
-
old_content = T.must(f.read)
|
|
36
|
-
ensure
|
|
37
|
-
f.close
|
|
38
|
-
end
|
|
39
44
|
|
|
40
45
|
if Regexp.new(Regexp.escape(COMMENT_BEGIN), Regexp::IGNORECASE).match?(old_content)
|
|
41
46
|
return old_content.gsub(
|
data/lib/mdtoc.rb
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
# typed: strict
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
|
-
require
|
|
5
|
-
require
|
|
6
|
-
require_relative
|
|
7
|
-
require_relative
|
|
8
|
-
require_relative
|
|
4
|
+
require 'optparse'
|
|
5
|
+
require 'tempfile'
|
|
6
|
+
require_relative 'mdtoc/cli'
|
|
7
|
+
require_relative 'mdtoc/node'
|
|
8
|
+
require_relative 'mdtoc/writer'
|
|
9
9
|
|
|
10
10
|
module Mdtoc
|
|
11
11
|
class << self
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: mdtoc
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- andornaut
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date:
|
|
11
|
+
date: 2026-03-03 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: minitest
|
|
@@ -66,20 +66,6 @@ dependencies:
|
|
|
66
66
|
- - "~>"
|
|
67
67
|
- !ruby/object:Gem::Version
|
|
68
68
|
version: '1.50'
|
|
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: 2.0.0
|
|
76
|
-
type: :development
|
|
77
|
-
prerelease: false
|
|
78
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
79
|
-
requirements:
|
|
80
|
-
- - ">="
|
|
81
|
-
- !ruby/object:Gem::Version
|
|
82
|
-
version: 2.0.0
|
|
83
69
|
- !ruby/object:Gem::Dependency
|
|
84
70
|
name: rubocop-sorbet
|
|
85
71
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -165,7 +151,7 @@ description: |
|
|
|
165
151
|
Create a `Rakefile` with the contents below, then run
|
|
166
152
|
[`rake`](https://github.com/ruby/rake) to:
|
|
167
153
|
|
|
168
|
-
* `git
|
|
154
|
+
* `git pullgem push pkg/mdtoc-0.2.0.gem`
|
|
169
155
|
* `git add` any *.md files
|
|
170
156
|
* Run `mdtoc` to update the generated table of contents in the ./README.md file
|
|
171
157
|
* Git commit and push any changes
|
|
@@ -204,10 +190,11 @@ description: |
|
|
|
204
190
|
Requirements:
|
|
205
191
|
|
|
206
192
|
* [Bundler](https://bundler.io/)
|
|
193
|
+
* [chruby](https://github.com/postmodern/chruby) (recommended)
|
|
207
194
|
|
|
208
195
|
```bash
|
|
209
|
-
#
|
|
210
|
-
|
|
196
|
+
# Setup development environment
|
|
197
|
+
bin/setup
|
|
211
198
|
```
|
|
212
199
|
|
|
213
200
|
### Tasks
|
|
@@ -215,32 +202,38 @@ description: |
|
|
|
215
202
|
```bash
|
|
216
203
|
# List rake tasks
|
|
217
204
|
$ rake -T
|
|
218
|
-
rake build # Build
|
|
219
|
-
rake default # Run the build, rubocop
|
|
220
|
-
rake install # Build and install
|
|
221
|
-
rake install:local # Build and install mdtoc-0.1.5.gem into system gems without network access
|
|
222
|
-
rake release[remote] # Create tag v0.1.5 and build and push mdtoc-0.1.5.gem to rubygems.org
|
|
205
|
+
rake build # Build gem into the pkg directory
|
|
206
|
+
rake default # Run the build, rubocop, sorbet and test tasks
|
|
207
|
+
rake install # Build and install gem into system gems
|
|
223
208
|
rake rubocop # Run RuboCop
|
|
224
|
-
rake rubocop:autocorrect # Autocorrect RuboCop offenses (only when it's safe)
|
|
225
|
-
rake rubocop:autocorrect_all # Autocorrect RuboCop offenses (safe and unsafe)
|
|
226
209
|
rake sorbet # Run the Sorbet type checker
|
|
227
210
|
rake test # Run tests
|
|
228
|
-
|
|
211
|
+
```
|
|
229
212
|
# Run mdtoc with test inputs
|
|
230
213
|
$ ruby -Ilib bin/mdtoc test/samples
|
|
231
214
|
|
|
232
215
|
# Run mdtoc with test inputs, and write to a newly created output file
|
|
233
216
|
$ f=$(mktemp) && ruby -Ilib bin/mdtoc -aco ${f} test/samples ; cat ${f}
|
|
234
217
|
```
|
|
218
|
+
|
|
219
|
+
### Publishing
|
|
220
|
+
|
|
221
|
+
```bash
|
|
222
|
+
rake release
|
|
223
|
+
```
|
|
235
224
|
email:
|
|
236
225
|
executables:
|
|
237
226
|
- mdtoc
|
|
238
227
|
extensions: []
|
|
239
228
|
extra_rdoc_files: []
|
|
240
229
|
files:
|
|
230
|
+
- LICENSE
|
|
231
|
+
- README.md
|
|
241
232
|
- bin/mdtoc
|
|
233
|
+
- bin/setup
|
|
242
234
|
- lib/mdtoc.rb
|
|
243
235
|
- lib/mdtoc/cli.rb
|
|
236
|
+
- lib/mdtoc/markdown/fragment_generator.rb
|
|
244
237
|
- lib/mdtoc/markdown/header.rb
|
|
245
238
|
- lib/mdtoc/markdown/parser.rb
|
|
246
239
|
- lib/mdtoc/node.rb
|
|
@@ -249,7 +242,8 @@ files:
|
|
|
249
242
|
homepage: https://github.com/andornaut/mdtoc
|
|
250
243
|
licenses:
|
|
251
244
|
- MIT
|
|
252
|
-
metadata:
|
|
245
|
+
metadata:
|
|
246
|
+
rubygems_mfa_required: 'true'
|
|
253
247
|
post_install_message:
|
|
254
248
|
rdoc_options: []
|
|
255
249
|
require_paths:
|
|
@@ -265,7 +259,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
265
259
|
- !ruby/object:Gem::Version
|
|
266
260
|
version: '0'
|
|
267
261
|
requirements: []
|
|
268
|
-
rubygems_version: 3.
|
|
262
|
+
rubygems_version: 3.4.10
|
|
269
263
|
signing_key:
|
|
270
264
|
specification_version: 4
|
|
271
265
|
summary: Read Markdown files and output a table of contents
|