rspec-documentation 0.0.1 → 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (101) hide show
  1. checksums.yaml +4 -4
  2. data/.rspec +1 -0
  3. data/.rubocop.yml +3 -0
  4. data/Gemfile +3 -0
  5. data/Gemfile.lock +25 -2
  6. data/Makefile +9 -0
  7. data/README.md +18 -22
  8. data/Rakefile +1 -0
  9. data/exe/rspec-documentation +17 -0
  10. data/lib/rspec/documentation/version.rb +2 -2
  11. data/lib/rspec/documentation.rb +15 -2
  12. data/lib/rspec_documentation/configuration.rb +28 -0
  13. data/lib/rspec_documentation/document.rb +75 -0
  14. data/lib/rspec_documentation/documentation.rb +85 -0
  15. data/lib/rspec_documentation/formatters/ansi.rb +44 -0
  16. data/lib/rspec_documentation/formatters/html.rb +31 -0
  17. data/lib/rspec_documentation/formatters/json.rb +32 -0
  18. data/lib/rspec_documentation/formatters/ruby.rb +31 -0
  19. data/lib/rspec_documentation/formatters/yaml.rb +36 -0
  20. data/lib/rspec_documentation/formatters.rb +20 -0
  21. data/lib/rspec_documentation/html_element.rb +58 -0
  22. data/lib/rspec_documentation/javascript_bundle.rb +17 -0
  23. data/lib/rspec_documentation/markdown_renderer.rb +7 -0
  24. data/lib/rspec_documentation/page_collection.rb +64 -0
  25. data/lib/rspec_documentation/page_tree.rb +81 -0
  26. data/lib/rspec_documentation/page_tree_element.rb +78 -0
  27. data/lib/rspec_documentation/parsed_document.rb +72 -0
  28. data/lib/rspec_documentation/project_initialization.rb +52 -0
  29. data/lib/rspec_documentation/rspec/failure.rb +52 -0
  30. data/lib/rspec_documentation/rspec.rb +12 -0
  31. data/lib/rspec_documentation/spec.rb +108 -0
  32. data/lib/rspec_documentation/stylesheet_bundle.rb +17 -0
  33. data/lib/rspec_documentation/summary.rb +87 -0
  34. data/lib/rspec_documentation/util.rb +58 -0
  35. data/lib/rspec_documentation.rb +71 -0
  36. data/lib/tasks/rspec/documentation/generate.rake +10 -0
  37. data/lib/templates/000-Introduction.md.erb +35 -0
  38. data/lib/templates/010-Usage.md.erb +3 -0
  39. data/lib/templates/500-License.md.erb +3 -0
  40. data/lib/templates/bootstrap.js.erb +7 -0
  41. data/lib/templates/footer.html.erb +6 -0
  42. data/lib/templates/header.html.erb +20 -0
  43. data/lib/templates/layout.css.erb +418 -0
  44. data/lib/templates/layout.html.erb +31 -0
  45. data/lib/templates/layout.js.erb +67 -0
  46. data/lib/templates/modal_spec.html.erb +51 -0
  47. data/lib/templates/moon.svg.erb +1 -0
  48. data/lib/templates/script_tags.html.erb +1 -0
  49. data/lib/templates/stylesheet_links.html.erb +1 -0
  50. data/lib/templates/sun.svg.erb +1 -0
  51. data/lib/templates/tabbed_spec.html.erb +82 -0
  52. data/lib/templates/themes/bootstrap.min.css +11 -0
  53. data/lib/templates/themes/cerulean.css +11 -0
  54. data/lib/templates/themes/cosmo.css +11 -0
  55. data/lib/templates/themes/cyborg.css +11 -0
  56. data/lib/templates/themes/darkly.css +11 -0
  57. data/lib/templates/themes/flatly.css +11 -0
  58. data/lib/templates/themes/journal.css +11 -0
  59. data/lib/templates/themes/litera.css +11 -0
  60. data/lib/templates/themes/lumen.css +11 -0
  61. data/lib/templates/themes/lux.css +11 -0
  62. data/lib/templates/themes/materia.css +11 -0
  63. data/lib/templates/themes/minty.css +11 -0
  64. data/lib/templates/themes/morph.css +11 -0
  65. data/lib/templates/themes/pulse.css +11 -0
  66. data/lib/templates/themes/quartz.css +11 -0
  67. data/lib/templates/themes/sandstone.css +11 -0
  68. data/lib/templates/themes/simplex.css +11 -0
  69. data/lib/templates/themes/sketchy.css +11 -0
  70. data/lib/templates/themes/slate.css +11 -0
  71. data/lib/templates/themes/solar.css +11 -0
  72. data/lib/templates/themes/spacelab.css +11 -0
  73. data/lib/templates/themes/superhero.css +11 -0
  74. data/lib/templates/themes/united.css +11 -0
  75. data/lib/templates/themes/vapor.css +11 -0
  76. data/lib/templates/themes/yeti.css +11 -0
  77. data/lib/templates/themes/zephyr.css +11 -0
  78. data/rspec-documentation/pages/000-Introduction/000-Quickstart.md +17 -0
  79. data/rspec-documentation/pages/000-Introduction.md +23 -0
  80. data/rspec-documentation/pages/010-File System/000-Ordering.md +14 -0
  81. data/rspec-documentation/pages/010-File System/010-Documentation Bundle.md +9 -0
  82. data/rspec-documentation/pages/010-File System.md +26 -0
  83. data/rspec-documentation/pages/020-Running Tests.md +41 -0
  84. data/rspec-documentation/pages/030-Examples/010-Basic.md +51 -0
  85. data/rspec-documentation/pages/030-Examples/020-HTML.md +45 -0
  86. data/rspec-documentation/pages/030-Examples/030-ANSI.md +33 -0
  87. data/rspec-documentation/pages/030-Examples/040-JSON.md +39 -0
  88. data/rspec-documentation/pages/030-Examples/050-YAML.md +40 -0
  89. data/rspec-documentation/pages/030-Examples.md +7 -0
  90. data/rspec-documentation/pages/040-Spec Helper.md +11 -0
  91. data/rspec-documentation/pages/050-Linking.md +20 -0
  92. data/rspec-documentation/pages/060-Configuration/010-Context.md +26 -0
  93. data/rspec-documentation/pages/060-Configuration/020-Build Paths.md +33 -0
  94. data/rspec-documentation/pages/060-Configuration/030-Attribution.md +23 -0
  95. data/rspec-documentation/pages/060-Configuration.md +13 -0
  96. data/rspec-documentation/pages/070-Publishing.md +13 -0
  97. data/rspec-documentation/pages/500-License.md +11 -0
  98. data/rspec-documentation/spec_helper.rb +8 -0
  99. data/rspec-documentation.gemspec +10 -1
  100. data/sig/rspec/documentation.rbs +1 -1
  101. metadata +193 -5
@@ -0,0 +1,58 @@
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 code_source
18
+ formatter = Rouge::Formatters::HTML.new
19
+ lexer = Rouge::Lexers::Ruby.new
20
+ Formatters.with_translated_html_entities(formatter.format(lexer.lex(spec.source)))
21
+ end
22
+
23
+ def prettified_output
24
+ Formatters.with_translated_html_entities(formatter.prettified_output)
25
+ end
26
+
27
+ def rendered_output
28
+ return formatter.rendered_output if render_raw?
29
+
30
+ Formatters.with_translated_html_entities(formatter.rendered_output)
31
+ end
32
+
33
+ def render_raw?
34
+ formatter.render_raw?
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 formatter
46
+ @formatter ||= {
47
+ html: Formatters::Html,
48
+ ansi: Formatters::Ansi,
49
+ json: Formatters::Json,
50
+ yaml: Formatters::Yaml
51
+ }.fetch(spec.format, Formatters::Ruby).new(subject: spec.subject)
52
+ end
53
+
54
+ def tabbed_spec
55
+ RSpecDocumentation.template('tabbed_spec').result(binding)
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RSpecDocumentation
4
+ # Compiles Javascript assets into a single file.
5
+ class JavascriptBundle
6
+ def flush
7
+ Util.bundle_dir.join('assets').mkpath
8
+ javascript_bundle_path.write(RSpecDocumentation.template(:layout, :js).result(binding))
9
+ end
10
+
11
+ private
12
+
13
+ def javascript_bundle_path
14
+ Util.bundle_dir.join('assets/bundle.js')
15
+ end
16
+ end
17
+ 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,64 @@
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, :page_paths
8
+
9
+ def initialize(page_paths:)
10
+ @page_paths = page_paths.sort
11
+ @buffer = {}
12
+ @failures = []
13
+ end
14
+
15
+ def generate
16
+ page_paths.zip(documents).each do |path, document|
17
+ buffer[bundle_path_for(path)] = document.render
18
+ failures.concat(document.failures)
19
+ end
20
+ end
21
+
22
+ def documents
23
+ @documents ||= page_paths.map do |path|
24
+ Document.new(document: path.read, path: path, page_tree: page_tree(path: path))
25
+ end
26
+ end
27
+
28
+ def flush
29
+ Util.bundle_dir.rmtree if Util.bundle_dir.directory?
30
+ Util.bundle_dir.mkpath
31
+
32
+ buffer.each do |path, content|
33
+ path.dirname.mkpath
34
+ Util.bundle_path(path).write(content)
35
+ end
36
+ write_index unless buffer.empty?
37
+ end
38
+
39
+ def examples_count
40
+ documents.map(&:specs).flatten.size
41
+ end
42
+
43
+ private
44
+
45
+ attr_reader :buffer
46
+
47
+ def write_index
48
+ _path, content = buffer.first
49
+ Util.bundle_index_path.write(content)
50
+ end
51
+
52
+ def page_tree(path:)
53
+ PageTree.new(page_paths: page_paths, current_path: path)
54
+ end
55
+
56
+ def bundle_path_for(path)
57
+ Util.bundle_path(path)
58
+ end
59
+
60
+ def root_path
61
+ Pathname.new(Dir.pwd).join('rspec-documentation')
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,81 @@
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:, current_path:)
7
+ @page_paths = page_paths
8
+ @current_path = current_path
9
+ @structure = {}
10
+ @nodes = []
11
+ end
12
+
13
+ def elements
14
+ build_nodes(
15
+ root: tree['rspec-documentation']['pages'],
16
+ path: root_path.join('rspec-documentation/pages')
17
+ )
18
+ nodes.flatten.compact
19
+ end
20
+
21
+ private
22
+
23
+ attr_reader :page_paths, :structure, :nodes, :current_path
24
+
25
+ def tree
26
+ @tree ||= begin
27
+ build_tree
28
+ structure
29
+ end
30
+ end
31
+
32
+ def build_nodes(root:, path:)
33
+ root[:children].sort.each do |child|
34
+ node = page_tree_node(path: path, child: child)
35
+ next nil if node.nil?
36
+
37
+ li_open, *li_body, li_close = node
38
+ nodes.push(li_open)
39
+ nodes.concat(li_body)
40
+ push_children(root: root, path: path, child: child)
41
+ nodes.push(li_close)
42
+ end
43
+ end
44
+
45
+ def push_children(root:, path:, child:)
46
+ nodes.push('<ol>')
47
+ build_nodes(root: root[child], path: path.join(child)) unless root[child].nil?
48
+ nodes.last == '<ol>' ? nodes.pop : nodes.push('</ol>')
49
+ end
50
+
51
+ def build_tree(branch: structure, depth: 0)
52
+ normalized_paths.each do |path|
53
+ first, second, *rest = path_segments(path: path, depth: depth)
54
+ next if second.nil?
55
+
56
+ branch[first] ||= {}
57
+ branch[first][:children] ||= Set.new
58
+ branch[first][:children].add(second)
59
+ build_tree(branch: branch[first], depth: depth + 1)
60
+ end
61
+ end
62
+
63
+ def path_segments(path:, depth:)
64
+ path.to_s.split('/')[depth..]
65
+ end
66
+
67
+ def root_path
68
+ @root_path ||= Pathname.new(Dir.pwd)
69
+ end
70
+
71
+ def normalized_paths
72
+ @normalized_paths ||= page_paths.sort.map do |path|
73
+ path.relative_path_from(root_path)
74
+ end
75
+ end
76
+
77
+ def page_tree_node(path:, child:)
78
+ PageTreeElement.new(path: path, child: child, current_path: current_path).node
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,78 @@
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:, current_path:)
9
+ @path = path
10
+ @child = child
11
+ @current_path = current_path
12
+ @nodes = []
13
+ raise MissingFileError, "Missing file: #{entry_path.relative_path_from(Util.base_dir)}" unless entry?
14
+ end
15
+
16
+ def node
17
+ return nil if entry_and_directory?
18
+
19
+ nodes.push(li_open)
20
+ nodes.push(link)
21
+ nodes.push('</li>')
22
+ nodes
23
+ end
24
+
25
+ private
26
+
27
+ attr_reader :path, :child, :current_path, :nodes
28
+
29
+ def entry_and_directory?
30
+ return false unless path.join(child).sub_ext('.md').file?
31
+ return false unless path.join(child).sub_ext('').directory?
32
+ return false unless path.join(child).file?
33
+
34
+ true
35
+ end
36
+
37
+ def entry_path
38
+ path.join(child).sub_ext('.md')
39
+ end
40
+
41
+ def entry?
42
+ entry_path.file?
43
+ end
44
+
45
+ def li_open
46
+ "<li id='#{Util.path_id(path.join(child))}' #{active_class} data-list-item-id='##{path_id}' " \
47
+ "data-parent-id='##{parent_path_id}'>"
48
+ end
49
+
50
+ def link
51
+ "<a #{active_class} href='#{href}'>#{title}</a>"
52
+ end
53
+
54
+ def path_id
55
+ @path_id ||= Util.path_id(path.join(child))
56
+ end
57
+
58
+ def parent_path_id
59
+ @parent_path_id ||= Util.path_id(path)
60
+ end
61
+
62
+ def active_class
63
+ active? ? 'class="active"' : nil
64
+ end
65
+
66
+ def href
67
+ Util.href(path.join(child))
68
+ end
69
+
70
+ def active?
71
+ Util.href(current_path) == href
72
+ end
73
+
74
+ def title
75
+ Util.label(child)
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,72 @@
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, path:)
9
+ @document = Kramdown::Document.new(document, input: 'GFM', syntax_highlighter: 'rouge')
10
+ @path = path
11
+ @failures = []
12
+ end
13
+
14
+ def html
15
+ document.to_html
16
+ end
17
+
18
+ def execute_and_substitute_examples!
19
+ specs.each do |spec|
20
+ spec.run
21
+ break failures << spec.failure unless spec.failure.nil?
22
+
23
+ spec.parent.children[spec.index] = spec_element(spec)
24
+ end
25
+ end
26
+
27
+ def specs
28
+ @specs ||= recursive_specs
29
+ end
30
+
31
+ private
32
+
33
+ attr_reader :document, :path
34
+
35
+ def recursive_specs(element: document.root)
36
+ element.children.each.with_index.map do |child, index|
37
+ next recursive_specs(element: child) unless child.children.empty?
38
+ next nil unless rspec_codeblock?(child)
39
+
40
+ spec_for(element: child, parent: element, index: index)
41
+ end.flatten.compact
42
+ end
43
+
44
+ def rspec_codeblock?(element)
45
+ return false unless element.type == :codeblock
46
+ return false unless element.options.key?(:lang)
47
+
48
+ element.options[:lang] == 'rspec' || element.options[:lang].start_with?('rspec:')
49
+ end
50
+
51
+ def spec_for(element:, parent:, index:)
52
+ Spec.new(
53
+ spec: element.value,
54
+ format: element.options[:lang].partition(':').last,
55
+ parent: parent,
56
+ index: index,
57
+ path: path,
58
+ location: element.options[:location]
59
+ )
60
+ end
61
+
62
+ def spec_element(spec)
63
+ Kramdown::Element.new(:html_element, 'div', { location: spec.location }).tap do |element|
64
+ element.children = HtmlElement.new(spec: spec).element.children
65
+ end
66
+ end
67
+
68
+ def report_error(spec)
69
+ $stderr.write(spec.failure.message)
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RSpecDocumentation
4
+ # Initializes a new project if `rspec-documentation/` directory is empty.
5
+ class ProjectInitialization
6
+ include Paintbrush
7
+
8
+ def flush
9
+ print_welcome
10
+ create_base_dir
11
+ create_sample_files
12
+ print_initialization_complete
13
+ end
14
+
15
+ private
16
+
17
+ def print_welcome
18
+ warn(paintbrush { blue "\nWelcome to #{cyan 'RSpec Documentation'}. A new project is being initialized.\n" })
19
+ warn(paintbrush { blue "If you want undo at any point, simply delete #{cyan 'rspec-documentation/'}.\n" })
20
+ end
21
+
22
+ def create_base_dir
23
+ Util.base_dir.mkpath
24
+ print_created(Util.base_dir)
25
+ end
26
+
27
+ def create_sample_files
28
+ sample_files.each do |sample_file|
29
+ Util.base_dir.join(sample_file).sub_ext('.md').write(RSpecDocumentation.template(sample_file, :md).result)
30
+ print_created(Util.base_dir.join(sample_file).sub_ext('.md'))
31
+ end
32
+ end
33
+
34
+ def print_created(path)
35
+ warn(paintbrush { " #{green_b 'create'} #{cyan path.relative_path_from(pwd)}" })
36
+ end
37
+
38
+ def print_initialization_complete
39
+ warn(paintbrush do
40
+ blue "\nYour documentation project has been #{green 'initialized'} wlth smoe example pages."
41
+ end)
42
+ end
43
+
44
+ def pwd
45
+ Pathname.new(Dir.pwd)
46
+ end
47
+
48
+ def sample_files
49
+ %w[000-Introduction 010-Usage 500-License]
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RSpecDocumentation
4
+ module RSpec
5
+ # Stores information about a failed RSpec example. Thin wrapper around `RSpec::Failure`.
6
+ class Failure
7
+ include Paintbrush
8
+
9
+ attr_reader :spec
10
+
11
+ def initialize(cause:, spec:)
12
+ @cause = cause
13
+ @spec = spec
14
+ end
15
+
16
+ def message
17
+ "\n#{formatted_header}\n\n#{formatted_source}\n\n#{formatted_cause}\n\n"
18
+ end
19
+
20
+ private
21
+
22
+ attr_reader :cause
23
+
24
+ def formatted_header
25
+ paintbrush { cyan indented("# #{path}:#{spec.location}") }
26
+ end
27
+
28
+ def path
29
+ spec.path.relative_path_from(Pathname.new(Dir.pwd))
30
+ end
31
+
32
+ def formatted_source
33
+ paintbrush { white indented(spec.source) }
34
+ end
35
+
36
+ def formatted_cause
37
+ paintbrush { red indented(without_anonymous_group_text(cause.message)) }
38
+ end
39
+
40
+ def indented(text)
41
+ text.split("\n").map { |line| " #{line}" }.join("\n")
42
+ end
43
+
44
+ # If an error occurs outside of a test, RSpec will provide an error to the Reporter
45
+ # referring to an anonymous group due to the way specs are evaluated. This does not help
46
+ # with debugging so we remove it. TODO: Find a better way ?
47
+ def without_anonymous_group_text(text)
48
+ text&.gsub(/ for #<RSpec::ExampleGroups::Anonymous.*$/, '')
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'rspec/failure'
4
+
5
+ module RSpecDocumentation
6
+ # Provides various classes that wrap or interface RSpec internals.
7
+ module RSpec
8
+ def self.with_failure_notifier(callable, &block)
9
+ ::RSpec::Support.with_failure_notifier(callable, &block)
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,108 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RSpecDocumentation
4
+ # Executes specs from a Markdown code block and provides the outcome in the appropriate format.
5
+ class Spec
6
+ attr_reader :parent, :location, :index, :format, :path
7
+
8
+ @durations = []
9
+
10
+ class << self
11
+ attr_accessor :subjects, :durations
12
+ end
13
+
14
+ def initialize(spec:, format:, parent:, location:, path:, index:) # rubocop:disable Metrics/ParameterLists
15
+ @spec = spec
16
+ @format = format.empty? ? :plaintext : format.to_sym
17
+ @parent = parent
18
+ @location = location
19
+ @path = path
20
+ @index = index
21
+ @failures = []
22
+ end
23
+
24
+ def subject
25
+ raise Error, "Code block did not define an example (e.g. with `it`).\n#{spec}" if examples.empty?
26
+ raise Error, "Code block did not define a subject:\n#{spec}" if subjects.empty?
27
+ raise Error, "Cannot define more than one example per code block:\n#{spec}" if subjects.size > 1
28
+
29
+ subjects.last
30
+ end
31
+
32
+ def source
33
+ spec
34
+ end
35
+
36
+ def failure
37
+ failures.first
38
+ end
39
+
40
+ def reporter
41
+ @reporter ||= ::RSpec::Core::Reporter.new(::RSpec::Core::Configuration.new)
42
+ end
43
+
44
+ def run
45
+ self.class.subjects = []
46
+ RSpec.with_failure_notifier(failure_notifier) do
47
+ succeeded = example_group.run(reporter)
48
+ durations << run_time if run_time
49
+ next succeeded if succeeded
50
+
51
+ notify_failure(reported_failure)
52
+ succeeded
53
+ end
54
+ end
55
+
56
+ private
57
+
58
+ attr_reader :spec, :failures
59
+
60
+ def subjects
61
+ self.class.subjects
62
+ end
63
+
64
+ def durations
65
+ self.class.durations
66
+ end
67
+
68
+ def examples
69
+ @examples ||= example_group.examples
70
+ end
71
+
72
+ def example_group
73
+ # rubocop:disable Style/DocumentDynamicEvalDefinition, Security/Eval
74
+ @example_group ||= binding.eval(
75
+ <<-SPEC, __FILE__, __LINE__.to_i
76
+ ::RSpec::Core::ExampleGroup.describe do
77
+ after { RSpecDocumentation::Spec.subjects << subject }
78
+ include_context '__rspec_documentation' do
79
+ #{spec}
80
+ end
81
+ end
82
+ SPEC
83
+ )
84
+ # rubocop:enable Style/DocumentDynamicEvalDefinition, Security/Eval
85
+ end
86
+
87
+ def run_time
88
+ return nil unless reporter&.examples&.first
89
+
90
+ @run_time ||= reporter.examples.first.execution_result.run_time
91
+ end
92
+
93
+ def notify_failure(failure)
94
+ failures << RSpec::Failure.new(cause: failure, spec: self)
95
+ end
96
+
97
+ def reported_failure
98
+ exception = reporter.failed_examples.first.exception
99
+ return exception unless exception.is_a?(::RSpec::Core::MultipleExceptionError)
100
+
101
+ exception.all_exceptions.first
102
+ end
103
+
104
+ def failure_notifier
105
+ proc { |failure| notify_failure(failure) }
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RSpecDocumentation
4
+ # Compiles CSS assets into a single file.
5
+ class StylesheetBundle
6
+ def flush
7
+ Util.bundle_dir.join('assets').mkpath
8
+ stylesheet_bundle_path.write(RSpecDocumentation.template(:layout, :css).result(binding))
9
+ end
10
+
11
+ private
12
+
13
+ def stylesheet_bundle_path
14
+ Util.bundle_dir.join('assets/bundle.css')
15
+ end
16
+ end
17
+ end