govuk_tech_docs 1.5.0 → 1.6.0

Sign up to get free protection for your applications and to get access to all the features.
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