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.
Files changed (133) hide show
  1. checksums.yaml +4 -4
  2. data/.devcontainer/devcontainer.json +37 -17
  3. data/.github/config.yml +14 -0
  4. data/.github/workflows/tests.yml +5 -15
  5. data/.gitignore +1 -1
  6. data/.inch.yml +6 -0
  7. data/.rubocop.yml +65 -0
  8. data/CHANGELOG.md +16 -0
  9. data/Gemfile +5 -3
  10. data/Gemfile.lock +54 -5
  11. data/README.md +58 -11
  12. data/Rakefile +5 -3
  13. data/bin/console +4 -3
  14. data/docker/Dockerfile +36 -0
  15. data/docker/Dockerfile.ci +10 -0
  16. data/docker/bash +5 -1
  17. data/docker/build +10 -0
  18. data/docker/ci +16 -0
  19. data/docker/run +9 -0
  20. data/docker/tag_and_push_latest +17 -0
  21. data/lefthook.yml +6 -0
  22. data/lib/kitchen/ancestor.rb +38 -1
  23. data/lib/kitchen/book_document.rb +20 -2
  24. data/lib/kitchen/book_element.rb +24 -3
  25. data/lib/kitchen/book_element_enumerator.rb +4 -0
  26. data/lib/kitchen/book_recipe.rb +15 -1
  27. data/lib/kitchen/chapter_element.rb +43 -3
  28. data/lib/kitchen/chapter_element_enumerator.rb +9 -1
  29. data/lib/kitchen/clipboard.rb +35 -4
  30. data/lib/kitchen/composite_chapter_element.rb +20 -1
  31. data/lib/kitchen/composite_page_element.rb +25 -2
  32. data/lib/kitchen/composite_page_element_enumerator.rb +8 -0
  33. data/lib/kitchen/config.rb +14 -7
  34. data/lib/kitchen/counter.rb +9 -2
  35. data/lib/kitchen/debug/print_recipe_error.rb +53 -35
  36. data/lib/kitchen/directions/.rubocop.yml +22 -0
  37. data/lib/kitchen/directions/bake_appendix.rb +4 -4
  38. data/lib/kitchen/directions/bake_chapter_glossary.rb +10 -7
  39. data/lib/kitchen/directions/bake_chapter_introductions.rb +6 -6
  40. data/lib/kitchen/directions/bake_chapter_key_equations.rb +9 -6
  41. data/lib/kitchen/directions/bake_chapter_summary.rb +16 -13
  42. data/lib/kitchen/directions/bake_chapter_title/main.rb +11 -0
  43. data/lib/kitchen/directions/bake_chapter_title/v1.rb +24 -0
  44. data/lib/kitchen/directions/bake_composite_pages.rb +2 -2
  45. data/lib/kitchen/directions/bake_example.rb +6 -4
  46. data/lib/kitchen/directions/bake_exercises/main.rb +11 -0
  47. data/lib/kitchen/directions/bake_exercises/v1.rb +166 -0
  48. data/lib/kitchen/directions/bake_figure.rb +8 -5
  49. data/lib/kitchen/directions/bake_footnotes/main.rb +2 -2
  50. data/lib/kitchen/directions/bake_footnotes/v1.rb +4 -4
  51. data/lib/kitchen/directions/bake_index/main.rb +2 -2
  52. data/lib/kitchen/directions/bake_index/v1.rb +22 -15
  53. data/lib/kitchen/directions/bake_link_placeholders.rb +24 -0
  54. data/lib/kitchen/directions/bake_math_in_paragraph.rb +5 -3
  55. data/lib/kitchen/directions/bake_notes.rb +8 -8
  56. data/lib/kitchen/directions/bake_numbered_table/main.rb +2 -2
  57. data/lib/kitchen/directions/bake_numbered_table/v1.rb +21 -16
  58. data/lib/kitchen/directions/bake_page_abstracts.rb +14 -0
  59. data/lib/kitchen/directions/bake_preface/main.rb +11 -0
  60. data/lib/kitchen/directions/bake_preface/v1.rb +18 -0
  61. data/lib/kitchen/directions/bake_stepwise.rb +7 -7
  62. data/lib/kitchen/directions/bake_suggested_reading.rb +26 -0
  63. data/lib/kitchen/directions/bake_toc.rb +41 -22
  64. data/lib/kitchen/directions/bake_unit_title/main.rb +11 -0
  65. data/lib/kitchen/directions/bake_unit_title/v1.rb +23 -0
  66. data/lib/kitchen/directions/bake_unnumbered_tables.rb +7 -5
  67. data/lib/kitchen/directions/move_title_text_into_span.rb +2 -2
  68. data/lib/kitchen/document.rb +72 -13
  69. data/lib/kitchen/element.rb +11 -0
  70. data/lib/kitchen/element_base.rb +276 -56
  71. data/lib/kitchen/element_enumerator.rb +8 -0
  72. data/lib/kitchen/element_enumerator_base.rb +210 -28
  73. data/lib/kitchen/element_enumerator_factory.rb +59 -52
  74. data/lib/kitchen/element_factory.rb +27 -12
  75. data/lib/kitchen/errors.rb +5 -0
  76. data/lib/kitchen/example_element.rb +19 -1
  77. data/lib/kitchen/example_element_enumerator.rb +9 -1
  78. data/lib/kitchen/figure_element.rb +36 -2
  79. data/lib/kitchen/figure_element_enumerator.rb +9 -1
  80. data/lib/kitchen/metadata_element.rb +28 -0
  81. data/lib/kitchen/metadata_element_enumerator.rb +21 -0
  82. data/lib/kitchen/mixins/block_error_if.rb +24 -4
  83. data/lib/kitchen/note_element.rb +37 -7
  84. data/lib/kitchen/note_element_enumerator.rb +9 -1
  85. data/lib/kitchen/oven.rb +66 -15
  86. data/lib/kitchen/page_element.rb +62 -13
  87. data/lib/kitchen/page_element_enumerator.rb +9 -1
  88. data/lib/kitchen/pantry.rb +28 -1
  89. data/lib/kitchen/patches/nokogiri.rb +19 -2
  90. data/lib/kitchen/patches/renderable.rb +9 -3
  91. data/lib/kitchen/patches/string.rb +8 -0
  92. data/lib/kitchen/recipe.rb +38 -34
  93. data/lib/kitchen/search_history.rb +43 -4
  94. data/lib/kitchen/search_query.rb +84 -0
  95. data/lib/kitchen/selectors/base.rb +26 -0
  96. data/lib/kitchen/selectors/standard_1.rb +8 -0
  97. data/lib/kitchen/table_element.rb +54 -3
  98. data/lib/kitchen/table_element_enumerator.rb +9 -1
  99. data/lib/kitchen/term_element.rb +15 -1
  100. data/lib/kitchen/term_element_enumerator.rb +9 -1
  101. data/lib/kitchen/transliterations.rb +7 -5
  102. data/lib/kitchen/type_casting_element_enumerator.rb +17 -1
  103. data/lib/kitchen/unit_element.rb +39 -0
  104. data/lib/kitchen/unit_element_enumerator.rb +20 -0
  105. data/lib/kitchen/utils.rb +10 -13
  106. data/lib/kitchen/version.rb +5 -1
  107. data/lib/locales/en.yml +6 -0
  108. data/lib/openstax_kitchen.rb +43 -42
  109. data/openstax_kitchen.gemspec +26 -20
  110. data/tutorials/00/solution1.rb +9 -0
  111. data/tutorials/00/solution2.rb +8 -0
  112. data/tutorials/01/solution1.rb +18 -0
  113. data/tutorials/01/solution2.rb +26 -0
  114. data/tutorials/02/solution1.rb +31 -0
  115. data/tutorials/03/{solution_1.rb → solution1.rb} +6 -4
  116. data/tutorials/03/solution2.rb +18 -0
  117. data/tutorials/04/{solution_1.rb → solution1.rb} +4 -2
  118. data/tutorials/04/{solution_2.rb → solution2.rb} +6 -4
  119. data/tutorials/05/solution1.rb +11 -0
  120. data/tutorials/check_it +16 -15
  121. data/tutorials/setup_my_recipes +7 -6
  122. metadata +101 -22
  123. data/Dockerfile +0 -19
  124. data/docker-compose.yml +0 -12
  125. data/docker/entrypoint +0 -9
  126. data/lib/kitchen/directions/bake_exercises.rb +0 -164
  127. data/tutorials/00/solution_1.rb +0 -7
  128. data/tutorials/00/solution_2.rb +0 -6
  129. data/tutorials/01/solution_1.rb +0 -16
  130. data/tutorials/01/solution_2.rb +0 -24
  131. data/tutorials/02/solution_1.rb +0 -29
  132. data/tutorials/03/solution_2.rb +0 -15
  133. data/tutorials/05/solution_1.rb +0 -9
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kitchen
4
+ # Records the search history that was used to find a certain element
5
+ #
6
+ class SearchQuery
7
+ attr_reader :css_or_xpath
8
+ attr_reader :only
9
+ attr_reader :except
10
+
11
+ # Create a new SearchQuery
12
+ #
13
+ # @param css_or_xpath [String, Array<String>] selectors to use to limit iteration results
14
+ # a "$" in this argument can be replaced with a default selector via
15
+ # #apply_default_css_or_xpath_and_normalize
16
+ # @param only [Symbol, Callable] the name of a method to call on an element or a
17
+ # lambda or proc that accepts an element; elements will only be included in the
18
+ # search results if the method or callable returns true
19
+ # @param except [Symbol, Callable] the name of a method to call on an element or a
20
+ # lambda or proc that accepts an element; elements will not be included in the
21
+ # search results if the method or callable returns false
22
+ #
23
+ def initialize(css_or_xpath: nil, only: nil, except: nil)
24
+ @css_or_xpath = css_or_xpath
25
+ @only = only.is_a?(String) ? only.to_sym : only
26
+ @except = except.is_a?(String) ? except.to_sym : except
27
+ end
28
+
29
+ # Returns true iff the element passes the `only` and `except` conditions
30
+ #
31
+ # @return [Boolean]
32
+ #
33
+ def conditions_match?(element)
34
+ condition_passes?(except, element, false) && condition_passes?(only, element, true)
35
+ end
36
+
37
+ # Replaces '$' in the `css_or_xpath` with the provided value; also normalizes
38
+ # `css_or_xpath` to an array
39
+ #
40
+ def apply_default_css_or_xpath_and_normalize(default_css_or_xpath=nil)
41
+ @as_type = nil
42
+ @css_or_xpath = [css_or_xpath || '$'].flatten.map do |item|
43
+ item.gsub(/\$/, [default_css_or_xpath].flatten.join(', '))
44
+ end
45
+ end
46
+
47
+ # Returns the search query as a spaceless string suitable for use as an element type
48
+ #
49
+ def as_type
50
+ @as_type ||= [
51
+ [css_or_xpath].flatten.join(','),
52
+ stringify_condition(only, 'only'),
53
+ stringify_condition(except, 'except')
54
+ ].compact.join(';')
55
+ end
56
+
57
+ # Returns a string representation of the search query
58
+ #
59
+ def to_s
60
+ as_type
61
+ end
62
+
63
+ protected
64
+
65
+ def condition_passes?(method_or_callable, element, success_outcome)
66
+ return true if method_or_callable.nil?
67
+
68
+ result =
69
+ if method_or_callable.is_a?(Symbol)
70
+ element.send(method_or_callable)
71
+ else
72
+ method_or_callable.call(element)
73
+ end
74
+
75
+ !!result == success_outcome
76
+ end
77
+
78
+ def stringify_condition(condition, name)
79
+ return nil unless condition
80
+
81
+ "#{name}:#{condition.is_a?(Symbol) ? condition : condition.source_location.join(':')}"
82
+ end
83
+ end
84
+ end
@@ -1,8 +1,34 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Kitchen
4
+ # A centralized place to put common CSS selectors so they aren't sprinkled
5
+ # throughout the baseline (and so that they can be changed easily if needed)
6
+ #
2
7
  module Selectors
8
+ # Base class for different selector configurations
9
+ #
3
10
  class Base
11
+ # Selector for the title in a page
12
+ # @return [String]
4
13
  attr_accessor :title_in_page
14
+ # Selector for the title in an introduction page
15
+ # @return [String]
5
16
  attr_accessor :title_in_introduction_page
17
+ # Selector for the summary in a page
18
+ # @return [String]
19
+ attr_accessor :page_summary
20
+
21
+ # Override specific selectors
22
+ #
23
+ # @param hash [Hash] a hash of selectors to selector values, e.g. {title_in_page: '.title'}
24
+ # @return [Base] this object
25
+ #
26
+ def override(hash={})
27
+ hash.each do |selector, value|
28
+ send("#{selector}=", value)
29
+ end
30
+ self
31
+ end
6
32
  end
7
33
  end
8
34
  end
@@ -1,10 +1,18 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Kitchen
2
4
  module Selectors
5
+ # A specific set of selectors
6
+ #
3
7
  class Standard1 < Base
4
8
 
9
+ # Create a new instance
10
+ #
5
11
  def initialize
12
+ super
6
13
  self.title_in_page = "./*[@data-type = 'document-title']"
7
14
  self.title_in_introduction_page = "./*[@data-type = 'document-title']"
15
+ self.page_summary = 'section.summary'
8
16
  end
9
17
 
10
18
  end
@@ -1,6 +1,15 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Kitchen
4
+ # An element for a table
5
+ #
2
6
  class TableElement < ElementBase
3
7
 
8
+ # Creates a new +TableElement+
9
+ #
10
+ # @param node [Nokogiri::XML::Node] the node this element wraps
11
+ # @param document [Document] this element's document
12
+ #
4
13
  def initialize(node:, document: nil)
5
14
  super(node: node,
6
15
  document: document,
@@ -8,28 +17,70 @@ module Kitchen
8
17
  short_type: :table)
9
18
  end
10
19
 
20
+ # Returns an element for the title row, if present
21
+ #
22
+ # @return [Element, nil]
23
+ #
11
24
  def title_row
12
25
  top_titled? ? first('thead').first('tr') : nil
13
26
  end
14
27
 
28
+ # Returns the title nodes in the first title row element
29
+ #
30
+ # @return [Nokogiri::XML::NodeSet] Unusual to return the raw Nokogiri nodes!
31
+ #
15
32
  def title
16
- title_row&.first('th').children
33
+ # TODO: replace +children+ with +element_children+?
34
+ title_row&.first('th')&.children
17
35
  end
18
36
 
37
+ # Returns true if the table has a title at the top
38
+ #
39
+ # @return [Boolean]
40
+ #
19
41
  def top_titled?
20
42
  has_class?('top-titled')
21
43
  end
22
44
 
45
+ # Returns true if the table is unnumbered
46
+ #
47
+ # @return [Boolean]
48
+ #
23
49
  def unnumbered?
24
50
  has_class?('unnumbered')
25
51
  end
26
52
 
53
+ # Returns true if the table is unstyled
54
+ #
55
+ # @return [Boolean]
56
+ #
57
+ def unstyled?
58
+ has_class?('unstyled')
59
+ end
60
+
61
+ # Returns true if the table has a column header
62
+ #
63
+ # @return [Boolean]
64
+ #
65
+ def column_header?
66
+ has_class?('column-header')
67
+ end
68
+
69
+ # Returns an element for the table caption, if present
70
+ #
71
+ # @return [Element, nil]
72
+ #
27
73
  def caption
28
- first("caption")
74
+ first('caption')
29
75
  end
30
76
 
77
+ # Returns true if this class represents the element for the given node
78
+ #
79
+ # @param node [Nokogiri::XML::Node] the underlying node
80
+ # @return [Boolean]
81
+ #
31
82
  def self.is_the_element_class_for?(node)
32
- node.name == "table"
83
+ node.name == 'table'
33
84
  end
34
85
 
35
86
  end
@@ -1,9 +1,17 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Kitchen
4
+ # An enumerator for table elements
5
+ #
2
6
  class TableElementEnumerator < ElementEnumeratorBase
3
7
 
8
+ # Returns a factory for this enumerator
9
+ #
10
+ # @return [ElementEnumeratorFactory]
11
+ #
4
12
  def self.factory
5
13
  ElementEnumeratorFactory.new(
6
- default_css_or_xpath: "table", # TODO get from config?
14
+ default_css_or_xpath: 'table', # TODO: get from config?
7
15
  sub_element_class: TableElement,
8
16
  enumerator_class: self
9
17
  )
@@ -1,6 +1,15 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Kitchen
4
+ # An element for a term
5
+ #
2
6
  class TermElement < ElementBase
3
7
 
8
+ # Creates a new +TermElement+
9
+ #
10
+ # @param node [Nokogiri::XML::Node] the node this element wraps
11
+ # @param document [Document] this element's document
12
+ #
4
13
  def initialize(node:, document: nil)
5
14
  super(node: node,
6
15
  document: document,
@@ -8,8 +17,13 @@ module Kitchen
8
17
  short_type: :term)
9
18
  end
10
19
 
20
+ # Returns true if this class represents the element for the given node
21
+ #
22
+ # @param node [Nokogiri::XML::Node] the underlying node
23
+ # @return [Boolean]
24
+ #
11
25
  def self.is_the_element_class_for?(node)
12
- node['data-type'] == "term"
26
+ node['data-type'] == 'term'
13
27
  end
14
28
 
15
29
  end
@@ -1,9 +1,17 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Kitchen
4
+ # An enumerator for term elements
5
+ #
2
6
  class TermElementEnumerator < ElementEnumeratorBase
3
7
 
8
+ # Returns a factory for this enumerator
9
+ #
10
+ # @return [ElementEnumeratorFactory]
11
+ #
4
12
  def self.factory
5
13
  ElementEnumeratorFactory.new(
6
- default_css_or_xpath: "span[data-type='term']", # TODO get from config?
14
+ default_css_or_xpath: "span[data-type='term']", # TODO: get from config?
7
15
  sub_element_class: TermElement,
8
16
  enumerator_class: self
9
17
  )
@@ -1,19 +1,21 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Kitchen
2
4
  # These are added to every translation locale, including the `test` locale
3
5
  # set by `stub_locales`. When we sort strings with accent marks, we use
4
6
  # `ActiveSupport::Inflector.transliterate` to ensure that the sorting is
5
7
  # sensible. This method does not know about Greek characters by default so
6
8
  # we teach it about them by adding the rules below to the i18n configuration.
7
-
9
+ #
8
10
  TRANSLITERATIONS = {
9
11
  i18n: {
10
12
  transliterate: {
11
13
  rule: {
12
- σ: "σ",
13
- Δ: "Δ",
14
- π: "π",
14
+ σ: 'σ',
15
+ Δ: 'Δ',
16
+ π: 'π'
15
17
  }
16
18
  }
17
19
  }
18
- }
20
+ }.freeze
19
21
  end
@@ -1,6 +1,15 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Kitchen
4
+ # An enumerator that detects the element type as it iterates and returns specific,
5
+ # different element classes based on that type.
6
+ #
2
7
  class TypeCastingElementEnumerator < ElementEnumeratorBase
3
8
 
9
+ # Returns a factory for this enumerator
10
+ #
11
+ # @return [ElementEnumeratorFactory]
12
+ #
4
13
  def self.factory
5
14
  ElementEnumeratorFactory.new(
6
15
  enumerator_class: self,
@@ -8,12 +17,19 @@ module Kitchen
8
17
  )
9
18
  end
10
19
 
20
+ # Returns a new enumerator that returns only the specified element classes within the
21
+ # scope of this enumerator.
22
+ #
23
+ # @param element_classes [Array<ElementBase>] the element classes to limit iteration to
24
+ # @return [TypeCastingElementEnumerator]
25
+ #
11
26
  def only(*element_classes)
12
27
  element_classes.flatten!
13
28
 
14
29
  TypeCastingElementEnumerator.new do |block|
15
- self.each do |element|
30
+ each do |element|
16
31
  next unless element_classes.include?(element.class)
32
+
17
33
  block.yield(element)
18
34
  end
19
35
  end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kitchen
4
+ # An element for a unit
5
+ #
6
+ class UnitElement < ElementBase
7
+
8
+ # Creates a new +UnitElement+
9
+ #
10
+ # @param node [Nokogiri::XML::Node] the node this element wraps
11
+ # @param document [Document] this element's document
12
+ #
13
+ def initialize(node:, document: nil)
14
+ super(node: node,
15
+ document: document,
16
+ enumerator_class: UnitElementEnumerator,
17
+ short_type: :unit)
18
+ end
19
+
20
+ # Get the title in the immediate children, not the one in the metadata. Could use
21
+ # CSS of ":not([data-type='metadata']) >
22
+ # [data-type='document-title'], [data-type='document-title']"
23
+ # but xpath is shorter
24
+ # @return [Element]
25
+ #
26
+ def title
27
+ first!("./*[@data-type = 'document-title']")
28
+ end
29
+
30
+ # Returns true if this class represents the element for the given node
31
+ #
32
+ # @param node [Nokogiri::XML::Node] the underlying node
33
+ # @return [Boolean]
34
+ #
35
+ def self.is_the_element_class_for?(node)
36
+ node['data-type'] == 'unit'
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kitchen
4
+ # An enumerator for unit elements
5
+ #
6
+ class UnitElementEnumerator < ElementEnumeratorBase
7
+
8
+ # Returns a factory for this enumerator
9
+ #
10
+ # @return [ElementEnumeratorFactory]
11
+ #
12
+ def self.factory
13
+ ElementEnumeratorFactory.new(
14
+ default_css_or_xpath: "div[data-type='unit']",
15
+ sub_element_class: UnitElement,
16
+ enumerator_class: self
17
+ )
18
+ end
19
+ end
20
+ end
data/lib/kitchen/utils.rb CHANGED
@@ -1,19 +1,16 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Kitchen
4
+ # Utility methods
5
+ #
2
6
  module Utils
3
-
7
+ # A standard way to convert a search path to an element type
8
+ #
9
+ # @param search_path [String, Array<String>] selectors
10
+ # @return [String]
11
+ #
4
12
  def self.search_path_to_type(search_path)
5
- [search_path].flatten.join(",")
13
+ [search_path].flatten.join(',')
6
14
  end
7
-
8
- def self.normalized_xhtml_string(xml_thing_with_to_s)
9
- doc = Nokogiri::XML(xml_thing_with_to_s.to_s) do |config|
10
- config.noblanks
11
- end
12
-
13
- doc.alphabetize_attributes!
14
-
15
- doc.to_xhtml(indent: 2)
16
- end
17
-
18
15
  end
19
16
  end