asciidoctor-doctest 1.5.2.0 → 2.0.0.beta.1

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 (43) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +1 -1
  3. data/README.adoc +48 -68
  4. data/features/fixtures/html-slim/Rakefile +5 -11
  5. data/features/generator_html.feature +6 -6
  6. data/features/test_html.feature +70 -28
  7. data/lib/asciidoctor/doctest.rb +11 -14
  8. data/lib/asciidoctor/doctest/asciidoc_converter.rb +85 -0
  9. data/lib/asciidoctor/doctest/{base_example.rb → example.rb} +6 -27
  10. data/lib/asciidoctor/doctest/factory.rb +36 -0
  11. data/lib/asciidoctor/doctest/generator.rb +30 -23
  12. data/lib/asciidoctor/doctest/html/converter.rb +64 -0
  13. data/lib/asciidoctor/doctest/{html/normalizer.rb → html_normalizer.rb} +4 -4
  14. data/lib/asciidoctor/doctest/io.rb +14 -0
  15. data/lib/asciidoctor/doctest/{asciidoc/examples_suite.rb → io/asciidoc.rb} +4 -8
  16. data/lib/asciidoctor/doctest/{base_examples_suite.rb → io/base.rb} +28 -46
  17. data/lib/asciidoctor/doctest/io/xml.rb +69 -0
  18. data/lib/asciidoctor/doctest/no_fallback_template_converter.rb +42 -0
  19. data/lib/asciidoctor/doctest/rake_tasks.rb +229 -0
  20. data/lib/asciidoctor/doctest/test_reporter.rb +110 -0
  21. data/lib/asciidoctor/doctest/tester.rb +134 -0
  22. data/lib/asciidoctor/doctest/version.rb +1 -1
  23. data/spec/asciidoc_converter_spec.rb +64 -0
  24. data/spec/{base_example_spec.rb → example_spec.rb} +4 -5
  25. data/spec/factory_spec.rb +46 -0
  26. data/spec/html/converter_spec.rb +95 -0
  27. data/spec/{html/normalizer_spec.rb → html_normalizer_spec.rb} +1 -1
  28. data/spec/{asciidoc/examples_suite_spec.rb → io/asciidoc_spec.rb} +3 -8
  29. data/spec/{html/examples_suite_spec.rb → io/xml_spec.rb} +3 -106
  30. data/spec/no_fallback_template_converter_spec.rb +38 -0
  31. data/spec/shared_examples/{base_examples_suite.rb → base_examples.rb} +25 -28
  32. data/spec/spec_helper.rb +4 -0
  33. data/spec/tester_spec.rb +153 -0
  34. metadata +52 -59
  35. data/features/fixtures/html-slim/test/html_test.rb +0 -6
  36. data/features/fixtures/html-slim/test/test_helper.rb +0 -5
  37. data/lib/asciidoctor/doctest/asciidoc_renderer.rb +0 -111
  38. data/lib/asciidoctor/doctest/generator_task.rb +0 -115
  39. data/lib/asciidoctor/doctest/html/example.rb +0 -21
  40. data/lib/asciidoctor/doctest/html/examples_suite.rb +0 -118
  41. data/lib/asciidoctor/doctest/test.rb +0 -125
  42. data/spec/asciidoc_renderer_spec.rb +0 -103
  43. data/spec/test_spec.rb +0 -164
@@ -6,7 +6,7 @@ module Asciidoctor
6
6
  module DocTest
7
7
  ##
8
8
  # This class represents a single test example.
9
- class BaseExample
9
+ class Example
10
10
 
11
11
  NAME_SEPARATOR = ':'
12
12
 
@@ -101,39 +101,18 @@ module Asciidoctor
101
101
  end
102
102
 
103
103
  ##
104
- # @note The default implementation returns content as-is; subclasses
105
- # should override this method.
106
- #
107
- # @return [String] copy of the content in a form that is suitable for
108
- # semantic comparison with another content.
109
- #
110
- def content_normalized
111
- content.dup
112
- end
113
-
114
- ##
115
- # @note (see #content_normalized)
116
- #
117
- # @return [String] copy of the content in a human-readable (formatted)
118
- # shape for pretty print.
119
- #
120
- def content_pretty
121
- content.dup
122
- end
123
-
124
- ##
125
- # @return (see #content_pretty)
104
+ # @return [String] a copy of the content.
126
105
  def to_s
127
- content_pretty
106
+ content.dup
128
107
  end
129
108
 
130
109
  ##
131
110
  # @param other the object to compare with.
132
111
  # @return [Boolean] +true+ if +self+ and +other+ equals in attributes
133
- # +group_name+, +local_name+ and +content_normalized+ (compared
134
- # using +==+), otherwise +false+.
112
+ # +group_name+, +local_name+ and +content+ (compared using +==+),
113
+ # otherwise +false+.
135
114
  def ==(other)
136
- [:group_name, :local_name, :content_normalized].all? do |name|
115
+ [:group_name, :local_name, :content].all? do |name|
137
116
  other.respond_to?(name) &&
138
117
  public_send(name) == other.public_send(name)
139
118
  end
@@ -0,0 +1,36 @@
1
+ module Asciidoctor::DocTest
2
+ module Factory
3
+
4
+ ##
5
+ # Registers the given class in the factory under the specified name.
6
+ #
7
+ # @param name [#to_sym] the name under which to register the class.
8
+ # @param klass [Class] the class to register.
9
+ # @param default_opts [Hash] default options to be passed into the class'
10
+ # initializer. May be overriden by +opts+ passed to {.create}.
11
+ # @return [self]
12
+ #
13
+ def register(name, klass, default_opts = {})
14
+ @factory_registry ||= {}
15
+ @factory_registry[name.to_sym] = ->(opts) { klass.new(default_opts.merge(opts)) }
16
+ self
17
+ end
18
+
19
+ ##
20
+ # @param name [#to_sym] name of the class to create.
21
+ # @param opts [Hash] options to be passed into the class' initializer.
22
+ # @return [Object] a new instance of the class registered under the
23
+ # specified name.
24
+ # @raise ArgumentError if no class was found for the given name.
25
+ #
26
+ def create(name, opts = {})
27
+ @factory_registry ||= {}
28
+
29
+ if (obj = @factory_registry[name.to_sym])
30
+ obj.call(opts)
31
+ else
32
+ fail ArgumentError, "No class registered with name: #{name}"
33
+ end
34
+ end
35
+ end
36
+ end
@@ -5,62 +5,69 @@ using Corefines::String::color
5
5
 
6
6
  module Asciidoctor
7
7
  module DocTest
8
- module Generator
8
+ class Generator
9
9
 
10
10
  ##
11
- # Generates missing, or rewrite existing output examples from the
12
- # input examples converted using the +renderer+.
11
+ # @param input_suite [IO::Base] an instance of {IO::Base} subclass to
12
+ # read the reference input examples.
13
13
  #
14
- # @param output_suite [BaseExamplesSuite] an instance of
15
- # {BaseExamplesSuite} subclass to read and generate the output
16
- # examples.
14
+ # @param output_suite [IO::Base] an instance of {IO::Base} subclass to
15
+ # read and generate the output examples.
17
16
  #
18
- # @param input_suite [BaseExamplesSuite] an instance of
19
- # {BaseExamplesSuite} subclass to read the reference input
20
- # examples.
17
+ # @param converter [#call] a callable that accepts a string content of
18
+ # an input example and a hash with options for the converter, and
19
+ # returns the converted content.
21
20
  #
22
- # @param renderer [#convert]
21
+ # @param io [#<<] output stream where to write log messages.
22
+ #
23
+ def initialize(input_suite, output_suite, converter, io = $stdout)
24
+ @input_suite = input_suite
25
+ @output_suite = output_suite
26
+ @converter = converter
27
+ @io = io
28
+ end
29
+
30
+ ##
31
+ # Generates missing, or rewrite existing output examples from the
32
+ # input examples converted using the +converter+.
23
33
  #
24
34
  # @param pattern [String] glob-like pattern to select examples to
25
- # (re)generate (see {BaseExample#name_match?}).
35
+ # (re)generate (see {Example#name_match?}).
26
36
  #
27
37
  # @param rewrite [Boolean] whether to rewrite an already existing
28
38
  # example.
29
39
  #
30
- # @param log_os [#<<] output stream where to write log messages.
31
- #
32
- def self.generate!(output_suite, input_suite, renderer, pattern: '*:*',
33
- rewrite: false, log_os: $stdout)
40
+ def generate!(pattern: '*:*', rewrite: false)
34
41
  updated = []
35
42
 
36
- input_suite.pair_with(output_suite).each do |input, output|
43
+ @input_suite.pair_with(@output_suite).each do |input, output|
37
44
  next unless input.name_match? pattern
38
45
 
39
46
  log = ->(msg, color = :default) do
40
- log_os << " --> #{(msg % input.name).color(color)}\n" if log_os
47
+ @io << " --> #{(msg % input.name).color(color)}\n" if @io
41
48
  end
42
49
 
43
50
  if input.empty?
44
51
  log["Unknown %s, doesn't exist in input examples!"]
45
52
  else
46
- rendered = output_suite.convert_example(input, output.opts, renderer)
47
- rendered.desc = output.desc
53
+ actual, expected = @converter.convert_examples(input, output)
54
+ generated = output.dup.tap { |ex| ex.content = actual }
48
55
 
49
56
  if output.empty?
50
57
  log['Generating %s', :magenta]
51
- updated << rendered
52
- elsif rendered == output
58
+ updated << generated
59
+ elsif actual == expected
53
60
  log['Unchanged %s', :green]
54
61
  elsif rewrite
55
62
  log['Rewriting %s', :red]
56
- updated << rendered
63
+ updated << generated
57
64
  else
58
65
  log['Skipping %s', :yellow]
59
66
  end
60
67
  end
61
68
  end
62
69
 
63
- output_suite.update_examples updated unless updated.empty?
70
+ @output_suite.update_examples updated unless updated.empty?
64
71
  end
65
72
  end
66
73
  end
@@ -0,0 +1,64 @@
1
+ require 'asciidoctor/doctest/html_normalizer'
2
+ require 'corefines'
3
+ require 'htmlbeautifier'
4
+ require 'nokogiri'
5
+
6
+ using Corefines::Object::then
7
+
8
+ module Asciidoctor::DocTest
9
+ module HTML
10
+ class Converter < AsciidocConverter
11
+
12
+ def initialize(paragraph_xpath: './p/node()', **opts)
13
+ @paragraph_xpath = paragraph_xpath
14
+ super opts
15
+ end
16
+
17
+ def convert_examples(input_exmpl, output_exmpl)
18
+ opts = output_exmpl.opts.dup
19
+
20
+ # The header & footer are excluded by default; always enable for document examples.
21
+ opts[:header_footer] ||= input_exmpl.name.start_with?('document')
22
+
23
+ # When asserting inline examples, defaults to ignore paragraph "wrapper".
24
+ opts[:include] ||= (@paragraph_xpath if input_exmpl.name.start_with? 'inline_')
25
+
26
+ actual = convert(input_exmpl.content, header_footer: opts[:header_footer])
27
+ .then { |s| parse_html s, !opts[:header_footer] }
28
+ .then { |h| find_nodes h, opts[:include] }
29
+ .then { |h| remove_nodes h, opts[:exclude] }
30
+ .then { |h| normalize(h) }
31
+
32
+ expected = normalize(output_exmpl.content)
33
+
34
+ [actual, expected]
35
+ end
36
+
37
+ protected
38
+
39
+ def normalize(content)
40
+ content = parse_html(content) if content.is_a? String
41
+ HtmlBeautifier.beautify(content.normalize!)
42
+ end
43
+
44
+ def find_nodes(html, xpaths)
45
+ Array(xpaths).reduce(html) do |htm, xpath|
46
+ # XPath returns NodeSet, but we need DocumentFragment, so convert it again.
47
+ parse_html htm.xpath(xpath).to_html
48
+ end
49
+ end
50
+
51
+ def remove_nodes(html, xpaths)
52
+ return html unless xpaths
53
+
54
+ Array(xpaths).each_with_object(html.clone) do |xpath, htm|
55
+ htm.xpath(xpath).remove
56
+ end
57
+ end
58
+
59
+ def parse_html(str, fragment = true)
60
+ fragment ? ::Nokogiri::HTML.fragment(str) : ::Nokogiri::HTML.parse(str)
61
+ end
62
+ end
63
+ end
64
+ end
@@ -3,8 +3,8 @@ require 'nokogiri'
3
3
 
4
4
  using Corefines::Object::try
5
5
 
6
- module Asciidoctor::DocTest
7
- module HTML
6
+ module Asciidoctor
7
+ module DocTest
8
8
  ##
9
9
  # Module to be included into +Nokogiri::HTML::Document+
10
10
  # or +DocumentFragment+ to add {#normalize!} feature.
@@ -12,7 +12,7 @@ module Asciidoctor::DocTest
12
12
  # @example
13
13
  # Nokogiri::HTML.parse(str).normalize!
14
14
  # Nokogiri::HTML.fragment(str).normalize!
15
- module Normalizer
15
+ module HtmlNormalizer
16
16
 
17
17
  ##
18
18
  # Normalizes the HTML document or fragment so it can be easily compared
@@ -116,5 +116,5 @@ module Asciidoctor::DocTest
116
116
  end
117
117
 
118
118
  [Nokogiri::HTML::Document, Nokogiri::HTML::DocumentFragment].each do |klass|
119
- klass.send :include, Asciidoctor::DocTest::HTML::Normalizer
119
+ klass.send :include, Asciidoctor::DocTest::HtmlNormalizer
120
120
  end
@@ -0,0 +1,14 @@
1
+ require 'asciidoctor/doctest/io/base'
2
+ require 'asciidoctor/doctest/io/asciidoc'
3
+ require 'asciidoctor/doctest/io/xml'
4
+ require 'asciidoctor/doctest/factory'
5
+
6
+ module Asciidoctor::DocTest
7
+ module IO
8
+ extend Factory
9
+
10
+ register :asciidoc, Asciidoc, file_ext: '.adoc'
11
+ register :xml, XML, file_ext: '.xml'
12
+ register :html, XML, file_ext: '.html'
13
+ end
14
+ end
@@ -1,4 +1,4 @@
1
- require 'asciidoctor/doctest/base_examples_suite'
1
+ require 'asciidoctor/doctest/io/base'
2
2
  require 'corefines'
3
3
 
4
4
  using Corefines::Enumerable::map_send
@@ -6,9 +6,9 @@ using Corefines::Object[:blank?, :presence]
6
6
  using Corefines::String::concat!
7
7
 
8
8
  module Asciidoctor::DocTest
9
- module Asciidoc
9
+ module IO
10
10
  ##
11
- # Subclass of {BaseExamplesSuite} for reference input examples.
11
+ # Subclass of {IO::Base} for reference input examples.
12
12
  #
13
13
  # @example Format of the example's header
14
14
  # // .example-name
@@ -22,11 +22,7 @@ module Asciidoctor::DocTest
22
22
  #
23
23
  # NOTE: The trailing new line (below this) will be removed.
24
24
  #
25
- class ExamplesSuite < BaseExamplesSuite
26
-
27
- def initialize(file_ext: '.adoc', **kwargs)
28
- super
29
- end
25
+ class Asciidoc < Base
30
26
 
31
27
  def parse(input, group_name)
32
28
  examples = []
@@ -4,28 +4,28 @@ require 'pathname'
4
4
  using Corefines::Object::blank?
5
5
  using Corefines::Enumerable::index_by
6
6
 
7
- module Asciidoctor
8
- module DocTest
7
+ module Asciidoctor::DocTest
8
+ module IO
9
9
  ##
10
10
  # @abstract
11
11
  # This is a base class that should be extended for specific example
12
12
  # formats.
13
- class BaseExamplesSuite
13
+ class Base
14
14
 
15
- attr_reader :examples_path, :file_ext
15
+ attr_reader :path, :file_ext
16
16
 
17
17
  ##
18
+ # @param path [String, Array<String>] path of the directory (or multiple
19
+ # directories) where to look for the examples.
20
+ #
18
21
  # @param file_ext [String] the filename extension (e.g. +.adoc+) of the
19
22
  # examples group files. Must not be +nil+ or blank. (required)
20
23
  #
21
- # @param examples_path [String, Array<String>] path of the directory (or
22
- # multiple directories) where to look for the examples.
23
- #
24
- def initialize(file_ext: nil, examples_path: DocTest.examples_path)
24
+ def initialize(path: DocTest.examples_path, file_ext: nil)
25
25
  fail ArgumentError, 'file_ext must not be blank or nil' if file_ext.blank?
26
26
 
27
+ @path = Array(path).freeze
27
28
  @file_ext = file_ext.strip.freeze
28
- @examples_path = Array(examples_path).freeze
29
29
  @examples_cache = {}
30
30
  end
31
31
 
@@ -36,7 +36,7 @@ module Asciidoctor
36
36
  # @abstract
37
37
  # @param input [#each_line] the file content to parse.
38
38
  # @param group_name [String] the examples group name.
39
- # @return [Array<BaseExample>] parsed examples.
39
+ # @return [Array<Example>] parsed examples.
40
40
  # :nocov:
41
41
  def parse(input, group_name)
42
42
  fail NotImplementedError
@@ -47,7 +47,7 @@ module Asciidoctor
47
47
  # Serializes the given examples into string.
48
48
  #
49
49
  # @abstract
50
- # @param examples [Array<BaseExample>]
50
+ # @param examples [Array<Example>]
51
51
  # @return [String]
52
52
  # :nocov:
53
53
  def serialize(examples)
@@ -55,30 +55,6 @@ module Asciidoctor
55
55
  end
56
56
  # :nocov:
57
57
 
58
- ##
59
- # Returns a new example based on the given input example.
60
- # This method should render (AsciiDoc) content of the given example using
61
- # the preconfigured +renderer+ and eventually apply some transformations.
62
- #
63
- # XXX describe it better...
64
- #
65
- # @abstract
66
- # @param example [BaseExample] the input example to convert.
67
- # @param opts [Hash] the options to pass to a new example.
68
- # @param renderer [#convert]
69
- # @return [BaseExample]
70
- # :nocov:
71
- def convert_example(example, opts, renderer)
72
- fail NotImplementedError
73
- end
74
- # :nocov:
75
-
76
- ##
77
- # (see BaseExample#initialize)
78
- def create_example(*args)
79
- BaseExample.new(*args)
80
- end
81
-
82
58
  ##
83
59
  # Returns enumerator that yields pairs of the examples from this suite
84
60
  # and the +other_suite+ (examples with the same name) in order of this
@@ -89,7 +65,7 @@ module Asciidoctor
89
65
  # In the case of missing example from this suite, the pair is placed at
90
66
  # the end of the examples group.
91
67
  #
92
- # @param other_suite [BaseExamplesSuite]
68
+ # @param other_suite [Base]
93
69
  # @return [Enumerator]
94
70
  #
95
71
  def pair_with(other_suite)
@@ -112,9 +88,9 @@ module Asciidoctor
112
88
 
113
89
  ##
114
90
  # Reads the named examples group from file(s). When multiple matching
115
- # files are found on the {#examples_path}, it merges them together. If
91
+ # files are found on the {#path}, it merges them together. If
116
92
  # two files defines example with the same name, then the first wins (i.e.
117
- # first on the {#examples_path}).
93
+ # first on the {#path}).
118
94
  #
119
95
  # @param group_name [String] the examples group name.
120
96
  # @return [Array<Example>] an array of parsed examples, or an empty array
@@ -129,14 +105,14 @@ module Asciidoctor
129
105
 
130
106
  ##
131
107
  # Writes the given examples into file(s)
132
- # +{examples_path.first}/{group_name}{file_ext}+. Already existing files
133
- # will be overwritten!
108
+ # +{path.first}/{group_name}{file_ext}+. Already existing files will
109
+ # be overwritten!
134
110
  #
135
- # @param examples [Array<BaseExample>]
111
+ # @param examples [Array<Example>]
136
112
  #
137
113
  def write_examples(examples)
138
114
  examples.group_by(&:group_name).each do |group_name, exmpls|
139
- path = file_path(@examples_path.first, group_name)
115
+ path = file_path(@path.first, group_name)
140
116
  File.write(path, serialize(exmpls))
141
117
  end
142
118
  end
@@ -144,7 +120,7 @@ module Asciidoctor
144
120
  ##
145
121
  # Replaces existing examples with the given ones.
146
122
  #
147
- # @param examples [Array<BaseExample] the updated examples.
123
+ # @param examples [Array<Example] the updated examples.
148
124
  # @see #write_examples
149
125
  #
150
126
  def update_examples(examples)
@@ -162,12 +138,12 @@ module Asciidoctor
162
138
 
163
139
  ##
164
140
  # Returns names of all the example groups (files with {#file_ext})
165
- # found on the {#examples_path}.
141
+ # found on the {#path}.
166
142
  #
167
143
  # @return [Array<String>]
168
144
  #
169
145
  def group_names
170
- @examples_path.reduce(Set.new) { |acc, path|
146
+ @path.reduce(Set.new) { |acc, path|
171
147
  acc | Pathname.new(path).each_child
172
148
  .select { |p| p.file? && p.extname == @file_ext }
173
149
  .map { |p| p.sub_ext('').basename.to_s }
@@ -176,6 +152,12 @@ module Asciidoctor
176
152
 
177
153
  protected
178
154
 
155
+ ##
156
+ # (see Example#initialize)
157
+ def create_example(*args)
158
+ DocTest::Example.new(*args)
159
+ end
160
+
179
161
  ##
180
162
  # Converts the given options into the format used in examples file.
181
163
  #
@@ -207,7 +189,7 @@ module Asciidoctor
207
189
  private
208
190
 
209
191
  def read_files(file_name)
210
- @examples_path
192
+ @path
211
193
  .map { |dir| file_path(dir, file_name) }
212
194
  .select(&:readable?)
213
195
  .map(&:read)