rspec-documentation 0.0.1 → 0.0.2

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.
Files changed (67) hide show
  1. checksums.yaml +4 -4
  2. data/.rspec +1 -0
  3. data/.rubocop.yml +2 -0
  4. data/Gemfile +3 -0
  5. data/Gemfile.lock +25 -2
  6. data/README.md +1 -1
  7. data/Rakefile +1 -0
  8. data/exe/rspec-documentation +5 -0
  9. data/lib/rspec/documentation/version.rb +2 -2
  10. data/lib/rspec/documentation.rb +65 -2
  11. data/lib/rspec_documentation/ansi_html.rb +28 -0
  12. data/lib/rspec_documentation/configuration.rb +15 -0
  13. data/lib/rspec_documentation/context.rb +44 -0
  14. data/lib/rspec_documentation/document.rb +61 -0
  15. data/lib/rspec_documentation/html_element.rb +55 -0
  16. data/lib/rspec_documentation/markdown_renderer.rb +7 -0
  17. data/lib/rspec_documentation/page_collection.rb +49 -0
  18. data/lib/rspec_documentation/page_tree.rb +73 -0
  19. data/lib/rspec_documentation/page_tree_element.rb +56 -0
  20. data/lib/rspec_documentation/parsed_document.rb +70 -0
  21. data/lib/rspec_documentation/rspec/example_group.rb +26 -0
  22. data/lib/rspec_documentation/rspec/failure.rb +37 -0
  23. data/lib/rspec_documentation/rspec/reporter.rb +45 -0
  24. data/lib/rspec_documentation/rspec.rb +14 -0
  25. data/lib/rspec_documentation/spec.rb +62 -0
  26. data/lib/rspec_documentation/util.rb +46 -0
  27. data/lib/rspec_documentation.rb +58 -0
  28. data/lib/tasks/rspec/documentation/generate.rake +10 -0
  29. data/lib/templates/footer.html.erb +1 -0
  30. data/lib/templates/header.html.erb +7 -0
  31. data/lib/templates/layout.css.erb +39 -0
  32. data/lib/templates/layout.html.erb +100 -0
  33. data/lib/templates/tabbed_spec.html.erb +119 -0
  34. data/lib/templates/themes/cerulean.css +12 -0
  35. data/lib/templates/themes/cosmo.css +12 -0
  36. data/lib/templates/themes/cyborg.css +12 -0
  37. data/lib/templates/themes/darkly.css +12 -0
  38. data/lib/templates/themes/flatly.css +12 -0
  39. data/lib/templates/themes/journal.css +12 -0
  40. data/lib/templates/themes/litera.css +12 -0
  41. data/lib/templates/themes/lumen.css +12 -0
  42. data/lib/templates/themes/lux.css +12 -0
  43. data/lib/templates/themes/materia.css +12 -0
  44. data/lib/templates/themes/minty.css +12 -0
  45. data/lib/templates/themes/morph.css +12 -0
  46. data/lib/templates/themes/pulse.css +12 -0
  47. data/lib/templates/themes/quartz.css +12 -0
  48. data/lib/templates/themes/sandstone.css +12 -0
  49. data/lib/templates/themes/simplex.css +12 -0
  50. data/lib/templates/themes/sketchy.css +12 -0
  51. data/lib/templates/themes/slate.css +12 -0
  52. data/lib/templates/themes/solar.css +12 -0
  53. data/lib/templates/themes/spacelab.css +12 -0
  54. data/lib/templates/themes/superhero.css +12 -0
  55. data/lib/templates/themes/united.css +12 -0
  56. data/lib/templates/themes/vapor.css +12 -0
  57. data/lib/templates/themes/yeti.css +12 -0
  58. data/lib/templates/themes/zephyr.css +12 -0
  59. data/rspec-documentation/pages/000-Introduction.md +39 -0
  60. data/rspec-documentation/pages/010-File System/000-Ordering.md +14 -0
  61. data/rspec-documentation/pages/010-File System/010-Standalone Directories.md +17 -0
  62. data/rspec-documentation/pages/010-File System/020-Standalone Directory/Example Page.md +3 -0
  63. data/rspec-documentation/pages/010-File System.md +26 -0
  64. data/rspec-documentation/pages/020-Running Specs.md +11 -0
  65. data/rspec-documentation.gemspec +9 -1
  66. data/sig/rspec/documentation.rbs +1 -1
  67. metadata +158 -4
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 31073f998af7b0e0f74ebd590721dc9ba5c4aa73b9ed88bf6848a77c189d7355
4
- data.tar.gz: 83343ddbbd95d4e100b2675a7e72a92c5ca9df225e99454308bd2af2b078b7ae
3
+ metadata.gz: 1de5016a8e2e1a370ac923d9501c654c56e87e1f236f2f260ea634a8a28167b4
4
+ data.tar.gz: d75c74d43b80b502daa00778c3d4e8ede22514c2f500d67ee6dc31cb994772e3
5
5
  SHA512:
6
- metadata.gz: e5d845f98a5ceaa7c0c6462cad990a598d84086955f61334b3011c5f3fb15d0e90cd1ba8fff75f3d1559aa8e8f841f78c16391e37a3c048bd1876916b0b352af
7
- data.tar.gz: '04739b7fb732fc7a1a6492f23d12d31d598b21d769c89238d0b29aa535a1620069fbc0b1610c8d1fa3d6a29a85a8d614768167b750c3416bbe83871f8990c11b'
6
+ metadata.gz: 1bc358ecfcda3670c648dac2f11698facb57c2dc147826a87a26b24af07a1517fda6cd43431be25facb996280e7de50848630f0f8aab9ead11743ac1ec76e77e
7
+ data.tar.gz: a622ad2f53e2c743431678ed97e9a36a037c86f5b4b15c67c3eff439158ff0af026878be1f55a1aa5d4ee45b1a672a57db66c28912de012471d5bcd7aeedb6b1
data/.rspec CHANGED
@@ -1,3 +1,4 @@
1
1
  --format documentation
2
2
  --color
3
3
  --require spec_helper
4
+ --exclude spec/fixtures/**/*_spec.rb
data/.rubocop.yml CHANGED
@@ -4,3 +4,5 @@ require:
4
4
 
5
5
  AllCops:
6
6
  NewCops: enable
7
+ Exclude:
8
+ - 'spec/fixtures/**/*'
data/Gemfile CHANGED
@@ -7,7 +7,10 @@ gemspec
7
7
 
8
8
  gem 'rake', '~> 13.0'
9
9
 
10
+ gem 'devpack', '~> 0.4.1'
10
11
  gem 'rspec', '~> 3.0'
12
+ gem 'rspec-file_fixtures', '~> 0.1.6'
13
+ gem 'rspec-its', '~> 1.3'
11
14
  gem 'rubocop', '~> 1.51'
12
15
  gem 'rubocop-rake', '~> 0.6.0'
13
16
  gem 'rubocop-rspec', '~> 2.22'
data/Gemfile.lock CHANGED
@@ -1,25 +1,41 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- rspec-documentation (0.0.1)
4
+ rspec-documentation (0.0.2)
5
+ htmlbeautifier (~> 1.4)
6
+ kramdown (~> 2.4)
7
+ kramdown-parser-gfm (~> 1.1)
8
+ paintbrush (~> 0.1.1)
9
+ redcarpet (~> 3.6)
10
+ rouge (~> 4.1)
11
+ rspec (~> 3.12)
5
12
 
6
13
  GEM
7
14
  remote: https://rubygems.org/
8
15
  specs:
9
16
  ast (2.4.2)
10
17
  concurrent-ruby (1.2.2)
18
+ devpack (0.4.1)
11
19
  diff-lcs (1.5.0)
20
+ htmlbeautifier (1.4.2)
12
21
  i18n (1.13.0)
13
22
  concurrent-ruby (~> 1.0)
14
23
  json (2.6.3)
24
+ kramdown (2.4.0)
25
+ rexml
26
+ kramdown-parser-gfm (1.1.0)
27
+ kramdown (~> 2.0)
15
28
  paint (2.3.0)
29
+ paintbrush (0.1.1)
16
30
  parallel (1.23.0)
17
31
  parser (3.2.2.1)
18
32
  ast (~> 2.4.1)
19
33
  rainbow (3.1.1)
20
34
  rake (13.0.6)
35
+ redcarpet (3.6.0)
21
36
  regexp_parser (2.8.0)
22
37
  rexml (3.2.5)
38
+ rouge (4.1.1)
23
39
  rspec (3.12.0)
24
40
  rspec-core (~> 3.12.0)
25
41
  rspec-expectations (~> 3.12.0)
@@ -29,6 +45,11 @@ GEM
29
45
  rspec-expectations (3.12.3)
30
46
  diff-lcs (>= 1.2.0, < 2.0)
31
47
  rspec-support (~> 3.12.0)
48
+ rspec-file_fixtures (0.1.6)
49
+ rspec (~> 3.0)
50
+ rspec-its (1.3.0)
51
+ rspec-core (>= 3.0.0)
52
+ rspec-expectations (>= 3.0.0)
32
53
  rspec-mocks (3.12.5)
33
54
  diff-lcs (>= 1.2.0, < 2.0)
34
55
  rspec-support (~> 3.12.0)
@@ -62,13 +83,15 @@ GEM
62
83
  unicode-display_width (2.4.2)
63
84
 
64
85
  PLATFORMS
65
- ruby
66
86
  x86_64-linux
67
87
 
68
88
  DEPENDENCIES
89
+ devpack (~> 0.4.1)
69
90
  rake (~> 13.0)
70
91
  rspec (~> 3.0)
71
92
  rspec-documentation!
93
+ rspec-file_fixtures (~> 0.1.6)
94
+ rspec-its (~> 1.3)
72
95
  rubocop (~> 1.51)
73
96
  rubocop-rake (~> 0.6.0)
74
97
  rubocop-rspec (~> 2.22)
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # Rspec::Documentation
1
+ # RSpec::Documentation
2
2
 
3
3
  TODO: Delete this and the text below, and describe your gem
4
4
 
data/Rakefile CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  require 'bundler/gem_tasks'
4
4
  require 'rspec/core/rake_task'
5
+ require 'rspec/documentation'
5
6
 
6
7
  RSpec::Core::RakeTask.new(:spec)
7
8
 
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require_relative '../lib/rspec/documentation'
5
+ RSpec::Documentation.generate_documentation
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Rspec
3
+ module RSpec
4
4
  module Documentation
5
- VERSION = '0.0.1'
5
+ VERSION = '0.0.2'
6
6
  end
7
7
  end
@@ -1,10 +1,73 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative 'documentation/version'
4
+ require_relative '../rspec_documentation'
5
+ load File.expand_path(File.join(__dir__, '../tasks/rspec/documentation/generate.rake'))
4
6
 
5
- module Rspec
7
+ module RSpec
8
+ # Extension for RSpec, locates RSpec examples found in a Markdown file and executes them,
9
+ # replacing their output into a tree of Markdown files.
6
10
  module Documentation
7
11
  class Error < StandardError; end
8
- # Your code goes here...
12
+
13
+ class << self
14
+ include Paintbrush
15
+
16
+ def generate_documentation
17
+ require_spec_helper
18
+ page_collection.generate
19
+ page_collection.flush unless failed?
20
+ print_summary
21
+ nil
22
+ end
23
+
24
+ def require_spec_helper
25
+ path = Pathname.new(Dir.pwd).join('rspec-documentation/spec_helper.rb')
26
+ require path if path.file?
27
+ end
28
+
29
+ def configure(&block)
30
+ RSpecDocumentation.configure(&block)
31
+ end
32
+
33
+ private
34
+
35
+ def print_success_summary
36
+ warn(paintbrush { green("\n Created #{blue(page_paths.size)} pages.\n") })
37
+ warn(paintbrush { cyan(" View your documentation here: #{white(bundle_index_path)}\n") })
38
+ end
39
+
40
+ def print_failure_summary
41
+ page_collection.failures.each do |failure|
42
+ $stderr.write(failure.message)
43
+ end
44
+ end
45
+
46
+ def bundle_index_path
47
+ RSpecDocumentation::Util.bundle_dir.glob('*.html').first.expand_path
48
+ end
49
+
50
+ def page_paths
51
+ @page_paths ||= Pathname.new(Dir.pwd).join('rspec-documentation/pages').glob('**/*.md')
52
+ end
53
+
54
+ def page_collection
55
+ @page_collection ||= RSpecDocumentation::PageCollection.new(page_paths: page_paths)
56
+ end
57
+
58
+ def failed?
59
+ !page_collection.failures.empty?
60
+ end
61
+
62
+ def print_summary
63
+ if failed?
64
+ print_failure_summary
65
+ else
66
+ print_success_summary
67
+ end
68
+
69
+ nil
70
+ end
71
+ end
9
72
  end
10
73
  end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RSpecDocumentation
4
+ # Outputs a string containing ANSI color code escape sequences into HTML with attached classes
5
+ # for each matched color code. Cleans any remaining escape codes.
6
+ class AnsiHTML
7
+ COLOR_CODES = [0, 1, 2, 3, 4, 5, 6, 7, 9].freeze
8
+
9
+ def initialize(content)
10
+ @content = content
11
+ end
12
+
13
+ def render
14
+ "<div class='ansi-html border m-1 p-4'><span>#{subbed_content}</span></div>"
15
+ end
16
+
17
+ private
18
+
19
+ attr_reader :content
20
+
21
+ def subbed_content
22
+ COLOR_CODES.reduce(content) do |string, color_code|
23
+ string.gsub("\e[3#{color_code}m", "</span><span class='ansi-color-#{color_code}'>")
24
+ .gsub("\e[0m", "</span><span class='ansi-color-reset'>")
25
+ end.gsub(/\e\[[0-9]+m/, '')
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RSpecDocumentation
4
+ # Configures the rspec-documentation gem, allows setting a context that makes values available to each example.
5
+ class Configuration
6
+ def initialize
7
+ @context = Context.new
8
+ end
9
+
10
+ def context
11
+ yield @context if block_given?
12
+ @context
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RSpecDocumentation
4
+ # Used to fetch missing methods called in examples, provides a context available to all
5
+ # examples to avoid repeated setup in each example. Usage:
6
+ #
7
+ # ```ruby
8
+ # RSpec::Documentation.configure do |config|
9
+ # config.context do |context|
10
+ # context.my_value = 'example value'
11
+ # end
12
+ # end
13
+ # ```
14
+ #
15
+ # `my_value` will now be available in every example.
16
+ class Context
17
+ def initialize
18
+ @context = {}
19
+ end
20
+
21
+ private
22
+
23
+ def method_missing(key, *args, &block)
24
+ return _assign(key, *args, &block) if key.to_s.end_with?('=')
25
+ return super unless @context.key?(key)
26
+
27
+ @context.fetch(key)
28
+ end
29
+
30
+ def respond_to_missing?(_key, *_)
31
+ true
32
+ end
33
+
34
+ def _assign(key, *args, &block)
35
+ key = key.to_s.rpartition('=').first.to_sym
36
+
37
+ @context[key] = if block_given?
38
+ block
39
+ else
40
+ args.first
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RSpecDocumentation
4
+ # Translates a Markdown document into a structure of parsed Markdown and embedded RSpec examples.
5
+ class Document
6
+ attr_reader :failures, :page_tree
7
+
8
+ def initialize(document:, page_tree:)
9
+ @document = document
10
+ @page_tree = page_tree
11
+ @failures = []
12
+ end
13
+
14
+ def specs
15
+ @specs ||= parsed_document.specs
16
+ end
17
+
18
+ def render
19
+ parsed_document.execute_and_substitute_examples!
20
+ if parsed_document.failures.empty?
21
+ RSpecDocumentation.template('layout').result(binding)
22
+ else
23
+ failures.concat(parsed_document.failures)
24
+ nil
25
+ end
26
+ end
27
+
28
+ def html
29
+ parsed_document.html
30
+ end
31
+
32
+ def title
33
+ # TODO: Try other methods of inferring documentation title, allow setting by configuration
34
+ gem_spec&.name
35
+ end
36
+
37
+ def version
38
+ gem_spec&.version
39
+ end
40
+
41
+ def header
42
+ RSpecDocumentation.template('header').result(binding)
43
+ end
44
+
45
+ def footer
46
+ RSpecDocumentation.template('footer').result(binding)
47
+ end
48
+
49
+ private
50
+
51
+ attr_reader :document
52
+
53
+ def parsed_document
54
+ @parsed_document ||= ParsedDocument.new(document)
55
+ end
56
+
57
+ def gem_spec
58
+ @gem_spec ||= Gem::Specification.load(Pathname.new(Dir.pwd).glob('*.gemspec').first.to_s)
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RSpecDocumentation
4
+ # Receives a spec manifest and generates a `Kramdown::Document` contaning an HTML node with a
5
+ # tabbed view displaying the example's code, its output and, if format is `:html`, the rendered
6
+ # HTML of the output. Injected into the parsed `Kramdown::Document` for the root Markdown file,
7
+ # replacing the code block it was produced from.
8
+ class HtmlElement
9
+ def initialize(spec:)
10
+ @spec = spec
11
+ end
12
+
13
+ def element
14
+ Kramdown::Document.new(tabbed_spec, input: 'html').root
15
+ end
16
+
17
+ def html_source
18
+ return nil unless spec.format == :html
19
+
20
+ formatter = Rouge::Formatters::HTML.new
21
+ lexer = Rouge::Lexers::HTML.new
22
+
23
+ formatter.format(lexer.lex(HtmlBeautifier.beautify(spec.described_object)))
24
+ end
25
+
26
+ def code_source
27
+ formatted_ruby(spec.source)
28
+ end
29
+
30
+ def rendered_result
31
+ return spec.described_object if spec.format == :html
32
+ return AnsiHTML.new(spec.described_object).render if spec.format == :ansi
33
+
34
+ formatted_ruby(spec.described_object.inspect)
35
+ end
36
+
37
+ def element_id
38
+ @element_id ||= SecureRandom.uuid
39
+ end
40
+
41
+ private
42
+
43
+ attr_reader :spec
44
+
45
+ def tabbed_spec
46
+ RSpecDocumentation.template('tabbed_spec').result(binding)
47
+ end
48
+
49
+ def formatted_ruby(code)
50
+ formatter = Rouge::Formatters::HTML.new
51
+ lexer = Rouge::Lexers::Ruby.new
52
+ formatter.format(lexer.lex(code))
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RSpecDocumentation
4
+ class MarkdownRenderer < Redcarpet::Render::HTML
5
+ include Rouge::Plugins::Redcarpet
6
+ end
7
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RSpecDocumentation
4
+ # Builds content for a collection of page paths, collates failures from embedded examples.
5
+ # Writes the final structure to disk.
6
+ class PageCollection
7
+ attr_reader :failures
8
+
9
+ def initialize(page_paths:)
10
+ @page_paths = page_paths
11
+ @buffer = {}
12
+ @failures = []
13
+ end
14
+
15
+ def generate
16
+ page_paths.sort.each do |path|
17
+ document = Document.new(document: path.read, page_tree: page_tree)
18
+ buffer[bundle_path_for(path)] = document.render
19
+ @failures.concat(document.failures)
20
+ end
21
+ end
22
+
23
+ def flush
24
+ Util.bundle_dir.rmtree if Util.bundle_dir.directory?
25
+ Util.bundle_dir.mkpath
26
+
27
+ @buffer.each do |path, content|
28
+ path.dirname.mkpath
29
+ Util.bundle_path(path).write(content)
30
+ end
31
+ end
32
+
33
+ private
34
+
35
+ attr_reader :page_paths, :buffer
36
+
37
+ def page_tree
38
+ PageTree.new(page_paths: page_paths)
39
+ end
40
+
41
+ def bundle_path_for(path)
42
+ Util.bundle_path(path)
43
+ end
44
+
45
+ def root_path
46
+ Pathname.new(Dir.pwd).join('rspec-documentation')
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RSpecDocumentation
4
+ # A hierarchical structure of all pages in the documentation tree. Used for rendering a navigation section.
5
+ class PageTree
6
+ def initialize(page_paths:)
7
+ @page_paths = page_paths
8
+ @structure = {}
9
+ @nodes = []
10
+ end
11
+
12
+ def elements
13
+ build_nodes(
14
+ root: tree['rspec-documentation']['pages'],
15
+ path: root_path.join('rspec-documentation/pages')
16
+ )
17
+ nodes.flatten.compact
18
+ end
19
+
20
+ private
21
+
22
+ attr_reader :page_paths, :structure, :nodes
23
+
24
+ def tree
25
+ @tree ||= begin
26
+ build_tree
27
+ structure
28
+ end
29
+ end
30
+
31
+ def build_nodes(root:, path:)
32
+ root[:children].sort.map do |child|
33
+ node = page_tree_node(path: path, child: child)
34
+ next nil if node.nil?
35
+
36
+ nodes.concat(node)
37
+ nodes.push('<ul>')
38
+ build_nodes(root: root[child], path: path.join(child)) unless root[child].nil?
39
+ nodes.push('</ul>')
40
+ end
41
+ end
42
+
43
+ def build_tree(branch: structure, depth: 0)
44
+ normalized_paths.each do |path|
45
+ first, second, *rest = path_segments(path: path, depth: depth)
46
+ next if second.nil?
47
+
48
+ branch[first] ||= {}
49
+ branch[first][:children] ||= Set.new
50
+ branch[first][:children].add(second)
51
+ build_tree(branch: branch[first], depth: depth + 1)
52
+ end
53
+ end
54
+
55
+ def path_segments(path:, depth:)
56
+ path.to_s.split('/')[depth..]
57
+ end
58
+
59
+ def root_path
60
+ @root_path ||= Pathname.new(Dir.pwd)
61
+ end
62
+
63
+ def normalized_paths
64
+ @normalized_paths ||= page_paths.sort.map do |path|
65
+ path.relative_path_from(root_path)
66
+ end
67
+ end
68
+
69
+ def page_tree_node(path:, child:)
70
+ PageTreeElement.new(path: path, child: child).node
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RSpecDocumentation
4
+ # An element in a page tree, generates a single entry as a list item, linked to the relevant
5
+ # document (or just a text entry if no document exists, i.e. if it is a directory without an
6
+ # index file).
7
+ class PageTreeElement
8
+ def initialize(path:, child:)
9
+ @path = path
10
+ @child = child
11
+ @nodes = []
12
+ end
13
+
14
+ def node
15
+ return nil if entry_and_directory?
16
+
17
+ nodes.push('<li>')
18
+ nodes.push(link) if entry?
19
+ nodes.push(bullet) unless entry?
20
+ nodes.push('</li>')
21
+ nodes
22
+ end
23
+
24
+ private
25
+
26
+ attr_reader :path, :child, :nodes
27
+
28
+ def entry_and_directory?
29
+ return false unless path.join(child).sub_ext('.md').file?
30
+ return false unless path.join(child).sub_ext('').directory?
31
+ return false unless path.join(child).file?
32
+
33
+ true
34
+ end
35
+
36
+ def entry?
37
+ path.join(child).sub_ext('.md').file?
38
+ end
39
+
40
+ def link
41
+ "<a href='#{href}'>#{title}</a>"
42
+ end
43
+
44
+ def bullet
45
+ "<b>#{title}</b>"
46
+ end
47
+
48
+ def href
49
+ Util.href(path.join(child))
50
+ end
51
+
52
+ def title
53
+ Util.label(child)
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RSpecDocumentation
4
+ # A parsed Markdown file, provides matched RSpec examples from the document and their location.
5
+ class ParsedDocument
6
+ attr_reader :failures
7
+
8
+ def initialize(document)
9
+ @document = Kramdown::Document.new(document, input: 'GFM')
10
+ @failures = []
11
+ end
12
+
13
+ def html
14
+ document.to_html
15
+ end
16
+
17
+ def execute_and_substitute_examples!
18
+ specs.each do |spec|
19
+ spec.run
20
+ break failures << spec.failure unless spec.failure.nil?
21
+
22
+ spec.parent.children[spec.index] = spec_element(spec)
23
+ end
24
+ end
25
+
26
+ def specs
27
+ @specs ||= recursive_specs
28
+ end
29
+
30
+ private
31
+
32
+ attr_reader :document
33
+
34
+ def recursive_specs(element: document.root)
35
+ element.children.each.with_index.map do |child, index|
36
+ next recursive_specs(element: child) unless child.children.empty?
37
+ next nil unless rspec_codeblock?(child)
38
+
39
+ spec_for(element: child, parent: element, index: index)
40
+ end.flatten.compact
41
+ end
42
+
43
+ def rspec_codeblock?(element)
44
+ return false unless element.type == :codeblock
45
+ return false unless element.options.key?(:lang)
46
+
47
+ element.options[:lang] == 'rspec' || element.options[:lang].start_with?('rspec:')
48
+ end
49
+
50
+ def spec_for(element:, parent:, index:)
51
+ Spec.new(
52
+ spec: element.value,
53
+ format: element.options[:lang].partition(':').last,
54
+ parent: parent,
55
+ index: index,
56
+ location: element.options[:location]
57
+ )
58
+ end
59
+
60
+ def spec_element(spec)
61
+ Kramdown::Element.new(:html_element, 'div', { location: spec.location }).tap do |element|
62
+ element.children = HtmlElement.new(spec: spec).element.children
63
+ end
64
+ end
65
+
66
+ def report_error(spec)
67
+ $stderr.write(spec.failure.message)
68
+ end
69
+ end
70
+ end