govuk_tech_docs 1.5.0 → 1.6.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.
Files changed (35) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +3 -0
  3. data/CHANGELOG.md +90 -0
  4. data/docs/configuration.md +15 -0
  5. data/docs/frontmatter.md +2 -14
  6. data/docs/page-expiry.md +69 -0
  7. data/example/Gemfile +1 -0
  8. data/example/config/tech-docs.yml +6 -0
  9. data/example/source/api-path.html.md +7 -0
  10. data/example/source/api-reference.html.md +5 -0
  11. data/example/source/pets.yml +106 -0
  12. data/govuk_tech_docs.gemspec +2 -0
  13. data/lib/assets/javascripts/_analytics.js +12 -0
  14. data/lib/assets/javascripts/_modules/collapsible-navigation.js +5 -3
  15. data/lib/assets/javascripts/_modules/search.js +175 -6
  16. data/lib/assets/stylesheets/modules/_collapsible.scss +12 -5
  17. data/lib/assets/stylesheets/modules/_technical-documentation.scss +16 -11
  18. data/lib/assets/stylesheets/modules/_toc.scss +1 -1
  19. data/lib/govuk_tech_docs.rb +13 -2
  20. data/lib/govuk_tech_docs/api_reference/api_reference_extension.rb +100 -0
  21. data/lib/govuk_tech_docs/api_reference/api_reference_renderer.rb +279 -0
  22. data/lib/govuk_tech_docs/api_reference/templates/api_reference_full.html.erb +9 -0
  23. data/lib/govuk_tech_docs/api_reference/templates/operation.html.erb +11 -0
  24. data/lib/govuk_tech_docs/api_reference/templates/parameters.html.erb +28 -0
  25. data/lib/govuk_tech_docs/api_reference/templates/path.html.erb +4 -0
  26. data/lib/govuk_tech_docs/api_reference/templates/responses.html.erb +33 -0
  27. data/lib/govuk_tech_docs/api_reference/templates/schema.html.erb +29 -0
  28. data/lib/govuk_tech_docs/page_review.rb +15 -3
  29. data/lib/govuk_tech_docs/pages.rb +3 -2
  30. data/lib/govuk_tech_docs/tech_docs_html_renderer.rb +10 -0
  31. data/lib/govuk_tech_docs/version.rb +1 -1
  32. data/lib/source/layouts/_header.erb +2 -4
  33. metadata +42 -4
  34. data/lib/source/images/arrow-down.svg +0 -9
  35. data/lib/source/images/arrow-up.svg +0 -9
@@ -11,6 +11,10 @@
11
11
  display: block
12
12
  }
13
13
  }
14
+ .collapsible__heading,
15
+ .toc__list > ul > li > a:link.collapsible__heading {
16
+ margin-right: 30px;
17
+ }
14
18
  .collapsible__toggle {
15
19
  position: absolute;
16
20
  top: 0;
@@ -34,12 +38,15 @@
34
38
  &::after {
35
39
  content: '';
36
40
  display: block;
37
- background: no-repeat file-url('arrow-down.svg') center center;
38
- background-size: 18px auto;
39
- width: 20px;
40
- height: 40px;
41
+ border: 2px solid $black;
42
+ border-width: 2px 2px 0 0;
43
+ transform: rotate(135deg);
44
+ width: 10px;
45
+ height: 10px;
46
+ margin-top: 10px;
41
47
  }
42
48
  .collapsible.is-open &::after {
43
- background-image: file-url('arrow-up.svg');
49
+ transform: rotate(315deg);
50
+ margin-top: 18px;
44
51
  }
45
52
  }
@@ -3,11 +3,11 @@
3
3
  @mixin heading-offset($tabletTopMargin) {
4
4
  // Scale margins with font size on mobile (16/19ths)
5
5
  $mobileTopMargin: ceil($tabletTopMargin * (16 / 19));
6
-
6
+
7
7
  // Offset headings down on mobile so that linking to anchors they appear after
8
8
  // the sticky 'table of contents' element
9
9
  $stickyTocOffset: 20px + $gutter-half + 10px + 1px;
10
-
10
+
11
11
  // Pad the heading so that when linking to an anchor there is at most a
12
12
  // $gutter-half (mobile) or $gutter (tablet and above) sized gap between the
13
13
  // top of the viewport and the heading.
@@ -25,9 +25,9 @@
25
25
  display: block;
26
26
  margin: 0 $gutter-half 10px;
27
27
  max-width: 40em;
28
-
28
+
29
29
  line-height: 1.4;
30
-
30
+
31
31
  color: $text-colour;
32
32
 
33
33
  @include media(tablet) {
@@ -45,14 +45,14 @@
45
45
  @include bold-48;
46
46
  @include heading-offset($gutter * 2);
47
47
  border-top: 5px solid $text-colour;
48
-
48
+
49
49
  &:first-of-type {
50
50
  @include heading-offset($gutter);
51
51
  border-top: none;
52
52
  }
53
53
  }
54
54
 
55
- h2 {
55
+ h2 {
56
56
  @include bold-36;
57
57
  @include heading-offset($gutter * 1.5);
58
58
  }
@@ -125,7 +125,7 @@
125
125
 
126
126
  ol + p, ul + p, .table-container + p {
127
127
  margin-top: ceil($gutter * (16 / 19));
128
-
128
+
129
129
  @include media(tablet) {
130
130
  margin-top: $gutter;
131
131
  }
@@ -144,6 +144,11 @@
144
144
  overflow: auto;
145
145
  position: relative;
146
146
  border: 1px solid $code-02;
147
+ // Restrict the width of pre tags, as they have a tendency grow larger than
148
+ // the viewport when placed within table cells.
149
+ // @todo: Use table-layout: fixed, and remove the max-width definition from
150
+ // .technical-documentation so tables can fill the viewport.
151
+ max-width: 40em;
147
152
  }
148
153
 
149
154
  pre code {
@@ -156,11 +161,11 @@
156
161
  background: $code-01;
157
162
  padding: 3px 5px;
158
163
  border-radius: 1px;
159
-
164
+
160
165
  font-family: monaco, Consolas, "Lucida Console", monospace;
161
166
  font-size: 15px;
162
167
  color: $code-0E;
163
-
168
+
164
169
  @include media(tablet) {
165
170
  font-size: 16px;
166
171
  }
@@ -191,11 +196,11 @@
191
196
  display: block;
192
197
  max-width: 100%;
193
198
  overflow-x: auto;
194
-
199
+
195
200
  margin-top: $gutter-half;
196
201
  }
197
202
 
198
- table {
203
+ table {
199
204
  width: 100%;
200
205
 
201
206
  border-collapse: collapse;
@@ -27,7 +27,7 @@
27
27
 
28
28
  a:link, a:visited {
29
29
  display: block;
30
- padding: 8px 40px 8px $gutter-half;
30
+ padding: 8px $gutter 8px $gutter-half;
31
31
  margin: 0 $gutter-half * -1;
32
32
  border-left: 5px solid transparent;
33
33
 
@@ -20,6 +20,7 @@ require 'govuk_tech_docs/pages'
20
20
  require 'govuk_tech_docs/tech_docs_html_renderer'
21
21
  require 'govuk_tech_docs/unique_identifier_extension'
22
22
  require 'govuk_tech_docs/unique_identifier_generator'
23
+ require 'govuk_tech_docs/api_reference/api_reference_extension'
23
24
 
24
25
  module GovukTechDocs
25
26
  # Configure the tech docs template
@@ -37,7 +38,9 @@ module GovukTechDocs
37
38
  context.set :markdown_engine, :redcarpet
38
39
  context.set :markdown,
39
40
  renderer: TechDocsHTMLRenderer.new(
40
- with_toc_data: true
41
+ with_toc_data: true,
42
+ api: true,
43
+ context: context
41
44
  ),
42
45
  fenced_code_blocks: true,
43
46
  tables: true,
@@ -56,6 +59,8 @@ module GovukTechDocs
56
59
  context.config[:tech_docs] = YAML.load_file('config/tech-docs.yml').with_indifferent_access
57
60
  context.activate :unique_identifier
58
61
 
62
+ context.activate :api_reference
63
+
59
64
  context.helpers do
60
65
  include GovukTechDocs::TableOfContents::Helpers
61
66
  include GovukTechDocs::ContributionBanner
@@ -65,7 +70,7 @@ module GovukTechDocs
65
70
  end
66
71
 
67
72
  def current_page_review
68
- @current_page_review ||= GovukTechDocs::PageReview.new(current_page)
73
+ @current_page_review ||= GovukTechDocs::PageReview.new(current_page, config)
69
74
  end
70
75
 
71
76
  def format_date(date)
@@ -102,6 +107,12 @@ module GovukTechDocs
102
107
  content: { boost: 50, store: true },
103
108
  url: { index: false, store: true },
104
109
  }
110
+
111
+ search.pipeline_remove = [
112
+ 'stopWordFilter'
113
+ ]
114
+
115
+ search.tokenizer_separator = '/[\s\-/]+/'
105
116
  end
106
117
  end
107
118
  end
@@ -0,0 +1,100 @@
1
+ require 'erb'
2
+ require 'openapi3_parser'
3
+ require 'uri'
4
+ require 'pry'
5
+ require 'govuk_tech_docs/api_reference/api_reference_renderer'
6
+
7
+ module GovukTechDocs
8
+ module ApiReference
9
+ class Extension < Middleman::Extension
10
+ expose_to_application api: :api
11
+
12
+ def initialize(app, options_hash = {}, &block)
13
+ super
14
+
15
+ @app = app
16
+ @config = @app.config[:tech_docs]
17
+
18
+ # If no api path then just return.
19
+ if @config['api_path'].to_s.empty?
20
+ raise 'No api path defined in tech-docs.yml'
21
+ end
22
+
23
+ # Is the api_path a url or path?
24
+ if uri?(@config['api_path'])
25
+ @api_parser = true
26
+ @document = Openapi3Parser.load_url(@config['api_path'])
27
+ elsif File.exist?(@config['api_path'])
28
+ # Load api file and set existence flag.
29
+ @api_parser = true
30
+ @document = Openapi3Parser.load_file(@config['api_path'])
31
+ else
32
+ @api_parser = false
33
+ raise 'Unable to load api path from tech-docs.yml'
34
+ end
35
+ @render = Renderer.new(@app, @document)
36
+ end
37
+
38
+ def uri?(string)
39
+ uri = URI.parse(string)
40
+ %w(http https).include?(uri.scheme)
41
+ rescue URI::BadURIError
42
+ false
43
+ rescue URI::InvalidURIError
44
+ false
45
+ end
46
+
47
+ def api(text)
48
+ if @api_parser == true
49
+
50
+ keywords = {
51
+ 'api&gt;' => 'default',
52
+ 'api_schema&gt;' => 'schema'
53
+ }
54
+
55
+ regexp = keywords.map { |k, _| Regexp.escape(k) }.join('|')
56
+
57
+ md = text.match(/^<p>(#{regexp})/)
58
+ if md
59
+ key = md.captures[0]
60
+ type = keywords[key]
61
+
62
+ text.gsub!(/#{ Regexp.escape(key) }\s+?/, '')
63
+
64
+ # Strip paragraph tags from text
65
+ text = text.gsub(/<\/?[^>]*>/, '')
66
+ text = text.strip
67
+
68
+ if text == 'api&gt;'
69
+ @render.api_full(api_info, api_server)
70
+ elsif type == 'default'
71
+ output = @render.path(text)
72
+ # Render any schemas referenced in the above path
73
+ output += @render.schemas_from_path(text)
74
+ output
75
+ else
76
+ @render.schema(text)
77
+ end
78
+
79
+ else
80
+ return text
81
+ end
82
+ else
83
+ text
84
+ end
85
+ end
86
+
87
+ private
88
+
89
+ def api_info
90
+ @document.info
91
+ end
92
+
93
+ def api_server
94
+ @document.servers[0]
95
+ end
96
+ end
97
+ end
98
+ end
99
+
100
+ ::Middleman::Extensions.register(:api_reference, GovukTechDocs::ApiReference::Extension)
@@ -0,0 +1,279 @@
1
+ require 'erb'
2
+ require 'json'
3
+
4
+ module GovukTechDocs
5
+ module ApiReference
6
+ class Renderer
7
+ def initialize(app, document)
8
+ @app = app
9
+ @document = document
10
+
11
+ # Load template files
12
+ @template_api_full = get_renderer('api_reference_full.html.erb')
13
+ @template_path = get_renderer('path.html.erb')
14
+ @template_schema = get_renderer('schema.html.erb')
15
+ @template_operation = get_renderer('operation.html.erb')
16
+ @template_parameters = get_renderer('parameters.html.erb')
17
+ @template_responses = get_renderer('responses.html.erb')
18
+ end
19
+
20
+ def api_full(info, server)
21
+ paths = ''
22
+ paths_data = @document.paths
23
+ paths_data.each do |path_data|
24
+ # For some reason paths.each returns an array of arrays [title, object]
25
+ # instead of an array of objects
26
+ text = path_data[0]
27
+ paths += path(text)
28
+ end
29
+ schemas = ''
30
+ schemas_data = @document.components.schemas
31
+ schemas_data.each do |schema_data|
32
+ text = schema_data[0]
33
+ schemas += schema(text)
34
+ end
35
+ @template_api_full.result(binding)
36
+ end
37
+
38
+ def path(text)
39
+ path = @document.paths[text]
40
+ id = text.parameterize
41
+ operations = operations(path, id)
42
+ @template_path.result(binding)
43
+ end
44
+
45
+ def schema(text)
46
+ schemas = ''
47
+ schemas_data = @document.components.schemas
48
+ schemas_data.each do |schema_data|
49
+ all_of = schema_data[1]["allOf"]
50
+ properties = []
51
+ if !all_of.blank?
52
+ all_of.each do |schema_nested|
53
+ schema_nested.properties.each do |property|
54
+ properties.push property
55
+ end
56
+ end
57
+ end
58
+
59
+ schema_data[1].properties.each do |property|
60
+ properties.push property
61
+ end
62
+
63
+ if schema_data[0] == text
64
+ title = schema_data[0]
65
+ schema = schema_data[1]
66
+ return @template_schema.result(binding)
67
+ end
68
+ end
69
+ end
70
+
71
+ def schemas_from_path(text)
72
+ path = @document.paths[text]
73
+ operations = get_operations(path)
74
+ # Get all referenced schemas
75
+ schemas = []
76
+ operations.compact.each_value do |operation|
77
+ responses = operation.responses
78
+ responses.each do |_rkey, response|
79
+ if response.content['application/json']
80
+ schema = response.content['application/json'].schema
81
+ schema_name = get_schema_name(schema.node_context.source_location.to_s)
82
+ if !schema_name.nil?
83
+ schemas.push schema_name
84
+ end
85
+ schemas.concat(schemas_from_schema(schema))
86
+ end
87
+ end
88
+ end
89
+ # Render all referenced schemas
90
+ output = ''
91
+ schemas.uniq.each do |schema_name|
92
+ output += schema(schema_name)
93
+ end
94
+ if !output.empty?
95
+ output.prepend('<h2 id="schemas">Schemas</h2>')
96
+ end
97
+ output
98
+ end
99
+
100
+ def schemas_from_schema(schema)
101
+ schemas = []
102
+ properties = []
103
+ schema.properties.each do |property|
104
+ properties.push property[1]
105
+ end
106
+ if schema.type == 'array'
107
+ properties.push schema.items
108
+ end
109
+ all_of = schema["allOf"]
110
+ if !all_of.blank?
111
+ all_of.each do |schema_nested|
112
+ schema_nested.properties.each do |property|
113
+ properties.push property[1]
114
+ end
115
+ end
116
+ end
117
+ properties.each do |property|
118
+ # Must be a schema be referenced by another schema
119
+ # And not a property of a schema
120
+ if property.node_context.referenced_by.to_s.include?('#/components/schemas') &&
121
+ !property.node_context.source_location.to_s.include?('/properties/')
122
+ schema_name = get_schema_name(property.node_context.source_location.to_s)
123
+ end
124
+ if !schema_name.nil?
125
+ schemas.push schema_name
126
+ end
127
+ # Check sub-properties for references
128
+ schemas.concat(schemas_from_schema(property))
129
+ end
130
+ schemas
131
+ end
132
+
133
+ def operations(path, path_id)
134
+ output = ''
135
+ operations = get_operations(path)
136
+ operations.compact.each do |key, operation|
137
+ id = "#{path_id}-#{key.parameterize}"
138
+ parameters = parameters(operation, id)
139
+ responses = responses(operation, id)
140
+ output += @template_operation.result(binding)
141
+ end
142
+ output
143
+ end
144
+
145
+ def parameters(operation, operation_id)
146
+ parameters = operation.parameters
147
+ id = "#{operation_id}-parameters"
148
+ output = @template_parameters.result(binding)
149
+ output
150
+ end
151
+
152
+ def responses(operation, operation_id)
153
+ responses = operation.responses
154
+ id = "#{operation_id}-responses"
155
+ output = @template_responses.result(binding)
156
+ output
157
+ end
158
+
159
+ def markdown(text)
160
+ if text
161
+ Tilt['markdown'].new(context: @app) { text }.render
162
+ end
163
+ end
164
+
165
+ def json_output(schema)
166
+ properties = schema_properties(schema)
167
+ JSON.pretty_generate(properties)
168
+ end
169
+
170
+ def json_prettyprint(data)
171
+ JSON.pretty_generate(data)
172
+ end
173
+
174
+ def schema_properties(schema_data)
175
+ properties = Hash.new
176
+ if defined? schema_data.properties
177
+ schema_data.properties.each do |key, property|
178
+ properties[key] = property
179
+ end
180
+ end
181
+ properties.merge! get_all_of_hash(schema_data)
182
+ properties_hash = Hash.new
183
+ properties.each do |pkey, property|
184
+ if property.type == 'object'
185
+ properties_hash[pkey] = Hash.new
186
+ items = property.items
187
+ if !items.blank?
188
+ properties_hash[pkey] = schema_properties(items)
189
+ end
190
+ if !property.properties.blank?
191
+ properties_hash[pkey] = schema_properties(property)
192
+ end
193
+ elsif property.type == 'array'
194
+ properties_hash[pkey] = Array.new
195
+ items = property.items
196
+ if !items.blank?
197
+ properties_hash[pkey].push schema_properties(items)
198
+ end
199
+ else
200
+ properties_hash[pkey] = !property.example.nil? ? property.example : property.type
201
+ end
202
+ end
203
+
204
+ properties_hash
205
+ end
206
+
207
+ private
208
+
209
+ def get_all_of_array(schema)
210
+ properties = Array.new
211
+ if schema.is_a?(Array)
212
+ schema = schema[1]
213
+ end
214
+ if schema["allOf"]
215
+ all_of = schema["allOf"]
216
+ end
217
+ if !all_of.blank?
218
+ all_of.each do |schema_nested|
219
+ schema_nested.properties.each do |property|
220
+ if property.is_a?(Array)
221
+ property = property[1]
222
+ end
223
+ properties.push property
224
+ end
225
+ end
226
+ end
227
+ properties
228
+ end
229
+
230
+ def get_all_of_hash(schema)
231
+ properties = Hash.new
232
+ if schema["allOf"]
233
+ all_of = schema["allOf"]
234
+ end
235
+ if !all_of.blank?
236
+ all_of.each do |schema_nested|
237
+ schema_nested.properties.each do |key, property|
238
+ properties[key] = property
239
+ end
240
+ end
241
+ end
242
+ properties
243
+ end
244
+
245
+ def get_renderer(file)
246
+ template_path = File.join(File.dirname(__FILE__), 'templates/' + file)
247
+ template = File.open(template_path, 'r').read
248
+ ERB.new(template)
249
+ end
250
+
251
+ def get_operations(path)
252
+ operations = {}
253
+ operations['get'] = path.get if defined? path.get
254
+ operations['put'] = path.put if defined? path.put
255
+ operations['post'] = path.post if defined? path.post
256
+ operations['delete'] = path.delete if defined? path.delete
257
+ operations['patch'] = path.patch if defined? path.patch
258
+ operations
259
+ end
260
+
261
+ def get_schema_name(text)
262
+ unless text.is_a?(String)
263
+ return nil
264
+ end
265
+ # Schema dictates that it's always components['schemas']
266
+ text.gsub(/#\/components\/schemas\//, '')
267
+ end
268
+
269
+ def get_schema_link(schema)
270
+ schema_name = get_schema_name schema.node_context.source_location.to_s
271
+ if !schema_name.nil?
272
+ id = "schema-#{schema_name.parameterize}"
273
+ output = "<a href='\##{id}'>#{schema_name}</a>"
274
+ output
275
+ end
276
+ end
277
+ end
278
+ end
279
+ end