rspec-documentation 0.0.1 → 0.0.2

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