howdoc 0.1.0 → 0.2.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 +4 -4
- data/CHANGELOG.md +18 -0
- data/README.md +52 -4
- data/lib/howdoc/capybara_actions.rb +10 -9
- data/lib/howdoc/configuration.rb +7 -4
- data/lib/howdoc/document.rb +57 -26
- data/lib/howdoc/edition.rb +67 -0
- data/lib/howdoc/narrator.rb +107 -13
- data/lib/howdoc/recorder.rb +9 -7
- data/lib/howdoc/step.rb +39 -5
- data/lib/howdoc/version.rb +1 -1
- data/lib/howdoc/writers.rb +32 -5
- data/lib/howdoc.rb +29 -3
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3c4cdda2ccd66b3c800f8711f0ea76b5220bee90496b337bccb15d1478b078b0
|
|
4
|
+
data.tar.gz: 7c2252c5adce29342f4483f3cf6529d8a9fd3f009e6b6e92747e408110d173ba
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: fa0d4d38e5ee59ad0b05acd8e9db25f87e99024b9c62fd7eff1b8139a1b83712a75c68e3f540c82393658fd3febd451bf420c33bea5e69e7e6c446079574e3cf
|
|
7
|
+
data.tar.gz: 7e5147574f8c072a49e31d6f2ce9c001606f63d90604f1e7e9bc9a51257ce08ce2ae943449df76d0dbc84413766ecd58869c507a3f8c11ad32259b9a526748bf
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.0
|
|
4
|
+
|
|
5
|
+
Guides are now written in every language the application publishes, from a
|
|
6
|
+
single test run. Previously a run produced guides in the language the suite
|
|
7
|
+
happened to be running in, and the other languages were silently left behind.
|
|
8
|
+
|
|
9
|
+
- `config.locales` names the languages every guide is written in, not only the
|
|
10
|
+
languages an index is built for.
|
|
11
|
+
- A step is recorded as an action and its values rather than as a finished
|
|
12
|
+
sentence, and narrated once per language when the guide is written.
|
|
13
|
+
- Screenshots are taken once and copied into each language's guide.
|
|
14
|
+
- A guide's own heading and introduction can be translated under
|
|
15
|
+
`howdoc.documents.<permalink>`, since they come from the test rather than
|
|
16
|
+
from the engine.
|
|
17
|
+
- `Howdoc::Narrator.field` defers a field label until the language is known.
|
|
18
|
+
`Howdoc.start(locale:)` still pins a guide to one language.
|
|
19
|
+
- Untranslated keys are reported with the language they are missing from.
|
|
20
|
+
|
|
3
21
|
## 0.1.0
|
|
4
22
|
|
|
5
23
|
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,10 @@ 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
|
|
165
|
-
than leaving a silent hole in the
|
|
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.
|
|
166
214
|
|
|
167
215
|
## Templates
|
|
168
216
|
|
|
@@ -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(
|
|
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.
|
|
120
|
+
Howdoc::Narrator.field(locator)
|
|
119
121
|
end
|
|
120
122
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
data/lib/howdoc/configuration.rb
CHANGED
|
@@ -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
|
-
#
|
|
26
|
-
#
|
|
27
|
-
#
|
|
28
|
-
#
|
|
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
|
data/lib/howdoc/document.rb
CHANGED
|
@@ -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, :
|
|
21
|
+
attr_reader :id, :permalink, :steps, :locales
|
|
17
22
|
|
|
18
|
-
|
|
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
|
|
29
|
-
@
|
|
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
|
|
33
|
-
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
def
|
|
52
|
-
|
|
53
|
-
|
|
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
|
data/lib/howdoc/narrator.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
26
|
-
text = I18n.t(key, locale:, default: nil, **escape(payload))
|
|
49
|
+
sentence = lookup(ACTIONS, action, locale:)
|
|
27
50
|
|
|
28
|
-
if
|
|
29
|
-
|
|
51
|
+
if sentence.nil?
|
|
52
|
+
record_missing(action_key(action), locale)
|
|
30
53
|
return nil
|
|
31
54
|
end
|
|
32
55
|
|
|
33
|
-
|
|
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,17 @@ module Howdoc
|
|
|
41
82
|
def field_label(locator, locale:)
|
|
42
83
|
return '' if locator.nil?
|
|
43
84
|
|
|
44
|
-
|
|
45
|
-
|
|
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?
|
|
46
94
|
|
|
47
|
-
|
|
95
|
+
lookup(DOCUMENTS, key, field, locale:) || default
|
|
48
96
|
end
|
|
49
97
|
|
|
50
98
|
# Rails names fields after the model that owns them, which is noise to
|
|
@@ -63,10 +111,56 @@ module Howdoc
|
|
|
63
111
|
|
|
64
112
|
private
|
|
65
113
|
|
|
66
|
-
#
|
|
67
|
-
#
|
|
68
|
-
|
|
114
|
+
# Nothing here asks I18n for a leaf.
|
|
115
|
+
#
|
|
116
|
+
# How a key is resolved belongs to the application, and an application
|
|
117
|
+
# that has switched cascading on answers a missing
|
|
118
|
+
# "howdoc.documents.some_guide.title" with whatever unrelated top-level
|
|
119
|
+
# "title" it happens to have. A guide would then be headed with a word
|
|
120
|
+
# from somewhere else entirely. So the gem's own tree is fetched whole --
|
|
121
|
+
# it is always there, so nothing cascades past it -- and the leaf is dug
|
|
122
|
+
# out of the result, where a key that is missing is simply missing.
|
|
123
|
+
#
|
|
124
|
+
# An application's fallback chain is still honoured, because a sentence
|
|
125
|
+
# in the wrong language reads better than a numbered step with nothing
|
|
126
|
+
# in it.
|
|
127
|
+
def lookup(*path, locale:)
|
|
128
|
+
keys = path.map(&:to_sym)
|
|
129
|
+
|
|
130
|
+
fallback_chain(locale).each do |candidate|
|
|
131
|
+
tree = I18n.t(ROOT, locale: candidate, default: nil)
|
|
132
|
+
next unless tree.is_a?(Hash)
|
|
133
|
+
|
|
134
|
+
value = tree.dig(*keys)
|
|
135
|
+
return value if value.is_a?(String)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
nil
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def fallback_chain(locale)
|
|
142
|
+
return [locale] unless I18n.respond_to?(:fallbacks)
|
|
143
|
+
|
|
144
|
+
I18n.fallbacks[locale]
|
|
145
|
+
rescue StandardError
|
|
146
|
+
[locale]
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def action_key(action)
|
|
150
|
+
"#{ROOT}.#{ACTIONS}.#{action}"
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def record_missing(key, locale)
|
|
154
|
+
entry = "#{key} (#{locale})"
|
|
155
|
+
missing_keys << entry unless missing_keys.include?(entry)
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# Values that only a language can settle are settled here, and then
|
|
159
|
+
# escaped like any other: translations carry the markup, values do not, so
|
|
160
|
+
# a label containing an angle bracket cannot break the page it lands on.
|
|
161
|
+
def resolve(payload, locale)
|
|
69
162
|
payload.transform_values do |value|
|
|
163
|
+
value = value.to_howdoc_text(locale) if value.respond_to?(:to_howdoc_text)
|
|
70
164
|
value.is_a?(String) || value.is_a?(Symbol) ? ERB::Util.html_escape(value.to_s) : value
|
|
71
165
|
end
|
|
72
166
|
end
|
data/lib/howdoc/recorder.rb
CHANGED
|
@@ -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
|
|
41
|
-
# <tt>html:</tt> instead to supply
|
|
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
|
-
|
|
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+
|
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:
|
|
5
|
-
# and a flag for the steps that mark the reader's arrival
|
|
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
|
-
|
|
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
|
data/lib/howdoc/version.rb
CHANGED
data/lib/howdoc/writers.rb
CHANGED
|
@@ -28,16 +28,43 @@ 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
|
-
|
|
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
|
|
44
|
+
document: edition, config: Howdoc.config
|
|
38
45
|
)
|
|
39
|
-
File.write(
|
|
40
|
-
|
|
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
|
|
data/lib/howdoc.rb
CHANGED
|
@@ -7,6 +7,7 @@ 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'
|
|
@@ -58,11 +59,36 @@ module Howdoc
|
|
|
58
59
|
end
|
|
59
60
|
|
|
60
61
|
# Begins a guide. Everything recorded until #finish belongs to it.
|
|
61
|
-
|
|
62
|
+
#
|
|
63
|
+
# A test runs in one language; the guide it records is written in every
|
|
64
|
+
# language the application publishes, which is config.locales unless this
|
|
65
|
+
# call names them. Pass +locale:+ for the rare guide that belongs to one
|
|
66
|
+
# language only -- a page describing something only that language's users
|
|
67
|
+
# ever see.
|
|
68
|
+
def start(id: nil, title: nil, locale: nil, locales: nil, permalink: nil, intro: nil)
|
|
62
69
|
return nil unless enabled?
|
|
63
|
-
return nil if title
|
|
70
|
+
return nil if blank_title?(title)
|
|
64
71
|
|
|
65
|
-
Recorder.current = Document.new(
|
|
72
|
+
Recorder.current = Document.new(
|
|
73
|
+
id:, title:, permalink:, intro:,
|
|
74
|
+
locales: document_locales(locale, locales)
|
|
75
|
+
)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Which languages a guide is written in: what the caller asked for, else
|
|
79
|
+
# what the application publishes, else the language the suite is running
|
|
80
|
+
# in, which is all a single-language application ever needs.
|
|
81
|
+
def document_locales(locale, locales)
|
|
82
|
+
chosen = locales || locale || config.locales
|
|
83
|
+
chosen = I18n.locale if chosen.nil? || Array(chosen).empty?
|
|
84
|
+
|
|
85
|
+
Array(chosen).map(&:to_sym)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def blank_title?(title)
|
|
89
|
+
return title.values.all? { |value| blank_title?(value) } if title.is_a?(Hash)
|
|
90
|
+
|
|
91
|
+
title.to_s.strip.empty?
|
|
66
92
|
end
|
|
67
93
|
|
|
68
94
|
# Ends the guide and writes it out. Called even when the test failed, so a
|
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.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Priit Tark
|
|
@@ -58,6 +58,7 @@ 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
|
|
63
64
|
- lib/howdoc/recorder.rb
|