howdoc 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 02cc27d9848b0932cc7ba10c66b8e5682c955071f2cde03f27e6c9061c52d82c
4
- data.tar.gz: 1bed4d6da3860c0ea8ac1f3f680218f24941f45fbb7d8e3b0699085e70588811
3
+ metadata.gz: 72edea92225be55592acd2d56c732d5d82e140193b06dc276b0c691440ff2e3f
4
+ data.tar.gz: 2ec1eea553ec1b5ca6e23f9f320e082367ded34280cf6701e8f284f06f1ef34c
5
5
  SHA512:
6
- metadata.gz: 72e52519fec7d24abdb8765aa146702ef053a627fd87e4b40e1ff77775ba69ed626db5250accbb307719bbfee68a7f12e8d1fac6d966a0ab05df9859a1b38326
7
- data.tar.gz: 70208bdc3b1557985b69bbdceb70ade90a91c39b9063412ed922df7cd1cae26e5629875dd1f66a16277364af39d49eff10ede8df80695c678a1792675f0f0b42
6
+ metadata.gz: 9692dcfc9505fa3cf80eff5ea7a6c2dcb244619a41aa163cbab98c04a16b01bb13d82d646db46b1e98025f386a47594553abfd4b59a6e937040b555a6f9ecadb
7
+ data.tar.gz: 8ee1913418a639198f1134958ce9748ff83a231a5d86b8fb237675ab264d62b50ad9f5cdfd9e8433310b0c6c226bbf35ec6a21d9afd22c5eee0884b6b9042c86
data/CHANGELOG.md CHANGED
@@ -1,5 +1,43 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.3.0
4
+
5
+ Every page now carries the menu, so a guide opened from a link is a whole page
6
+ rather than a dead end, and a documentation set no longer needs a frame to hold
7
+ its navigation together.
8
+
9
+ - A guide leaves an empty `<nav data-howdoc-nav>`, and the index pass fills it
10
+ in once every guide is on disk -- a guide cannot draw a menu of guides when it
11
+ is written, because no other guide exists yet. A template without the
12
+ placeholder is left exactly as it was.
13
+ - The same pass writes previous/next links into `<nav data-howdoc-pager>`.
14
+ - The menu links the same guide in the other languages, matched by identifier,
15
+ since a translated heading gives a translated file name. Each language names
16
+ itself in its own language.
17
+ - `config.group_label` now heads sections in the menu as well as the index.
18
+ - The index carries `howdoc.index.intro` when an application translates it, so
19
+ the landing page can say something of its own without a hand-kept file.
20
+ - Templates read the gem's own words through a lookup an application's I18n
21
+ cascade cannot answer with something unrelated.
22
+
23
+ ## 0.2.0
24
+
25
+ Guides are now written in every language the application publishes, from a
26
+ single test run. Previously a run produced guides in the language the suite
27
+ happened to be running in, and the other languages were silently left behind.
28
+
29
+ - `config.locales` names the languages every guide is written in, not only the
30
+ languages an index is built for.
31
+ - A step is recorded as an action and its values rather than as a finished
32
+ sentence, and narrated once per language when the guide is written.
33
+ - Screenshots are taken once and copied into each language's guide.
34
+ - A guide's own heading and introduction can be translated under
35
+ `howdoc.documents.<permalink>`, since they come from the test rather than
36
+ from the engine.
37
+ - `Howdoc::Narrator.field` defers a field label until the language is known.
38
+ `Howdoc.start(locale:)` still pins a guide to one language.
39
+ - Untranslated keys are reported with the language they are missing from.
40
+
3
41
  ## 0.1.0
4
42
 
5
43
  First release. Extracted from a working in-house documentation helper that had
data/README.md CHANGED
@@ -45,6 +45,7 @@ Howdoc.configure do |config|
45
45
  config.enabled = ENV['DOC'].present? # off unless asked for
46
46
  config.host = 'example.com' # the address a reader should open
47
47
  config.formats = %i[html]
48
+ config.locales = %i[en et] # languages every guide is written in
48
49
 
49
50
  config.register_locale_path 'test/howdoc/locales/*.yml'
50
51
  config.register_template_path 'test/howdoc/templates'
@@ -70,8 +71,7 @@ class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
70
71
  id: metadata[:doc],
71
72
  permalink: metadata[:permalink],
72
73
  intro: metadata[:intro],
73
- title: "How to #{metadata[:description]}?",
74
- locale: I18n.locale
74
+ title: "How to #{metadata[:description]}?"
75
75
  )
76
76
  end
77
77
 
@@ -137,6 +137,52 @@ Three actions come translated even though no wrapper records them, because
137
137
  almost every suite ends up writing a helper that needs one: `menu` (a `path`),
138
138
  `fill_in_editor` (a `value`) and `sign_in_required` (a `text`).
139
139
 
140
+ ## Languages
141
+
142
+ A test runs in one language. Its guide is written in all of them.
143
+
144
+ Name the languages your application publishes and every recorded guide is
145
+ written once per language, from the same run:
146
+
147
+ ```ruby
148
+ config.locales = %i[en et]
149
+ ```
150
+
151
+ ```
152
+ public/docs/en/how_to_add_a_claim.html
153
+ public/docs/et/how_to_add_a_claim.html
154
+ ```
155
+
156
+ Nothing is recorded as a finished sentence, which is what makes this possible:
157
+ a step is kept as the action and the values it happened with, and the sentence
158
+ is composed once per language when the page is written. The screenshot is taken
159
+ once, while the browser is still on the page, and copied into each language's
160
+ guide -- it is a picture of the same application either way.
161
+
162
+ A guide's own heading is the exception, because it comes from the test rather
163
+ than from the engine. Translate `howdoc.documents.<permalink>.title` to give the
164
+ same guide a heading in another language, and `.intro` for its introduction:
165
+
166
+ ```yaml
167
+ et:
168
+ howdoc:
169
+ documents:
170
+ how_to_add_a_claim:
171
+ title: Kuidas lisada nõue?
172
+ ```
173
+
174
+ Left untranslated, every language gets the heading the test gave it. A guide
175
+ that belongs to one language only -- something only that language's users ever
176
+ see -- says so: `Howdoc.start(..., locale: :et)`.
177
+
178
+ A value that reads differently in different languages must not be resolved
179
+ while the test runs, or it freezes into the language the test ran in. Field
180
+ labels are the usual case, and `Howdoc::Narrator.field` defers one:
181
+
182
+ ```ruby
183
+ Howdoc.record(:check, field: Howdoc::Narrator.field(locator))
184
+ ```
185
+
140
186
  ## Wording
141
187
 
142
188
  Every sentence is an I18n key under `howdoc.actions`, so the engine contains no
@@ -161,8 +207,43 @@ own by translating `howdoc.fields.<locator>`, or replace the whole convention:
161
207
  config.field_label { |locator| MyLabels.for(locator) }
162
208
  ```
163
209
 
164
- Any key that is used but not translated is reported at the end of the run rather
165
- than leaving a silent hole in the guide.
210
+ Any key that is used but not translated is reported at the end of the run,
211
+ naming the language it is missing from, rather than leaving a silent hole in the
212
+ guide. A step no language can describe is not recorded at all; a step one
213
+ language cannot describe is left out of that language's guide only.
214
+
215
+ ## Navigation
216
+
217
+ Every page carries the menu, and a guide links the ones before and after it:
218
+
219
+ ```
220
+ public/docs/en/how_to_add_a_claim.html
221
+ ┌────────────┬────────────────────┐
222
+ │ Guides │ 1.3. How to add a … │
223
+ │ EVERYDAY │ 1. Log in … │
224
+ │ 1.1 … │ [screenshot] │
225
+ │ 1.3 ● │ ‹ Previous Next › │
226
+ └────────────┴────────────────────┘
227
+ ```
228
+
229
+ A guide cannot draw that menu when it is written: tests run in forked workers,
230
+ each one writes its own page, and no other guide exists yet. So the guide leaves
231
+ an empty placeholder and the index pass -- which has just read every page back
232
+ -- fills it in:
233
+
234
+ ```haml
235
+ %nav.howdoc-nav{ data: { howdoc_nav: true } } -# the menu
236
+ %nav.howdoc-pager{ data: { howdoc_pager: true } } -# previous / next
237
+ ```
238
+
239
+ Both are optional. A template that carries neither is left exactly as it is.
240
+
241
+ The menu also links the same guide in every other language the application
242
+ publishes. Guides are matched by their identifier rather than their file name,
243
+ because a translated heading gives a translated file name, and each language
244
+ names itself in its own language (`howdoc.languages.<code>`).
245
+
246
+ `config.group_label` heads the sections, in the menu and in the index alike.
166
247
 
167
248
  ## Templates
168
249
 
@@ -19,6 +19,11 @@ en:
19
19
  visit: Open <strong>%{url}</strong> in your browser.
20
20
  document:
21
21
  follow_steps: 'Follow these steps:'
22
+ languages:
23
+ en: English
24
+ pager:
25
+ next: Next
26
+ previous: Previous
22
27
  index:
23
28
  empty: No guides have been generated yet.
24
29
  title: Guides
@@ -19,6 +19,11 @@ et:
19
19
  visit: Palun sisesta <strong>%{url}</strong> oma brauserisse.
20
20
  document:
21
21
  follow_steps: 'Palun jälgige järgnevaid samme:'
22
+ languages:
23
+ et: Eesti
24
+ pager:
25
+ next: Järgmine
26
+ previous: Eelmine
22
27
  index:
23
28
  empty: Ühtegi juhendit ei ole veel loodud.
24
29
  title: Juhendid
@@ -29,9 +29,8 @@ module Howdoc
29
29
 
30
30
  def fill_in(locator = nil, with:, currently_with: nil, fill_options: {}, **options)
31
31
  doc = Howdoc.extract_options!(options)
32
- label = howdoc_field_label(locator)
33
32
 
34
- narrate(fill_in_action(label), doc, field: label, value: with)
33
+ narrate(fill_in_action(locator), doc, field: howdoc_field_label(locator), value: with)
35
34
 
36
35
  super(locator, with:, currently_with:, fill_options:, **options)
37
36
  end
@@ -114,16 +113,18 @@ module Howdoc
114
113
 
115
114
  private
116
115
 
116
+ # Not the label itself: a guide is written in every language the
117
+ # application publishes, and what a reader calls a field is a translation,
118
+ # so the label is settled once per language when the page is written.
117
119
  def howdoc_field_label(locator)
118
- Howdoc::Narrator.field_label(locator, locale: howdoc_locale)
120
+ Howdoc::Narrator.field(locator)
119
121
  end
120
122
 
121
- def howdoc_locale
122
- Howdoc.current&.locale || I18n.locale
123
- end
124
-
125
- def fill_in_action(label)
126
- case label.to_s
123
+ # Which sentence a fill_in gets is decided from the field's own name rather
124
+ # than from its label, because the name is the same in every language and
125
+ # the choice must be too.
126
+ def fill_in_action(locator)
127
+ case Howdoc.config.field_label.call(locator).to_s
127
128
  when 'email' then :fill_in_email
128
129
  when 'password' then :fill_in_password
129
130
  else :fill_in
@@ -22,10 +22,13 @@ module Howdoc
22
22
  # Writers to run for every finished document, in order.
23
23
  attr_accessor :formats
24
24
 
25
- # Locales to build an index for. Left nil, only the locales that actually
26
- # produced a guide get one, which means a run in a single language quietly
27
- # drops the other languages' indexes. Naming them keeps every index in
28
- # place, empty ones included.
25
+ # The languages the application publishes its guides in. A test runs in one
26
+ # language, but every guide it records is written in all of these, so a
27
+ # second language costs a translation file rather than a second test run.
28
+ #
29
+ # These are also the locales an index is built for. Left nil, a guide is
30
+ # written only in the language the suite is running in, and only the
31
+ # locales that actually produced a guide get an index.
29
32
  attr_accessor :locales
30
33
 
31
34
  # Screenshots are taken at this height unless the step asked for a full
@@ -4,6 +4,11 @@ module Howdoc
4
4
  # One guide: the identity a test gave it, plus the steps recorded while that
5
5
  # test ran. A document collects everything in memory and is written out once,
6
6
  # so a writer always sees the whole guide rather than a half-built page.
7
+ #
8
+ # A test runs in one language but a guide is published in all of them, so a
9
+ # document holds nothing that a language would change. Every such thing --
10
+ # the heading, the sentences, the file name -- belongs to an Edition, and a
11
+ # document has one per language.
7
12
  class Document
8
13
  # Letters that carry no accent to strip: they are their own character, so
9
14
  # Unicode decomposition leaves them untouched and a filename filter would
@@ -13,24 +18,44 @@ module Howdoc
13
18
  'đ' => 'd', 'ð' => 'd', 'ł' => 'l', 'þ' => 'th'
14
19
  }.freeze
15
20
 
16
- attr_reader :id, :permalink, :title, :intro, :locale, :steps
21
+ attr_reader :id, :permalink, :steps, :locales
17
22
 
18
- def initialize(id:, title:, locale:, permalink: nil, intro: nil)
23
+ # +title+ and +intro+ are whatever the test knew: a string in the language
24
+ # it was written in, or a hash keyed by locale when the test itself knows
25
+ # more than one. Either way a translation under howdoc.documents wins, so a
26
+ # guide gets a heading in a language no test speaks.
27
+ def initialize(id:, title:, locales:, permalink: nil, intro: nil)
19
28
  @id = id
20
29
  @title = title
21
- @locale = locale.to_sym
22
- @permalink = permalink
23
30
  @intro = intro
31
+ @permalink = permalink
32
+ @locales = Array(locales).map(&:to_sym)
24
33
  @steps = []
25
34
  @counter = 0
26
35
  end
27
36
 
28
- def slug
29
- @slug ||= (permalink || slugify(title)).to_s
37
+ def editions
38
+ @editions ||= locales.map { |locale| Edition.new(self, locale) }
39
+ end
40
+
41
+ # The edition a test is recording into. Screenshots are taken while the
42
+ # browser is still on the page, long before the other editions are written,
43
+ # so they are saved where this one keeps its pictures and copied across when
44
+ # the guides are written.
45
+ def primary
46
+ editions.first
47
+ end
48
+
49
+ def locale
50
+ primary.locale
30
51
  end
31
52
 
32
- def heading
33
- id.nil? ? title : "#{id}. #{title}"
53
+ def title_for(locale)
54
+ Narrator.document_text(:title, key: translation_key, locale:, default: in_locale(@title, locale))
55
+ end
56
+
57
+ def intro_for(locale)
58
+ Narrator.document_text(:intro, key: translation_key, locale:, default: in_locale(@intro, locale))
34
59
  end
35
60
 
36
61
  def new_step(**attributes)
@@ -44,23 +69,14 @@ module Howdoc
44
69
  steps.last
45
70
  end
46
71
 
47
- def dir
48
- File.join(Howdoc.config.root, locale.to_s)
49
- end
50
-
51
- def image_dir
52
- File.join(dir, 'images')
53
- end
54
-
55
- def path(extension)
56
- File.join(dir, "#{slug}.#{extension}")
57
- end
58
-
59
- def image_filename(number)
60
- "#{slug}_#{number}.png"
61
- end
62
-
63
- private
72
+ # What the recording edition answers, asked of the document itself, because
73
+ # that is what a recorder has in its hand.
74
+ def slug = primary.slug
75
+ def heading = primary.heading
76
+ def dir = primary.dir
77
+ def image_dir = primary.image_dir
78
+ def path(extension) = primary.path(extension)
79
+ def image_filename(number) = primary.image_filename(number)
64
80
 
65
81
  # Deliberately not ActiveSupport's parameterize: the engine has no business
66
82
  # dragging Rails into a project that only wanted a documentation generator.
@@ -68,7 +84,7 @@ module Howdoc
68
84
  # Accents are decomposed and their marks dropped rather than deleted whole,
69
85
  # so an Estonian title turns into "kuidas_lisada_noue" instead of the
70
86
  # unreadable "kuidas_lisada_n_ue" a plain ASCII filter would leave behind.
71
- def slugify(string)
87
+ def self.slugify(string)
72
88
  string
73
89
  .to_s
74
90
  .downcase
@@ -79,5 +95,20 @@ module Howdoc
79
95
  .gsub(/[^a-z0-9]+/, '_')
80
96
  .gsub(/\A_+|_+\z/, '')
81
97
  end
98
+
99
+ private
100
+
101
+ # The name a translation of this guide's own words is filed under. A
102
+ # permalink is the obvious one; without it the untranslated title has to do,
103
+ # since it is the only stable thing a test gave us.
104
+ def translation_key
105
+ @translation_key ||= (permalink || self.class.slugify(in_locale(@title, locales.first))).to_s
106
+ end
107
+
108
+ def in_locale(value, locale)
109
+ return value unless value.is_a?(Hash)
110
+
111
+ value[locale.to_sym] || value[locale.to_s] || value.values.first
112
+ end
82
113
  end
83
114
  end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Howdoc
4
+ # One language's version of a guide.
5
+ #
6
+ # A guide is recorded once -- the same clicks, the same screenshots -- and
7
+ # written out once per language. Everything that differs between those
8
+ # versions is here: the heading, the sentences, the directory the page lands
9
+ # in and the names of the pictures in it. A template is handed the edition for
10
+ # the language being written, so nothing in a template has to know that a
11
+ # second language exists.
12
+ class Edition
13
+ attr_reader :document, :locale
14
+
15
+ def initialize(document, locale)
16
+ @document = document
17
+ @locale = locale.to_sym
18
+ end
19
+
20
+ def id
21
+ document.id
22
+ end
23
+
24
+ def permalink
25
+ document.permalink
26
+ end
27
+
28
+ def title
29
+ @title ||= document.title_for(locale)
30
+ end
31
+
32
+ def intro
33
+ @intro ||= document.intro_for(locale)
34
+ end
35
+
36
+ def heading
37
+ id.nil? ? title : "#{id}. #{title}"
38
+ end
39
+
40
+ # A permalink is the same in every language, so the guides sit beside each
41
+ # other under one name. Without one the file is named after the heading,
42
+ # which means a translated heading gives a translated file name.
43
+ def slug
44
+ @slug ||= (permalink || Document.slugify(title)).to_s
45
+ end
46
+
47
+ def steps
48
+ @steps ||= document.steps.map { |step| step.narrate(self) }
49
+ end
50
+
51
+ def dir
52
+ File.join(Howdoc.config.root, locale.to_s)
53
+ end
54
+
55
+ def image_dir
56
+ File.join(dir, 'images')
57
+ end
58
+
59
+ def path(extension)
60
+ File.join(dir, "#{slug}.#{extension}")
61
+ end
62
+
63
+ def image_filename(number)
64
+ "#{slug}_#{number}.png"
65
+ end
66
+ end
67
+ end
@@ -7,12 +7,30 @@ module Howdoc
7
7
  # sentence itself -- every one of them is an I18n key, so the vocabulary is
8
8
  # translatable and an application can reword any instruction without touching
9
9
  # the code that records it.
10
+ #
11
+ # Nothing here is settled while a test runs. A guide is recorded once and
12
+ # written out in every language it is published in, so a sentence is composed
13
+ # at writing time, once per language.
10
14
  module Narrator
11
- SCOPE = 'howdoc.actions'
15
+ ROOT = 'howdoc'
16
+ ACTIONS = :actions
17
+ FIELDS = :fields
18
+ DOCUMENTS = :documents
19
+
20
+ # A value that cannot be settled until the language is known. A form field
21
+ # is the usual case: what a reader calls it is a translation, so recording
22
+ # the resolved label would freeze the guide into the language the test
23
+ # happened to run in.
24
+ Field = Struct.new(:locator) do
25
+ def to_howdoc_text(locale)
26
+ Narrator.field_label(locator, locale:)
27
+ end
28
+ end
12
29
 
13
30
  class << self
14
31
  # Keys that were asked for but not translated, reported once at the end of
15
- # a run rather than silently producing a guide with holes in it.
32
+ # a run rather than silently producing a guide with holes in it. A key is
33
+ # missing in a particular language, so the language is part of it.
16
34
  def missing_keys
17
35
  @missing_keys ||= []
18
36
  end
@@ -21,16 +39,39 @@ module Howdoc
21
39
  @missing_keys = []
22
40
  end
23
41
 
42
+ # Wraps a form field locator so it is named in the language of whichever
43
+ # edition is being written, rather than in the one the test ran in.
44
+ def field(locator)
45
+ Field.new(locator)
46
+ end
47
+
24
48
  def call(action, locale:, **payload)
25
- key = "#{SCOPE}.#{action}"
26
- text = I18n.t(key, locale:, default: nil, **escape(payload))
49
+ sentence = lookup(ACTIONS, action, locale:)
27
50
 
28
- if text.nil?
29
- missing_keys << key unless missing_keys.include?(key)
51
+ if sentence.nil?
52
+ record_missing(action_key(action), locale)
30
53
  return nil
31
54
  end
32
55
 
33
- text
56
+ values = resolve(payload, locale)
57
+ values.empty? ? sentence : I18n.interpolate(sentence, values)
58
+ end
59
+
60
+ # Whether any of the languages a guide is published in has a sentence for
61
+ # this action. A step no language can say anything about is not recorded
62
+ # at all -- it would otherwise leave a numbered instruction with nothing
63
+ # in it, or an illustration of nothing.
64
+ def known?(action, locales)
65
+ return false if action.nil?
66
+
67
+ return true if locales.any? { |locale| translated?(action, locale) }
68
+
69
+ locales.each { |locale| record_missing(action_key(action), locale) }
70
+ false
71
+ end
72
+
73
+ def translated?(action, locale)
74
+ !lookup(ACTIONS, action, locale:).nil?
34
75
  end
35
76
 
36
77
  # The label a reader would recognise for a form field.
@@ -41,10 +82,25 @@ module Howdoc
41
82
  def field_label(locator, locale:)
42
83
  return '' if locator.nil?
43
84
 
44
- translated = I18n.t("howdoc.fields.#{locator}", locale:, default: nil)
45
- return translated if translated
85
+ lookup(FIELDS, locator, locale:) || Howdoc.config.field_label.call(locator)
86
+ end
87
+
88
+ # A guide's own words -- its title, its introduction -- come from the test
89
+ # that recorded it and are therefore written in one language. Translating
90
+ # howdoc.documents.<permalink>.title gives the same guide a heading in
91
+ # another language without the test knowing there is another language.
92
+ def document_text(field, key:, locale:, default: nil)
93
+ return default if key.nil? || key.empty?
94
+
95
+ lookup(DOCUMENTS, key, field, locale:) || default
96
+ end
46
97
 
47
- Howdoc.config.field_label.call(locator)
98
+ # The gem's own words -- the ones its templates print rather than the ones
99
+ # it narrates a step with. Looked up the safe way, so an application whose
100
+ # I18n cascades cannot answer an untranslated "howdoc.index.intro" with
101
+ # some unrelated top-level "intro" of its own.
102
+ def phrase(*path, locale:, default: nil)
103
+ lookup(*path, locale:) || default
48
104
  end
49
105
 
50
106
  # Rails names fields after the model that owns them, which is noise to
@@ -63,10 +119,56 @@ module Howdoc
63
119
 
64
120
  private
65
121
 
66
- # Translations carry the markup, values do not. Escaping here means a
67
- # label containing an angle bracket cannot break the page it lands on.
68
- def escape(payload)
122
+ # Nothing here asks I18n for a leaf.
123
+ #
124
+ # How a key is resolved belongs to the application, and an application
125
+ # that has switched cascading on answers a missing
126
+ # "howdoc.documents.some_guide.title" with whatever unrelated top-level
127
+ # "title" it happens to have. A guide would then be headed with a word
128
+ # from somewhere else entirely. So the gem's own tree is fetched whole --
129
+ # it is always there, so nothing cascades past it -- and the leaf is dug
130
+ # out of the result, where a key that is missing is simply missing.
131
+ #
132
+ # An application's fallback chain is still honoured, because a sentence
133
+ # in the wrong language reads better than a numbered step with nothing
134
+ # in it.
135
+ def lookup(*path, locale:)
136
+ keys = path.map(&:to_sym)
137
+
138
+ fallback_chain(locale).each do |candidate|
139
+ tree = I18n.t(ROOT, locale: candidate, default: nil)
140
+ next unless tree.is_a?(Hash)
141
+
142
+ value = tree.dig(*keys)
143
+ return value if value.is_a?(String)
144
+ end
145
+
146
+ nil
147
+ end
148
+
149
+ def fallback_chain(locale)
150
+ return [locale] unless I18n.respond_to?(:fallbacks)
151
+
152
+ I18n.fallbacks[locale]
153
+ rescue StandardError
154
+ [locale]
155
+ end
156
+
157
+ def action_key(action)
158
+ "#{ROOT}.#{ACTIONS}.#{action}"
159
+ end
160
+
161
+ def record_missing(key, locale)
162
+ entry = "#{key} (#{locale})"
163
+ missing_keys << entry unless missing_keys.include?(entry)
164
+ end
165
+
166
+ # Values that only a language can settle are settled here, and then
167
+ # escaped like any other: translations carry the markup, values do not, so
168
+ # a label containing an angle bracket cannot break the page it lands on.
169
+ def resolve(payload, locale)
69
170
  payload.transform_values do |value|
171
+ value = value.to_howdoc_text(locale) if value.respond_to?(:to_howdoc_text)
70
172
  value.is_a?(String) || value.is_a?(Symbol) ? ERB::Util.html_escape(value.to_s) : value
71
173
  end
72
174
  end
@@ -0,0 +1,118 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Howdoc
4
+ # The menu, and the links from one guide to the next.
5
+ #
6
+ # A guide is written the moment its test finishes, when no other guide is
7
+ # known yet -- tests run in forked workers and each one writes its own page --
8
+ # so a guide cannot draw a menu of guides. It leaves an empty <nav> instead,
9
+ # and this fills it in at the end of the run, when the index has just read
10
+ # every page back and knows what there is.
11
+ #
12
+ # Filling in rather than re-rendering: the steps a guide was made of are long
13
+ # gone by then, but its page is not, and this gem wrote that page. Running the
14
+ # pass again simply replaces what it wrote the last time.
15
+ #
16
+ # A template that carries no placeholder is left exactly as it is, so an
17
+ # application with its own page chrome loses nothing by not having one.
18
+ module Navigation
19
+ module_function
20
+
21
+ # Writes the menu and the pager into every guide. +by_locale+ is the sorted
22
+ # records the index was built from, so the menu lists the guides in the same
23
+ # order the index does.
24
+ def install(by_locale, root: Howdoc.config.root)
25
+ by_locale.each_value.flat_map do |records|
26
+ records.each_with_index.filter_map do |record, position|
27
+ write(record, records:, position:, by_locale:, root:)
28
+ end
29
+ end
30
+ end
31
+
32
+ def write(record, records:, position:, by_locale:, root:)
33
+ path = File.join(root, record.locale.to_s, record.filename)
34
+ return nil unless File.file?(path)
35
+
36
+ markup = File.read(path)
37
+ filled = fill(markup, :nav) { nav(record, records, by_locale) }
38
+ filled = fill(filled, :pager) { pager(record, records, position) }
39
+ return nil if filled == markup
40
+
41
+ File.write(path, filled)
42
+ path
43
+ end
44
+
45
+ # Stripped, so that filling a page in a second time leaves it exactly as the
46
+ # first time left it: the pass runs once per suite and again at the end of a
47
+ # run, and a page that grew a blank line each time would show up as changed
48
+ # in a repository for no reason at all.
49
+ def fill(markup, name)
50
+ markup.sub(placeholder(name)) { yield.strip }
51
+ end
52
+
53
+ def placeholder(name)
54
+ %r{<nav[^>]*data-howdoc-#{name}[^>]*>.*?</nav>}m
55
+ end
56
+
57
+ def nav(record, records, by_locale)
58
+ Templates.render(
59
+ 'shared/html/nav.haml',
60
+ locale: record.locale, records:, current: record, config: Howdoc.config,
61
+ languages: languages(current_locale: record.locale, record:, by_locale:)
62
+ )
63
+ end
64
+
65
+ def pager(record, records, position)
66
+ Templates.render(
67
+ 'shared/html/pager.haml',
68
+ locale: record.locale, config: Howdoc.config,
69
+ previous: (records[position - 1] if position.positive?),
70
+ next: records[position + 1]
71
+ )
72
+ end
73
+
74
+ # The same guide in the other languages the application publishes.
75
+ #
76
+ # Matched by identifier rather than by file name: a guide with a translated
77
+ # heading gets a translated file name, so "1.3" is the only thing the two
78
+ # pages are sure to have in common. A guide that was never written in the
79
+ # other language points at that language's index instead of nowhere.
80
+ def languages(current_locale:, by_locale:, record: nil)
81
+ ordered(by_locale.keys).map do |locale|
82
+ current = locale.to_s == current_locale.to_s
83
+
84
+ {
85
+ code: locale.to_s,
86
+ label: label(locale),
87
+ current:,
88
+ href: (destination(locale, record, by_locale) unless current)
89
+ }
90
+ end
91
+ end
92
+
93
+ def destination(locale, record, by_locale)
94
+ counterpart = record && counterpart(record, by_locale[locale] || [])
95
+
96
+ "../#{locale}/#{counterpart ? counterpart.filename : Registry::INDEX_FILENAME}"
97
+ end
98
+
99
+ def counterpart(record, records)
100
+ return records.find { |other| other.id == record.id } unless record.id.nil?
101
+
102
+ records.find { |other| other.permalink == record.permalink }
103
+ end
104
+
105
+ # A language names itself in its own language, which is how a reader who
106
+ # cannot read the current page still recognises the way out of it.
107
+ def label(locale)
108
+ Narrator.phrase(:languages, locale, locale:, default: locale.to_s.upcase)
109
+ end
110
+
111
+ def ordered(locales)
112
+ configured = Howdoc.config.locales&.map(&:to_s)
113
+ return locales.map(&:to_s).sort if configured.nil?
114
+
115
+ (configured & locales.map(&:to_s)) | locales.map(&:to_s).sort
116
+ end
117
+ end
118
+ end
@@ -37,16 +37,18 @@ module Howdoc
37
37
  end
38
38
 
39
39
  # Records one instruction. +action+ names an I18n key under howdoc.actions;
40
- # the remaining keyword arguments are interpolated into it. Pass
41
- # <tt>html:</tt> instead to supply already-rendered markup, for the rare
42
- # step no sentence describes.
40
+ # the remaining keyword arguments are interpolated into it when the guide is
41
+ # written, once per language. Pass <tt>html:</tt> instead to supply
42
+ # already-rendered markup, for the rare step no sentence describes.
43
+ #
44
+ # Values that read differently in different languages must not be resolved
45
+ # here: pass Howdoc::Narrator.field(locator) and the label is settled by
46
+ # each edition as it is written.
43
47
  def record(action = nil, html: nil, arrival: false, nodoc: false, **payload)
44
48
  return nil if nodoc || !recording?
49
+ return nil if html.nil? && !Narrator.known?(action, current.locales)
45
50
 
46
- markup = html || Narrator.call(action, locale: current.locale, **payload)
47
- return nil if markup.nil?
48
-
49
- current.new_step(html: markup, arrival:)
51
+ current.new_step(action:, payload:, html:, arrival:)
50
52
  end
51
53
 
52
54
  # Illustrates the step recorded most recently. Separate from +record+
@@ -80,13 +80,23 @@ module Howdoc
80
80
  Howdoc.config.locales.to_h { |locale| [locale.to_s, found.fetch(locale.to_s, [])] }
81
81
  end
82
82
 
83
- # Builds the index for every locale that produced at least one guide.
83
+ # Builds the index for every locale that produced at least one guide, and
84
+ # then writes the menu into the guides themselves: they were written one at
85
+ # a time, before there was a menu to draw.
84
86
  def write_indexes(root = Howdoc.config.root)
85
87
  Templates.install_assets(File.join(root, 'assets')) if Howdoc.config.install_assets
86
88
 
87
- by_locale(root).map do |locale, records|
88
- Writers::Index.call(locale:, records: sort(records), root:)
89
+ catalogue = by_locale(root).transform_values { |records| sort(records) }
90
+
91
+ written = catalogue.map do |locale, records|
92
+ Writers::Index.call(
93
+ locale:, records:, root:,
94
+ languages: Navigation.languages(current_locale: locale, by_locale: catalogue)
95
+ )
89
96
  end
97
+
98
+ Navigation.install(catalogue, root:)
99
+ written
90
100
  end
91
101
 
92
102
  def sort(records)
data/lib/howdoc/step.rb CHANGED
@@ -1,21 +1,55 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Howdoc
4
- # One numbered instruction in a guide: a sentence, optionally an illustration,
5
- # and a flag for the steps that mark the reader's arrival on a new page.
4
+ # One numbered instruction in a guide: what happened, optionally an
5
+ # illustration of it, and a flag for the steps that mark the reader's arrival
6
+ # on a new page.
7
+ #
8
+ # What happened is kept as the action and the values it was recorded with,
9
+ # not as a finished sentence, because the same step is told in every language
10
+ # the guide is published in.
6
11
  class Step
7
- attr_reader :number
12
+ # A step as one language tells it: the sentence composed, the illustration
13
+ # named as that language's guide refers to it. This is what a template is
14
+ # handed, so a template never has to know which language it is rendering.
15
+ Narrated = Struct.new(:number, :html, :screenshot, :arrival, keyword_init: true) do
16
+ def empty?
17
+ html.nil? && screenshot.nil?
18
+ end
19
+ end
20
+
21
+ attr_reader :number, :action, :payload
8
22
  attr_accessor :html, :screenshot, :arrival
9
23
 
10
- def initialize(number:, html: nil, screenshot: nil, arrival: false)
24
+ def initialize(number:, action: nil, payload: {}, html: nil, screenshot: nil, arrival: false)
11
25
  @number = number
26
+ @action = action
27
+ @payload = payload
12
28
  @html = html
13
29
  @screenshot = screenshot
14
30
  @arrival = arrival
15
31
  end
16
32
 
17
33
  def empty?
18
- html.nil? && screenshot.nil?
34
+ html.nil? && action.nil? && screenshot.nil?
35
+ end
36
+
37
+ # Markup a caller supplied stands in every language: it was written by the
38
+ # application, which knew what it was doing.
39
+ def html_in(locale)
40
+ return html unless html.nil?
41
+ return nil if action.nil?
42
+
43
+ Narrator.call(action, locale:, **payload)
44
+ end
45
+
46
+ def narrate(edition)
47
+ Narrated.new(
48
+ number:,
49
+ html: html_in(edition.locale),
50
+ screenshot: (edition.image_filename(number) if screenshot),
51
+ arrival:
52
+ )
19
53
  end
20
54
  end
21
55
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Howdoc
4
- VERSION = '0.1.0'
4
+ VERSION = '0.3.0'
5
5
  end
@@ -28,28 +28,55 @@ module Howdoc
28
28
  Howdoc.config.formats.map { |format| fetch(format).call(document) }
29
29
  end
30
30
 
31
- # Writes the illustrated guide a reader actually opens.
31
+ # Writes the illustrated guide a reader actually opens -- one page per
32
+ # language the guide is published in.
32
33
  class Html
33
34
  def self.call(document)
34
- FileUtils.mkdir_p(document.dir)
35
+ document.editions.map { |edition| write(document, edition) }
36
+ end
37
+
38
+ def self.write(document, edition)
39
+ FileUtils.mkdir_p(edition.dir)
40
+ copy_screenshots(document, edition)
41
+
35
42
  markup = Templates.render(
36
43
  'document/html/layout.haml',
37
- document:, config: Howdoc.config
44
+ document: edition, config: Howdoc.config
38
45
  )
39
- File.write(document.path('html'), markup)
40
- document.path('html')
46
+ File.write(edition.path('html'), markup)
47
+ edition.path('html')
48
+ end
49
+
50
+ # A picture of the application is the same picture whatever language the
51
+ # page around it is written in, so it is taken once, while the browser is
52
+ # still on the page, and copied to where each edition looks for it.
53
+ def self.copy_screenshots(document, edition)
54
+ return if edition.equal?(document.primary)
55
+
56
+ document.steps.each do |step|
57
+ next if step.screenshot.nil?
58
+
59
+ copy_screenshot(File.join(document.image_dir, step.screenshot), edition, step.number)
60
+ end
61
+ end
62
+
63
+ def self.copy_screenshot(source, edition, number)
64
+ return unless File.file?(source)
65
+
66
+ FileUtils.mkdir_p(edition.image_dir)
67
+ FileUtils.cp(source, File.join(edition.image_dir, edition.image_filename(number)))
41
68
  end
42
69
  end
43
70
 
44
71
  # Writes one index per locale, from the guides the run just produced.
45
72
  class Index
46
- def self.call(locale:, records:, root: Howdoc.config.root)
73
+ def self.call(locale:, records:, root: Howdoc.config.root, languages: [])
47
74
  dir = File.join(root, locale.to_s)
48
75
  FileUtils.mkdir_p(dir)
49
76
 
50
77
  markup = Templates.render(
51
78
  'index/html/layout.haml',
52
- locale:, records:, config: Howdoc.config
79
+ locale:, records:, languages:, config: Howdoc.config
53
80
  )
54
81
  path = File.join(dir, 'index.html')
55
82
  File.write(path, markup)
data/lib/howdoc.rb CHANGED
@@ -7,11 +7,13 @@ require_relative 'howdoc/version'
7
7
  require_relative 'howdoc/configuration'
8
8
  require_relative 'howdoc/narrator'
9
9
  require_relative 'howdoc/step'
10
+ require_relative 'howdoc/edition'
10
11
  require_relative 'howdoc/document'
11
12
  require_relative 'howdoc/screenshot'
12
13
  require_relative 'howdoc/recorder'
13
14
  require_relative 'howdoc/templates'
14
15
  require_relative 'howdoc/writers'
16
+ require_relative 'howdoc/navigation'
15
17
  require_relative 'howdoc/registry'
16
18
 
17
19
  # Generates end-user documentation from Capybara system tests.
@@ -58,11 +60,36 @@ module Howdoc
58
60
  end
59
61
 
60
62
  # Begins a guide. Everything recorded until #finish belongs to it.
61
- def start(id: nil, title: nil, locale: I18n.locale, permalink: nil, intro: nil)
63
+ #
64
+ # A test runs in one language; the guide it records is written in every
65
+ # language the application publishes, which is config.locales unless this
66
+ # call names them. Pass +locale:+ for the rare guide that belongs to one
67
+ # language only -- a page describing something only that language's users
68
+ # ever see.
69
+ def start(id: nil, title: nil, locale: nil, locales: nil, permalink: nil, intro: nil)
62
70
  return nil unless enabled?
63
- return nil if title.to_s.strip.empty?
71
+ return nil if blank_title?(title)
64
72
 
65
- Recorder.current = Document.new(id:, title:, locale:, permalink:, intro:)
73
+ Recorder.current = Document.new(
74
+ id:, title:, permalink:, intro:,
75
+ locales: document_locales(locale, locales)
76
+ )
77
+ end
78
+
79
+ # Which languages a guide is written in: what the caller asked for, else
80
+ # what the application publishes, else the language the suite is running
81
+ # in, which is all a single-language application ever needs.
82
+ def document_locales(locale, locales)
83
+ chosen = locales || locale || config.locales
84
+ chosen = I18n.locale if chosen.nil? || Array(chosen).empty?
85
+
86
+ Array(chosen).map(&:to_sym)
87
+ end
88
+
89
+ def blank_title?(title)
90
+ return title.values.all? { |value| blank_title?(value) } if title.is_a?(Hash)
91
+
92
+ title.to_s.strip.empty?
66
93
  end
67
94
 
68
95
  # Ends the guide and writes it out. Called even when the test failed, so a
@@ -27,7 +27,6 @@ body.howdoc {
27
27
 
28
28
  .howdoc-document,
29
29
  .howdoc-index {
30
- max-width: 52rem;
31
30
  margin: 0 auto;
32
31
  padding: 3rem 1.25rem 6rem;
33
32
  }
@@ -57,12 +56,6 @@ body.howdoc {
57
56
  padding-left: 0.25rem;
58
57
  }
59
58
 
60
- .howdoc-step.howdoc-arrival {
61
- border-left: 3px solid var(--howdoc-accent);
62
- margin-left: -1rem;
63
- padding-left: 1rem;
64
- }
65
-
66
59
  .howdoc-shot {
67
60
  display: block;
68
61
  max-width: 100%;
@@ -103,7 +96,114 @@ body.howdoc {
103
96
  text-decoration: underline;
104
97
  }
105
98
 
99
+ /* The menu sits beside the page it belongs to, and above it when there is no
100
+ room beside. A guide written before the index pass has an empty menu, and an
101
+ empty menu is no menu at all. */
102
+ .howdoc-nav:empty,
103
+ .howdoc-pager:empty {
104
+ display: none;
105
+ }
106
+
107
+ .howdoc-nav {
108
+ padding: 1.5rem 1.25rem;
109
+ border-bottom: 1px solid var(--howdoc-rule);
110
+ }
111
+
112
+ @media (min-width: 60rem) {
113
+ body.howdoc {
114
+ display: grid;
115
+ grid-template-columns: 19rem minmax(0, 1fr);
116
+ align-items: start;
117
+ }
118
+
119
+ .howdoc-nav {
120
+ position: sticky;
121
+ top: 0;
122
+ max-height: 100vh;
123
+ overflow-y: auto;
124
+ padding: 2rem 1.25rem 3rem;
125
+ border-right: 1px solid var(--howdoc-rule);
126
+ border-bottom: 0;
127
+ }
128
+ }
129
+
130
+ .howdoc-nav-head {
131
+ display: flex;
132
+ align-items: baseline;
133
+ justify-content: space-between;
134
+ gap: 1rem;
135
+ }
136
+
137
+ .howdoc-nav-home {
138
+ color: var(--howdoc-fg);
139
+ font-weight: 600;
140
+ text-decoration: none;
141
+ }
142
+
143
+ .howdoc-languages {
144
+ display: flex;
145
+ gap: 0.5rem;
146
+ margin: 0;
147
+ padding: 0;
148
+ list-style: none;
149
+ font-size: 0.85rem;
150
+ }
151
+
152
+ .howdoc-language a {
153
+ color: var(--howdoc-accent);
154
+ text-decoration: none;
155
+ }
156
+
157
+ .howdoc-language.howdoc-current {
158
+ color: var(--howdoc-muted);
159
+ }
160
+
161
+ .howdoc-toc-entry.howdoc-current > a {
162
+ color: var(--howdoc-fg);
163
+ font-weight: 600;
164
+ }
165
+
166
+ .howdoc-pager {
167
+ display: flex;
168
+ gap: 1rem;
169
+ margin-top: 3rem;
170
+ padding-top: 1.5rem;
171
+ border-top: 1px solid var(--howdoc-rule);
172
+ }
173
+
174
+ .howdoc-pager-link {
175
+ display: flex;
176
+ flex: 1 1 0;
177
+ flex-direction: column;
178
+ gap: 0.25rem;
179
+ padding: 0.9rem 1rem;
180
+ border: 1px solid var(--howdoc-rule);
181
+ border-radius: 8px;
182
+ text-decoration: none;
183
+ }
184
+
185
+ .howdoc-pager-next {
186
+ margin-left: auto;
187
+ text-align: right;
188
+ }
189
+
190
+ .howdoc-pager-label {
191
+ color: var(--howdoc-muted);
192
+ font-size: 0.8rem;
193
+ letter-spacing: 0.08em;
194
+ text-transform: uppercase;
195
+ }
196
+
197
+ .howdoc-pager-heading {
198
+ color: var(--howdoc-accent);
199
+ }
200
+
106
201
  @media print {
202
+ .howdoc-nav,
203
+ .howdoc-pager {
204
+ display: none;
205
+ }
206
+
107
207
  .howdoc-shot {
108
208
  box-shadow: none;
109
209
  }
@@ -6,11 +6,16 @@
6
6
  %title= @document.heading
7
7
  %link{ rel: 'stylesheet', href: '../assets/howdoc.css' }
8
8
  %body.howdoc
9
+ -# Empty on purpose: the index pass writes the menu and the pager in here
10
+ -# once it knows what the other guides are. A page is a whole page either
11
+ -# way, so a guide opened straight from a link is never a dead end.
12
+ %nav.howdoc-nav{ data: { howdoc_nav: true } }
9
13
  %main.howdoc-document
10
14
  %h1= @document.heading
11
15
  - unless @document.intro.to_s.strip.empty?
12
16
  .howdoc-intro!= @document.intro
13
- %p.howdoc-lede= I18n.t('howdoc.document.follow_steps', locale: @document.locale)
17
+ %p.howdoc-lede= Howdoc::Narrator.phrase(:document, :follow_steps, locale: @document.locale)
14
18
  %ol.howdoc-steps
15
19
  - @document.steps.reject(&:empty?).each do |step|
16
20
  != render('document/html/step.haml', step: step)
21
+ %nav.howdoc-pager{ data: { howdoc_pager: true } }
@@ -3,20 +3,15 @@
3
3
  %head
4
4
  %meta{ charset: 'utf-8' }
5
5
  %meta{ name: 'viewport', content: 'width=device-width, initial-scale=1' }
6
- %title= I18n.t('howdoc.index.title', locale: @locale)
6
+ %title= Howdoc::Narrator.phrase(:index, :title, locale: @locale)
7
7
  %link{ rel: 'stylesheet', href: '../assets/howdoc.css' }
8
8
  %body.howdoc
9
+ -# The index knows every guide already, so it draws the menu itself.
10
+ != render('shared/html/nav.haml', current: nil)
9
11
  %main.howdoc-index
10
- %h1= I18n.t('howdoc.index.title', locale: @locale)
12
+ %h1= Howdoc::Narrator.phrase(:index, :title, locale: @locale)
13
+ - intro = Howdoc::Narrator.phrase(:index, :intro, locale: @locale)
14
+ - if intro
15
+ .howdoc-intro!= intro
11
16
  - if @records.empty?
12
- %p= I18n.t('howdoc.index.empty', locale: @locale)
13
- - else
14
- %ul.howdoc-toc
15
- - previous_group = nil
16
- - @records.each do |record|
17
- - group = @config.group_label.call(record)
18
- - if group && group != previous_group
19
- %li.howdoc-toc-group= group
20
- - previous_group = group
21
- %li.howdoc-toc-entry
22
- %a{ href: "./#{record.filename}" }= record.heading
17
+ %p= Howdoc::Narrator.phrase(:index, :empty, locale: @locale)
@@ -0,0 +1,25 @@
1
+ -# The menu every page carries. A guide cannot draw this when it is written --
2
+ -# no other guide exists yet -- so a guide leaves the empty <nav> below and the
3
+ -# index pass fills it in once every guide is on disk. The index renders it
4
+ -# directly, because by then it knows.
5
+ %nav.howdoc-nav{ data: { howdoc_nav: true } }
6
+ .howdoc-nav-head
7
+ %a.howdoc-nav-home{ href: './index.html' }= Howdoc::Narrator.phrase(:index, :title, locale: @locale)
8
+ - if @languages.size > 1
9
+ %ul.howdoc-languages
10
+ - @languages.each do |language|
11
+ %li.howdoc-language{ class: ('howdoc-current' if language[:current]) }
12
+ - if language[:href]
13
+ %a{ href: language[:href], lang: language[:code] }= language[:label]
14
+ - else
15
+ %span{ lang: language[:code] }= language[:label]
16
+ %ul.howdoc-toc
17
+ - previous_group = nil
18
+ - @records.each do |record|
19
+ - group = @config.group_label.call(record)
20
+ - if group && group != previous_group
21
+ %li.howdoc-toc-group= group
22
+ - previous_group = group
23
+ - current = @current && record.filename == @current.filename
24
+ %li.howdoc-toc-entry{ class: ('howdoc-current' if current) }
25
+ %a{ href: "./#{record.filename}", 'aria-current': (current ? 'page' : nil) }= record.heading
@@ -0,0 +1,11 @@
1
+ -# Step-by-step guides are read in order, so each one says what comes next.
2
+ -# Written by the same pass as the menu, for the same reason.
3
+ %nav.howdoc-pager{ data: { howdoc_pager: true } }
4
+ - if @previous
5
+ %a.howdoc-pager-link.howdoc-pager-previous{ href: "./#{@previous.filename}", rel: 'prev' }
6
+ %span.howdoc-pager-label= Howdoc::Narrator.phrase(:pager, :previous, locale: @locale)
7
+ %span.howdoc-pager-heading= @previous.heading
8
+ - if @next
9
+ %a.howdoc-pager-link.howdoc-pager-next{ href: "./#{@next.filename}", rel: 'next' }
10
+ %span.howdoc-pager-label= Howdoc::Narrator.phrase(:pager, :next, locale: @locale)
11
+ %span.howdoc-pager-heading= @next.heading
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: howdoc
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Priit Tark
@@ -58,8 +58,10 @@ files:
58
58
  - lib/howdoc/capybara_actions.rb
59
59
  - lib/howdoc/configuration.rb
60
60
  - lib/howdoc/document.rb
61
+ - lib/howdoc/edition.rb
61
62
  - lib/howdoc/minitest.rb
62
63
  - lib/howdoc/narrator.rb
64
+ - lib/howdoc/navigation.rb
63
65
  - lib/howdoc/recorder.rb
64
66
  - lib/howdoc/registry.rb
65
67
  - lib/howdoc/screenshot.rb
@@ -72,6 +74,8 @@ files:
72
74
  - templates/default/document/html/layout.haml
73
75
  - templates/default/document/html/step.haml
74
76
  - templates/default/index/html/layout.haml
77
+ - templates/default/shared/html/nav.haml
78
+ - templates/default/shared/html/pager.haml
75
79
  homepage: https://github.com/priit/howdoc
76
80
  licenses:
77
81
  - MIT