asciidoctor-doctest 1.5.0

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 (71) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.adoc +0 -0
  3. data/LICENSE +21 -0
  4. data/README.adoc +327 -0
  5. data/Rakefile +12 -0
  6. data/data/examples/asciidoc/block_admonition.adoc +27 -0
  7. data/data/examples/asciidoc/block_audio.adoc +13 -0
  8. data/data/examples/asciidoc/block_colist.adoc +46 -0
  9. data/data/examples/asciidoc/block_dlist.adoc +99 -0
  10. data/data/examples/asciidoc/block_example.adoc +21 -0
  11. data/data/examples/asciidoc/block_floating_title.adoc +27 -0
  12. data/data/examples/asciidoc/block_image.adoc +28 -0
  13. data/data/examples/asciidoc/block_listing.adoc +68 -0
  14. data/data/examples/asciidoc/block_literal.adoc +30 -0
  15. data/data/examples/asciidoc/block_olist.adoc +55 -0
  16. data/data/examples/asciidoc/block_open.adoc +40 -0
  17. data/data/examples/asciidoc/block_outline.adoc +60 -0
  18. data/data/examples/asciidoc/block_page_break.adoc +6 -0
  19. data/data/examples/asciidoc/block_paragraph.adoc +17 -0
  20. data/data/examples/asciidoc/block_pass.adoc +5 -0
  21. data/data/examples/asciidoc/block_preamble.adoc +19 -0
  22. data/data/examples/asciidoc/block_quote.adoc +30 -0
  23. data/data/examples/asciidoc/block_sidebar.adoc +22 -0
  24. data/data/examples/asciidoc/block_stem.adoc +28 -0
  25. data/data/examples/asciidoc/block_table.adoc +168 -0
  26. data/data/examples/asciidoc/block_thematic_break.adoc +2 -0
  27. data/data/examples/asciidoc/block_toc.adoc +50 -0
  28. data/data/examples/asciidoc/block_ulist.adoc +43 -0
  29. data/data/examples/asciidoc/block_verse.adoc +37 -0
  30. data/data/examples/asciidoc/block_video.adoc +24 -0
  31. data/data/examples/asciidoc/document.adoc +51 -0
  32. data/data/examples/asciidoc/embedded.adoc +10 -0
  33. data/data/examples/asciidoc/inline_anchor.adoc +27 -0
  34. data/data/examples/asciidoc/inline_break.adoc +8 -0
  35. data/data/examples/asciidoc/inline_button.adoc +3 -0
  36. data/data/examples/asciidoc/inline_callout.adoc +5 -0
  37. data/data/examples/asciidoc/inline_footnote.adoc +9 -0
  38. data/data/examples/asciidoc/inline_image.adoc +44 -0
  39. data/data/examples/asciidoc/inline_kbd.adoc +7 -0
  40. data/data/examples/asciidoc/inline_menu.adoc +11 -0
  41. data/data/examples/asciidoc/inline_quoted.adoc +59 -0
  42. data/data/examples/asciidoc/section.adoc +74 -0
  43. data/doc/img/doctest-diag.odf +0 -0
  44. data/doc/img/doctest-diag.svg +56 -0
  45. data/doc/img/failing-test-term.gif +0 -0
  46. data/lib/asciidoctor-doctest.rb +1 -0
  47. data/lib/asciidoctor/doctest.rb +30 -0
  48. data/lib/asciidoctor/doctest/asciidoc/examples_suite.rb +44 -0
  49. data/lib/asciidoctor/doctest/asciidoc_renderer.rb +103 -0
  50. data/lib/asciidoctor/doctest/base_example.rb +161 -0
  51. data/lib/asciidoctor/doctest/base_examples_suite.rb +188 -0
  52. data/lib/asciidoctor/doctest/core_ext.rb +49 -0
  53. data/lib/asciidoctor/doctest/generator.rb +63 -0
  54. data/lib/asciidoctor/doctest/generator_task.rb +111 -0
  55. data/lib/asciidoctor/doctest/html/example.rb +21 -0
  56. data/lib/asciidoctor/doctest/html/examples_suite.rb +111 -0
  57. data/lib/asciidoctor/doctest/html/html_beautifier.rb +17 -0
  58. data/lib/asciidoctor/doctest/html/normalizer.rb +118 -0
  59. data/lib/asciidoctor/doctest/minitest_diffy.rb +74 -0
  60. data/lib/asciidoctor/doctest/test.rb +120 -0
  61. data/lib/asciidoctor/doctest/version.rb +5 -0
  62. data/spec/asciidoc/examples_suite_spec.rb +99 -0
  63. data/spec/base_example_spec.rb +176 -0
  64. data/spec/core_ext_spec.rb +67 -0
  65. data/spec/html/examples_suite_spec.rb +249 -0
  66. data/spec/html/normalizer_spec.rb +70 -0
  67. data/spec/shared_examples/base_examples_suite.rb +262 -0
  68. data/spec/spec_helper.rb +33 -0
  69. data/spec/support/matchers.rb +7 -0
  70. data/spec/test_spec.rb +164 -0
  71. metadata +360 -0
@@ -0,0 +1,49 @@
1
+ module Enumerable
2
+
3
+ ##
4
+ # Sends a message to each element and collects the result.
5
+ #
6
+ # @example
7
+ # [1, 2, 3].map_send(:+, 3) #=> [4, 5, 6]
8
+ #
9
+ # @param method_name [Symbol] name of the public method to call.
10
+ # @param args arguments to pass to the method.
11
+ # @param block [Proc] block to pass to the method.
12
+ # @return [Enumerable]
13
+ #
14
+ def map_send(method_name, *args, &block)
15
+ map { |e| e.public_send(method_name, *args, &block) }
16
+ end
17
+ end
18
+
19
+ class Module
20
+
21
+ ##
22
+ # Makes +new_name+ a new copy of the class method +old_name+.
23
+ #
24
+ # @param new_name [Symbol] name of the new class method to create.
25
+ # @param old_name [Symbol] name of the existing class method to alias.
26
+ #
27
+ def alias_class_method(new_name, old_name)
28
+ singleton_class.send(:alias_method, new_name, old_name)
29
+ end
30
+ end
31
+
32
+ class String
33
+
34
+ ##
35
+ # Appends (concatenates) the given object to +str+.
36
+ #
37
+ # @param obj [String, Integer] the string, or codepoint to append.
38
+ # @param separator [String, nil] the separator to append when this +str+ is
39
+ # not empty.
40
+ # @return [String] self
41
+ #
42
+ def concat(obj, separator = nil)
43
+ if separator && !self.empty?
44
+ self << separator << obj
45
+ else
46
+ self << obj
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,63 @@
1
+ require 'asciidoctor'
2
+ require 'colorize'
3
+
4
+ module Asciidoctor
5
+ module DocTest
6
+ module Generator
7
+
8
+ ##
9
+ # Generates missing, or rewrite existing output examples from the
10
+ # input examples converted using the +renderer+.
11
+ #
12
+ # @param output_suite [BaseExamplesSuite] an instance of
13
+ # {BaseExamplesSuite} subclass to read and generate the output
14
+ # examples.
15
+ #
16
+ # @param input_suite [BaseExamplesSuite] an instance of
17
+ # {BaseExamplesSuite} subclass to read the reference input
18
+ # examples.
19
+ #
20
+ # @param renderer [#render]
21
+ #
22
+ # @param pattern [String] glob-like pattern to select examples to
23
+ # (re)generate (see {BaseExample#name_match?}).
24
+ #
25
+ # @param rewrite [Boolean] whether to rewrite an already existing
26
+ # example.
27
+ #
28
+ # @param log_os [#<<] output stream where to write log messages.
29
+ #
30
+ def self.generate!(output_suite, input_suite, renderer, pattern: '*:*',
31
+ rewrite: false, log_os: $stdout)
32
+ updated = []
33
+
34
+ input_suite.pair_with(output_suite).each do |input, output|
35
+ next unless input.name_match? pattern
36
+
37
+ log = ->(msg, color = :default) do
38
+ log_os << " --> #{(msg % input.name).colorize(color)}\n" if log_os
39
+ end
40
+
41
+ if input.empty?
42
+ log["Unknown %s, doesn't exist in input examples!"]
43
+ else
44
+ rendered = output_suite.convert_example(input, output.opts, renderer)
45
+ if output.empty?
46
+ log['Generating %s', :magenta]
47
+ updated << rendered
48
+ elsif rendered == output
49
+ log['Unchanged %s', :green]
50
+ elsif rewrite
51
+ log['Rewriting %s', :red]
52
+ updated << rendered
53
+ else
54
+ log['Skipping %s', :yellow]
55
+ end
56
+ end
57
+ end
58
+
59
+ output_suite.update_examples updated unless updated.empty?
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,111 @@
1
+ require 'active_support/core_ext/string/strip'
2
+ require 'asciidoctor/doctest/generator'
3
+ require 'asciidoctor/doctest/core_ext'
4
+ require 'rake/tasklib'
5
+
6
+ module Asciidoctor
7
+ module DocTest
8
+ ##
9
+ # Rake task for generating output examples.
10
+ # @see Generator
11
+ class GeneratorTask < Rake::TaskLib
12
+
13
+ # List of values representing +true+.
14
+ TRUE_VALUES = %w[yes y true]
15
+
16
+ # This attribute is used only for the default {#input_suite}.
17
+ # @return (see DocTest.examples_path)
18
+ attr_accessor :examples_path
19
+
20
+ # @return [Boolean] whether to rewrite an already existing testing
21
+ # example. May be overriden with +FORCE+ variable on the command line
22
+ # (default: false).
23
+ attr_accessor :force
24
+
25
+ # @return [BaseExamplesSuite] an instance of {BaseExamplesSuite} subclass
26
+ # to read the reference input examples
27
+ # (default: +Asciidoc::ExamplesSuite.new(examples_path: examples_path)+).
28
+ attr_accessor :input_suite
29
+
30
+ # @return [BaseExamplesSuite] an instance of {BaseExamplesSuite} subclass
31
+ # to read and generate the output examples.
32
+ attr_accessor :output_suite
33
+
34
+ # @return [#to_sym] name of the task.
35
+ attr_accessor :name
36
+
37
+ # @return [String] glob pattern to select examples to (re)generate.
38
+ # May be overriden with +PATTERN+ variable on the command line
39
+ # (default: *:*).
40
+ attr_accessor :pattern
41
+
42
+ # @return [Hash] options for Asciidoctor renderer.
43
+ # @see AsciidocRenderer#initialize
44
+ attr_accessor :renderer_opts
45
+
46
+ # @return [String] title of the task's description.
47
+ attr_accessor :title
48
+
49
+
50
+ ##
51
+ # @param name [#to_sym] name of the task.
52
+ # @yield The block to configure this task.
53
+ def initialize(name)
54
+ @name = name
55
+ @examples_path = DocTest.examples_path
56
+ @force = false
57
+ @input_suite = nil
58
+ @output_suite = nil
59
+ @renderer_opts = {}
60
+ @pattern = '*:*'
61
+ @title = "Generate testing examples #{pattern}#{" for #{name}" if name != :generate}."
62
+
63
+ yield self
64
+
65
+ fail 'The output_suite is not provided!' unless @output_suite
66
+ if @output_suite.examples_path.first == DocTest::BUILTIN_EXAMPLES_PATH
67
+ fail "The examples_path in output suite is invalid: #{@output_suite.examples_path}"
68
+ end
69
+
70
+ @input_suite ||= Asciidoc::ExamplesSuite.new(examples_path: @examples_path)
71
+ @renderer ||= AsciidocRenderer.new(renderer_opts)
72
+
73
+ define
74
+ end
75
+
76
+ def pattern
77
+ ENV['PATTERN'] || @pattern
78
+ end
79
+
80
+ def force?
81
+ return TRUE_VALUES.include?(ENV['FORCE'].downcase) if ENV.key? 'FORCE'
82
+ !!force
83
+ end
84
+
85
+ private
86
+
87
+ def define
88
+ desc description
89
+
90
+ task name.to_sym do
91
+ puts title
92
+ Generator.generate! output_suite, input_suite, @renderer,
93
+ pattern: pattern, rewrite: force?
94
+ end
95
+ self
96
+ end
97
+
98
+ def description
99
+ <<-EOS.strip_heredoc
100
+ #{title}
101
+
102
+ Options (environment variables):
103
+ PATTERN glob pattern to select examples to (re)generate. [default: #{@pattern}]
104
+ E.g. *:*, block_toc:basic, block*:*, *list:with*, ...
105
+ FORCE overwrite existing examples (yes/no)? [default: #{@force ? 'yes' : 'no'}]
106
+
107
+ EOS
108
+ end
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,21 @@
1
+ require 'asciidoctor/doctest/base_example'
2
+ require 'asciidoctor/doctest/html/html_beautifier'
3
+ require 'asciidoctor/doctest/html/normalizer'
4
+ require 'nokogiri'
5
+
6
+ module Asciidoctor::DocTest
7
+ module HTML
8
+ ##
9
+ # Subclass of {BaseExample} for HTML-based backends.
10
+ class Example < BaseExample
11
+
12
+ def content_normalized
13
+ Nokogiri::HTML.fragment(content).normalize!.to_s
14
+ end
15
+
16
+ def to_s
17
+ HtmlBeautifier.beautify content_normalized
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,111 @@
1
+ require 'active_support/core_ext/array/wrap'
2
+ require 'active_support/core_ext/object/blank'
3
+ require 'asciidoctor/doctest/base_examples_suite'
4
+ require 'asciidoctor/doctest/core_ext'
5
+ require 'asciidoctor/doctest/html/example'
6
+ require 'asciidoctor/doctest/html/normalizer'
7
+ require 'nokogiri'
8
+
9
+ module Asciidoctor::DocTest
10
+ module HTML
11
+ ##
12
+ # Subclass of {BaseExamplesSuite} for HTML-based backends.
13
+ #
14
+ # @example Format of the example's header
15
+ # <!-- .example-name
16
+ # Any text that is not the example's name or an option and doesn't
17
+ # start with // is considered as a description.
18
+ # :option-1: value 1
19
+ # :option-2: value 1
20
+ # :option-2: value 2
21
+ # :boolean-option:
22
+ # -->
23
+ # <p>The example's content in <strong>HTML</strong>.</p>
24
+ #
25
+ # <div class="note">The trailing new line (below this) will be removed.</div>
26
+ #
27
+ class ExamplesSuite < BaseExamplesSuite
28
+
29
+ def initialize(file_ext: '.html', paragraph_xpath: './p/node()', **kwargs)
30
+ super file_ext: file_ext, **kwargs
31
+ @paragraph_xpath = paragraph_xpath
32
+ end
33
+
34
+ def parse(input, group_name)
35
+ examples = []
36
+ current = create_example(nil)
37
+ in_comment = false
38
+
39
+ input.each_line do |line|
40
+ line.chomp!
41
+ if line =~ /^<!--\s*\.([^ \n]+)/
42
+ name = $1
43
+ current.content.chomp!
44
+ examples << (current = create_example([group_name, name]))
45
+ in_comment = true
46
+ elsif in_comment
47
+ if line =~ /^\s*:([^:]+):(.*)/
48
+ current[$1.to_sym] = $2.blank? ? true : $2.strip
49
+ elsif !line.start_with?('//')
50
+ desc = line.rstrip.chomp('-->').strip
51
+ (current.desc ||= '').concat(desc, "\n") unless desc.empty?
52
+ end
53
+ else
54
+ current.content.concat(line, "\n")
55
+ end
56
+ in_comment &= !line.end_with?('-->')
57
+ end
58
+
59
+ examples
60
+ end
61
+
62
+ def serialize(examples)
63
+ Array.wrap(examples).map { |exmpl|
64
+ header = [ ".#{exmpl.local_name}", exmpl.desc.presence ].compact
65
+
66
+ exmpl.opts.each do |name, vals|
67
+ Array.wrap(vals).each do |val|
68
+ header << (val == true ? ":#{name}:" : ":#{name}: #{val}")
69
+ end
70
+ end
71
+ header_str = header.one? ? (header.first + ' ') : (header.join("\n") + "\n")
72
+
73
+ [ "<!-- #{header_str}-->", exmpl.content.presence ].compact.join("\n") + "\n"
74
+ }.join("\n")
75
+ end
76
+
77
+ def create_example(*args)
78
+ Example.new(*args)
79
+ end
80
+
81
+ def convert_example(example, opts, renderer)
82
+ header_footer = !!opts[:header_footer] || example.name.start_with?('document')
83
+
84
+ html = renderer.render(example.to_s, header_footer: header_footer)
85
+ html = parse_html(html, !header_footer)
86
+
87
+ # When asserting inline examples, ignore paragraph "wrapper".
88
+ includes = opts[:include] || (@paragraph_xpath if example.name.start_with? 'inline_')
89
+
90
+ Array.wrap(includes).each do |xpath|
91
+ # XPath returns NodeSet, but we need DocumentFragment, so convert it again.
92
+ html = parse_html(html.xpath(xpath).to_html)
93
+ end
94
+
95
+ Array.wrap(opts[:exclude]).each do |xpath|
96
+ html.xpath(xpath).remove
97
+ end
98
+
99
+ html.normalize!
100
+
101
+ create_example example.name, content: html.to_s, opts: opts
102
+ end
103
+
104
+ private
105
+
106
+ def parse_html(str, fragment = true)
107
+ fragment ? ::Nokogiri::HTML.fragment(str) : ::Nokogiri::HTML.parse(str)
108
+ end
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,17 @@
1
+ require 'htmlbeautifier'
2
+
3
+ module HtmlBeautifier
4
+
5
+ ##
6
+ # Beautifies the +input+ HTML.
7
+ #
8
+ # @param input [String, #to_html]
9
+ # @return [String] a beautified copy of the +input+.
10
+ #
11
+ def self.beautify(input)
12
+ input = input.to_html unless input.is_a? String
13
+ output = []
14
+ Beautifier.new(output).scan(input)
15
+ output.join
16
+ end
17
+ end
@@ -0,0 +1,118 @@
1
+ require 'active_support/core_ext/object/try'
2
+ require 'nokogiri'
3
+
4
+ module Asciidoctor::DocTest
5
+ module HTML
6
+ ##
7
+ # Module to be included into +Nokogiri::HTML::Document+
8
+ # or +DocumentFragment+ to add {#normalize!} feature.
9
+ #
10
+ # @example
11
+ # Nokogiri::HTML.parse(str).normalize!
12
+ # Nokogiri::HTML.fragment(str).normalize!
13
+ module Normalizer
14
+
15
+ ##
16
+ # Normalizes the HTML document or fragment so it can be easily compared
17
+ # with another HTML.
18
+ #
19
+ # What does it actually do?
20
+ #
21
+ # * sorts element attributes by name
22
+ # * sorts inline CSS declarations inside a +style+ attribute by name
23
+ # * removes all blank text nodes (i.e. node that contain just whitespaces)
24
+ # * strips nonsignificant leading and trailing whitespaces around text
25
+ # * strips nonsignificant repeated whitespaces
26
+ #
27
+ # @return [Object] self
28
+ #
29
+ def normalize!
30
+ traverse do |node|
31
+ case node.type
32
+
33
+ when Nokogiri::XML::Node::ELEMENT_NODE
34
+ sort_element_attrs! node
35
+ sort_element_style_attr! node
36
+
37
+ when Nokogiri::XML::Node::TEXT_NODE
38
+ # Remove text node that contains whitespaces only.
39
+ if node.blank?
40
+ node.remove
41
+
42
+ elsif !preformatted_block? node
43
+ strip_redundant_spaces! node
44
+ strip_spaces_around_text! node
45
+ end
46
+ end
47
+ end
48
+ self
49
+ end
50
+
51
+ private
52
+
53
+ # Sorts attributes of the element +node+ by name.
54
+ def sort_element_attrs!(node)
55
+ node.attributes.sort_by(&:first).each do |name, value|
56
+ node.delete(name)
57
+ node[name] = value
58
+ end
59
+ end
60
+
61
+ # Sorts CSS declarations in style attribute of the element +node+ by name.
62
+ def sort_element_style_attr!(node)
63
+ return unless node.has_attribute? 'style'
64
+ decls = node['style'].scan(/([\w-]+):\s*([^;]+);?/).sort_by(&:first)
65
+ node['style'] = decls.map { |name, val| "#{name}: #{val};" }.join(' ')
66
+ end
67
+
68
+ # Note: muttable methods like +gsub!+ doesn't work on node content.
69
+
70
+ # Strips repeated whitespaces in the text +node+.
71
+ def strip_redundant_spaces!(node)
72
+ node.content = node.content.gsub("\n", ' ').gsub(/(\s)+/, '\1')
73
+ end
74
+
75
+ # Strips nonsignificant leading and trailing whitespaces in the text +node+.
76
+ def strip_spaces_around_text!(node)
77
+ node.content = node.content.lstrip if text_block_boundary? node, :left
78
+ node.content = node.content.rstrip if text_block_boundary? node, :right
79
+ end
80
+
81
+ ##
82
+ # Returns +true+ if the text +node+ is the first (+:left+), or the last
83
+ # (+:right+) inline element of the nearest block element ancestor or
84
+ # direct sibling of +<br>+ element.
85
+ #
86
+ # @return [Boolean]
87
+ #
88
+ def text_block_boundary?(node, side)
89
+ method = { left: :previous_sibling, right: :next_sibling }[side]
90
+
91
+ return true if node.send(method).try(:name) == 'br'
92
+ loop do
93
+ if (sibling = node.send(method))
94
+ return false if sibling.text? || inline_element?(sibling)
95
+ end
96
+ node = node.parent
97
+ return true unless inline_element? node
98
+ end
99
+ end
100
+
101
+ HTML_INLINE_ELEMENTS = Nokogiri::HTML::ElementDescription::HTML_INLINE.flatten
102
+
103
+ # @return [Boolean] true if the +node+ represents an inline HTML element.
104
+ def inline_element?(node)
105
+ node.element? && HTML_INLINE_ELEMENTS.include?(node.name)
106
+ end
107
+
108
+ # @return [Boolean] true if the +node+ is descendant of +<pre>+ node.
109
+ def preformatted_block?(node)
110
+ node.path =~ %r{/pre/}
111
+ end
112
+ end
113
+ end
114
+ end
115
+
116
+ [Nokogiri::HTML::Document, Nokogiri::HTML::DocumentFragment].each do |klass|
117
+ klass.send :include, Asciidoctor::DocTest::HTML::Normalizer
118
+ end