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.
@@ -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
@@ -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
@@ -0,0 +1,3 @@
1
+ require 'toc/markdown_file'
2
+ require 'toc/config'
3
+ require 'toc/plugin'
@@ -0,0 +1 @@
1
+ require 'toc/gem_version'
@@ -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,3 @@
1
+ module Toc
2
+ VERSION = '0.1.0'.freeze
3
+ 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
@@ -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
@@ -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,11 @@
1
+ # Prelude
2
+
3
+ The quick brown fox jumps over the lazy dog.
4
+
5
+ # Table of Contents
6
+
7
+ - [What is That?](#what-is-that)
8
+
9
+ # What is This?
10
+
11
+ The quick brown fox jumps over the lazy dog.
@@ -0,0 +1,11 @@
1
+ # Prelude
2
+
3
+ The quick brown fox jumps over the lazy dog.
4
+
5
+ ## Table of Contents
6
+
7
+ - [What is This?](#what-is-this)
8
+
9
+ ## What is This?
10
+
11
+ The quick brown fox jumps over the lazy dog.
@@ -0,0 +1,11 @@
1
+ # Prelude
2
+
3
+ The quick brown fox jumps over the lazy dog.
4
+
5
+ # Table of Contents
6
+
7
+ - [What is This?](#what-is-this)
8
+
9
+ # What is This?
10
+
11
+ The quick brown fox jumps over the lazy dog.
@@ -0,0 +1,5 @@
1
+ The quick brown fox jumps over the lazy dog.
2
+
3
+ # What is This?
4
+
5
+ The quick brown fox jumps over the lazy dog.
@@ -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