danger-toc 0.1.0

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