howdoc 0.2.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: 3c4cdda2ccd66b3c800f8711f0ea76b5220bee90496b337bccb15d1478b078b0
4
- data.tar.gz: 7c2252c5adce29342f4483f3cf6529d8a9fd3f009e6b6e92747e408110d173ba
3
+ metadata.gz: 72edea92225be55592acd2d56c732d5d82e140193b06dc276b0c691440ff2e3f
4
+ data.tar.gz: 2ec1eea553ec1b5ca6e23f9f320e082367ded34280cf6701e8f284f06f1ef34c
5
5
  SHA512:
6
- metadata.gz: fa0d4d38e5ee59ad0b05acd8e9db25f87e99024b9c62fd7eff1b8139a1b83712a75c68e3f540c82393658fd3febd451bf420c33bea5e69e7e6c446079574e3cf
7
- data.tar.gz: 7e5147574f8c072a49e31d6f2ce9c001606f63d90604f1e7e9bc9a51257ce08ce2ae943449df76d0dbc84413766ecd58869c507a3f8c11ad32259b9a526748bf
6
+ metadata.gz: 9692dcfc9505fa3cf80eff5ea7a6c2dcb244619a41aa163cbab98c04a16b01bb13d82d646db46b1e98025f386a47594553abfd4b59a6e937040b555a6f9ecadb
7
+ data.tar.gz: 8ee1913418a639198f1134958ce9748ff83a231a5d86b8fb237675ab264d62b50ad9f5cdfd9e8433310b0c6c226bbf35ec6a21d9afd22c5eee0884b6b9042c86
data/CHANGELOG.md CHANGED
@@ -1,5 +1,25 @@
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
+
3
23
  ## 0.2.0
4
24
 
5
25
  Guides are now written in every language the application publishes, from a
data/README.md CHANGED
@@ -212,6 +212,39 @@ naming the language it is missing from, rather than leaving a silent hole in the
212
212
  guide. A step no language can describe is not recorded at all; a step one
213
213
  language cannot describe is left out of that language's guide only.
214
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.
247
+
215
248
  ## Templates
216
249
 
217
250
  Page chrome lives in templates, never in the code that records steps, so
@@ -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
@@ -95,6 +95,14 @@ module Howdoc
95
95
  lookup(DOCUMENTS, key, field, locale:) || default
96
96
  end
97
97
 
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
104
+ end
105
+
98
106
  # Rails names fields after the model that owns them, which is noise to
99
107
  # anyone who does not know the schema, so the leading segment is dropped.
100
108
  def humanize_locator(locator)
@@ -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
@@ -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)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Howdoc
4
- VERSION = '0.2.0'
4
+ VERSION = '0.3.0'
5
5
  end
@@ -70,13 +70,13 @@ module Howdoc
70
70
 
71
71
  # Writes one index per locale, from the guides the run just produced.
72
72
  class Index
73
- def self.call(locale:, records:, root: Howdoc.config.root)
73
+ def self.call(locale:, records:, root: Howdoc.config.root, languages: [])
74
74
  dir = File.join(root, locale.to_s)
75
75
  FileUtils.mkdir_p(dir)
76
76
 
77
77
  markup = Templates.render(
78
78
  'index/html/layout.haml',
79
- locale:, records:, config: Howdoc.config
79
+ locale:, records:, languages:, config: Howdoc.config
80
80
  )
81
81
  path = File.join(dir, 'index.html')
82
82
  File.write(path, markup)
data/lib/howdoc.rb CHANGED
@@ -13,6 +13,7 @@ require_relative 'howdoc/screenshot'
13
13
  require_relative 'howdoc/recorder'
14
14
  require_relative 'howdoc/templates'
15
15
  require_relative 'howdoc/writers'
16
+ require_relative 'howdoc/navigation'
16
17
  require_relative 'howdoc/registry'
17
18
 
18
19
  # Generates end-user documentation from Capybara system tests.
@@ -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.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Priit Tark
@@ -61,6 +61,7 @@ files:
61
61
  - lib/howdoc/edition.rb
62
62
  - lib/howdoc/minitest.rb
63
63
  - lib/howdoc/narrator.rb
64
+ - lib/howdoc/navigation.rb
64
65
  - lib/howdoc/recorder.rb
65
66
  - lib/howdoc/registry.rb
66
67
  - lib/howdoc/screenshot.rb
@@ -73,6 +74,8 @@ files:
73
74
  - templates/default/document/html/layout.haml
74
75
  - templates/default/document/html/step.haml
75
76
  - templates/default/index/html/layout.haml
77
+ - templates/default/shared/html/nav.haml
78
+ - templates/default/shared/html/pager.haml
76
79
  homepage: https://github.com/priit/howdoc
77
80
  licenses:
78
81
  - MIT