openstax_kitchen 2.0.0 → 4.1.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 +2 -2
- data/.inch.yml +6 -0
- data/.rubocop.yml +65 -0
- data/CHANGELOG.md +85 -1
- data/Gemfile +5 -3
- data/Gemfile.lock +65 -17
- data/README.md +65 -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 +15 -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 +40 -5
- data/lib/kitchen/book_element_enumerator.rb +4 -0
- data/lib/kitchen/book_recipe.rb +15 -1
- data/lib/kitchen/chapter_element.rb +43 -6
- data/lib/kitchen/chapter_element_enumerator.rb +9 -1
- data/lib/kitchen/clipboard.rb +35 -4
- data/lib/kitchen/composite_chapter_element.rb +21 -6
- data/lib/kitchen/composite_page_element.rb +35 -7
- data/lib/kitchen/composite_page_element_enumerator.rb +9 -1
- data/lib/kitchen/config.rb +20 -6
- 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/main.rb +18 -0
- data/lib/kitchen/directions/bake_chapter_glossary/v1.rb +30 -0
- data/lib/kitchen/directions/bake_chapter_introductions.rb +7 -7
- data/lib/kitchen/directions/bake_chapter_key_concepts/main.rb +16 -0
- data/lib/kitchen/directions/bake_chapter_key_concepts/v1.rb +35 -0
- data/lib/kitchen/directions/bake_chapter_key_equations.rb +30 -20
- data/lib/kitchen/directions/bake_chapter_references/main.rb +16 -0
- data/lib/kitchen/directions/bake_chapter_references/v1.rb +35 -0
- data/lib/kitchen/directions/bake_chapter_section_exercises/main.rb +11 -0
- data/lib/kitchen/directions/bake_chapter_section_exercises/v1.rb +28 -0
- data/lib/kitchen/directions/bake_chapter_summary.rb +45 -36
- 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_checkpoint.rb +44 -0
- data/lib/kitchen/directions/bake_composite_chapters.rb +14 -0
- data/lib/kitchen/directions/bake_composite_pages.rb +2 -2
- data/lib/kitchen/directions/bake_equations.rb +37 -0
- data/lib/kitchen/directions/bake_example.rb +39 -11
- data/lib/kitchen/directions/bake_figure.rb +8 -5
- data/lib/kitchen/directions/bake_first_elements.rb +16 -0
- data/lib/kitchen/directions/bake_footnotes/main.rb +2 -2
- data/lib/kitchen/directions/bake_footnotes/v1.rb +6 -5
- data/lib/kitchen/directions/bake_free_response/free_response.xhtml.erb +10 -0
- data/lib/kitchen/directions/bake_free_response/main.rb +11 -0
- data/lib/kitchen/directions/bake_free_response/v1.rb +29 -0
- data/lib/kitchen/directions/bake_further_research.rb +59 -0
- data/lib/kitchen/directions/bake_index/main.rb +2 -2
- data/lib/kitchen/directions/bake_index/v1.rb +46 -18
- 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_non_introduction_pages.rb +26 -0
- data/lib/kitchen/directions/bake_notes/bake_autotitled_notes.rb +29 -0
- data/lib/kitchen/directions/bake_notes/bake_note_subtitle.rb +22 -0
- data/lib/kitchen/directions/bake_notes/bake_numbered_notes.rb +51 -0
- data/lib/kitchen/directions/bake_notes/bake_unclassified_notes.rb +30 -0
- data/lib/kitchen/directions/bake_numbered_exercise/main.rb +15 -0
- data/lib/kitchen/directions/bake_numbered_exercise/v1.rb +47 -0
- data/lib/kitchen/directions/bake_numbered_table/main.rb +4 -4
- data/lib/kitchen/directions/bake_numbered_table/v1.rb +37 -18
- data/lib/kitchen/directions/bake_page_abstracts.rb +30 -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_references/main.rb +16 -0
- data/lib/kitchen/directions/bake_references/v1.rb +48 -0
- data/lib/kitchen/directions/bake_stepwise.rb +8 -12
- data/lib/kitchen/directions/bake_suggested_reading.rb +31 -0
- data/lib/kitchen/directions/bake_theorem/main.rb +11 -0
- data/lib/kitchen/directions/bake_theorem/v1.rb +28 -0
- data/lib/kitchen/directions/bake_toc.rb +49 -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/book_answer_key_container/eob_solutions_container.xhtml.erb +9 -0
- data/lib/kitchen/directions/book_answer_key_container/main.rb +11 -0
- data/lib/kitchen/directions/book_answer_key_container/v1.rb +13 -0
- data/lib/kitchen/directions/chapter_review_container/chapter_review.xhtml.erb +9 -0
- data/lib/kitchen/directions/chapter_review_container/main.rb +11 -0
- data/lib/kitchen/directions/chapter_review_container/v1.rb +13 -0
- data/lib/kitchen/directions/eoc_section_title_link_snippet.rb +20 -0
- data/lib/kitchen/directions/move_exercises_to_eoc/main.rb +27 -0
- data/lib/kitchen/directions/move_exercises_to_eoc/v1.rb +36 -0
- data/lib/kitchen/directions/move_exercises_to_eoc/v2.rb +49 -0
- data/lib/kitchen/directions/move_solutions_to_answer_key/main.rb +14 -0
- data/lib/kitchen/directions/move_solutions_to_answer_key/strategies/american_government.rb +19 -0
- data/lib/kitchen/directions/move_solutions_to_answer_key/strategies/calculus.rb +41 -0
- data/lib/kitchen/directions/move_solutions_to_answer_key/strategies/uphysics.rb +63 -0
- data/lib/kitchen/directions/move_solutions_to_answer_key/v1.rb +34 -0
- data/lib/kitchen/directions/move_title_text_into_span.rb +2 -2
- data/lib/kitchen/document.rb +83 -13
- data/lib/kitchen/element.rb +20 -3
- data/lib/kitchen/element_base.rb +373 -63
- data/lib/kitchen/element_enumerator.rb +8 -0
- data/lib/kitchen/element_enumerator_base.rb +297 -28
- data/lib/kitchen/element_enumerator_factory.rb +64 -53
- data/lib/kitchen/element_factory.rb +27 -12
- data/lib/kitchen/errors.rb +5 -0
- data/lib/kitchen/example_element.rb +21 -6
- data/lib/kitchen/example_element_enumerator.rb +9 -1
- data/lib/kitchen/exercise_element.rb +42 -0
- data/lib/kitchen/exercise_element_enumerator.rb +21 -0
- data/lib/kitchen/figure_element.rb +36 -5
- data/lib/kitchen/figure_element_enumerator.rb +9 -1
- data/lib/kitchen/metadata_element.rb +34 -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 +48 -20
- data/lib/kitchen/note_element_enumerator.rb +9 -1
- data/lib/kitchen/oven.rb +66 -15
- data/lib/kitchen/page_element.rb +84 -14
- data/lib/kitchen/page_element_enumerator.rb +9 -1
- data/lib/kitchen/pantry.rb +28 -1
- data/lib/kitchen/patches/nokogiri.rb +59 -2
- data/lib/kitchen/patches/renderable.rb +9 -3
- data/lib/kitchen/patches/string.rb +8 -0
- data/lib/kitchen/recipe.rb +69 -32
- data/lib/kitchen/reference_element.rb +27 -0
- data/lib/kitchen/references_element_enumerator.rb +20 -0
- data/lib/kitchen/search_history.rb +43 -4
- data/lib/kitchen/search_query.rb +106 -0
- data/lib/kitchen/selector.rb +24 -0
- data/lib/kitchen/selectors/base.rb +65 -0
- data/lib/kitchen/selectors/standard_1.rb +21 -0
- data/lib/kitchen/table_element.rb +55 -7
- data/lib/kitchen/table_element_enumerator.rb +9 -1
- data/lib/kitchen/templates/eob_section_title_template.xhtml.erb +10 -0
- data/lib/kitchen/templates/eoc_section_title_template.xhtml.erb +10 -0
- data/lib/kitchen/term_element.rb +15 -4
- 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 +45 -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 +18 -7
- data/lib/locales/pl.yml +24 -0
- data/lib/openstax_kitchen.rb +44 -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 +149 -24
- data/Dockerfile +0 -19
- data/docker-compose.yml +0 -12
- data/docker/entrypoint +0 -9
- data/lib/kitchen/directions/bake_chapter_glossary.rb +0 -34
- data/lib/kitchen/directions/bake_exercises.rb +0 -164
- data/lib/kitchen/directions/bake_notes.rb +0 -58
- 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
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,
|
@@ -8,8 +19,14 @@ module Kitchen
|
|
8
19
|
short_type: short_type)
|
9
20
|
end
|
10
21
|
|
11
|
-
#
|
12
|
-
#
|
13
|
-
#
|
22
|
+
# Returns true if this class represents the element for the given node; always false
|
23
|
+
# for this generic class
|
24
|
+
#
|
25
|
+
# @param node [Nokogiri::XML::Node] the underlying node
|
26
|
+
# @return [Boolean]
|
27
|
+
#
|
28
|
+
def self.is_the_element_class_for?(_node, **)
|
29
|
+
false
|
30
|
+
end
|
14
31
|
end
|
15
32
|
end
|
data/lib/kitchen/element_base.rb
CHANGED
@@ -1,8 +1,10 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'forwardable'
|
2
4
|
require 'securerandom'
|
3
5
|
|
6
|
+
# rubocop:disable Metrics/ClassLength
|
4
7
|
module Kitchen
|
5
|
-
|
6
8
|
# Abstract base class for all elements. If you are looking for a simple concrete
|
7
9
|
# element class, use `Element`.
|
8
10
|
#
|
@@ -10,71 +12,207 @@ module Kitchen
|
|
10
12
|
extend Forwardable
|
11
13
|
include Mixins::BlockErrorIf
|
12
14
|
|
15
|
+
# The element's document
|
16
|
+
# @return [Document]
|
13
17
|
attr_reader :document
|
18
|
+
|
19
|
+
# The element's type, e.g. +:page+
|
20
|
+
# @return [Symbol, String]
|
14
21
|
attr_reader :short_type
|
22
|
+
|
23
|
+
# The enumerator class for this element
|
24
|
+
# @return [Class]
|
15
25
|
attr_reader :enumerator_class
|
16
|
-
|
26
|
+
|
27
|
+
# The search query that located this element in the DOM
|
28
|
+
# @return [SearchQuery]
|
29
|
+
attr_accessor :search_query_that_found_me
|
17
30
|
|
18
31
|
# @!method name
|
19
32
|
# Get the element name (the tag)
|
33
|
+
# @see https://www.rubydoc.info/github/sparklemotion/nokogiri/Nokogiri/XML/Node#name-instance_method Nokogiri::XML::Node#name
|
34
|
+
# @return [String]
|
20
35
|
# @!method name=
|
21
36
|
# Set the element name (the tag)
|
37
|
+
# @see https://www.rubydoc.info/github/sparklemotion/nokogiri/Nokogiri/XML/Node#name=-instance_method Nokogiri::XML::Node#name=
|
22
38
|
# @!method []
|
23
39
|
# Get an element attribute
|
40
|
+
# @see https://www.rubydoc.info/github/sparklemotion/nokogiri/Nokogiri/XML/Node#[]-instance_method Nokogiri::XML::Node#[]
|
41
|
+
# @return [String]
|
24
42
|
# @!method []=
|
25
43
|
# Set an element attribute
|
44
|
+
# @see https://www.rubydoc.info/github/sparklemotion/nokogiri/Nokogiri/XML/Node#[]=-instance_method Nokogiri::XML::Node#[]=
|
26
45
|
# @!method add_class
|
27
46
|
# Add a class to the element
|
47
|
+
# @see https://www.rubydoc.info/github/sparklemotion/nokogiri/Nokogiri/XML/Node#add_class-instance_method Nokogiri::XML::Node#add_class
|
28
48
|
# @!method remove_class
|
29
49
|
# Remove a class from the element
|
50
|
+
# @see https://www.rubydoc.info/github/sparklemotion/nokogiri/Nokogiri/XML/Node#remove_class-instance_method Nokogiri::XML::Node#remove_class
|
51
|
+
# @!method text
|
52
|
+
# Get the element text
|
53
|
+
# @see https://www.rubydoc.info/github/sparklemotion/nokogiri/Nokogiri/XML/Node#text-instance_method Nokogiri::XML::Node#text
|
54
|
+
# @return [String]
|
55
|
+
# @!method wrap
|
56
|
+
# Add HTML around this element
|
57
|
+
# @see https://www.rubydoc.info/github/sparklemotion/nokogiri/Nokogiri/XML/Node#wrap-instance_method Nokogiri::XML::Node#wrap
|
58
|
+
# @return [Nokogiri::XML::Node]
|
59
|
+
# @!method children
|
60
|
+
# Get the element's children
|
61
|
+
# @see https://www.rubydoc.info/github/sparklemotion/nokogiri/Nokogiri/XML/Node#children-instance_method Nokogiri::XML::Node#children
|
62
|
+
# @return [Nokogiri::XML::NodeSet]
|
63
|
+
# @!method to_html
|
64
|
+
# Get the element as HTML
|
65
|
+
# @see https://www.rubydoc.info/github/sparklemotion/nokogiri/Nokogiri/XML/Node#to_html-instance_method Nokogiri::XML::Node#to_html
|
66
|
+
# @return [String]
|
67
|
+
# @!method remove_attribute
|
68
|
+
# Removes an attribute from the element
|
69
|
+
# @see https://www.rubydoc.info/github/sparklemotion/nokogiri/Nokogiri/XML/Node#remove_attribute-instance_method Nokogiri::XML::Node#remove_attribute
|
70
|
+
# @!method classes
|
71
|
+
# Gets the element's classes
|
72
|
+
# @see https://www.rubydoc.info/github/sparklemotion/nokogiri/Nokogiri/XML/Node#classes-instance_method Nokogiri::XML::Node#classes
|
73
|
+
# @return [Array<String>]
|
74
|
+
# @!method path
|
75
|
+
# Get the path for this element
|
76
|
+
# @see https://www.rubydoc.info/github/sparklemotion/nokogiri/Nokogiri/XML/Node#path-instance_method Nokogiri::XML::Node#path
|
77
|
+
# @return [String]
|
78
|
+
# @!method inner_html=
|
79
|
+
# Set the inner HTML for this element
|
80
|
+
# @see https://www.rubydoc.info/github/sparklemotion/nokogiri/Nokogiri/XML/Node#inner_html=-instance_method Nokogiri::XML::Node#inner_html=
|
81
|
+
# @return Object
|
30
82
|
def_delegators :@node, :name=, :name, :[], :[]=, :add_class, :remove_class,
|
31
|
-
|
32
|
-
|
83
|
+
:text, :wrap, :children, :to_html, :remove_attribute,
|
84
|
+
:classes, :path, :inner_html=
|
33
85
|
|
86
|
+
# @!method config
|
87
|
+
# Get the config for this element's document
|
88
|
+
# @return [Config]
|
34
89
|
def_delegators :document, :config
|
90
|
+
|
91
|
+
# @!method selectors
|
92
|
+
# Get the selectors for this element's document
|
93
|
+
# @return [Selectors::Base]
|
35
94
|
def_delegators :config, :selectors
|
36
95
|
|
96
|
+
# @!method pantry
|
97
|
+
# Access the pantry for this element's document
|
98
|
+
# @return [Pantry]
|
99
|
+
# @!method :clipboard
|
100
|
+
# Access the clipboard for this element's document
|
101
|
+
# @return [Clipboard]
|
102
|
+
def_delegators :document, :pantry, :clipboard
|
103
|
+
|
104
|
+
# Creates a new instance
|
105
|
+
#
|
106
|
+
# @param node [Nokogiri::XML::Node] the wrapped element
|
107
|
+
# @param document [Document] the element's document
|
108
|
+
# @param enumerator_class [ElementEnumeratorBase] the enumerator that matches this element type
|
109
|
+
# @param short_type [Symbol, String] the type of this element
|
110
|
+
#
|
37
111
|
def initialize(node:, document:, enumerator_class:, short_type: nil)
|
38
|
-
raise(ArgumentError,
|
112
|
+
raise(ArgumentError, 'node cannot be nil') if node.nil?
|
113
|
+
|
39
114
|
@node = node
|
40
115
|
|
41
|
-
raise(ArgumentError,
|
116
|
+
raise(ArgumentError, 'enumerator_class cannot be nil') if enumerator_class.nil?
|
117
|
+
|
42
118
|
@enumerator_class = enumerator_class
|
43
119
|
|
44
|
-
@short_type = short_type ||
|
120
|
+
@short_type = short_type ||
|
121
|
+
self.class.try(:short_type) ||
|
122
|
+
"unknown_type_#{SecureRandom.hex(4)}"
|
45
123
|
|
46
124
|
@document =
|
47
125
|
case document
|
48
126
|
when Kitchen::Document
|
49
127
|
document
|
50
128
|
else
|
51
|
-
raise(ArgumentError,
|
129
|
+
raise(ArgumentError, '`document` is not a known document type')
|
52
130
|
end
|
53
131
|
|
54
132
|
@ancestors = HashWithIndifferentAccess.new
|
55
|
-
@
|
56
|
-
@css_or_xpath_that_has_been_counted = {}
|
133
|
+
@search_query_matches_that_have_been_counted = {}
|
57
134
|
@is_a_clone = false
|
58
135
|
end
|
59
136
|
|
60
|
-
|
61
|
-
|
62
|
-
|
137
|
+
# Returns ElementBase descendent type or nil if none found
|
138
|
+
#
|
139
|
+
# @param type [Symbol] the descendant type, e.g. `:page`
|
140
|
+
# @return [Class] the child class for the given type
|
141
|
+
#
|
142
|
+
def self.descendant(type)
|
143
|
+
@types_to_descendants ||=
|
144
|
+
descendants.each_with_object({}) do |descendant, hash|
|
145
|
+
next unless descendant.try(:short_type)
|
146
|
+
|
147
|
+
hash[descendant.short_type] = descendant
|
148
|
+
end
|
149
|
+
|
150
|
+
@types_to_descendants[type]
|
63
151
|
end
|
64
152
|
|
153
|
+
# Returns ElementBase descendent type or Error if none found
|
154
|
+
#
|
155
|
+
# @param type [Symbol] the descendant type, e.g. `:page`
|
156
|
+
# @raise if the type is unknown
|
157
|
+
# @return [Class] the child class for the given type
|
158
|
+
#
|
159
|
+
def self.descendant!(type)
|
160
|
+
descendant(type) || raise("Unknown ElementBase descendant type '#{type}'")
|
161
|
+
end
|
162
|
+
|
163
|
+
# Returns true if this element is the given type
|
164
|
+
#
|
165
|
+
# @param type [Symbol] the descendant type, e.g. `:page`
|
166
|
+
# @raise if the type is unknown
|
167
|
+
# @return [Boolean]
|
168
|
+
#
|
169
|
+
def is?(type)
|
170
|
+
ElementBase.descendant!(type).is_the_element_class_for?(raw, config: config)
|
171
|
+
end
|
172
|
+
|
173
|
+
# Returns true if this class represents the element for the given node
|
174
|
+
#
|
175
|
+
# @param node [Nokogiri::XML::Node] the underlying node
|
176
|
+
# @param config [Kitchen::Config]
|
177
|
+
# @return [Boolean]
|
178
|
+
#
|
179
|
+
def self.is_the_element_class_for?(node, config:)
|
180
|
+
Selector.named(short_type).matches?(node, config: config)
|
181
|
+
end
|
182
|
+
|
183
|
+
# Returns true if this element has the given class
|
184
|
+
#
|
185
|
+
# @param klass [String] the class to test for
|
186
|
+
# @return [Boolean]
|
187
|
+
#
|
65
188
|
def has_class?(klass)
|
66
|
-
(self[:class] ||
|
189
|
+
(self[:class] || '').include?(klass)
|
67
190
|
end
|
68
191
|
|
192
|
+
# Returns the element's ID
|
193
|
+
#
|
194
|
+
# @return [String]
|
195
|
+
#
|
69
196
|
def id
|
70
197
|
self[:id]
|
71
198
|
end
|
72
199
|
|
200
|
+
# Sets the element's ID
|
201
|
+
#
|
202
|
+
# @param value [String] the new value for the ID
|
203
|
+
#
|
73
204
|
def id=(value)
|
74
205
|
self[:id] = value
|
75
206
|
end
|
76
207
|
|
77
208
|
# A way to set values and chain them
|
209
|
+
#
|
210
|
+
# @param property [String, Symbol] the name of the property to set
|
211
|
+
# @param value [String] the value to set
|
212
|
+
#
|
213
|
+
# @example
|
214
|
+
# element.set(:name,"div").set("id","foo")
|
215
|
+
#
|
78
216
|
def set(property, value)
|
79
217
|
case property.to_sym
|
80
218
|
when :name
|
@@ -85,18 +223,37 @@ module Kitchen
|
|
85
223
|
self
|
86
224
|
end
|
87
225
|
|
226
|
+
# Returns this element's ancestor of the given type
|
227
|
+
#
|
228
|
+
# @param type [String, Symbol] e.g. +:page+, +:term+
|
229
|
+
# @return [Ancestor]
|
230
|
+
# @raise [StandardError] if there is no ancestor of the given type
|
231
|
+
#
|
88
232
|
def ancestor(type)
|
89
233
|
@ancestors[type.to_sym]&.element || raise("No ancestor of type '#{type}'")
|
90
234
|
end
|
91
235
|
|
236
|
+
# Returns true iff this element has an ancestor of the given type
|
237
|
+
#
|
238
|
+
# @param type [String, Symbol] e.g. +:page+, +:term+
|
239
|
+
# @return [Boolean]
|
240
|
+
#
|
92
241
|
def has_ancestor?(type)
|
93
242
|
@ancestors[type.to_sym].present?
|
94
243
|
end
|
95
244
|
|
96
|
-
|
97
|
-
|
98
|
-
|
245
|
+
# Returns the element's ancestors
|
246
|
+
#
|
247
|
+
# @return [Array<Ancestor>]
|
248
|
+
#
|
249
|
+
attr_reader :ancestors
|
99
250
|
|
251
|
+
# Adds ancestors to this element, for each incrementing descendant counts for this type
|
252
|
+
#
|
253
|
+
# @param args [Array<Hash, Ancestor, Element, Document>] the ancestors
|
254
|
+
# @raise [StandardError] if there is already an ancestor with the one of
|
255
|
+
# the given ancestors' types
|
256
|
+
#
|
100
257
|
def add_ancestors(*args)
|
101
258
|
args.each do |arg|
|
102
259
|
case arg
|
@@ -112,50 +269,93 @@ module Kitchen
|
|
112
269
|
end
|
113
270
|
end
|
114
271
|
|
272
|
+
# Adds one ancestor, incrementing its descendant counts for this element type
|
273
|
+
#
|
274
|
+
# @param ancestor [Ancestor]
|
275
|
+
# @raise [StandardError] if there is already an ancestor with the given ancestor's type
|
276
|
+
#
|
115
277
|
def add_ancestor(ancestor)
|
116
278
|
if @ancestors[ancestor.type].present?
|
117
279
|
raise "Trying to add an ancestor of type '#{ancestor.type}' but one of that " \
|
118
280
|
"type is already present"
|
119
281
|
end
|
120
282
|
|
283
|
+
ancestor.increment_descendant_count(short_type)
|
121
284
|
@ancestors[ancestor.type] = ancestor
|
122
285
|
end
|
123
286
|
|
287
|
+
# Return the elements in all of the ancestors
|
288
|
+
#
|
289
|
+
# @return [Array<ElementBase>]
|
290
|
+
#
|
124
291
|
def ancestor_elements
|
125
292
|
@ancestors.values.map(&:element)
|
126
293
|
end
|
127
294
|
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
end
|
133
|
-
|
295
|
+
# Returns the count of this element's type in the given ancestor type
|
296
|
+
#
|
297
|
+
# @param ancestor_type [String, Symbol]
|
298
|
+
#
|
134
299
|
def count_in(ancestor_type)
|
135
|
-
@
|
136
|
-
|
137
|
-
|
138
|
-
def remember_that_sub_elements_are_already_counted(css_or_xpath:, count:)
|
139
|
-
@css_or_xpath_that_has_been_counted[css_or_xpath] = count
|
300
|
+
@ancestors[ancestor_type]&.get_descendant_count(short_type) ||
|
301
|
+
raise("No ancestor of type '#{ancestor_type}'")
|
140
302
|
end
|
141
303
|
|
142
|
-
|
143
|
-
|
304
|
+
# Track that a sub element found by the given query has been counted
|
305
|
+
#
|
306
|
+
# @param search_query [SearchQuery] the search query matching the counted element
|
307
|
+
# @param type [String] the type of the sub element that was counted
|
308
|
+
#
|
309
|
+
def remember_that_a_sub_element_was_counted(search_query, type)
|
310
|
+
@search_query_matches_that_have_been_counted[search_query.to_s] ||= Hash.new(0)
|
311
|
+
@search_query_matches_that_have_been_counted[search_query.to_s][type] += 1
|
144
312
|
end
|
145
313
|
|
146
|
-
|
147
|
-
|
314
|
+
# Undo the counts from a prior search query (so that they can be counted again)
|
315
|
+
#
|
316
|
+
# @param search_query [SearchQuery] the prior search query whose counts need to be undone
|
317
|
+
#
|
318
|
+
def uncount(search_query)
|
319
|
+
@search_query_matches_that_have_been_counted.delete(search_query.to_s)&.each do |type, count|
|
320
|
+
ancestors.each_value do |ancestor|
|
321
|
+
ancestor.decrement_descendant_count(type, by: count)
|
322
|
+
end
|
323
|
+
end
|
148
324
|
end
|
149
325
|
|
326
|
+
# Returns the search history that found this element
|
327
|
+
#
|
328
|
+
# @return [SearchHistory]
|
329
|
+
#
|
150
330
|
def search_history
|
151
|
-
|
152
|
-
|
331
|
+
SearchHistory.new(
|
332
|
+
ancestor_elements.last&.search_history || SearchHistory.empty,
|
333
|
+
search_query_that_found_me
|
334
|
+
)
|
153
335
|
end
|
154
336
|
|
155
|
-
|
337
|
+
# Returns an ElementEnumerator that iterates over the provided selector or xpath queries
|
338
|
+
#
|
339
|
+
# @param selector_or_xpath_args [Array<String>] Selector or XPath queries
|
340
|
+
# @param only [Symbol, Callable] the name of a method to call on an element or a
|
341
|
+
# lambda or proc that accepts an element; elements will only be included in the
|
342
|
+
# search results if the method or callable returns true
|
343
|
+
# @param except [Symbol, Callable] the name of a method to call on an element or a
|
344
|
+
# lambda or proc that accepts an element; elements will not be included in the
|
345
|
+
# search results if the method or callable returns false
|
346
|
+
# @return [ElementEnumerator]
|
347
|
+
#
|
348
|
+
def search(*selector_or_xpath_args, only: nil, except: nil)
|
156
349
|
block_error_if(block_given?)
|
157
350
|
|
158
|
-
ElementEnumerator.factory.build_within(
|
351
|
+
ElementEnumerator.factory.build_within(
|
352
|
+
self,
|
353
|
+
search_query: SearchQuery.new(
|
354
|
+
css_or_xpath: selector_or_xpath_args,
|
355
|
+
only: only,
|
356
|
+
except: except
|
357
|
+
)
|
358
|
+
)
|
159
359
|
end
|
160
360
|
|
161
361
|
# Yields and returns the first child element that matches the provided
|
@@ -185,16 +385,32 @@ module Kitchen
|
|
185
385
|
end
|
186
386
|
end
|
187
387
|
|
388
|
+
# @!method at
|
389
|
+
# @see first
|
188
390
|
alias_method :at, :first
|
189
391
|
|
190
|
-
|
392
|
+
# Returns an enumerator over the direct child elements of this element, with the
|
393
|
+
# specific type (e.g. +TermElement+) if such type is available.
|
394
|
+
#
|
395
|
+
# @return [TypeCastingElementEnumerator]
|
396
|
+
#
|
397
|
+
def element_children
|
191
398
|
block_error_if(block_given?)
|
192
|
-
TypeCastingElementEnumerator.factory.build_within(
|
399
|
+
TypeCastingElementEnumerator.factory.build_within(
|
400
|
+
self,
|
401
|
+
search_query: SearchQuery.new(css_or_xpath: './*')
|
402
|
+
)
|
193
403
|
end
|
194
404
|
|
405
|
+
# Searches for elements handled by a list of enumerator classes. All element that
|
406
|
+
# matches one of those enumerator classes are iterated over.
|
407
|
+
#
|
408
|
+
# @param enumerator_classes [Array<ElementEnumeratorBase>]
|
409
|
+
# @return [TypeCastingElementEnumerator]
|
410
|
+
#
|
195
411
|
def search_with(*enumerator_classes)
|
196
412
|
block_error_if(block_given?)
|
197
|
-
raise
|
413
|
+
raise 'must supply at least one enumerator class' if enumerator_classes.empty?
|
198
414
|
|
199
415
|
factory = enumerator_classes[0].factory
|
200
416
|
enumerator_classes[1..-1].each do |enumerator_class|
|
@@ -214,7 +430,7 @@ module Kitchen
|
|
214
430
|
block_error_if(block_given?)
|
215
431
|
|
216
432
|
node.remove
|
217
|
-
|
433
|
+
get_clipboard(to).add(self) if to.present?
|
218
434
|
self
|
219
435
|
end
|
220
436
|
|
@@ -232,9 +448,10 @@ module Kitchen
|
|
232
448
|
the_copy = clone
|
233
449
|
the_copy.raw.traverse do |node|
|
234
450
|
next if node.text? || node.document?
|
451
|
+
|
235
452
|
document.record_id_copied(node[:id])
|
236
453
|
end
|
237
|
-
|
454
|
+
get_clipboard(to).add(the_copy) if to.present?
|
238
455
|
the_copy
|
239
456
|
end
|
240
457
|
|
@@ -247,11 +464,18 @@ module Kitchen
|
|
247
464
|
temp_copy = clone
|
248
465
|
temp_copy.raw.traverse do |node|
|
249
466
|
next if node.text? || node.document?
|
467
|
+
|
250
468
|
node[:id] = document.modified_id_to_paste(node[:id]) unless node[:id].blank?
|
251
469
|
end
|
252
470
|
temp_copy.to_s
|
253
471
|
end
|
254
472
|
|
473
|
+
# Copy the element's id
|
474
|
+
def copied_id
|
475
|
+
document.record_id_copied(id)
|
476
|
+
document.modified_id_to_paste(id)
|
477
|
+
end
|
478
|
+
|
255
479
|
# Delete the element
|
256
480
|
#
|
257
481
|
def trash
|
@@ -263,20 +487,19 @@ module Kitchen
|
|
263
487
|
Element.new(node: raw.parent, document: document, short_type: "parent(#{short_type})")
|
264
488
|
end
|
265
489
|
|
266
|
-
# TODO make it clear if all of these methods take Element, Node, or String
|
490
|
+
# TODO: make it clear if all of these methods take Element, Node, or String
|
267
491
|
|
268
492
|
# If child argument given, prepends it before the element's current children.
|
269
493
|
# If sibling is given, prepends it as a sibling to this element.
|
270
494
|
#
|
271
495
|
# @param child [String] the child to prepend
|
272
496
|
# @param sibling [String] the sibling to prepend
|
497
|
+
# @raise [RecipeError] if specify other than just a child or a sibling
|
273
498
|
#
|
274
499
|
def prepend(child: nil, sibling: nil)
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
raise RecipeError, "One of `child` or `sibling` must be specified"
|
279
|
-
elsif child
|
500
|
+
require_one_of_child_or_sibling(child, sibling)
|
501
|
+
|
502
|
+
if child
|
280
503
|
if node.children.empty?
|
281
504
|
node.children = child.to_s
|
282
505
|
else
|
@@ -285,6 +508,7 @@ module Kitchen
|
|
285
508
|
else
|
286
509
|
node.add_previous_sibling(sibling)
|
287
510
|
end
|
511
|
+
|
288
512
|
self
|
289
513
|
end
|
290
514
|
|
@@ -293,13 +517,12 @@ module Kitchen
|
|
293
517
|
#
|
294
518
|
# @param child [String] the child to append
|
295
519
|
# @param sibling [String] the sibling to append
|
520
|
+
# @raise [RecipeError] if specify other than just a child or a sibling
|
296
521
|
#
|
297
522
|
def append(child: nil, sibling: nil)
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
raise RecipeError, "One of `child` or `sibling` must be specified"
|
302
|
-
elsif child
|
523
|
+
require_one_of_child_or_sibling(child, sibling)
|
524
|
+
|
525
|
+
if child
|
303
526
|
if node.children.empty?
|
304
527
|
node.children = child.to_s
|
305
528
|
else
|
@@ -308,6 +531,7 @@ module Kitchen
|
|
308
531
|
else
|
309
532
|
node.next = sibling
|
310
533
|
end
|
534
|
+
|
311
535
|
self
|
312
536
|
end
|
313
537
|
|
@@ -320,7 +544,36 @@ module Kitchen
|
|
320
544
|
self
|
321
545
|
end
|
322
546
|
|
323
|
-
#
|
547
|
+
# Wraps the element's children in a new element. Yields the new wrapper element
|
548
|
+
# to a block, if provided.
|
549
|
+
#
|
550
|
+
# @param name [String] the wrapper's tag name, defaults to 'div'.
|
551
|
+
# @param attributes [Hash] the wrapper's attributes. XML attributes often use hyphens
|
552
|
+
# (e.g. 'data-type') which are hard to put into symbols. Therefore underscores in
|
553
|
+
# keys passed to this method will be converted to hyphens. If you really want an
|
554
|
+
# underscore you can use a double underscore.
|
555
|
+
# @yieldparam [Element] the wrapper Element
|
556
|
+
# @return [Element] self
|
557
|
+
#
|
558
|
+
def wrap_children(name='div', attributes={})
|
559
|
+
if name.is_a?(Hash)
|
560
|
+
attributes = name
|
561
|
+
name = 'div'
|
562
|
+
end
|
563
|
+
|
564
|
+
node.children = node.document.create_element(name) do |new_node|
|
565
|
+
# For some reason passing attributes to create_element doesn't work, so doing here
|
566
|
+
attributes.each do |k, v|
|
567
|
+
new_node[k.to_s.gsub(/([^_])_([^_])/, '\1-\2').gsub('__', '_')] = v
|
568
|
+
end
|
569
|
+
new_node.children = children.to_s
|
570
|
+
yield Element.new(node: new_node, document: document, short_type: nil) if block_given?
|
571
|
+
end.to_s
|
572
|
+
|
573
|
+
self
|
574
|
+
end
|
575
|
+
|
576
|
+
# TODO: methods like replace_children that take string, either forbid or handle Element/Node args
|
324
577
|
|
325
578
|
# Get the content of children matching the provided selector. Mostly
|
326
579
|
# useful when there is one child with text you want to extract.
|
@@ -350,11 +603,19 @@ module Kitchen
|
|
350
603
|
# @return [String] the sub header tag name
|
351
604
|
#
|
352
605
|
def sub_header_name
|
353
|
-
first_header = node.search(
|
606
|
+
first_header = node.search('h1, h2, h3, h4, h5, h6').first
|
607
|
+
|
608
|
+
if first_header.nil?
|
609
|
+
'h1'
|
610
|
+
else
|
611
|
+
first_header.name.gsub(/\d/) { |num| (num.to_i + 1).to_s }
|
612
|
+
end
|
613
|
+
end
|
354
614
|
|
355
|
-
|
356
|
-
|
357
|
-
|
615
|
+
# Mark the location so that if there's an error we can show the developer where.
|
616
|
+
#
|
617
|
+
def mark_as_current_location!
|
618
|
+
document.location = self
|
358
619
|
end
|
359
620
|
|
360
621
|
# Returns the underlying Nokogiri object
|
@@ -365,22 +626,40 @@ module Kitchen
|
|
365
626
|
node
|
366
627
|
end
|
367
628
|
|
629
|
+
# Returns a string version of this element
|
630
|
+
#
|
631
|
+
# @return [String]
|
632
|
+
#
|
368
633
|
def inspect
|
369
634
|
to_s
|
370
635
|
end
|
371
636
|
|
637
|
+
# Returns a string version of this element
|
638
|
+
#
|
639
|
+
# @return [String]
|
640
|
+
#
|
372
641
|
def to_s
|
373
642
|
remove_default_namespaces_if_clone(node.to_s)
|
374
643
|
end
|
375
644
|
|
645
|
+
# Returns a string version of this element as XML
|
646
|
+
#
|
647
|
+
# @return [String]
|
648
|
+
#
|
376
649
|
def to_xml
|
377
650
|
remove_default_namespaces_if_clone(node.to_xml)
|
378
651
|
end
|
379
652
|
|
653
|
+
# Returns a string version of this element as XHTML
|
654
|
+
#
|
655
|
+
# @return [String]
|
656
|
+
#
|
380
657
|
def to_xhtml
|
381
658
|
remove_default_namespaces_if_clone(node.to_xhtml)
|
382
659
|
end
|
383
660
|
|
661
|
+
# Returns a clone of this object
|
662
|
+
#
|
384
663
|
def clone
|
385
664
|
super.tap do |element|
|
386
665
|
# When we call dup, the dup gets a bunch of default namespace stuff that
|
@@ -407,38 +686,69 @@ module Kitchen
|
|
407
686
|
end
|
408
687
|
end
|
409
688
|
|
689
|
+
def last_element
|
690
|
+
node.last_element_child
|
691
|
+
end
|
692
|
+
|
410
693
|
# @!method pages
|
411
694
|
# Returns a pages enumerator
|
412
|
-
def_delegators :as_enumerator, :pages, :chapters, :terms, :figures, :notes, :tables, :examples
|
695
|
+
def_delegators :as_enumerator, :pages, :chapters, :terms, :figures, :notes, :tables, :examples,
|
696
|
+
:metadatas, :non_introduction_pages, :units, :titles, :exercises, :references,
|
697
|
+
:composite_pages
|
413
698
|
|
699
|
+
# Returns this element as an enumerator (over only one element, itself)
|
700
|
+
#
|
701
|
+
# @return [ElementEnumeratorBase] (actually returns the appropriate enumerator class for this element)
|
702
|
+
#
|
414
703
|
def as_enumerator
|
415
|
-
enumerator_class.new(
|
704
|
+
enumerator_class.new(search_query: search_query_that_found_me) { |block| block.yield(self) }
|
416
705
|
end
|
417
706
|
|
418
707
|
protected
|
419
708
|
|
709
|
+
# The wrapped Nokogiri node
|
710
|
+
# @return [Nokogiri::XML::Node] the node
|
420
711
|
attr_accessor :node
|
712
|
+
|
713
|
+
# If this element is a clone
|
714
|
+
# @return [Boolean]
|
421
715
|
attr_accessor :is_a_clone
|
422
716
|
|
423
|
-
|
717
|
+
# Return a clipboard
|
718
|
+
#
|
719
|
+
# @param name_or_object [String, Clipboard] the name of the clipboard or the clipboard itself
|
720
|
+
# @return [Clipboard]
|
721
|
+
#
|
722
|
+
def get_clipboard(name_or_object)
|
424
723
|
case name_or_object
|
425
724
|
when Symbol
|
426
|
-
|
725
|
+
clipboard(name: name_or_object)
|
427
726
|
when Clipboard
|
428
727
|
name_or_object
|
429
728
|
else
|
430
|
-
raise ArgumentError, "The provided argument (#{name_or_object}) is not "
|
729
|
+
raise ArgumentError, "The provided argument (#{name_or_object}) is not " \
|
431
730
|
"a clipboard name or a clipboard"
|
432
731
|
end
|
433
732
|
end
|
434
733
|
|
734
|
+
# Clean up some default namespace junk for cloned elements
|
735
|
+
#
|
736
|
+
# @param string [String] the string to clean
|
435
737
|
def remove_default_namespaces_if_clone(string)
|
436
738
|
if is_a_clone
|
437
|
-
string.gsub(
|
739
|
+
string.gsub('xmlns:default="http://www.w3.org/1999/xhtml"', '')
|
740
|
+
.gsub('xmlns="http://www.w3.org/1999/xhtml"', '')
|
741
|
+
.gsub('default:', '')
|
438
742
|
else
|
439
743
|
string
|
440
744
|
end
|
441
745
|
end
|
442
746
|
|
747
|
+
def require_one_of_child_or_sibling(child, sibling)
|
748
|
+
raise RecipeError, 'Only one of `child` or `sibling` can be specified' if child && sibling
|
749
|
+
raise RecipeError, 'One of `child` or `sibling` must be specified' if !child && !sibling
|
750
|
+
end
|
751
|
+
|
443
752
|
end
|
444
753
|
end
|
754
|
+
# rubocop:enable Metrics/ClassLength
|