asciidoctor-doctest 1.5.0

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