danger-toc 0.1.0
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/.gitignore +5 -0
- data/.rspec +2 -0
- data/.rubocop.yml +13 -0
- data/.rubocop_todo.yml +92 -0
- data/.travis.yml +14 -0
- data/CHANGELOG.md +5 -0
- data/CONTRIBUTING.md +125 -0
- data/Dangerfile +4 -0
- data/Gemfile +5 -0
- data/Guardfile +19 -0
- data/LICENSE.txt +22 -0
- data/README.md +64 -0
- data/RELEASING.md +67 -0
- data/Rakefile +23 -0
- data/danger-toc.gemspec +33 -0
- data/images/toc-missing.png +0 -0
- data/lib/danger_plugin.rb +3 -0
- data/lib/danger_toc.rb +1 -0
- data/lib/toc/config.rb +29 -0
- data/lib/toc/constructor.rb +40 -0
- data/lib/toc/extractor.rb +27 -0
- data/lib/toc/gem_version.rb +3 -0
- data/lib/toc/markdown_file.rb +83 -0
- data/lib/toc/plugin.rb +68 -0
- data/spec/config_spec.rb +28 -0
- data/spec/fixtures/markdown_file/ONE-SECTION-WITH-INVALID-TOC.md +11 -0
- data/spec/fixtures/markdown_file/ONE-SECTION-WITH-TOC-LEVEL.md +11 -0
- data/spec/fixtures/markdown_file/ONE-SECTION-WITH-TOC.md +11 -0
- data/spec/fixtures/markdown_file/ONE-SECTION.md +5 -0
- data/spec/fixtures/markdown_file/WITH-QUOTED-EXAMPLE.md +22 -0
- data/spec/markdown_file/danger_toc_readme_spec.rb +19 -0
- data/spec/markdown_file/one_section_spec.rb +28 -0
- data/spec/markdown_file/one_section_with_toc_level_spec.rb +28 -0
- data/spec/markdown_file/one_section_with_toc_spec.rb +28 -0
- data/spec/markdown_file/with_quoted_example_spec.rb +33 -0
- data/spec/spec_helper.rb +38 -0
- data/spec/toc_spec.rb +87 -0
- metadata +262 -0
data/Rakefile
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'bundler/gem_tasks'
|
2
|
+
require 'rspec/core/rake_task'
|
3
|
+
require 'rubocop/rake_task'
|
4
|
+
|
5
|
+
RSpec::Core::RakeTask.new(:specs)
|
6
|
+
|
7
|
+
task default: %i[rubocop specs]
|
8
|
+
|
9
|
+
task :spec do
|
10
|
+
Rake::Task['specs'].invoke
|
11
|
+
Rake::Task['rubocop'].invoke
|
12
|
+
Rake::Task['spec_docs'].invoke
|
13
|
+
end
|
14
|
+
|
15
|
+
desc 'Run RuboCop on the lib/specs directory'
|
16
|
+
RuboCop::RakeTask.new(:rubocop) do |task|
|
17
|
+
task.patterns = ['lib/**/*.rb', 'spec/**/*.rb']
|
18
|
+
end
|
19
|
+
|
20
|
+
desc 'Ensure that the plugin passes `danger plugins lint`'
|
21
|
+
task :spec_docs do
|
22
|
+
sh 'bundle exec danger plugins lint'
|
23
|
+
end
|
data/danger-toc.gemspec
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
lib = File.expand_path('../lib', __FILE__)
|
2
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
3
|
+
require 'toc/gem_version.rb'
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = 'danger-toc'
|
7
|
+
spec.version = Toc::VERSION
|
8
|
+
spec.authors = ['dblock']
|
9
|
+
spec.email = ['dblock@dblock.org']
|
10
|
+
spec.description = 'A danger.systems plugin for your markdown TOC.'
|
11
|
+
spec.summary = 'A danger.systems plugin for your markdown TOC.'
|
12
|
+
spec.homepage = 'https://github.com/dblock/danger-toc'
|
13
|
+
spec.license = 'MIT'
|
14
|
+
|
15
|
+
spec.files = `git ls-files`.split($INPUT_RECORD_SEPARATOR)
|
16
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
17
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
18
|
+
spec.require_paths = ['lib']
|
19
|
+
|
20
|
+
spec.add_dependency 'activesupport'
|
21
|
+
spec.add_dependency 'kramdown'
|
22
|
+
spec.add_runtime_dependency 'danger-plugin-api', '~> 1.0'
|
23
|
+
|
24
|
+
spec.add_development_dependency 'bundler', '~> 1.3'
|
25
|
+
spec.add_development_dependency 'guard', '~> 2.14'
|
26
|
+
spec.add_development_dependency 'guard-rspec', '~> 4.7'
|
27
|
+
spec.add_development_dependency 'listen', '3.0.7'
|
28
|
+
spec.add_development_dependency 'pry'
|
29
|
+
spec.add_development_dependency 'rake', '~> 10.0'
|
30
|
+
spec.add_development_dependency 'rspec', '~> 3.4'
|
31
|
+
spec.add_development_dependency 'rubocop', '~> 0.41'
|
32
|
+
spec.add_development_dependency 'yard', '~> 0.8'
|
33
|
+
end
|
Binary file
|
data/lib/danger_toc.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require 'toc/gem_version'
|
data/lib/toc/config.rb
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
module Danger
|
2
|
+
module Toc
|
3
|
+
module Config
|
4
|
+
extend self
|
5
|
+
|
6
|
+
attr_accessor :files
|
7
|
+
|
8
|
+
def files=(value)
|
9
|
+
@files = value
|
10
|
+
end
|
11
|
+
|
12
|
+
def reset
|
13
|
+
self.files = ['README.md']
|
14
|
+
end
|
15
|
+
|
16
|
+
reset
|
17
|
+
end
|
18
|
+
|
19
|
+
class << self
|
20
|
+
def configure
|
21
|
+
block_given? ? yield(Config) : Config
|
22
|
+
end
|
23
|
+
|
24
|
+
def config
|
25
|
+
Config
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'kramdown/converter'
|
2
|
+
|
3
|
+
module Danger
|
4
|
+
module Toc
|
5
|
+
class Constructor < Kramdown::Converter::Toc
|
6
|
+
def flatten(el)
|
7
|
+
return [] unless el.type == :toc
|
8
|
+
result = []
|
9
|
+
if el.value
|
10
|
+
result << {
|
11
|
+
id: el.attr[:id],
|
12
|
+
text: el.value.options[:raw_text],
|
13
|
+
depth: el.value.options[:level]
|
14
|
+
}
|
15
|
+
end
|
16
|
+
if el.children
|
17
|
+
el.children.each do |child|
|
18
|
+
result.concat(flatten(child))
|
19
|
+
end
|
20
|
+
end
|
21
|
+
result
|
22
|
+
end
|
23
|
+
|
24
|
+
def convert(el)
|
25
|
+
toc = flatten(super(el))
|
26
|
+
has_toc = false
|
27
|
+
headers = []
|
28
|
+
toc.each do |line|
|
29
|
+
if !has_toc && line[:text] == 'Table of Contents' # TODO: make configurable
|
30
|
+
headers = [] # drop any headers prior to TOC
|
31
|
+
has_toc = true
|
32
|
+
else
|
33
|
+
headers << line
|
34
|
+
end
|
35
|
+
end
|
36
|
+
headers
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'kramdown/converter'
|
2
|
+
|
3
|
+
module Danger
|
4
|
+
module Toc
|
5
|
+
class Extractor < Kramdown::Converter::Base
|
6
|
+
def initialize(root, options)
|
7
|
+
super
|
8
|
+
@toc_start = nil
|
9
|
+
@toc_end = nil
|
10
|
+
@in_toc = false
|
11
|
+
end
|
12
|
+
|
13
|
+
def convert(el)
|
14
|
+
if el.type == :header && el.options[:raw_text] == 'Table of Contents'
|
15
|
+
@in_toc = true
|
16
|
+
@toc_start = el.options[:location]
|
17
|
+
elsif el.type == :header
|
18
|
+
@toc_end = el.options[:location] if @in_toc && !@toc_end
|
19
|
+
@in_toc = false
|
20
|
+
else
|
21
|
+
el.children.each { |child| convert(child) }
|
22
|
+
end
|
23
|
+
[@toc_start, @toc_end]
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
require 'active_support/core_ext/string/inflections'
|
2
|
+
|
3
|
+
require_relative 'extractor'
|
4
|
+
require_relative 'constructor'
|
5
|
+
|
6
|
+
module Danger
|
7
|
+
module Toc
|
8
|
+
class MarkdownFile
|
9
|
+
attr_reader :filename
|
10
|
+
attr_reader :exists
|
11
|
+
attr_reader :toc
|
12
|
+
attr_reader :headers
|
13
|
+
|
14
|
+
def initialize(filename = 'README.md')
|
15
|
+
@filename = filename
|
16
|
+
@exists = File.exist?(filename)
|
17
|
+
if @exists
|
18
|
+
parse!
|
19
|
+
reduce!
|
20
|
+
validate!
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def exists?
|
25
|
+
!!@exists
|
26
|
+
end
|
27
|
+
|
28
|
+
def bad?
|
29
|
+
!good?
|
30
|
+
end
|
31
|
+
|
32
|
+
def good?
|
33
|
+
!!@good
|
34
|
+
end
|
35
|
+
|
36
|
+
def has_toc?
|
37
|
+
!!@has_toc
|
38
|
+
end
|
39
|
+
|
40
|
+
def toc_from_headers
|
41
|
+
headers.map do |header|
|
42
|
+
[
|
43
|
+
' ' * header[:depth] * 2,
|
44
|
+
"- [#{header[:text]}]",
|
45
|
+
"(##{header[:id]})"
|
46
|
+
].compact.join
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
# Parse markdown file for TOC.
|
53
|
+
def parse!
|
54
|
+
md = File.read(filename)
|
55
|
+
doc = Kramdown::Document.new(md)
|
56
|
+
|
57
|
+
# extract toc
|
58
|
+
toc_start, toc_end = Danger::Toc::Extractor.convert(doc.root).first
|
59
|
+
@has_toc = toc_start && toc_end
|
60
|
+
@toc = md.split("\n")[toc_start, toc_end - toc_start - 1].reject(&:empty?) if @has_toc
|
61
|
+
|
62
|
+
# construct toc
|
63
|
+
@headers = Danger::Toc::Constructor.convert(doc.root).first
|
64
|
+
end
|
65
|
+
|
66
|
+
def reduce!
|
67
|
+
min_depth = nil
|
68
|
+
headers.each do |header|
|
69
|
+
min_depth = header[:depth] unless min_depth && min_depth < header[:depth]
|
70
|
+
end
|
71
|
+
if min_depth
|
72
|
+
headers.each do |header|
|
73
|
+
header[:depth] -= min_depth
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def validate!
|
79
|
+
@good = (toc_from_headers == toc)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
data/lib/toc/plugin.rb
ADDED
@@ -0,0 +1,68 @@
|
|
1
|
+
module Danger
|
2
|
+
# Check whether the TOC in .md file(s) has been updated.
|
3
|
+
#
|
4
|
+
# @example Run all checks on the default README.md.
|
5
|
+
#
|
6
|
+
# toc.check
|
7
|
+
#
|
8
|
+
# @example Customize filenames and remind the requester to update TOCs when necessary.
|
9
|
+
#
|
10
|
+
# toc.filenames = ['README.md']
|
11
|
+
# toc.is_toc_correct?
|
12
|
+
#
|
13
|
+
# @see dblock/danger-toc
|
14
|
+
# @tags toc
|
15
|
+
|
16
|
+
class DangerToc < Plugin
|
17
|
+
# The toc filenames, defaults to `[README.md]`.
|
18
|
+
# @return [Array]
|
19
|
+
attr_accessor :filenames
|
20
|
+
|
21
|
+
def initialize(dangerfile)
|
22
|
+
@filenames = ['README.md']
|
23
|
+
super
|
24
|
+
end
|
25
|
+
|
26
|
+
# Run all checks.
|
27
|
+
# @return [void]
|
28
|
+
def check
|
29
|
+
is_toc_correct?
|
30
|
+
end
|
31
|
+
|
32
|
+
# Has the CHANGELOG file been modified?
|
33
|
+
# @return [boolean]
|
34
|
+
def toc_changes?
|
35
|
+
(git.modified_files & filename).any? || (git.added_files & filenames).any?
|
36
|
+
end
|
37
|
+
|
38
|
+
# Is the TOC format correct?
|
39
|
+
# @return [boolean]
|
40
|
+
def is_toc_correct?
|
41
|
+
filenames.all? do |filename|
|
42
|
+
toc_file = Danger::Toc::MarkdownFile.new(filename)
|
43
|
+
if !toc_file.exists?
|
44
|
+
messaging.fail("The #{filename} file does not exist.", sticky: false)
|
45
|
+
false
|
46
|
+
elsif toc_file.good?
|
47
|
+
true
|
48
|
+
else
|
49
|
+
markdown <<-MARKDOWN
|
50
|
+
Here's the expected TOC for #{filename}:
|
51
|
+
|
52
|
+
```markdown
|
53
|
+
# Table of Contents
|
54
|
+
|
55
|
+
#{toc_file.toc_from_headers.join("\n")}
|
56
|
+
```
|
57
|
+
MARKDOWN
|
58
|
+
if toc_file.has_toc?
|
59
|
+
messaging.fail("The TOC found in #{filename} doesn't match the sections of the file.", sticky: false)
|
60
|
+
else
|
61
|
+
messaging.fail("The #{filename} file is missing a TOC.", sticky: false)
|
62
|
+
end
|
63
|
+
false
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
data/spec/config_spec.rb
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
require File.expand_path('../spec_helper', __FILE__)
|
2
|
+
|
3
|
+
describe Danger::Toc::Config do
|
4
|
+
after(:each) do
|
5
|
+
described_class.reset
|
6
|
+
end
|
7
|
+
|
8
|
+
describe 'configure' do
|
9
|
+
describe 'files' do
|
10
|
+
context 'defailt' do
|
11
|
+
it 'assumes README.md' do
|
12
|
+
expect(Danger::Toc.config.files).to eq ['README.md']
|
13
|
+
end
|
14
|
+
end
|
15
|
+
context 'when valid' do
|
16
|
+
before do
|
17
|
+
Danger::Toc.configure do |config|
|
18
|
+
config.files = ['README.md', 'SOMETHING.md']
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
it 'saves configuration' do
|
23
|
+
expect(Danger::Toc.config.files).to eq ['README.md', 'SOMETHING.md']
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# Prelude
|
2
|
+
|
3
|
+
The quick brown fox jumps over the lazy dog.
|
4
|
+
|
5
|
+
# Table of Contents
|
6
|
+
|
7
|
+
- [Example](#example)
|
8
|
+
- [Conclusion](#conclusion)
|
9
|
+
|
10
|
+
# Example
|
11
|
+
|
12
|
+
The quick brown fox jumps over the lazy dog.
|
13
|
+
|
14
|
+
```ruby
|
15
|
+
# example config.ru
|
16
|
+
|
17
|
+
require 'danger-toc'
|
18
|
+
```
|
19
|
+
|
20
|
+
# Conclusion
|
21
|
+
|
22
|
+
The quick brown fox jumps over the lazy dog.
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Danger::Toc::MarkdownFile do
|
4
|
+
describe 'README.md' do
|
5
|
+
let(:filename) { File.expand_path('../../../README.md', __FILE__) }
|
6
|
+
subject do
|
7
|
+
Danger::Toc::MarkdownFile.new(filename)
|
8
|
+
end
|
9
|
+
it 'exists?' do
|
10
|
+
expect(subject.exists?).to be true
|
11
|
+
end
|
12
|
+
it 'has_toc?' do
|
13
|
+
expect(subject.has_toc?).to be true
|
14
|
+
end
|
15
|
+
it 'good?' do
|
16
|
+
expect(subject.good?).to be true
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|