openstax_kitchen 2.0.0 → 3.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.devcontainer/devcontainer.json +37 -17
- data/.github/config.yml +14 -0
- data/.github/workflows/tests.yml +5 -15
- data/.gitignore +1 -1
- data/.inch.yml +6 -0
- data/.rubocop.yml +65 -0
- data/CHANGELOG.md +16 -0
- data/Gemfile +5 -3
- data/Gemfile.lock +54 -5
- data/README.md +58 -11
- data/Rakefile +5 -3
- data/bin/console +4 -3
- data/docker/Dockerfile +36 -0
- data/docker/Dockerfile.ci +10 -0
- data/docker/bash +5 -1
- data/docker/build +10 -0
- data/docker/ci +16 -0
- data/docker/run +9 -0
- data/docker/tag_and_push_latest +17 -0
- data/lefthook.yml +6 -0
- data/lib/kitchen/ancestor.rb +38 -1
- data/lib/kitchen/book_document.rb +20 -2
- data/lib/kitchen/book_element.rb +24 -3
- data/lib/kitchen/book_element_enumerator.rb +4 -0
- data/lib/kitchen/book_recipe.rb +15 -1
- data/lib/kitchen/chapter_element.rb +43 -3
- data/lib/kitchen/chapter_element_enumerator.rb +9 -1
- data/lib/kitchen/clipboard.rb +35 -4
- data/lib/kitchen/composite_chapter_element.rb +20 -1
- data/lib/kitchen/composite_page_element.rb +25 -2
- data/lib/kitchen/composite_page_element_enumerator.rb +8 -0
- data/lib/kitchen/config.rb +14 -7
- data/lib/kitchen/counter.rb +9 -2
- data/lib/kitchen/debug/print_recipe_error.rb +53 -35
- data/lib/kitchen/directions/.rubocop.yml +22 -0
- data/lib/kitchen/directions/bake_appendix.rb +4 -4
- data/lib/kitchen/directions/bake_chapter_glossary.rb +10 -7
- data/lib/kitchen/directions/bake_chapter_introductions.rb +6 -6
- data/lib/kitchen/directions/bake_chapter_key_equations.rb +9 -6
- data/lib/kitchen/directions/bake_chapter_summary.rb +16 -13
- data/lib/kitchen/directions/bake_chapter_title/main.rb +11 -0
- data/lib/kitchen/directions/bake_chapter_title/v1.rb +24 -0
- data/lib/kitchen/directions/bake_composite_pages.rb +2 -2
- data/lib/kitchen/directions/bake_example.rb +6 -4
- data/lib/kitchen/directions/bake_exercises/main.rb +11 -0
- data/lib/kitchen/directions/bake_exercises/v1.rb +166 -0
- data/lib/kitchen/directions/bake_figure.rb +8 -5
- data/lib/kitchen/directions/bake_footnotes/main.rb +2 -2
- data/lib/kitchen/directions/bake_footnotes/v1.rb +4 -4
- data/lib/kitchen/directions/bake_index/main.rb +2 -2
- data/lib/kitchen/directions/bake_index/v1.rb +22 -15
- data/lib/kitchen/directions/bake_link_placeholders.rb +24 -0
- data/lib/kitchen/directions/bake_math_in_paragraph.rb +5 -3
- data/lib/kitchen/directions/bake_notes.rb +8 -8
- data/lib/kitchen/directions/bake_numbered_table/main.rb +2 -2
- data/lib/kitchen/directions/bake_numbered_table/v1.rb +21 -16
- data/lib/kitchen/directions/bake_page_abstracts.rb +14 -0
- data/lib/kitchen/directions/bake_preface/main.rb +11 -0
- data/lib/kitchen/directions/bake_preface/v1.rb +18 -0
- data/lib/kitchen/directions/bake_stepwise.rb +7 -7
- data/lib/kitchen/directions/bake_suggested_reading.rb +26 -0
- data/lib/kitchen/directions/bake_toc.rb +41 -22
- data/lib/kitchen/directions/bake_unit_title/main.rb +11 -0
- data/lib/kitchen/directions/bake_unit_title/v1.rb +23 -0
- data/lib/kitchen/directions/bake_unnumbered_tables.rb +7 -5
- data/lib/kitchen/directions/move_title_text_into_span.rb +2 -2
- data/lib/kitchen/document.rb +72 -13
- data/lib/kitchen/element.rb +11 -0
- data/lib/kitchen/element_base.rb +276 -56
- data/lib/kitchen/element_enumerator.rb +8 -0
- data/lib/kitchen/element_enumerator_base.rb +210 -28
- data/lib/kitchen/element_enumerator_factory.rb +59 -52
- data/lib/kitchen/element_factory.rb +27 -12
- data/lib/kitchen/errors.rb +5 -0
- data/lib/kitchen/example_element.rb +19 -1
- data/lib/kitchen/example_element_enumerator.rb +9 -1
- data/lib/kitchen/figure_element.rb +36 -2
- data/lib/kitchen/figure_element_enumerator.rb +9 -1
- data/lib/kitchen/metadata_element.rb +28 -0
- data/lib/kitchen/metadata_element_enumerator.rb +21 -0
- data/lib/kitchen/mixins/block_error_if.rb +24 -4
- data/lib/kitchen/note_element.rb +37 -7
- data/lib/kitchen/note_element_enumerator.rb +9 -1
- data/lib/kitchen/oven.rb +66 -15
- data/lib/kitchen/page_element.rb +62 -13
- data/lib/kitchen/page_element_enumerator.rb +9 -1
- data/lib/kitchen/pantry.rb +28 -1
- data/lib/kitchen/patches/nokogiri.rb +19 -2
- data/lib/kitchen/patches/renderable.rb +9 -3
- data/lib/kitchen/patches/string.rb +8 -0
- data/lib/kitchen/recipe.rb +38 -34
- data/lib/kitchen/search_history.rb +43 -4
- data/lib/kitchen/search_query.rb +84 -0
- data/lib/kitchen/selectors/base.rb +26 -0
- data/lib/kitchen/selectors/standard_1.rb +8 -0
- data/lib/kitchen/table_element.rb +54 -3
- data/lib/kitchen/table_element_enumerator.rb +9 -1
- data/lib/kitchen/term_element.rb +15 -1
- data/lib/kitchen/term_element_enumerator.rb +9 -1
- data/lib/kitchen/transliterations.rb +7 -5
- data/lib/kitchen/type_casting_element_enumerator.rb +17 -1
- data/lib/kitchen/unit_element.rb +39 -0
- data/lib/kitchen/unit_element_enumerator.rb +20 -0
- data/lib/kitchen/utils.rb +10 -13
- data/lib/kitchen/version.rb +5 -1
- data/lib/locales/en.yml +6 -0
- data/lib/openstax_kitchen.rb +43 -42
- data/openstax_kitchen.gemspec +26 -20
- data/tutorials/00/solution1.rb +9 -0
- data/tutorials/00/solution2.rb +8 -0
- data/tutorials/01/solution1.rb +18 -0
- data/tutorials/01/solution2.rb +26 -0
- data/tutorials/02/solution1.rb +31 -0
- data/tutorials/03/{solution_1.rb → solution1.rb} +6 -4
- data/tutorials/03/solution2.rb +18 -0
- data/tutorials/04/{solution_1.rb → solution1.rb} +4 -2
- data/tutorials/04/{solution_2.rb → solution2.rb} +6 -4
- data/tutorials/05/solution1.rb +11 -0
- data/tutorials/check_it +16 -15
- data/tutorials/setup_my_recipes +7 -6
- metadata +101 -22
- data/Dockerfile +0 -19
- data/docker-compose.yml +0 -12
- data/docker/entrypoint +0 -9
- data/lib/kitchen/directions/bake_exercises.rb +0 -164
- data/tutorials/00/solution_1.rb +0 -7
- data/tutorials/00/solution_2.rb +0 -6
- data/tutorials/01/solution_1.rb +0 -16
- data/tutorials/01/solution_2.rb +0 -24
- data/tutorials/02/solution_1.rb +0 -29
- data/tutorials/03/solution_2.rb +0 -15
- data/tutorials/05/solution_1.rb +0 -9
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Kitchen::Directions::BakeUnitTitle
|
4
|
+
class V1
|
5
|
+
def bake(book:)
|
6
|
+
book.units.each do |unit|
|
7
|
+
compose_unit_title(unit: unit)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
def compose_unit_title(unit:)
|
12
|
+
heading = unit.title
|
13
|
+
heading.replace_children(with:
|
14
|
+
<<~HTML
|
15
|
+
<span class="os-part-text">#{I18n.t(:unit)} </span>
|
16
|
+
<span class="os-number">#{unit.count_in(:book)}</span>
|
17
|
+
<span class="os-divider"> </span>
|
18
|
+
<span data-type="" itemprop="" class="os-text">#{heading.text}</span>
|
19
|
+
HTML
|
20
|
+
)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -1,14 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Kitchen
|
2
4
|
module Directions
|
3
5
|
module BakeUnnumberedTables
|
4
|
-
|
5
6
|
def self.v1(book:)
|
6
|
-
book.tables(
|
7
|
-
table.wrap(%
|
8
|
-
table.remove_attribute(
|
7
|
+
book.tables('$.unnumbered').each do |table|
|
8
|
+
table.wrap(%(<div class="os-table">))
|
9
|
+
table.remove_attribute('summary')
|
10
|
+
table.parent.add_class('os-unstyled-container') if table.unstyled?
|
11
|
+
table.parent.add_class('os-column-header-container') if table.column_header?
|
9
12
|
end
|
10
13
|
end
|
11
|
-
|
12
14
|
end
|
13
15
|
end
|
14
16
|
end
|
data/lib/kitchen/document.rb
CHANGED
@@ -1,25 +1,56 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'forwardable'
|
2
4
|
|
3
5
|
module Kitchen
|
6
|
+
# Wrapper around a Nokogiri `Document`, adding search with Kitchen enumerators,
|
7
|
+
# clipboards, pantries, etc.
|
8
|
+
#
|
4
9
|
class Document
|
5
10
|
extend Forwardable
|
6
11
|
|
12
|
+
# @return [Element] the current element yielded from search results
|
7
13
|
attr_accessor :location
|
14
|
+
# @return [Config] the configuration used in this document
|
8
15
|
attr_reader :config
|
9
16
|
|
17
|
+
# @!method selectors
|
18
|
+
# The document's selectors
|
19
|
+
# @see Config#selectors
|
20
|
+
# @return [Selectors::Base]
|
10
21
|
def_delegators :config, :selectors
|
22
|
+
|
23
|
+
# @!method to_xhtml
|
24
|
+
# @see https://www.rubydoc.info/github/sparklemotion/nokogiri/Nokogiri/XML/Node#to_xhtml-instance_method Nokogiri::XML::Node#to_xhtml
|
25
|
+
# @return [String] the document as an XHTML string
|
26
|
+
# @!method to_s
|
27
|
+
# Turn this node in to a string. If the document is HTML, this method returns html.
|
28
|
+
# If the document is XML, this method returns XML.
|
29
|
+
# @see https://www.rubydoc.info/github/sparklemotion/nokogiri/Nokogiri/XML/Node#to_s-instance_method Nokogiri::XML::Node#to_s
|
30
|
+
# @return [String]
|
31
|
+
# @!method to_xml
|
32
|
+
# @see https://www.rubydoc.info/github/sparklemotion/nokogiri/Nokogiri/XML/Node#to_xml-instance_method Nokogiri::XML::Node#to_xml
|
33
|
+
# @return [String] the document as an XML string
|
34
|
+
# @!method to_html
|
35
|
+
# @see https://www.rubydoc.info/github/sparklemotion/nokogiri/Nokogiri/XML/Node#to_html-instance_method Nokogiri::XML::Node#to_html
|
36
|
+
# @return [String] the document as an HTML string
|
11
37
|
def_delegators :@nokogiri_document, :to_xhtml, :to_s, :to_xml, :to_html
|
12
38
|
|
39
|
+
# Return a new instance of Document
|
40
|
+
#
|
41
|
+
# @param nokogiri_document [Nokogiri::XML::Document]
|
42
|
+
# @param config [Config]
|
13
43
|
def initialize(nokogiri_document:, config: nil)
|
14
44
|
@nokogiri_document = nokogiri_document
|
15
45
|
@location = nil
|
16
|
-
@config = config || Config.
|
46
|
+
@config = config || Config.new
|
17
47
|
@next_paste_count_for_id = {}
|
18
|
-
@id_copy_suffix =
|
48
|
+
@id_copy_suffix = '_copy_'
|
19
49
|
end
|
20
50
|
|
21
51
|
# Returns an enumerator that iterates over all children of this document
|
22
|
-
# that match the provided selector or XPath arguments.
|
52
|
+
# that match the provided selector or XPath arguments. Updates `location`
|
53
|
+
# during iteration.
|
23
54
|
#
|
24
55
|
# @param selector_or_xpath_args [Array<String>] CSS selectors or XPath arguments
|
25
56
|
# @return [ElementEnumerator]
|
@@ -29,9 +60,11 @@ module Kitchen
|
|
29
60
|
|
30
61
|
ElementEnumerator.new do |block|
|
31
62
|
nokogiri_document.search(*selector_or_xpath_args).each do |inner_node|
|
32
|
-
element = Kitchen::Element.new(
|
33
|
-
|
34
|
-
|
63
|
+
element = Kitchen::Element.new(
|
64
|
+
node: inner_node,
|
65
|
+
document: self,
|
66
|
+
short_type: Utils.search_path_to_type(selector_or_xpath_args)
|
67
|
+
)
|
35
68
|
self.location = element
|
36
69
|
block.yield(element)
|
37
70
|
end
|
@@ -92,28 +125,52 @@ module Kitchen
|
|
92
125
|
)
|
93
126
|
end
|
94
127
|
|
128
|
+
# Create a new Element from a string
|
129
|
+
#
|
130
|
+
# @param string [String] the element as a string
|
131
|
+
#
|
132
|
+
# @example
|
133
|
+
# create_element_from_string("<div class='foo'>bar</div>") #=> <div class="foo">bar</div>
|
134
|
+
#
|
135
|
+
# @return [Element]
|
136
|
+
#
|
95
137
|
def create_element_from_string(string)
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
138
|
+
children = Nokogiri::XML("<foo>#{string}</foo>").search('foo').first.element_children
|
139
|
+
raise('new_element must only make one top-level element') if children.many?
|
140
|
+
|
141
|
+
node = children.first
|
142
|
+
|
143
|
+
create_element(node.name, node.attributes).tap do |element|
|
144
|
+
element.inner_html = node.children
|
145
|
+
end
|
101
146
|
end
|
102
147
|
|
148
|
+
# Keeps track that an element with the given ID has been copied. When such
|
149
|
+
# elements are pasted, this information is used to give those elements unique
|
150
|
+
# IDs that don't duplicate the original element.
|
151
|
+
#
|
152
|
+
# @param id [String] the ID
|
153
|
+
#
|
103
154
|
def record_id_copied(id)
|
104
155
|
return if id.blank?
|
156
|
+
|
105
157
|
@next_paste_count_for_id[id] ||= 1
|
106
158
|
end
|
107
159
|
|
160
|
+
# Returns a unique ID given the ID of an element that was copied and is about
|
161
|
+
# to be pasted
|
162
|
+
#
|
163
|
+
# @param original_id [String]
|
164
|
+
#
|
108
165
|
def modified_id_to_paste(original_id)
|
109
166
|
return nil if original_id.nil?
|
110
|
-
return
|
167
|
+
return '' if original_id.blank?
|
111
168
|
|
112
169
|
count = next_count_for_pasted_id(original_id)
|
113
170
|
|
114
171
|
# A count of 0 means the element was cut and this is the first paste, do not
|
115
172
|
# modify the ID; otherwise, use the uniquified ID.
|
116
|
-
if count
|
173
|
+
if count.zero?
|
117
174
|
original_id
|
118
175
|
else
|
119
176
|
"#{original_id}#{@id_copy_suffix}#{count}"
|
@@ -123,6 +180,7 @@ module Kitchen
|
|
123
180
|
# Returns the underlying Nokogiri Document object
|
124
181
|
#
|
125
182
|
# @return [Nokogiri::XML::Document]
|
183
|
+
#
|
126
184
|
def raw
|
127
185
|
@nokogiri_document
|
128
186
|
end
|
@@ -131,6 +189,7 @@ module Kitchen
|
|
131
189
|
|
132
190
|
def next_count_for_pasted_id(id)
|
133
191
|
return if id.blank?
|
192
|
+
|
134
193
|
(@next_paste_count_for_id[id] ||= 0).tap do
|
135
194
|
@next_paste_count_for_id[id] += 1
|
136
195
|
end
|
data/lib/kitchen/element.rb
CHANGED
@@ -1,6 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Kitchen
|
4
|
+
# An one-off element that isn't one of the main elements we have dedicated classes
|
5
|
+
# for (like ChapterElement). Provides a way to set an arbitrary +short_type+
|
6
|
+
#
|
2
7
|
class Element < ElementBase
|
3
8
|
|
9
|
+
# Creates a new +Element+
|
10
|
+
#
|
11
|
+
# @param node [Nokogiri::XML::Node] the node this element wraps
|
12
|
+
# @param document [Document] this element's document
|
13
|
+
# @param short_type [Symbol, String] the type for this element
|
14
|
+
#
|
4
15
|
def initialize(node:, document:, short_type: nil)
|
5
16
|
super(node: node,
|
6
17
|
document: document,
|
data/lib/kitchen/element_base.rb
CHANGED
@@ -1,8 +1,9 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'forwardable'
|
2
4
|
require 'securerandom'
|
3
5
|
|
4
6
|
module Kitchen
|
5
|
-
|
6
7
|
# Abstract base class for all elements. If you are looking for a simple concrete
|
7
8
|
# element class, use `Element`.
|
8
9
|
#
|
@@ -10,35 +11,101 @@ module Kitchen
|
|
10
11
|
extend Forwardable
|
11
12
|
include Mixins::BlockErrorIf
|
12
13
|
|
14
|
+
# The element's document
|
15
|
+
# @return [Document]
|
13
16
|
attr_reader :document
|
17
|
+
|
18
|
+
# The element's type, e.g. +:page+
|
19
|
+
# @return [Symbol, String]
|
14
20
|
attr_reader :short_type
|
21
|
+
|
22
|
+
# The enumerator class for this element
|
23
|
+
# @return [Class]
|
15
24
|
attr_reader :enumerator_class
|
16
|
-
|
25
|
+
|
26
|
+
# The search query that located this element in the DOM
|
27
|
+
# @return [SearchQuery]
|
28
|
+
attr_accessor :search_query_that_found_me
|
17
29
|
|
18
30
|
# @!method name
|
19
31
|
# Get the element name (the tag)
|
32
|
+
# @see https://www.rubydoc.info/github/sparklemotion/nokogiri/Nokogiri/XML/Node#name-instance_method Nokogiri::XML::Node#name
|
33
|
+
# @return [String]
|
20
34
|
# @!method name=
|
21
35
|
# Set the element name (the tag)
|
36
|
+
# @see https://www.rubydoc.info/github/sparklemotion/nokogiri/Nokogiri/XML/Node#name=-instance_method Nokogiri::XML::Node#name=
|
22
37
|
# @!method []
|
23
38
|
# Get an element attribute
|
39
|
+
# @see https://www.rubydoc.info/github/sparklemotion/nokogiri/Nokogiri/XML/Node#[]-instance_method Nokogiri::XML::Node#[]
|
40
|
+
# @return [String]
|
24
41
|
# @!method []=
|
25
42
|
# Set an element attribute
|
43
|
+
# @see https://www.rubydoc.info/github/sparklemotion/nokogiri/Nokogiri/XML/Node#[]=-instance_method Nokogiri::XML::Node#[]=
|
26
44
|
# @!method add_class
|
27
45
|
# Add a class to the element
|
46
|
+
# @see https://www.rubydoc.info/github/sparklemotion/nokogiri/Nokogiri/XML/Node#add_class-instance_method Nokogiri::XML::Node#add_class
|
28
47
|
# @!method remove_class
|
29
48
|
# Remove a class from the element
|
49
|
+
# @see https://www.rubydoc.info/github/sparklemotion/nokogiri/Nokogiri/XML/Node#remove_class-instance_method Nokogiri::XML::Node#remove_class
|
50
|
+
# @!method text
|
51
|
+
# Get the element text
|
52
|
+
# @see https://www.rubydoc.info/github/sparklemotion/nokogiri/Nokogiri/XML/Node#text-instance_method Nokogiri::XML::Node#text
|
53
|
+
# @return [String]
|
54
|
+
# @!method wrap
|
55
|
+
# Add HTML around this element
|
56
|
+
# @see https://www.rubydoc.info/github/sparklemotion/nokogiri/Nokogiri/XML/Node#wrap-instance_method Nokogiri::XML::Node#wrap
|
57
|
+
# @return [Nokogiri::XML::Node]
|
58
|
+
# @!method children
|
59
|
+
# Get the element's children
|
60
|
+
# @see https://www.rubydoc.info/github/sparklemotion/nokogiri/Nokogiri/XML/Node#children-instance_method Nokogiri::XML::Node#children
|
61
|
+
# @return [Nokogiri::XML::NodeSet]
|
62
|
+
# @!method to_html
|
63
|
+
# Get the element as HTML
|
64
|
+
# @see https://www.rubydoc.info/github/sparklemotion/nokogiri/Nokogiri/XML/Node#to_html-instance_method Nokogiri::XML::Node#to_html
|
65
|
+
# @return [String]
|
66
|
+
# @!method remove_attribute
|
67
|
+
# Removes an attribute from the element
|
68
|
+
# @see https://www.rubydoc.info/github/sparklemotion/nokogiri/Nokogiri/XML/Node#remove_attribute-instance_method Nokogiri::XML::Node#remove_attribute
|
69
|
+
# @!method classes
|
70
|
+
# Gets the element's classes
|
71
|
+
# @see https://www.rubydoc.info/github/sparklemotion/nokogiri/Nokogiri/XML/Node#classes-instance_method Nokogiri::XML::Node#classes
|
72
|
+
# @return [Array<String>]
|
73
|
+
# @!method path
|
74
|
+
# Get the path for this element
|
75
|
+
# @see https://www.rubydoc.info/github/sparklemotion/nokogiri/Nokogiri/XML/Node#path-instance_method Nokogiri::XML::Node#path
|
76
|
+
# @return [String]
|
77
|
+
# @!method inner_html=
|
78
|
+
# Set the inner HTML for this element
|
79
|
+
# @see https://www.rubydoc.info/github/sparklemotion/nokogiri/Nokogiri/XML/Node#inner_html=-instance_method Nokogiri::XML::Node#inner_html=
|
80
|
+
# @return Object
|
30
81
|
def_delegators :@node, :name=, :name, :[], :[]=, :add_class, :remove_class,
|
31
|
-
|
32
|
-
|
82
|
+
:text, :wrap, :children, :to_html, :remove_attribute,
|
83
|
+
:classes, :path, :inner_html=
|
33
84
|
|
85
|
+
# @!method config
|
86
|
+
# Get the config for this element's document
|
87
|
+
# @return [Config]
|
34
88
|
def_delegators :document, :config
|
89
|
+
|
90
|
+
# @!method selectors
|
91
|
+
# Get the selectors for this element's document
|
92
|
+
# @return [Selectors::Base]
|
35
93
|
def_delegators :config, :selectors
|
36
94
|
|
95
|
+
# Creates a new instance
|
96
|
+
#
|
97
|
+
# @param node [Nokogiri::XML::Node] the wrapped element
|
98
|
+
# @param document [Document] the element's document
|
99
|
+
# @param enumerator_class [ElementEnumeratorBase] the enumerator that matches this element type
|
100
|
+
# @param short_type [Symbol, String] the type of this element
|
101
|
+
#
|
37
102
|
def initialize(node:, document:, enumerator_class:, short_type: nil)
|
38
|
-
raise(ArgumentError,
|
103
|
+
raise(ArgumentError, 'node cannot be nil') if node.nil?
|
104
|
+
|
39
105
|
@node = node
|
40
106
|
|
41
|
-
raise(ArgumentError,
|
107
|
+
raise(ArgumentError, 'enumerator_class cannot be nil') if enumerator_class.nil?
|
108
|
+
|
42
109
|
@enumerator_class = enumerator_class
|
43
110
|
|
44
111
|
@short_type = short_type || "unknown_type_#{SecureRandom.hex(4)}"
|
@@ -48,33 +115,57 @@ module Kitchen
|
|
48
115
|
when Kitchen::Document
|
49
116
|
document
|
50
117
|
else
|
51
|
-
raise(ArgumentError,
|
118
|
+
raise(ArgumentError, '`document` is not a known document type')
|
52
119
|
end
|
53
120
|
|
54
121
|
@ancestors = HashWithIndifferentAccess.new
|
55
|
-
@
|
56
|
-
@css_or_xpath_that_has_been_counted = {}
|
122
|
+
@search_query_matches_that_have_been_counted = {}
|
57
123
|
@is_a_clone = false
|
58
124
|
end
|
59
125
|
|
60
|
-
|
126
|
+
# Returns true if this class represents the element for the given node
|
127
|
+
#
|
128
|
+
# @param node [Nokogiri::XML::Node] the underlying node
|
129
|
+
# @return [Boolean]
|
130
|
+
#
|
131
|
+
def self.is_the_element_class_for?(_node)
|
61
132
|
# override this in subclasses
|
62
133
|
false
|
63
134
|
end
|
64
135
|
|
136
|
+
# Returns true if this element has the given class
|
137
|
+
#
|
138
|
+
# @param klass [String] the class to test for
|
139
|
+
# @return [Boolean]
|
140
|
+
#
|
65
141
|
def has_class?(klass)
|
66
|
-
(self[:class] ||
|
142
|
+
(self[:class] || '').include?(klass)
|
67
143
|
end
|
68
144
|
|
145
|
+
# Returns the element's ID
|
146
|
+
#
|
147
|
+
# @return [String]
|
148
|
+
#
|
69
149
|
def id
|
70
150
|
self[:id]
|
71
151
|
end
|
72
152
|
|
153
|
+
# Sets the element's ID
|
154
|
+
#
|
155
|
+
# @param value [String] the new value for the ID
|
156
|
+
#
|
73
157
|
def id=(value)
|
74
158
|
self[:id] = value
|
75
159
|
end
|
76
160
|
|
77
161
|
# A way to set values and chain them
|
162
|
+
#
|
163
|
+
# @param property [String, Symbol] the name of the property to set
|
164
|
+
# @param value [String] the value to set
|
165
|
+
#
|
166
|
+
# @example
|
167
|
+
# element.set(:name,"div").set("id","foo")
|
168
|
+
#
|
78
169
|
def set(property, value)
|
79
170
|
case property.to_sym
|
80
171
|
when :name
|
@@ -85,18 +176,37 @@ module Kitchen
|
|
85
176
|
self
|
86
177
|
end
|
87
178
|
|
179
|
+
# Returns this element's ancestor of the given type
|
180
|
+
#
|
181
|
+
# @param type [String, Symbol] e.g. +:page+, +:term+
|
182
|
+
# @return [Ancestor]
|
183
|
+
# @raise [StandardError] if there is no ancestor of the given type
|
184
|
+
#
|
88
185
|
def ancestor(type)
|
89
186
|
@ancestors[type.to_sym]&.element || raise("No ancestor of type '#{type}'")
|
90
187
|
end
|
91
188
|
|
189
|
+
# Returns true iff this element has an ancestor of the given type
|
190
|
+
#
|
191
|
+
# @param type [String, Symbol] e.g. +:page+, +:term+
|
192
|
+
# @return [Boolean]
|
193
|
+
#
|
92
194
|
def has_ancestor?(type)
|
93
195
|
@ancestors[type.to_sym].present?
|
94
196
|
end
|
95
197
|
|
96
|
-
|
97
|
-
|
98
|
-
|
198
|
+
# Returns the element's ancestors
|
199
|
+
#
|
200
|
+
# @return [Array<Ancestor>]
|
201
|
+
#
|
202
|
+
attr_reader :ancestors
|
99
203
|
|
204
|
+
# Adds ancestors to this element, for each incrementing descendant counts for this type
|
205
|
+
#
|
206
|
+
# @param args [Array<Hash, Ancestor, Element, Document>] the ancestors
|
207
|
+
# @raise [StandardError] if there is already an ancestor with the one of
|
208
|
+
# the given ancestors' types
|
209
|
+
#
|
100
210
|
def add_ancestors(*args)
|
101
211
|
args.each do |arg|
|
102
212
|
case arg
|
@@ -112,50 +222,93 @@ module Kitchen
|
|
112
222
|
end
|
113
223
|
end
|
114
224
|
|
225
|
+
# Adds one ancestor, incrementing its descendant counts for this element type
|
226
|
+
#
|
227
|
+
# @param ancestor [Ancestor]
|
228
|
+
# @raise [StandardError] if there is already an ancestor with the given ancestor's type
|
229
|
+
#
|
115
230
|
def add_ancestor(ancestor)
|
116
231
|
if @ancestors[ancestor.type].present?
|
117
232
|
raise "Trying to add an ancestor of type '#{ancestor.type}' but one of that " \
|
118
233
|
"type is already present"
|
119
234
|
end
|
120
235
|
|
236
|
+
ancestor.increment_descendant_count(short_type)
|
121
237
|
@ancestors[ancestor.type] = ancestor
|
122
238
|
end
|
123
239
|
|
240
|
+
# Return the elements in all of the ancestors
|
241
|
+
#
|
242
|
+
# @return [Array<ElementBase>]
|
243
|
+
#
|
124
244
|
def ancestor_elements
|
125
245
|
@ancestors.values.map(&:element)
|
126
246
|
end
|
127
247
|
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
end
|
133
|
-
|
248
|
+
# Returns the count of this element's type in the given ancestor type
|
249
|
+
#
|
250
|
+
# @param ancestor_type [String, Symbol]
|
251
|
+
#
|
134
252
|
def count_in(ancestor_type)
|
135
|
-
@
|
253
|
+
@ancestors[ancestor_type]&.get_descendant_count(short_type) ||
|
254
|
+
raise("No ancestor of type '#{ancestor_type}'")
|
136
255
|
end
|
137
256
|
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
257
|
+
# Track that a sub element found by the given query has been counted
|
258
|
+
#
|
259
|
+
# @param search_query [SearchQuery] the search query matching the counted element
|
260
|
+
# @param type [String] the type of the sub element that was counted
|
261
|
+
#
|
262
|
+
def remember_that_a_sub_element_was_counted(search_query, type)
|
263
|
+
@search_query_matches_that_have_been_counted[search_query.to_s] ||= Hash.new(0)
|
264
|
+
@search_query_matches_that_have_been_counted[search_query.to_s][type] += 1
|
144
265
|
end
|
145
266
|
|
146
|
-
|
147
|
-
|
267
|
+
# Undo the counts from a prior search query (so that they can be counted again)
|
268
|
+
#
|
269
|
+
# @param search_query [SearchQuery] the prior search query whose counts need to be undone
|
270
|
+
#
|
271
|
+
def uncount(search_query)
|
272
|
+
@search_query_matches_that_have_been_counted.delete(search_query.to_s)&.each do |type, count|
|
273
|
+
ancestors.each_value do |ancestor|
|
274
|
+
ancestor.decrement_descendant_count(type, by: count)
|
275
|
+
end
|
276
|
+
end
|
148
277
|
end
|
149
278
|
|
279
|
+
# Returns the search history that found this element
|
280
|
+
#
|
281
|
+
# @return [SearchHistory]
|
282
|
+
#
|
150
283
|
def search_history
|
151
|
-
|
152
|
-
|
284
|
+
SearchHistory.new(
|
285
|
+
ancestor_elements.last&.search_history || SearchHistory.empty,
|
286
|
+
search_query_that_found_me
|
287
|
+
)
|
153
288
|
end
|
154
289
|
|
155
|
-
|
290
|
+
# Returns an ElementEnumerator that iterates over the provided selector or xpath queries
|
291
|
+
#
|
292
|
+
# @param selector_or_xpath_args [Array<String>] Selector or XPath queries
|
293
|
+
# @param only [Symbol, Callable] the name of a method to call on an element or a
|
294
|
+
# lambda or proc that accepts an element; elements will only be included in the
|
295
|
+
# search results if the method or callable returns true
|
296
|
+
# @param except [Symbol, Callable] the name of a method to call on an element or a
|
297
|
+
# lambda or proc that accepts an element; elements will not be included in the
|
298
|
+
# search results if the method or callable returns false
|
299
|
+
# @return [ElementEnumerator]
|
300
|
+
#
|
301
|
+
def search(*selector_or_xpath_args, only: nil, except: nil)
|
156
302
|
block_error_if(block_given?)
|
157
303
|
|
158
|
-
ElementEnumerator.factory.build_within(
|
304
|
+
ElementEnumerator.factory.build_within(
|
305
|
+
self,
|
306
|
+
search_query: SearchQuery.new(
|
307
|
+
css_or_xpath: selector_or_xpath_args,
|
308
|
+
only: only,
|
309
|
+
except: except
|
310
|
+
)
|
311
|
+
)
|
159
312
|
end
|
160
313
|
|
161
314
|
# Yields and returns the first child element that matches the provided
|
@@ -185,16 +338,32 @@ module Kitchen
|
|
185
338
|
end
|
186
339
|
end
|
187
340
|
|
341
|
+
# @!method at
|
342
|
+
# @see first
|
188
343
|
alias_method :at, :first
|
189
344
|
|
190
|
-
|
345
|
+
# Returns an enumerator over the direct child elements of this element, with the
|
346
|
+
# specific type (e.g. +TermElement+) if such type is available.
|
347
|
+
#
|
348
|
+
# @return [TypeCastingElementEnumerator]
|
349
|
+
#
|
350
|
+
def element_children
|
191
351
|
block_error_if(block_given?)
|
192
|
-
TypeCastingElementEnumerator.factory.build_within(
|
352
|
+
TypeCastingElementEnumerator.factory.build_within(
|
353
|
+
self,
|
354
|
+
search_query: SearchQuery.new(css_or_xpath: './*')
|
355
|
+
)
|
193
356
|
end
|
194
357
|
|
358
|
+
# Searches for elements handled by a list of enumerator classes. All element that
|
359
|
+
# matches one of those enumerator classes are iterated over.
|
360
|
+
#
|
361
|
+
# @param enumerator_classes [Array<ElementEnumeratorBase>]
|
362
|
+
# @return [TypeCastingElementEnumerator]
|
363
|
+
#
|
195
364
|
def search_with(*enumerator_classes)
|
196
365
|
block_error_if(block_given?)
|
197
|
-
raise
|
366
|
+
raise 'must supply at least one enumerator class' if enumerator_classes.empty?
|
198
367
|
|
199
368
|
factory = enumerator_classes[0].factory
|
200
369
|
enumerator_classes[1..-1].each do |enumerator_class|
|
@@ -232,6 +401,7 @@ module Kitchen
|
|
232
401
|
the_copy = clone
|
233
402
|
the_copy.raw.traverse do |node|
|
234
403
|
next if node.text? || node.document?
|
404
|
+
|
235
405
|
document.record_id_copied(node[:id])
|
236
406
|
end
|
237
407
|
clipboard(to).add(the_copy) if to.present?
|
@@ -247,6 +417,7 @@ module Kitchen
|
|
247
417
|
temp_copy = clone
|
248
418
|
temp_copy.raw.traverse do |node|
|
249
419
|
next if node.text? || node.document?
|
420
|
+
|
250
421
|
node[:id] = document.modified_id_to_paste(node[:id]) unless node[:id].blank?
|
251
422
|
end
|
252
423
|
temp_copy.to_s
|
@@ -263,20 +434,19 @@ module Kitchen
|
|
263
434
|
Element.new(node: raw.parent, document: document, short_type: "parent(#{short_type})")
|
264
435
|
end
|
265
436
|
|
266
|
-
# TODO make it clear if all of these methods take Element, Node, or String
|
437
|
+
# TODO: make it clear if all of these methods take Element, Node, or String
|
267
438
|
|
268
439
|
# If child argument given, prepends it before the element's current children.
|
269
440
|
# If sibling is given, prepends it as a sibling to this element.
|
270
441
|
#
|
271
442
|
# @param child [String] the child to prepend
|
272
443
|
# @param sibling [String] the sibling to prepend
|
444
|
+
# @raise [RecipeError] if specify other than just a child or a sibling
|
273
445
|
#
|
274
446
|
def prepend(child: nil, sibling: nil)
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
raise RecipeError, "One of `child` or `sibling` must be specified"
|
279
|
-
elsif child
|
447
|
+
require_one_of_child_or_sibling(child, sibling)
|
448
|
+
|
449
|
+
if child
|
280
450
|
if node.children.empty?
|
281
451
|
node.children = child.to_s
|
282
452
|
else
|
@@ -285,6 +455,7 @@ module Kitchen
|
|
285
455
|
else
|
286
456
|
node.add_previous_sibling(sibling)
|
287
457
|
end
|
458
|
+
|
288
459
|
self
|
289
460
|
end
|
290
461
|
|
@@ -293,13 +464,12 @@ module Kitchen
|
|
293
464
|
#
|
294
465
|
# @param child [String] the child to append
|
295
466
|
# @param sibling [String] the sibling to append
|
467
|
+
# @raise [RecipeError] if specify other than just a child or a sibling
|
296
468
|
#
|
297
469
|
def append(child: nil, sibling: nil)
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
raise RecipeError, "One of `child` or `sibling` must be specified"
|
302
|
-
elsif child
|
470
|
+
require_one_of_child_or_sibling(child, sibling)
|
471
|
+
|
472
|
+
if child
|
303
473
|
if node.children.empty?
|
304
474
|
node.children = child.to_s
|
305
475
|
else
|
@@ -308,6 +478,7 @@ module Kitchen
|
|
308
478
|
else
|
309
479
|
node.next = sibling
|
310
480
|
end
|
481
|
+
|
311
482
|
self
|
312
483
|
end
|
313
484
|
|
@@ -320,7 +491,7 @@ module Kitchen
|
|
320
491
|
self
|
321
492
|
end
|
322
493
|
|
323
|
-
# TODO methods like replace_children that take string, either forbid or handle Element/Node args
|
494
|
+
# TODO: methods like replace_children that take string, either forbid or handle Element/Node args
|
324
495
|
|
325
496
|
# Get the content of children matching the provided selector. Mostly
|
326
497
|
# useful when there is one child with text you want to extract.
|
@@ -350,11 +521,19 @@ module Kitchen
|
|
350
521
|
# @return [String] the sub header tag name
|
351
522
|
#
|
352
523
|
def sub_header_name
|
353
|
-
first_header = node.search(
|
524
|
+
first_header = node.search('h1, h2, h3, h4, h5, h6').first
|
525
|
+
|
526
|
+
if first_header.nil?
|
527
|
+
'h1'
|
528
|
+
else
|
529
|
+
first_header.name.gsub(/\d/) { |num| (num.to_i + 1).to_s }
|
530
|
+
end
|
531
|
+
end
|
354
532
|
|
355
|
-
|
356
|
-
|
357
|
-
|
533
|
+
# Mark the location so that if there's an error we can show the developer where.
|
534
|
+
#
|
535
|
+
def mark_as_current_location!
|
536
|
+
document.location = self
|
358
537
|
end
|
359
538
|
|
360
539
|
# Returns the underlying Nokogiri object
|
@@ -365,22 +544,40 @@ module Kitchen
|
|
365
544
|
node
|
366
545
|
end
|
367
546
|
|
547
|
+
# Returns a string version of this element
|
548
|
+
#
|
549
|
+
# @return [String]
|
550
|
+
#
|
368
551
|
def inspect
|
369
552
|
to_s
|
370
553
|
end
|
371
554
|
|
555
|
+
# Returns a string version of this element
|
556
|
+
#
|
557
|
+
# @return [String]
|
558
|
+
#
|
372
559
|
def to_s
|
373
560
|
remove_default_namespaces_if_clone(node.to_s)
|
374
561
|
end
|
375
562
|
|
563
|
+
# Returns a string version of this element as XML
|
564
|
+
#
|
565
|
+
# @return [String]
|
566
|
+
#
|
376
567
|
def to_xml
|
377
568
|
remove_default_namespaces_if_clone(node.to_xml)
|
378
569
|
end
|
379
570
|
|
571
|
+
# Returns a string version of this element as XHTML
|
572
|
+
#
|
573
|
+
# @return [String]
|
574
|
+
#
|
380
575
|
def to_xhtml
|
381
576
|
remove_default_namespaces_if_clone(node.to_xhtml)
|
382
577
|
end
|
383
578
|
|
579
|
+
# Returns a clone of this object
|
580
|
+
#
|
384
581
|
def clone
|
385
582
|
super.tap do |element|
|
386
583
|
# When we call dup, the dup gets a bunch of default namespace stuff that
|
@@ -409,17 +606,32 @@ module Kitchen
|
|
409
606
|
|
410
607
|
# @!method pages
|
411
608
|
# Returns a pages enumerator
|
412
|
-
def_delegators :as_enumerator, :pages, :chapters, :terms, :figures, :notes, :tables, :examples
|
609
|
+
def_delegators :as_enumerator, :pages, :chapters, :terms, :figures, :notes, :tables, :examples,
|
610
|
+
:metadatas, :units
|
413
611
|
|
612
|
+
# Returns this element as an enumerator (over only one element, itself)
|
613
|
+
#
|
614
|
+
# @return [ElementEnumeratorBase] (actually returns the appropriate enumerator class for this element)
|
615
|
+
#
|
414
616
|
def as_enumerator
|
415
|
-
enumerator_class.new(
|
617
|
+
enumerator_class.new(search_query: search_query_that_found_me) { |block| block.yield(self) }
|
416
618
|
end
|
417
619
|
|
418
620
|
protected
|
419
621
|
|
622
|
+
# The wrapped Nokogiri node
|
623
|
+
# @return [Nokogiri::XML::Node] the node
|
420
624
|
attr_accessor :node
|
625
|
+
|
626
|
+
# If this element is a clone
|
627
|
+
# @return [Boolean]
|
421
628
|
attr_accessor :is_a_clone
|
422
629
|
|
630
|
+
# Return a clipboard
|
631
|
+
#
|
632
|
+
# @param name_or_object [String, Clipboard] the name of the clipboard or the clipboard itself
|
633
|
+
# @return [Clipboard]
|
634
|
+
#
|
423
635
|
def clipboard(name_or_object)
|
424
636
|
case name_or_object
|
425
637
|
when Symbol
|
@@ -427,18 +639,26 @@ module Kitchen
|
|
427
639
|
when Clipboard
|
428
640
|
name_or_object
|
429
641
|
else
|
430
|
-
raise ArgumentError, "The provided argument (#{name_or_object}) is not "
|
642
|
+
raise ArgumentError, "The provided argument (#{name_or_object}) is not " \
|
431
643
|
"a clipboard name or a clipboard"
|
432
644
|
end
|
433
645
|
end
|
434
646
|
|
647
|
+
# Clean up some default namespace junk for cloned elements
|
648
|
+
#
|
649
|
+
# @param string [String] the string to clean
|
435
650
|
def remove_default_namespaces_if_clone(string)
|
436
651
|
if is_a_clone
|
437
|
-
string.gsub(
|
652
|
+
string.gsub('xmlns:default="http://www.w3.org/1999/xhtml"', '').gsub('default:', '')
|
438
653
|
else
|
439
654
|
string
|
440
655
|
end
|
441
656
|
end
|
442
657
|
|
658
|
+
def require_one_of_child_or_sibling(child, sibling)
|
659
|
+
raise RecipeError, 'Only one of `child` or `sibling` can be specified' if child && sibling
|
660
|
+
raise RecipeError, 'One of `child` or `sibling` must be specified' if !child && !sibling
|
661
|
+
end
|
662
|
+
|
443
663
|
end
|
444
664
|
end
|