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,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 5a4f70bbb3dd410d515f7501ed2b95635d4ca8b8369fe1b8ed5eda66e5c64c07
4
+ data.tar.gz: ec5151233398b0136e0494efe4b4637876ab3e318f6606ea66fe8a750c46cbf0
5
+ SHA512:
6
+ metadata.gz: fce19e98ac530ffd809acb5ff968dbd9b786b11eaa217faec40f7e0af7ad5e2a2f70fae5aed9eb0d2b71bce7456a4d4b9e3842d2d484b17f7da529ed02af9d3a
7
+ data.tar.gz: 36d66b6aa6cc8585726007f0edeff8eae49be3222dd33b367adb1b0b6554c0bfbc4f39537ad1e95fbf9bc0cf15a91049347c4940644dcf47348eb932bf42f623
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "Kitchen Dev",
3
+ "dockerComposeFile": ["../docker-compose.yml"],
4
+ "service": "app",
5
+ "workspaceFolder": "/code",
6
+ "shutdownAction": "stopCompose",
7
+ "extensions": [
8
+ "castwide.solargraph",
9
+ ],
10
+ "settings": {
11
+ "[ruby]": {
12
+ "editor.insertSpaces": true,
13
+ "editor.tabSize": 2
14
+ },
15
+ "solargraph.commandPath": "/usr/local/bundle/bin/solargraph",
16
+ "solargraph.bundlerPath": "/usr/local/bin/bundle",
17
+ },
18
+ "postCreateCommand": "bundle install"
19
+ }
@@ -0,0 +1,36 @@
1
+ name: Tests
2
+
3
+ on:
4
+ pull_request:
5
+ push:
6
+ branches:
7
+ - main
8
+ schedule:
9
+ - cron: '0 0 * * 0' # weekly
10
+
11
+ jobs:
12
+ tests:
13
+ timeout-minutes: 10
14
+ runs-on: ubuntu-18.04
15
+
16
+ steps:
17
+ - uses: actions/checkout@v2
18
+ - uses: actions/setup-ruby@v1
19
+ with:
20
+ ruby-version: 2.6
21
+ - uses: actions/cache@v2
22
+ with:
23
+ path: vendor/bundle
24
+ key: ${{ runner.os }}-gems-${{ hashFiles('**/Gemfile.lock') }}
25
+ restore-keys: |
26
+ ${{ runner.os }}-gems-
27
+ - name: Test
28
+ env:
29
+ ENABLE_CODECOV: 1
30
+ run: |
31
+ gem install bundler --force --no-document --version 2.1.4
32
+ bundle config deployment true
33
+ bundle config path vendor/bundle
34
+ bundle config jobs 2
35
+ bundle install
36
+ bundle exec rake
@@ -0,0 +1,20 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ .DS_Store
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+
11
+ # rspec failure tracking
12
+ .rspec_status
13
+ .irb_history
14
+ .byebug_history
15
+ bin/scratch
16
+ tutorial/outputs/*.html
17
+
18
+ tutorials/**/actual_baked.*html
19
+ tutorials/**/my_recipe.rb
20
+
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
@@ -0,0 +1,15 @@
1
+ ---
2
+ include:
3
+ - "**/*.rb"
4
+ exclude:
5
+ - spec/**/*
6
+ - test/**/*
7
+ - vendor/**/*
8
+ - ".bundle/**/*"
9
+ require: []
10
+ domains: []
11
+ reporters:
12
+ - rubocop
13
+ - require_not_found
14
+ require_paths: []
15
+ max_files: 5000
@@ -0,0 +1,11 @@
1
+ # Changelog
2
+ All notable changes to this project will be documented in this file.
3
+
4
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
5
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
+
7
+ ## [Unreleased]
8
+
9
+ ## [1.0.0] - 2020-10-03
10
+
11
+ First official version.
@@ -0,0 +1,74 @@
1
+ # Contributor Covenant Code of Conduct
2
+
3
+ ## Our Pledge
4
+
5
+ In the interest of fostering an open and welcoming environment, we as
6
+ contributors and maintainers pledge to making participation in our project and
7
+ our community a harassment-free experience for everyone, regardless of age, body
8
+ size, disability, ethnicity, gender identity and expression, level of experience,
9
+ nationality, personal appearance, race, religion, or sexual identity and
10
+ orientation.
11
+
12
+ ## Our Standards
13
+
14
+ Examples of behavior that contributes to creating a positive environment
15
+ include:
16
+
17
+ * Using welcoming and inclusive language
18
+ * Being respectful of differing viewpoints and experiences
19
+ * Gracefully accepting constructive criticism
20
+ * Focusing on what is best for the community
21
+ * Showing empathy towards other community members
22
+
23
+ Examples of unacceptable behavior by participants include:
24
+
25
+ * The use of sexualized language or imagery and unwelcome sexual attention or
26
+ advances
27
+ * Trolling, insulting/derogatory comments, and personal or political attacks
28
+ * Public or private harassment
29
+ * Publishing others' private information, such as a physical or electronic
30
+ address, without explicit permission
31
+ * Other conduct which could reasonably be considered inappropriate in a
32
+ professional setting
33
+
34
+ ## Our Responsibilities
35
+
36
+ Project maintainers are responsible for clarifying the standards of acceptable
37
+ behavior and are expected to take appropriate and fair corrective action in
38
+ response to any instances of unacceptable behavior.
39
+
40
+ Project maintainers have the right and responsibility to remove, edit, or
41
+ reject comments, commits, code, wiki edits, issues, and other contributions
42
+ that are not aligned to this Code of Conduct, or to ban temporarily or
43
+ permanently any contributor for other behaviors that they deem inappropriate,
44
+ threatening, offensive, or harmful.
45
+
46
+ ## Scope
47
+
48
+ This Code of Conduct applies both within project spaces and in public spaces
49
+ when an individual is representing the project or its community. Examples of
50
+ representing a project or community include using an official project e-mail
51
+ address, posting via an official social media account, or acting as an appointed
52
+ representative at an online or offline event. Representation of a project may be
53
+ further defined and clarified by project maintainers.
54
+
55
+ ## Enforcement
56
+
57
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be
58
+ reported by contacting the project team at jps@rice.edu. All
59
+ complaints will be reviewed and investigated and will result in a response that
60
+ is deemed necessary and appropriate to the circumstances. The project team is
61
+ obligated to maintain confidentiality with regard to the reporter of an incident.
62
+ Further details of specific enforcement policies may be posted separately.
63
+
64
+ Project maintainers who do not follow or enforce the Code of Conduct in good
65
+ faith may face temporary or permanent repercussions as determined by other
66
+ members of the project's leadership.
67
+
68
+ ## Attribution
69
+
70
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71
+ available at [https://contributor-covenant.org/version/1/4][version]
72
+
73
+ [homepage]: https://contributor-covenant.org
74
+ [version]: https://contributor-covenant.org/version/1/4/
@@ -0,0 +1,19 @@
1
+ FROM ruby:2.6-slim
2
+
3
+ RUN apt-get update && \
4
+ apt-get install -y --no-install-recommends \
5
+ git \
6
+ build-essential \
7
+ && rm -rf /var/lib/apt/lists/*
8
+
9
+ WORKDIR /code
10
+ COPY . /code/
11
+
12
+ RUN gem install bundler
13
+ RUN bundle install
14
+
15
+ # Install other things that make development good
16
+ RUN gem install solargraph
17
+ RUN bundle exec yard gems
18
+
19
+ ENTRYPOINT ["/code/docker/entrypoint"]
data/Gemfile ADDED
@@ -0,0 +1,9 @@
1
+ source "https://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in kitchen.gemspec
4
+ gemspec
5
+
6
+ gem "rake", "~> 12.0"
7
+ gem "rspec", "~> 3.0"
8
+
9
+ gem 'codecov', require: false
@@ -0,0 +1,74 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ openstax_kitchen (1.0.0)
5
+ activesupport
6
+ i18n
7
+ nokogiri
8
+ rainbow
9
+
10
+ GEM
11
+ remote: https://rubygems.org/
12
+ specs:
13
+ activesupport (6.0.3.3)
14
+ concurrent-ruby (~> 1.0, >= 1.0.2)
15
+ i18n (>= 0.7, < 2)
16
+ minitest (~> 5.1)
17
+ tzinfo (~> 1.1)
18
+ zeitwerk (~> 2.2, >= 2.2.2)
19
+ byebug (11.1.2)
20
+ codecov (0.2.13)
21
+ simplecov (~> 0.18.0)
22
+ concurrent-ruby (1.1.7)
23
+ diff-lcs (1.3)
24
+ docile (1.3.2)
25
+ i18n (1.8.5)
26
+ concurrent-ruby (~> 1.0)
27
+ mini_portile2 (2.4.0)
28
+ minitest (5.14.2)
29
+ nokogiri (1.10.9)
30
+ mini_portile2 (~> 2.4.0)
31
+ nokogiri-diff (0.2.0)
32
+ nokogiri (~> 1.5)
33
+ tdiff (~> 0.3, >= 0.3.2)
34
+ rainbow (3.0.0)
35
+ rake (12.3.3)
36
+ rspec (3.9.0)
37
+ rspec-core (~> 3.9.0)
38
+ rspec-expectations (~> 3.9.0)
39
+ rspec-mocks (~> 3.9.0)
40
+ rspec-core (3.9.1)
41
+ rspec-support (~> 3.9.1)
42
+ rspec-expectations (3.9.1)
43
+ diff-lcs (>= 1.2.0, < 2.0)
44
+ rspec-support (~> 3.9.0)
45
+ rspec-mocks (3.9.1)
46
+ diff-lcs (>= 1.2.0, < 2.0)
47
+ rspec-support (~> 3.9.0)
48
+ rspec-support (3.9.2)
49
+ simplecov (0.18.5)
50
+ docile (~> 1.1)
51
+ simplecov-html (~> 0.11)
52
+ simplecov-html (0.12.3)
53
+ tdiff (0.3.4)
54
+ thread_safe (0.3.6)
55
+ tzinfo (1.2.7)
56
+ thread_safe (~> 0.1)
57
+ yard (0.9.24)
58
+ zeitwerk (2.4.0)
59
+
60
+ PLATFORMS
61
+ ruby
62
+
63
+ DEPENDENCIES
64
+ byebug
65
+ codecov
66
+ nokogiri-diff
67
+ openstax_kitchen!
68
+ rainbow
69
+ rake (~> 12.0)
70
+ rspec (~> 3.0)
71
+ yard
72
+
73
+ BUNDLED WITH
74
+ 2.1.4
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2020 Rice University
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
@@ -0,0 +1,674 @@
1
+ # Kitchen
2
+
3
+ [![Tests](https://github.com/openstax/kitchen/workflows/Tests/badge.svg)](https://github.com/openstax/kitchen/actions?query=workflow:Tests)
4
+ [![Coverage Status](https://img.shields.io/codecov/c/github/openstax/kitchen.svg)](https://codecov.io/gh/openstax/kitchen)
5
+
6
+ Kitchen lets you modify the structure and content of XML files. You create a `Recipe` with instructions and `bake` it in the `Oven`
7
+
8
+ ## Installation
9
+
10
+ Add this line to your application's Gemfile:
11
+
12
+ ```ruby
13
+ gem 'kitchen'
14
+ ```
15
+
16
+ And then execute:
17
+
18
+ $ bundle install
19
+
20
+ Or install it yourself as:
21
+
22
+ $ gem install kitchen
23
+
24
+ ## Two Ways to Use Kitchen
25
+
26
+ There are two ways to use Kitchen: the "generic" way and the "book" way. The generic way provides mechanisms for traversing and modifying an XML document. The book way extends the generic way by adding mechanisms that are specific to the book content XML produced at OpenStax (e.g. the book way knows about chapters and pages, figures and terms, etc, whereas the generic way does not have this knowledge).
27
+
28
+ We'll first talk about the generic way since those tools are also available in the book way.
29
+
30
+ ## Generic Usage
31
+
32
+ Kitchen lets you modify the structure and content of XML files. You create a `Recipe` and `bake` it in the `Oven`:
33
+
34
+ ```ruby
35
+ require "kitchen"
36
+
37
+ recipe = Kitchen::Recipe.new do |document|
38
+ document.search("div.section").each do |element|
39
+ element.name = "section"
40
+ element.remove_class("section")
41
+ end
42
+ end
43
+
44
+ Kitchen::Oven.bake(
45
+ input_file: "some_file.xhtml",
46
+ recipes: recipe,
47
+ output_file: "some_other_file.xhtml"
48
+ )
49
+ ```
50
+
51
+ The above example changes all `<div class="section">` tags to `<section>`.
52
+
53
+ The `document` above is a `Kitchen::Document` and the `element` is a `Kitchen::Element`. Both have methods for reading and manipulating the XML. You can of course name the block argument whatever you want (see examples below).
54
+
55
+ ### The `search` method and enumerators
56
+
57
+ `search` takes one or more CSS and XPath selectors and returns an enumerator that iterates over the matching elements inside the document or element that `search` is called on.
58
+
59
+ The enumerator that is returned is an `ElementEnumerator` which is a subclass of Ruby's [`Enumerator`](https://ruby-doc.org/core-2.6/Enumerator.html). Enumerators are also [`Enumerable`](https://ruby-doc.org/core-2.6/Enumerable.html) which gives you a bunch of methods you can call on enumerators like:
60
+
61
+ * `count` - get the number of matching elements
62
+ * `map` - form a new array using the matching elements
63
+ * `each` - do something with each matching element
64
+ * `first` - return the first matching element
65
+ * [etc etc](https://ruby-doc.org/core-2.6/Enumerable.html)
66
+
67
+ Here's an example calling `search` on a document and then calling `each` on its result:
68
+
69
+ ```ruby
70
+ doc.search("div.example").each do |div| # find all "div.example" elements in the document
71
+ div.add_class("foo") # add a class to each of those elements
72
+ div.search("p").each do |p| # find all "p" elements inside the "div.example" elements
73
+ p.name = "div" # change them to "div" tags
74
+ end
75
+ end
76
+ ```
77
+
78
+ ### Clipboards, cut, copy, and paste
79
+
80
+ When baking our content, we often want to move content around or make copies of content to reuse elsewhere in the document. Kitchen provides clipboard functionality to help with this.
81
+
82
+ Every document holds a set of named clipboards. You can `cut` and `copy` to these named clipboards:
83
+
84
+ ```ruby
85
+ doc.search("div.example").each do |div|
86
+ div.cut(to: :my_special_clipboard)
87
+ end
88
+ ```
89
+
90
+ ```ruby
91
+ doc.first("p").copy to: :foo
92
+ ```
93
+
94
+ And then in some code where you are building up a string of HTML to insert you can
95
+
96
+ ```ruby
97
+ new_html = doc.clipboard(name: :my_special_clipboard).paste
98
+ ```
99
+
100
+ `cut` puts the element on the clipboard and removes the original from the document. `copy` leaves the element in the document and puts a copy of the element on the clipboard.
101
+
102
+ Instead of using named clipboards, you can also pass any `Clipboard` object to these methods:
103
+
104
+ ```ruby
105
+ my_clipboard = Clipboard.new
106
+ doc.search("div.example").each do |div|
107
+ div.cut(to: my_clipboard)
108
+ end
109
+
110
+ new_html = my_clipboard.paste
111
+ ```
112
+
113
+ This is often the better way to go because if you use the named ones in the document you have to remember to `clear` them before you use it to not get stuck with whatever you left there the last time you used it.
114
+
115
+ `ElementEnumerator` also provides extra clipboard-related methods to make your life easier. Instead of writing
116
+
117
+ ```ruby
118
+ doc.search("div.example").each do |div|
119
+ div.cut(to: :my_special_clipboard)
120
+ end
121
+ ```
122
+
123
+ You can say
124
+
125
+ ```ruby
126
+ doc.search("div.example").cut(to: :my_special_clipboard)
127
+ ```
128
+
129
+ The same applies to `copy` and these methods also work with passed-in `Clipboard` objects. If you don't pass in a clipboard name or a `Clipboard` object, these methods return a new `Clipboard` containing the cut or copied content:
130
+
131
+ ```ruby
132
+ a_new_clipboard = doc.search("div.example").cut
133
+ ```
134
+
135
+ Clipboards are also `Enumerable` so you can call the enumerable methods (`count`, `each`, etc) on them.
136
+
137
+ When elements that were copied are pasted (or when elements that were cut are pasted more than once), Kitchen will update the IDs of pasted elements to keep them unique. Kitchen adds `_copy_1`, `_copy_2`, etc to IDs to make this happen. The `_copy_` prefix is configurable (or at least close to it).
138
+
139
+ If you want to remove an element (or all elements matched by an enumerator) but NOT put those elements on a clipboard, you can use the `trash` method:
140
+
141
+ ```ruby
142
+ some_div.trash
143
+ doc.search(".not_needed").trash
144
+ ```
145
+
146
+ ### Pantries
147
+
148
+ A document also gives you access to named pantries. A pantry is a place to store items that you can label for later retrieval by that label.
149
+
150
+ ```ruby
151
+ doc.pantry.store "some text", label: "some label"
152
+ doc.pantry.get("some label") # => "some text"
153
+ ```
154
+
155
+ The above uses the `:default` pantry. You can also use named pantries:
156
+
157
+ ```ruby
158
+ doc.pantry(name: :figure_titles).store "Moon landing", label: "id42"
159
+ ```
160
+
161
+ ### Counters
162
+
163
+ Oftentimes we need to count things in a document, for example to number chapters and pages. A document provides named counters:
164
+
165
+ ```ruby
166
+ doc.counter(:chapter).increment
167
+ doc.counter(:chapter).get
168
+ doc.counter(:chapter).reset
169
+ ```
170
+
171
+ See book-oriented usage for a better way of counting elements.
172
+
173
+ ### Adding content
174
+
175
+ In kitchen we can prepend or append element children or siblings:
176
+
177
+ ```ruby
178
+ # <div><span>Hi</span></div> => <div><span><br/>Hi</span></div>
179
+ doc.search("span").first.prepend(child: "<br/>")
180
+ ```
181
+
182
+ ```ruby
183
+ # <div><span>Hi</span></div> => <div><div></div><span>Hi</span></div>
184
+ doc.search("span").first.prepend(sibling: "<div>")
185
+ ```
186
+
187
+ ```ruby
188
+ # <div><span>Hi</span></div> => <div><span>Hi<br/></span></div>
189
+ doc.search("span").first.append(child: "<br/>")
190
+ ```
191
+
192
+ ```ruby
193
+ # <div><span>Hi</span></div> => <div><span>Hi</span><p/></div>
194
+ doc.search("span").first.append(sibling: "<p/>")
195
+ ```
196
+
197
+ We can also replace all children:
198
+
199
+ ```ruby
200
+ # <div><span>Hi</span></div> => <div><span><p>Howdy</p></span></div>
201
+ doc.search("span").first.replace_children with: "<p>Howdy</p>"
202
+ ```
203
+
204
+ And we can wrap an element with another element:
205
+
206
+ ```ruby
207
+ # <div><span>Hi</span></div> => <div><span class="other"><span>Hi</span></span></div>
208
+ doc.search("span").first.wrap("<span class='other'>")
209
+ ```
210
+
211
+ ### Checking for elements
212
+
213
+ You can see if an element contains an element matching a selector:
214
+
215
+ ```ruby
216
+ my_element.contains?(".title") #=> true or false
217
+ ```
218
+
219
+ ### Miscellaneous
220
+
221
+ * `ElementEnumerator` also provides a `first!` method that is like the standard `first` except it raises an error if there is no matching first element to return.
222
+
223
+ ### Using `raw` to get at underlying Nokogiri objects.
224
+
225
+ Kitchen uses the Nokogiri gem to parse and manipulate XML documents. `Document` objects wraps a `Nokogiri::XML::Document` object, and `Element` objects wrap a `Nokogiri::XML::Node` object. If you want to do something wild and crazy you can access these underlying objects using the `raw` method on `Document` and `Element`. Note that many of the methods on the underlying objects are exposed on the Kitchen object, e.g. instead of saying `my_element.raw['data-type']` you can say `my_element['data-type']`.
226
+
227
+ ## Book-Oriented Usage
228
+
229
+ All of the above works, but it is generic and we have a specific problem handling books that use a specific schema. To that end, Kitchen also includes a `BookDocument` to use in place of `Document` as well as elements and enumerators specific to this schema, e.g. `BookElement`, `ChapterElement`, `PageElement`, `TableElement`, `FigureElement`, `NoteElement`, `ExampleElement`. `BookDocument` has a method called `book` that returns a `BookElement` that wraps the top-level `html` element. All of these elements have methods on them for searching for other of these specific elements, so that instead of
230
+
231
+ ```ruby
232
+ doc.book.search("[data-type='page']")
233
+ ```
234
+
235
+ we can say
236
+
237
+ ```ruby
238
+ doc.book.pages
239
+ ```
240
+
241
+ In the generic usage, you can chain `search` methods:
242
+
243
+ ```ruby
244
+ doc.book.search("[data-type='page']").search("figure")
245
+ ```
246
+
247
+ will find all figure elements inside pages inside `my_chapter`.
248
+
249
+ In the book-oriented usage, you can chain specific search methods to achieve the same effect:
250
+
251
+ ```ruby
252
+ doc.book.pages.figures
253
+ ```
254
+
255
+ This chaining of enumerators gives other benefits. The above search for figures will yield figures that know the page they were found in as well as their numerical position within that page. So you could do something like this:
256
+
257
+ ```ruby
258
+ doc.book.chapters.pages.figures.each do |figure|
259
+ figure.prepend(child:
260
+ "<span class='os-number'>Figure #{figure.count_in(:chapter)}.#{figure.count_in(:page)}</span>" \
261
+ "<span class='os-title'>A figure in chapter #{figure.ancestor(:chapter).title}</span>"
262
+ )
263
+ end
264
+ ```
265
+
266
+ This finds all figures that are in pages that are in chapters in the book. The `count_in` methods on the figure give the number position of the figure element within the chapter or page so we can form a figure number like "2.13". And as seen here, chapter elements (instances of `ChapterElement`) have a `title` method that returns the title text for the chapter. Figures have a `caption` element, etc.
267
+
268
+ The CSS for these specific search methods is hidden away so you don't have to deal with it. But if you want to customize that CSS you can. You can pass an overriding CSS selector to these methods, and if you use the `$` character in that argument the search method will replace it with the normal CSS selector, e.g. if you wanted to get rid of all of the table elements that have the "unnumbered" class you could say:
269
+
270
+ ```ruby
271
+ doc.book.tables("$.unnumbered").cut
272
+ ```
273
+
274
+ ## Directions
275
+
276
+ All of the above talks about the how to search through the XML file and perform basic operations on that file. Our recipes will be combinations of all of the above: search for elements; cut, copy and paste them; count them; rework them; etc.
277
+
278
+ One recipe for processing a book probably does 10-30 different kinds of operations: format and number tables, same for figures and examples, number and organize exercises and their solutions in different parts of the book, build chapter glossaries, build an index, build a table of contents, etc, etc.
279
+
280
+ We're not going to want to write out all of those steps in every receipe. Instead it'd be a better idea to write out each step in its own little piece of code. With the steps isolated from each other we'll be dealing with less code all at once and it'll be much easier to write tests to exercise that code.
281
+
282
+ In Kitchen, we've started the process of writing out these steps and we've put them in a `directions` folder (which is also a `Directions` module). E.g. `Kitchen::Directions::BakeChapterSummary` modifies a provided chapter to have a chapter summary at the end.
283
+
284
+ It is probably true that the `BakeChapterSummary` code will work for some number of books, but other books might have different requirements. As such we can expect that there will be different variants of the chapter summary baking step. To anticipate this, our first implementation of this step lives in a method named `v1` (so to run it you call `BakeChapterSummary.v1(chapter: some_chapter)`). Later if there's a tweak needed that can't fit into v1's approach, we can make a `v2` method that could live in its own file. This may or may not be the right approach to handle this kind of code variation, but it is at least a place to start.
285
+
286
+ ### Internationalization (I18n)
287
+
288
+ Recognizing that our books will be translated into multiple languages, Kitchen has support for internationalization (I18n). There's a spot for translation files in the `locales` directory, in which there is currently one `en.yml` translation file for English. Within our directions code you'll see uses of it like here to title an Example:
289
+
290
+ ```erb
291
+ <span class="os-title-label">#{I18n.t(:example)} </span>
292
+ ```
293
+
294
+ ### Building HTML strings
295
+
296
+ There are a number of valid ways of building up HTML strings to insert into documents.
297
+
298
+ Maybe you have a tiny bit of HTML to add and you can use vanilla Ruby strings:
299
+
300
+ ```ruby
301
+ some_element.append(child: "<br/>")
302
+ ```
303
+
304
+ You can continue doing this with multiline strings but it gets to be a pain -- you have to add your own newlines (`\n`) and line continuation symbols (`\`):
305
+
306
+ ```ruby
307
+ some_element.append(child: "first line\n" \
308
+ "second line")
309
+ ```
310
+
311
+ Ruby has a much better way of handling multiline strings: [heredocs](https://ruby-doc.org/core-2.5.0/doc/syntax/literals_rdoc.html#label-Here+Documents). The best of these is the "squiggly" heredoc, which captures string content between `<<~SOME_ARBITRARY_TEXT` and `SOME_ARBITRARY_TEXT`:
312
+
313
+ ```ruby
314
+ some_element.append(sibling: <<~HTML
315
+ <div class="os-caption-container">
316
+ <span class="os-caption">Awesomeness</span>
317
+ </div>
318
+ HTML
319
+ )
320
+ ```
321
+
322
+ The squiggly heredoc removes the shortest leading indentation from each line. It lets you use single and double quotes inside the string without escaping them. And at least in certain editors when you use `HTML` as your "some arbitrary text", you'll get HTML syntax highlighting. You can also do interpolate variables into the string using `#{some_variable}` The above example is equivalent to this Ruby string written in the earlier approach:
323
+
324
+ ```ruby
325
+ "<div class=\"os-caption-container\">\n" \
326
+ " <span class=\"os-caption\">Awesomeness</span>\n" \
327
+ "</div>\n"
328
+ ```
329
+
330
+ The big downside to all of these approaches is that for more complicated strings, we often need to use some Ruby logic to build up different parts of the string, and the techniques above don't allow for that.
331
+
332
+ Let's invent an example of needing to build some HTML that had a listing of all chapter titles in a bulleted list and then their page titles within them in a nested bulleted list (kind of like a table of contents). This is an example of what we'd be shooting for:
333
+
334
+ ```html
335
+ <ul>
336
+ <li>
337
+ <span>Chapter 1 Title</span>
338
+ <ul>
339
+ <li><span>Page 1.1 Title</span></li>
340
+ <li><span>Page 1.2 Title</span></li>
341
+ </ul>
342
+ </li>
343
+ <li>
344
+ ... etc etc
345
+ </li>
346
+ </ul>
347
+ ```
348
+
349
+ Here's one way we could build up this string using squiggly heredocs:
350
+
351
+ ```ruby
352
+ class SomethingThatBakes
353
+ def bake(doc)
354
+ @book = doc.book
355
+
356
+ chapter_bullets_array = @book.chapters.map do |chapter|
357
+ page_bullets_array = chapter.pages.map do
358
+ <<~HTML
359
+ <li><span>#{page.title.text}</span></li>
360
+ HTML
361
+ end
362
+
363
+ <<~HTML
364
+ <li>
365
+ <span>#{chapter.title.text}</span>
366
+ <ul>
367
+ #{page_bullets_array.join("\n")}
368
+ </ul>
369
+ </li>
370
+ HTML
371
+ end.join("\n")
372
+
373
+ final_string = <<~HTML
374
+ <ul>#{chapter_bullets_array.join("\n")}</ul>
375
+ HTML
376
+
377
+ # do something with that final_string
378
+ end
379
+ end
380
+ ```
381
+
382
+ The above works but it is a little fragmented to read. We have to build up parts of the bulleted lists in arrays, then join them together with newlines and embed them in other strings (some of which are also collected in an array and then later substituted and joined).
383
+
384
+ For these more complex strings we have another option: [ERB (Embedded RuBy)](https://www.stuartellis.name/articles/erb/). ERB is part of standard Ruby and had its heyday when Rails came out in the 2000s. ERB lets us make a separate HTML file with Ruby sprinkled within it. Let's call this file `blah.html.erb`:
385
+
386
+ ```erb
387
+ <ul>
388
+ <% @book.chapters.each do |chapter| %>
389
+ <li>
390
+ <span><%= chapter.title.text</span>
391
+ <ul>
392
+ <% chapter.pages.each do |page| %>
393
+ <li><span><%= page.title.text %></span></li>
394
+ <% end %>
395
+ </ul>
396
+ </li>
397
+ <% end %>
398
+ </ul>
399
+ ```
400
+
401
+ In our Ruby class doing the generation, we add a `renderable` statement at the top code we can then say:
402
+
403
+ ```ruby
404
+ class SomethingThatBakes
405
+ renderable
406
+
407
+ def bake(doc)
408
+ final_string = render(file: 'blah.html.erb')
409
+
410
+ # do something with that final_string
411
+ end
412
+ end
413
+ ```
414
+
415
+ This ERB approach is a lot easier to read -- you can see the nesting structure directly in the template file. The Ruby code in the ERB template will have access to any instance variable in the code that called it, i.e. the variables that start with `@`.
416
+
417
+ The `render` method takes a `file` argument that is a string file path. If the path is relative, it is relative to the directory in which the `render` call is made.
418
+
419
+ If you want to make relative file paths be relative to a different directory, you can pass a directory string to the `renderable` statement: `renderable dir: '/Some/other/directory'`.
420
+
421
+ Again, all these techniques work and there are times to use them all.
422
+
423
+ ## One-file scripts
424
+
425
+ Want to make a one-file script to do some baking? Use the "inline" form of bundler:
426
+
427
+ ```ruby
428
+ #!/usr/bin/env ruby
429
+
430
+ require "bundler/inline"
431
+
432
+ gemfile do
433
+ gem 'kitchen', git: 'https://github.com/openstax/kitchen.git', ref: 'some_sha_here'
434
+ end
435
+
436
+ require "kitchen"
437
+
438
+ recipe = Kitchen::Recipe.new do |doc|
439
+ # ... recipe steps here
440
+ end
441
+
442
+ Kitchen::Oven.bake(
443
+ input_file: "some_file.xhtml",
444
+ recipes: recipe,
445
+ output_file: "some_other_file.xhtml")
446
+ )
447
+ ```
448
+
449
+ Incidentally, the `bake` method returns timing information, if you `puts` its result you'll see it.
450
+
451
+ ## Recipe (and Gem) Development
452
+
453
+ ### Docker
454
+
455
+ You can use Docker for your development environment.
456
+
457
+ ```bash
458
+ $> docker-compose up -d
459
+ ```
460
+
461
+ to build and run the Docker container, then:
462
+
463
+ ```bash
464
+ $> ./docker/bash
465
+ ```
466
+
467
+ To drop into the container at the command line.
468
+
469
+ ### Non-Docker
470
+
471
+ After checking out the repo, run `bin/setup` to install dependencies. If you want to install this gem onto your local machine, run `bundle exec rake install`.
472
+
473
+ ### Console
474
+
475
+ You can also run `bin/console` for an interactive prompt that will allow you to experiment.
476
+
477
+ ### Tutorials
478
+
479
+ There are some tutorials you can work through in the `tutorials` directory. Each tutorial is in a separated numbered subdirectory, e.g. `tutorials/01`. Each tutorial directory contains a `raw.html` file that is your starting point (along with some instructions in comments at the top), an `expected_baked.html` file that is what you're trying to get to when your recipe is applied to the input file, as well as some number of solution files (don't look at those unless you get stuck!!). To get started, run:
480
+
481
+ ```bash
482
+ $> ./setup_my_recipes
483
+ ```
484
+
485
+ in the `tutorials` directory. That will make a blank `my_recipe.rb` file in each of the numbered tutorial subdirectories. This is where you'll do your work. The first "Hello world!" tutorial ("00") asks you to make a recipe that changes
486
+
487
+ ```html
488
+ <div class="hello">
489
+ <span>Planet?</span>
490
+ </div>
491
+ ```
492
+
493
+ to
494
+
495
+ ```html
496
+ <h1 class="hello">
497
+ <span>World!</span>
498
+ </h1>
499
+ ```
500
+
501
+ There's an included script to check to see if your recipe achieves the desired transformation:
502
+
503
+ ```bash
504
+ $> ./check_it 00
505
+ ```
506
+
507
+ Will check to see if your `tutorials/00/my_recipe.rb` file does what is needed. If it does, you'll see a "way to go" message. If it doesn't, you'll see a diff between the expected output and the actual output. E.g. if you run `./check_it 00` without having done any work yet, you'll see:
508
+
509
+ ```bash
510
+ The actual output does not match the expected output.
511
+ - = actual output
512
+ + = expected output
513
+
514
+ @@ -1,4 +1,4 @@
515
+ -<div class="hello">
516
+ - <span>Planet?</span>
517
+ -</div>
518
+ +<h1 class="hello">
519
+ + <span>World!</span>
520
+ +</h1>
521
+ ```
522
+
523
+ The `check_it` script can also check the solutions. E.g. if you say
524
+
525
+ ```bash
526
+ $> ./check_it 00 solution_1
527
+ ```
528
+
529
+ you'll see
530
+
531
+ ```bash
532
+ The actual output matches the expected output! Way to go!
533
+ ```
534
+
535
+ There is normally more than one way to achieve the desired output, so feel free to diverge from what is shown in the solution files. Note that the `my_recipe.rb` files and all `actual_baked.html` files are ignored by Git.
536
+
537
+ **Important:** If things aren't working in your tutorial work (or actually in any recipe work), use the `debugger`! Just add a `debugger` line anywhere in your code to stop execution there so you can poke around. You can print variables by typing out their name, run methods on objects, say `s` to step into function calls, `n` ("next") to step over function calls, `b 97` to set a new breakpoint at line 97, and `c` to continue to the next debugger statement, breakpoint, or the end of the script.
538
+
539
+ ### Error Messages
540
+
541
+ Kitchen tries to give helpful error messages instead of huge stack traces, e.g.:
542
+
543
+ ```
544
+ The recipe has an error: undefined method `bleach' for main:Object
545
+ at or near the following highlighted line
546
+
547
+ -----+ ./my_work/test.rb -----
548
+ 17|
549
+ 18| doc.chapters.each do |chapter|
550
+ 19| chapter.bleach("div.exercise") do |elem|
551
+ 20| elem.first("h3").trash
552
+ 21| elem.cut to: :review_questions
553
+
554
+ Encountered on line 64 in the input document on element:
555
+ <div data-type="chapter">...</div>
556
+ ```
557
+
558
+ If you'd still like the huge stack trace, you can set the `VERBOSE` environment variable to anything, e.g.
559
+
560
+ ```bash
561
+ $> VERBOSE=1 ./my_work/test.rb
562
+ ```
563
+
564
+ ### Comparing old recipe output to new
565
+
566
+ When comparing new baking output to legacy baking output, I have found it useful to prepare the files before applying a standard diff:
567
+
568
+ 1. Parse the documents ignoring blank elements. (this one may not be that important actually)
569
+ 2. Traverse the documents, sorting their attributes alphabetically by attribute name.
570
+ 3. Writing the output back out with a standardized indentation scheme.
571
+
572
+ I have some code to do this, I'll try to get it into this repo.
573
+
574
+ ### Releases
575
+
576
+ To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
577
+
578
+ ### Documentation
579
+
580
+ Documentation is handled via YARD. The [Solargraph gem](https://solargraph.org/guides/getting-started) can be used in popular editors for code completion.
581
+
582
+ Run `yard server --reload` to watch for changes in your local codebase everytime you refresh the page.
583
+
584
+ Navidate to `http://localhost:8808/` to view documentation in your browser.
585
+
586
+ ### Specs
587
+
588
+ Run `bundle exec rspec` to run the specs. `rake rspec` probably does the same thing.
589
+
590
+ Spec offer two ways to compare expected XML output to actual output.
591
+
592
+ `match_normalized_html` gets rid of extra blanks, sorts all tag attributes alphabetically by attribute name (e.g. sorts "<a href='blah' class='foo'>" to "<a class='foo' href='blah'>" so that attribute order doesn't impact a match), prints the HTML back out with a standard indent, and then does a normal string diff.
593
+
594
+ ```ruby
595
+ expect(book_1).to match_normalized_html("some string of HTML here")
596
+ ```
597
+
598
+ `match_html_nodes` does a node-by-node diff using the `nokogiri-diff` gem. It gives more specific node diff data but is also not quite as clear.
599
+
600
+ ```ruby
601
+ expect(book_1).to match_html_nodes("some string of HTML here")
602
+ ```
603
+
604
+ ### VSCode
605
+
606
+ 1. Visit `vscode:extension/ms-vscode-remote.remote-containers` in a browser
607
+ 2. It'll open VSCode and bring you to an extension install screen, click "Install"
608
+ 3. Click the remote button now in the bottom left hand corner.
609
+ 4. Click "Remote-Containers: Open Folder in Container"
610
+ 5. Select the cloned kitchen folder.
611
+
612
+ This (assuming you have Docker installed) will launch a docker container for Kitchen, install Ruby and needed libraries, and then let you edit the code running in that container through VSCode. Solargraph will work (code completion and inline documentation).
613
+
614
+ #### Misc References
615
+
616
+ * https://marketplace.visualstudio.com/items?itemName=castwide.solargraph
617
+ * https://github.com/castwide/solargraph/issues/211
618
+ * https://github.com/castwide/vscode-solargraph/issues/7
619
+ * https://solargraph.org/guides/yard
620
+ * https://code.visualstudio.com/docs/remote/containers
621
+ * https://stelligent.com/2020/04/10/development-acceleration-through-vs-code-remote-containers-setting-up-a-foundational-configuration/
622
+ * https://code.visualstudio.com/docs/remote/containers#_attaching-to-running-containers
623
+ * https://code.visualstudio.com/docs/remote/containers#_attached-container-config-reference
624
+
625
+ ## Tutorials
626
+
627
+ * Fix up tutorials and describe how to use them here
628
+
629
+ ## Contributing
630
+
631
+ Bug reports and pull requests are welcome on GitHub at https://github.com/openstax/kitchen. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/openstax/kitchen/blob/master/CODE_OF_CONDUCT.md).
632
+
633
+ ## Tools
634
+
635
+ Helpful scripts in the `bin` directory:
636
+
637
+ * `normalize` - Normalizes content files to make it easier to compare them. E.g. if you want to compare kitchen baked output to cnx-recipes baked output, you should normalize the files first. `normalize somefile.xhtml` produces `somefile.normalized.xhtml` which has its attributes sorted by attribute name, copied element ID numbers masked (because they change based on order of operations in recipes, but their values are not important), and some errors in legacy baked files removed (e.g. unnumbered tables get a `summary` attribute with a bogus number).
638
+
639
+ ## License
640
+
641
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
642
+
643
+ ## TODO
644
+
645
+ * Specs galore :-)
646
+ * `$` search path substitution (making sure not to mess up xpath)
647
+ * Think up and handle a bunch more recipe errors, test they all raise some kind of `RecipeError`.
648
+ * Encapsulate numbering schemes (e.g. chapter pages are "5.2", appendix pages are "D7") and maybe set on book document? Right now we are doing inline things like `*('A'..'Z')][page.count_in(:book)-1]}#{table.count_in(:page)` which is ugly.
649
+ * Add rubocop for linting.
650
+ * Control I18n language in Oven.
651
+ * Get doc normalization scripts into this repo from testkitchen (for comparing two large baked outputs).
652
+ * Change from ElementBase <- Element to Element <- BasicElement
653
+ * README: element_children, .only, selectors, config files
654
+ * Use ERB for more readable string building?
655
+
656
+ ## Quirks
657
+
658
+ When Kitchen writes out HTML containing unicode characters it uses the hexadecimal form, whereas current CE baking uses the decimal form. I haven't found an internal way to change how Kitchen's underlying library writes these characters, so if you need to do a new-to-old comparison, you can use a few lines of ruby to do a search and replace:
659
+
660
+ ```ruby
661
+ original_output = File.read("kitchen_output.xhtml")
662
+ modified_output = original_output.gsub(/&#x([0-9A-F]+);/){"&##{$1.hex};"}
663
+ File.open("kitchen_output.xhtml", "w") {|file| file.puts modified_output}
664
+ ```
665
+
666
+ If this difference matters (if we need the decimal version), we can do more work to figure out a better implementation.
667
+
668
+ ## Ideas
669
+
670
+ * Use [tmux](https://github.com/tmuxinator/tmuxinator) for real-time evaluation of recipes to see output within one split terminal (source XML in one pane, recipe in middle, output on right).
671
+
672
+ ## Code of Conduct
673
+
674
+ Everyone interacting in the Kitchen project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/openstax/kitchen/blob/master/CODE_OF_CONDUCT.md).