openstax_kitchen 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (121) hide show
  1. checksums.yaml +7 -0
  2. data/.devcontainer/devcontainer.json +19 -0
  3. data/.github/workflows/tests.yml +36 -0
  4. data/.gitignore +20 -0
  5. data/.rspec +3 -0
  6. data/.solargraph.yml +15 -0
  7. data/CHANGELOG.md +11 -0
  8. data/CODE_OF_CONDUCT.md +74 -0
  9. data/Dockerfile +19 -0
  10. data/Gemfile +9 -0
  11. data/Gemfile.lock +74 -0
  12. data/LICENSE.txt +21 -0
  13. data/README.md +674 -0
  14. data/Rakefile +6 -0
  15. data/bin/console +14 -0
  16. data/bin/normalize +79 -0
  17. data/bin/setup +8 -0
  18. data/books/chemistry2e/bake.rb +133 -0
  19. data/codecov.yaml +27 -0
  20. data/docker-compose.yml +12 -0
  21. data/docker/bash +1 -0
  22. data/docker/entrypoint +9 -0
  23. data/lib/kitchen.rb +57 -0
  24. data/lib/kitchen/ancestor.rb +30 -0
  25. data/lib/kitchen/book_document.rb +18 -0
  26. data/lib/kitchen/book_element.rb +24 -0
  27. data/lib/kitchen/book_element_enumerator.rb +5 -0
  28. data/lib/kitchen/book_recipe.rb +25 -0
  29. data/lib/kitchen/chapter_element.rb +35 -0
  30. data/lib/kitchen/chapter_element_enumerator.rb +13 -0
  31. data/lib/kitchen/clipboard.rb +37 -0
  32. data/lib/kitchen/composite_chapter_element.rb +23 -0
  33. data/lib/kitchen/composite_page_element.rb +27 -0
  34. data/lib/kitchen/composite_page_element_enumerator.rb +13 -0
  35. data/lib/kitchen/config.rb +20 -0
  36. data/lib/kitchen/counter.rb +34 -0
  37. data/lib/kitchen/debug/print_recipe_error.rb +80 -0
  38. data/lib/kitchen/directions/bake_appendix.rb +26 -0
  39. data/lib/kitchen/directions/bake_chapter_glossary.rb +34 -0
  40. data/lib/kitchen/directions/bake_chapter_introductions.rb +58 -0
  41. data/lib/kitchen/directions/bake_chapter_key_equations.rb +30 -0
  42. data/lib/kitchen/directions/bake_chapter_summary.rb +52 -0
  43. data/lib/kitchen/directions/bake_composite_pages.rb +13 -0
  44. data/lib/kitchen/directions/bake_example.rb +31 -0
  45. data/lib/kitchen/directions/bake_exercises.rb +164 -0
  46. data/lib/kitchen/directions/bake_figure.rb +25 -0
  47. data/lib/kitchen/directions/bake_footnotes/main.rb +11 -0
  48. data/lib/kitchen/directions/bake_footnotes/v1.rb +38 -0
  49. data/lib/kitchen/directions/bake_index/main.rb +11 -0
  50. data/lib/kitchen/directions/bake_index/v1.rb +138 -0
  51. data/lib/kitchen/directions/bake_index/v1.xhtml.erb +28 -0
  52. data/lib/kitchen/directions/bake_math_in_paragraph.rb +13 -0
  53. data/lib/kitchen/directions/bake_notes.rb +58 -0
  54. data/lib/kitchen/directions/bake_numbered_table/main.rb +11 -0
  55. data/lib/kitchen/directions/bake_numbered_table/v1.rb +47 -0
  56. data/lib/kitchen/directions/bake_stepwise.rb +27 -0
  57. data/lib/kitchen/directions/bake_toc.rb +103 -0
  58. data/lib/kitchen/directions/bake_unnumbered_tables.rb +14 -0
  59. data/lib/kitchen/directions/move_title_text_into_span.rb +15 -0
  60. data/lib/kitchen/document.rb +142 -0
  61. data/lib/kitchen/element.rb +15 -0
  62. data/lib/kitchen/element_base.rb +444 -0
  63. data/lib/kitchen/element_enumerator.rb +12 -0
  64. data/lib/kitchen/element_enumerator_base.rb +101 -0
  65. data/lib/kitchen/element_enumerator_factory.rb +111 -0
  66. data/lib/kitchen/element_factory.rb +32 -0
  67. data/lib/kitchen/errors.rb +4 -0
  68. data/lib/kitchen/example_element.rb +20 -0
  69. data/lib/kitchen/example_element_enumerator.rb +13 -0
  70. data/lib/kitchen/figure_element.rb +20 -0
  71. data/lib/kitchen/figure_element_enumerator.rb +13 -0
  72. data/lib/kitchen/mixins/block_error_if.rb +19 -0
  73. data/lib/kitchen/note_element.rb +43 -0
  74. data/lib/kitchen/note_element_enumerator.rb +13 -0
  75. data/lib/kitchen/oven.rb +61 -0
  76. data/lib/kitchen/page_element.rb +51 -0
  77. data/lib/kitchen/page_element_enumerator.rb +13 -0
  78. data/lib/kitchen/pantry.rb +35 -0
  79. data/lib/kitchen/patches/nokogiri.rb +31 -0
  80. data/lib/kitchen/patches/renderable.rb +31 -0
  81. data/lib/kitchen/patches/string.rb +5 -0
  82. data/lib/kitchen/recipe.rb +78 -0
  83. data/lib/kitchen/search_history.rb +33 -0
  84. data/lib/kitchen/selectors/base.rb +8 -0
  85. data/lib/kitchen/selectors/standard_1.rb +12 -0
  86. data/lib/kitchen/table_element.rb +36 -0
  87. data/lib/kitchen/table_element_enumerator.rb +13 -0
  88. data/lib/kitchen/term_element.rb +16 -0
  89. data/lib/kitchen/term_element_enumerator.rb +13 -0
  90. data/lib/kitchen/transliterations.rb +19 -0
  91. data/lib/kitchen/type_casting_element_enumerator.rb +23 -0
  92. data/lib/kitchen/utils.rb +19 -0
  93. data/lib/kitchen/version.rb +3 -0
  94. data/lib/locales/en.yml +21 -0
  95. data/lib/notes.md +9 -0
  96. data/openstax_kitchen.gemspec +39 -0
  97. data/tutorials/00/expected_baked.html +3 -0
  98. data/tutorials/00/raw.html +3 -0
  99. data/tutorials/00/solution_1.rb +7 -0
  100. data/tutorials/00/solution_2.rb +6 -0
  101. data/tutorials/01/expected_baked.html +66 -0
  102. data/tutorials/01/raw.html +62 -0
  103. data/tutorials/01/solution_1.rb +16 -0
  104. data/tutorials/01/solution_2.rb +24 -0
  105. data/tutorials/02/expected_baked.html +207 -0
  106. data/tutorials/02/raw.html +201 -0
  107. data/tutorials/02/solution_1.rb +29 -0
  108. data/tutorials/03/expected_baked.html +33 -0
  109. data/tutorials/03/raw.html +31 -0
  110. data/tutorials/03/solution_1.rb +16 -0
  111. data/tutorials/03/solution_2.rb +15 -0
  112. data/tutorials/04/expected_baked.html +36 -0
  113. data/tutorials/04/raw.html +36 -0
  114. data/tutorials/04/solution_1.rb +20 -0
  115. data/tutorials/04/solution_2.rb +25 -0
  116. data/tutorials/05/expected_baked.html +11 -0
  117. data/tutorials/05/raw.html +11 -0
  118. data/tutorials/05/solution_1.rb +9 -0
  119. data/tutorials/check_it +64 -0
  120. data/tutorials/setup_my_recipes +30 -0
  121. metadata +278 -0
@@ -0,0 +1,12 @@
1
+ module Kitchen
2
+ class ElementEnumerator < ElementEnumeratorBase
3
+
4
+ def self.factory
5
+ ElementEnumeratorFactory.new(
6
+ sub_element_class: Element,
7
+ enumerator_class: self
8
+ )
9
+ end
10
+
11
+ end
12
+ end
@@ -0,0 +1,101 @@
1
+ module Kitchen
2
+ class ElementEnumeratorBase < Enumerator
3
+
4
+ def initialize(size=nil, css_or_xpath: nil, upstream_enumerator: nil)
5
+ @css_or_xpath = css_or_xpath
6
+ @upstream_enumerator = upstream_enumerator
7
+ super(size)
8
+ end
9
+
10
+ def search_history
11
+ (@upstream_enumerator&.search_history || SearchHistory.empty).add(@css_or_xpath)
12
+ end
13
+
14
+ def terms(css_or_xpath=nil, &block)
15
+ chain_to(TermElementEnumerator, css_or_xpath: css_or_xpath, &block)
16
+ end
17
+
18
+ def pages(css_or_xpath=nil, &block)
19
+ chain_to(PageElementEnumerator, css_or_xpath: css_or_xpath, &block)
20
+ end
21
+
22
+ def chapters(css_or_xpath=nil, &block)
23
+ chain_to(ChapterElementEnumerator, css_or_xpath: css_or_xpath, &block)
24
+ end
25
+
26
+ # use block_error_if
27
+ def figures(css_or_xpath=nil, &block)
28
+ chain_to(FigureElementEnumerator, css_or_xpath: css_or_xpath, &block)
29
+ end
30
+
31
+ def notes(css_or_xpath=nil, &block)
32
+ chain_to(NoteElementEnumerator, css_or_xpath: css_or_xpath, &block)
33
+ end
34
+
35
+ def tables(css_or_xpath=nil, &block)
36
+ chain_to(TableElementEnumerator, css_or_xpath: css_or_xpath, &block)
37
+ end
38
+
39
+ def examples(css_or_xpath=nil, &block)
40
+ chain_to(ExampleElementEnumerator, css_or_xpath: css_or_xpath, &block)
41
+ end
42
+
43
+ def search(css_or_xpath=nil, &block)
44
+ chain_to(ElementEnumerator, css_or_xpath: css_or_xpath, &block)
45
+ end
46
+
47
+ def chain_to(enumerator_class, css_or_xpath: nil, &block)
48
+ raise(RecipeError, "Did you forget a `.each` call on this enumerator?") if block_given?
49
+
50
+ enumerator_class.factory.build_within(self, css_or_xpath: css_or_xpath)
51
+ end
52
+
53
+ def first!(missing_message: "Could not return a first result")
54
+ first || raise(RecipeError, "#{missing_message} matching #{search_history.latest} " \
55
+ "inside [#{search_history.upstream}]")
56
+ end
57
+
58
+ # Removes enumerated elements from their parent and places them on the specified clipboard
59
+ #
60
+ # @param to [Symbol, String, Clipboard, nil] the name of the clipboard (or a Clipboard
61
+ # object) to cut to. String values are converted to symbols. If not provided, the
62
+ # elements are placed on a new clipboard.
63
+ # @return [Clipboard] the clipboard
64
+ #
65
+ def cut(to: nil)
66
+ to ||= Clipboard.new
67
+ self.each do |element|
68
+ element.cut(to: to)
69
+ end
70
+ to
71
+ end
72
+
73
+ # Makes a copy of the enumerated elements and places them on the specified clipboard.
74
+ #
75
+ # @param to [Symbol, String, Clipboard, nil] the name of the clipboard (or a Clipboard
76
+ # object) to copy to. String values are converted to symbols. If not provided, the
77
+ # copies are placed on a new clipboard.
78
+ # @return [Clipboard] the clipboard
79
+ #
80
+ def copy(to: nil)
81
+ to ||= Clipboard.new
82
+ self.each do |element|
83
+ element.copy(to: to)
84
+ end
85
+ to
86
+ end
87
+
88
+ def trash
89
+ self.each(&:trash)
90
+ end
91
+
92
+ def [](index)
93
+ to_a[index]
94
+ end
95
+
96
+ def to_s
97
+ self.map(&:to_s).join("")
98
+ end
99
+
100
+ end
101
+ end
@@ -0,0 +1,111 @@
1
+ module Kitchen
2
+ class ElementEnumeratorFactory
3
+
4
+ attr_reader :default_css_or_xpath
5
+ attr_reader :enumerator_class
6
+ attr_reader :sub_element_class
7
+ attr_reader :detect_sub_element_class
8
+
9
+ def initialize(default_css_or_xpath: nil, sub_element_class: nil,
10
+ enumerator_class:, detect_sub_element_class: false)
11
+ @default_css_or_xpath = default_css_or_xpath
12
+ @sub_element_class = sub_element_class
13
+ @enumerator_class = enumerator_class
14
+ @detect_sub_element_class = detect_sub_element_class
15
+ end
16
+
17
+ # TODO spec this!
18
+ def apply_default_css_or_xpath_and_normalize(css_or_xpath=nil)
19
+ css_or_xpath ||= "$"
20
+ [css_or_xpath].flatten.each {|item| item.gsub!(/\$/, [default_css_or_xpath].flatten.join(", ")) }
21
+ [css_or_xpath].flatten
22
+ end
23
+
24
+ def build_within(enumerator_or_element, css_or_xpath: nil)
25
+ css_or_xpath = apply_default_css_or_xpath_and_normalize(css_or_xpath)
26
+
27
+ case enumerator_or_element
28
+ when ElementBase
29
+ build_within_element(enumerator_or_element, css_or_xpath: css_or_xpath)
30
+ when ElementEnumeratorBase
31
+ build_within_other_enumerator(enumerator_or_element, css_or_xpath: css_or_xpath)
32
+ end
33
+ end
34
+
35
+ def or_with(other_factory)
36
+ self.class.new(
37
+ default_css_or_xpath: default_css_or_xpath + ", " + other_factory.default_css_or_xpath,
38
+ enumerator_class: TypeCastingElementEnumerator,
39
+ detect_sub_element_class: true
40
+ )
41
+ end
42
+
43
+ protected
44
+
45
+ def build_within_element(element, css_or_xpath:)
46
+ enumerator_class.new(css_or_xpath: css_or_xpath) do |block|
47
+ grand_ancestors = element.ancestors
48
+ parent_ancestor = Ancestor.new(element)
49
+
50
+ num_sub_elements = 0
51
+
52
+ element.raw.search(*css_or_xpath).each_with_index do |sub_node, index|
53
+ sub_element = ElementFactory.build_from_node(
54
+ node: sub_node,
55
+ document: element.document,
56
+ element_class: sub_element_class,
57
+ default_short_type: Utils.search_path_to_type(css_or_xpath),
58
+ detect_element_class: detect_sub_element_class
59
+ )
60
+
61
+ # If the provided `css_or_xpath` has already been counted, we need to uncount
62
+ # them on the ancestors so that when they are counted again below, the counts
63
+ # are correct. Only do this on the first loop!
64
+ if index == 0
65
+ if element.have_sub_elements_already_been_counted?(css_or_xpath)
66
+ grand_ancestors.values.each do |ancestor|
67
+ ancestor.decrement_descendant_count(
68
+ sub_element.short_type,
69
+ by: element.number_of_sub_elements_already_counted(css_or_xpath)
70
+ )
71
+ end
72
+ end
73
+ end
74
+
75
+ # Record this sub element's ancestors and increment their descendant counts
76
+ sub_element.add_ancestors(grand_ancestors, parent_ancestor)
77
+ sub_element.count_as_descendant
78
+
79
+ # Remember how this sub element was found so can trace search history given
80
+ # any element.
81
+ sub_element.css_or_xpath_that_found_me = css_or_xpath
82
+
83
+ # Count runs through this loop for below
84
+ num_sub_elements += 1
85
+
86
+ # Mark the location so that if there's an error we can show the developer where.
87
+ sub_element.document.location = sub_element
88
+
89
+ block.yield(sub_element)
90
+ end
91
+
92
+ element.remember_that_sub_elements_are_already_counted(
93
+ css_or_xpath: css_or_xpath, count: num_sub_elements
94
+ )
95
+ end
96
+ end
97
+
98
+ def build_within_other_enumerator(other_enumerator, css_or_xpath:)
99
+ # Return a new enumerator instance that internally iterates over `other_enumerator`
100
+ # running a new enumerator for each element returned by that other enumerator.
101
+ enumerator_class.new(css_or_xpath: css_or_xpath, upstream_enumerator: other_enumerator) do |block|
102
+ other_enumerator.each do |element|
103
+ build_within_element(element, css_or_xpath: css_or_xpath).each do |sub_element|
104
+ block.yield(sub_element)
105
+ end
106
+ end
107
+ end
108
+ end
109
+
110
+ end
111
+ end
@@ -0,0 +1,32 @@
1
+ module Kitchen
2
+ class ElementFactory
3
+
4
+ ELEMENT_CLASSES = ElementBase.descendants
5
+
6
+ def self.specific_element_class_for_node(node)
7
+ ELEMENT_CLASSES.find do |klass|
8
+ klass.is_the_element_class_for?(node)
9
+ end || Element
10
+ end
11
+
12
+ def self.build_from_node(node:,
13
+ document:,
14
+ element_class: nil,
15
+ default_short_type: nil,
16
+ detect_element_class: false)
17
+ element_class ||= detect_element_class ?
18
+ specific_element_class_for_node(node) :
19
+ Element
20
+
21
+ if element_class == Element
22
+ element_class.new(node: node,
23
+ document: document,
24
+ short_type: default_short_type)
25
+ else
26
+ element_class.new(node: node,
27
+ document: document)
28
+ end
29
+ end
30
+
31
+ end
32
+ end
@@ -0,0 +1,4 @@
1
+ module Kitchen
2
+ class RecipeError < StandardError; end
3
+ class ElementNotFoundError < StandardError; end
4
+ end
@@ -0,0 +1,20 @@
1
+ module Kitchen
2
+ class ExampleElement < ElementBase
3
+
4
+ def initialize(node:, document: nil)
5
+ super(node: node,
6
+ document: document,
7
+ enumerator_class: ExampleElementEnumerator,
8
+ short_type: :example)
9
+ end
10
+
11
+ def titles
12
+ search("span[data-type='title']")
13
+ end
14
+
15
+ def self.is_the_element_class_for?(node)
16
+ node['data-type'] == "example"
17
+ end
18
+
19
+ end
20
+ end
@@ -0,0 +1,13 @@
1
+ module Kitchen
2
+ class ExampleElementEnumerator < ElementEnumeratorBase
3
+
4
+ def self.factory
5
+ ElementEnumeratorFactory.new(
6
+ default_css_or_xpath: "div[data-type='example']", # TODO element.document.selectors.example
7
+ sub_element_class: ExampleElement,
8
+ enumerator_class: self
9
+ )
10
+ end
11
+
12
+ end
13
+ end
@@ -0,0 +1,20 @@
1
+ module Kitchen
2
+ class FigureElement < ElementBase
3
+
4
+ def initialize(node:, document: nil)
5
+ super(node: node,
6
+ document: document,
7
+ enumerator_class: FigureElementEnumerator,
8
+ short_type: :figure)
9
+ end
10
+
11
+ def caption
12
+ first("figcaption")
13
+ end
14
+
15
+ def self.is_the_element_class_for?(node)
16
+ node.name == "figure"
17
+ end
18
+
19
+ end
20
+ end
@@ -0,0 +1,13 @@
1
+ module Kitchen
2
+ class FigureElementEnumerator < ElementEnumeratorBase
3
+
4
+ def self.factory
5
+ ElementEnumeratorFactory.new(
6
+ default_css_or_xpath: "figure", # TODO get from config?
7
+ sub_element_class: FigureElement,
8
+ enumerator_class: self
9
+ )
10
+ end
11
+
12
+ end
13
+ end
@@ -0,0 +1,19 @@
1
+ module Kitchen
2
+ module Mixins
3
+ module BlockErrorIf
4
+
5
+ def block_error_if(block_given)
6
+ calling_method = begin
7
+ this_method_location_index = caller_locations.find_index do |location|
8
+ location.label == "block_error_if"
9
+ end
10
+
11
+ caller_locations[(this_method_location_index || -1) + 1].label
12
+ end
13
+
14
+ raise(RecipeError, "The `#{calling_method}` method does not take a block argument") if block_given
15
+ end
16
+
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,43 @@
1
+ module Kitchen
2
+ class NoteElement < ElementBase
3
+
4
+ TITLE_TRANSLATION_KEYS = %w(
5
+ link-to-learning
6
+ everyday-life
7
+ sciences-interconnect
8
+ chemist-portrait
9
+ )
10
+
11
+ def initialize(node:, document: nil)
12
+ super(node: node,
13
+ document: document,
14
+ enumerator_class: NoteElementEnumerator,
15
+ short_type: :note)
16
+ end
17
+
18
+ def title
19
+ block_error_if(block_given?)
20
+ first("[data-type='title']")
21
+ end
22
+
23
+ def indicates_autogenerated_title?
24
+ translation_key_in(TITLE_TRANSLATION_KEYS).present?
25
+ end
26
+
27
+ def autogenerated_title
28
+ translation_key = translation_key_in(TITLE_TRANSLATION_KEYS)
29
+ I18n.t(:"notes.#{document.short_name}.#{translation_key}", default: :"notes.#{translation_key}")
30
+ end
31
+
32
+ def translation_key_in(possible_translation_keys)
33
+ keys = possible_translation_keys & classes
34
+ raise("too many translation keys: #{keys.join(', ')}") if keys.many?
35
+ keys.first
36
+ end
37
+
38
+ def self.is_the_element_class_for?(node)
39
+ node['data-type'] == "note"
40
+ end
41
+
42
+ end
43
+ end
@@ -0,0 +1,13 @@
1
+ module Kitchen
2
+ class NoteElementEnumerator < ElementEnumeratorBase
3
+
4
+ def self.factory
5
+ ElementEnumeratorFactory.new(
6
+ default_css_or_xpath: "div[data-type='note']", # TODO get from config?
7
+ sub_element_class: NoteElement,
8
+ enumerator_class: self
9
+ )
10
+ end
11
+
12
+ end
13
+ end
@@ -0,0 +1,61 @@
1
+ module Kitchen
2
+ class Oven
3
+
4
+ def self.bake(input_file:,
5
+ config_file: nil,
6
+ recipes:,
7
+ output_file:)
8
+
9
+ profile = BakeProfile.new
10
+ profile.started!
11
+
12
+ nokogiri_doc = File.open(input_file) do |f|
13
+ profile.opened!
14
+ Nokogiri::XML(f).tap { profile.parsed! }
15
+ end
16
+
17
+ config = config_file.nil? ? nil : Config.new_from_file(File.open(config_file))
18
+
19
+ doc = Kitchen::Document.new(
20
+ nokogiri_document: nokogiri_doc,
21
+ config: config
22
+ )
23
+
24
+ [recipes].flatten.each do |recipe|
25
+ recipe.document = doc
26
+ recipe.bake
27
+ end
28
+ profile.baked!
29
+
30
+ File.open(output_file, "w") do |f|
31
+ f.write doc.to_xhtml(indent:2)
32
+ end
33
+ profile.written!
34
+
35
+ profile
36
+ end
37
+
38
+ class BakeProfile
39
+ def started!; @started_at = Time.now; end
40
+ def opened!; @opened_at = Time.now; end
41
+ def parsed!; @parsed_at = Time.now; end
42
+ def baked!; @baked_at = Time.now; end
43
+ def written!; @written_at = Time.now; end
44
+
45
+ def open_seconds; @opened_at - @started_at; end
46
+ def parse_seconds; @parsed_at - @opened_at; end
47
+ def bake_seconds; @baked_at - @parsed_at; end
48
+ def write_seconds; @written_at - @baked_at; end
49
+
50
+ def to_s
51
+ <<~STRING
52
+ Open: #{open_seconds} s
53
+ Parse: #{parse_seconds} s
54
+ Bake: #{bake_seconds} s
55
+ Write: #{write_seconds} s
56
+ STRING
57
+ end
58
+ end
59
+
60
+ end
61
+ end