graphql-docs 5.2.0 → 6.0.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/.yardopts +9 -0
- data/CHANGELOG.md +76 -0
- data/README.md +404 -8
- data/bin/console +6 -7
- data/bin/setup +8 -5
- data/exe/graphql-docs +9 -9
- data/graphql-docs.gemspec +50 -38
- data/lib/graphql-docs/app.rb +488 -0
- data/lib/graphql-docs/configuration.rb +28 -11
- data/lib/graphql-docs/generator.rb +147 -42
- data/lib/graphql-docs/helpers.rb +132 -12
- data/lib/graphql-docs/layouts/assets/_sass/_api-box.scss +2 -2
- data/lib/graphql-docs/layouts/assets/_sass/_content.scss +20 -19
- data/lib/graphql-docs/layouts/assets/_sass/_deprecations.scss +7 -0
- data/lib/graphql-docs/layouts/assets/_sass/_fonts.scss +6 -21
- data/lib/graphql-docs/layouts/assets/_sass/_header.scss +8 -2
- data/lib/graphql-docs/layouts/assets/_sass/_mobile.scss +10 -3
- data/lib/graphql-docs/layouts/assets/_sass/_search.scss +32 -10
- data/lib/graphql-docs/layouts/assets/_sass/_sidebar.scss +22 -15
- data/lib/graphql-docs/layouts/assets/_sass/_syntax.scss +1 -1
- data/lib/graphql-docs/layouts/assets/css/screen.scss +54 -5
- data/lib/graphql-docs/layouts/default.html +75 -1
- data/lib/graphql-docs/layouts/includes/sidebar.html +4 -2
- data/lib/graphql-docs/parser.rb +89 -33
- data/lib/graphql-docs/renderer.rb +98 -31
- data/lib/graphql-docs/version.rb +2 -1
- data/lib/graphql-docs.rb +78 -12
- data/lib/tasks/graphql-docs.rake +57 -0
- metadata +74 -78
- data/.github/ISSUE_TEMPLATE/bug_report.md +0 -31
- data/.github/ISSUE_TEMPLATE/feature_request.md +0 -20
- data/.github/workflows/tests.yml +0 -21
- data/.gitignore +0 -15
- data/.rubocop.yml +0 -11
- data/CODE_OF_CONDUCT.md +0 -84
- data/CONTRIBUTING.md +0 -18
- data/Gemfile +0 -6
- data/Rakefile +0 -86
- data/lib/graphql-docs/layouts/assets/images/graphiql-headers.png +0 -0
- data/lib/graphql-docs/layouts/assets/images/graphiql-variables.png +0 -0
- data/lib/graphql-docs/layouts/assets/images/graphiql.png +0 -0
- data/lib/graphql-docs/layouts/assets/images/search.svg +0 -3
- data/lib/graphql-docs/layouts/assets/webfonts/2C4B9D_B_0.eot +0 -0
- data/lib/graphql-docs/layouts/assets/webfonts/2C4B9D_B_0.ttf +0 -0
- data/lib/graphql-docs/layouts/assets/webfonts/2C4B9D_B_0.woff +0 -0
- data/lib/graphql-docs/layouts/assets/webfonts/2C4B9D_B_0.woff2 +0 -0
- data/lib/graphql-docs/layouts/assets/webfonts/2C4B9D_C_0.eot +0 -0
- data/lib/graphql-docs/layouts/assets/webfonts/2C4B9D_C_0.ttf +0 -0
- data/lib/graphql-docs/layouts/assets/webfonts/2C4B9D_C_0.woff +0 -0
- data/lib/graphql-docs/layouts/assets/webfonts/2C4B9D_C_0.woff2 +0 -0
- data/lib/graphql-docs/layouts/assets/webfonts/2C4B9D_D_0.eot +0 -0
- data/lib/graphql-docs/layouts/assets/webfonts/2C4B9D_D_0.ttf +0 -0
- data/lib/graphql-docs/layouts/assets/webfonts/2C4B9D_D_0.woff +0 -0
- data/lib/graphql-docs/layouts/assets/webfonts/2C4B9D_D_0.woff2 +0 -0
- data/lib/graphql-docs/layouts/assets/webfonts/2C4B9D_E_0.eot +0 -0
- data/lib/graphql-docs/layouts/assets/webfonts/2C4B9D_E_0.ttf +0 -0
- data/lib/graphql-docs/layouts/assets/webfonts/2C4B9D_E_0.woff +0 -0
- data/lib/graphql-docs/layouts/assets/webfonts/2C4B9D_E_0.woff2 +0 -0
|
@@ -1,16 +1,42 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require
|
|
4
|
-
require
|
|
5
|
-
require
|
|
6
|
-
require
|
|
3
|
+
require "erb"
|
|
4
|
+
require "fileutils"
|
|
5
|
+
require "sass-embedded"
|
|
6
|
+
require "ostruct"
|
|
7
7
|
|
|
8
8
|
module GraphQLDocs
|
|
9
|
+
# Generates HTML documentation files from a parsed GraphQL schema.
|
|
10
|
+
#
|
|
11
|
+
# The Generator takes the parsed schema structure and creates individual HTML pages
|
|
12
|
+
# for each GraphQL type, along with landing pages and static assets. It uses ERB
|
|
13
|
+
# templates for layout and the Renderer for converting content to HTML.
|
|
14
|
+
#
|
|
15
|
+
# @example Basic usage
|
|
16
|
+
# generator = GraphQLDocs::Generator.new(parsed_schema, options)
|
|
17
|
+
# generator.generate
|
|
18
|
+
#
|
|
19
|
+
# @see Parser
|
|
20
|
+
# @see Renderer
|
|
9
21
|
class Generator
|
|
10
22
|
include Helpers
|
|
11
23
|
|
|
24
|
+
# @!attribute [rw] parsed_schema
|
|
25
|
+
# @return [Hash] The parsed schema structure from {Parser}
|
|
12
26
|
attr_accessor :parsed_schema
|
|
13
27
|
|
|
28
|
+
# Initializes a new Generator instance.
|
|
29
|
+
#
|
|
30
|
+
# Sets up ERB templates for all GraphQL types and landing pages. Validates that
|
|
31
|
+
# all required template files exist before generation begins.
|
|
32
|
+
#
|
|
33
|
+
# @param parsed_schema [Hash] The parsed schema from {Parser#parse}
|
|
34
|
+
# @param options [Hash] Configuration options
|
|
35
|
+
# @option options [Hash] :templates Paths to ERB template files
|
|
36
|
+
# @option options [Hash] :landing_pages Paths to landing page files
|
|
37
|
+
# @option options [Class] :renderer Renderer class to use
|
|
38
|
+
#
|
|
39
|
+
# @raise [IOError] If any required template or landing page file is not found
|
|
14
40
|
def initialize(parsed_schema, options)
|
|
15
41
|
@parsed_schema = parsed_schema
|
|
16
42
|
@options = options
|
|
@@ -31,9 +57,9 @@ module GraphQLDocs
|
|
|
31
57
|
end
|
|
32
58
|
|
|
33
59
|
landing_page_contents = File.read(@options[:landing_pages][sym])
|
|
34
|
-
metadata =
|
|
60
|
+
metadata = ""
|
|
35
61
|
|
|
36
|
-
if File.extname(
|
|
62
|
+
if File.extname(@options[:landing_pages][sym]) == ".erb"
|
|
37
63
|
opts = @options.merge(@options[:landing_pages][:variables]).merge(helper_methods)
|
|
38
64
|
if yaml?(landing_page_contents)
|
|
39
65
|
metadata, landing_page = split_into_metadata_and_contents(landing_page_contents, parse: false)
|
|
@@ -49,6 +75,17 @@ module GraphQLDocs
|
|
|
49
75
|
end
|
|
50
76
|
end
|
|
51
77
|
|
|
78
|
+
# Generates all HTML documentation files.
|
|
79
|
+
#
|
|
80
|
+
# This is the main generation method that creates:
|
|
81
|
+
# - Individual pages for each GraphQL type
|
|
82
|
+
# - Landing pages for type categories
|
|
83
|
+
# - Static assets (CSS, fonts, images) if using default styles
|
|
84
|
+
#
|
|
85
|
+
# @return [Boolean] Returns true on successful generation
|
|
86
|
+
#
|
|
87
|
+
# @example
|
|
88
|
+
# generator.generate # Creates all docs in output directory
|
|
52
89
|
def generate
|
|
53
90
|
FileUtils.rm_rf(@options[:output_dir]) if @options[:delete_output]
|
|
54
91
|
|
|
@@ -63,44 +100,47 @@ module GraphQLDocs
|
|
|
63
100
|
create_graphql_scalar_pages
|
|
64
101
|
create_graphql_directive_pages
|
|
65
102
|
|
|
66
|
-
write_file(
|
|
103
|
+
write_file("static", "index", @graphql_index_landing_page, trim: false) unless @graphql_index_landing_page.nil?
|
|
67
104
|
|
|
68
|
-
write_file(
|
|
105
|
+
write_file("static", "object", @graphql_object_landing_page, trim: false) unless @graphql_object_landing_page.nil?
|
|
69
106
|
|
|
70
|
-
write_file(
|
|
107
|
+
write_file("operation", "query", @graphql_query_landing_page, trim: false) if !@graphql_query_landing_page.nil? && !has_query
|
|
71
108
|
|
|
72
|
-
write_file(
|
|
109
|
+
write_file("operation", "mutation", @graphql_mutation_landing_page, trim: false) unless @graphql_mutation_landing_page.nil?
|
|
73
110
|
|
|
74
|
-
write_file(
|
|
111
|
+
write_file("static", "interface", @graphql_interface_landing_page, trim: false) unless @graphql_interface_landing_page.nil?
|
|
75
112
|
|
|
76
|
-
write_file(
|
|
113
|
+
write_file("static", "enum", @graphql_enum_landing_page, trim: false) unless @graphql_enum_landing_page.nil?
|
|
77
114
|
|
|
78
|
-
write_file(
|
|
115
|
+
write_file("static", "union", @graphql_union_landing_page, trim: false) unless @graphql_union_landing_page.nil?
|
|
79
116
|
|
|
80
|
-
write_file(
|
|
117
|
+
write_file("static", "input_object", @graphql_input_object_landing_page, trim: false) unless @graphql_input_object_landing_page.nil?
|
|
81
118
|
|
|
82
|
-
write_file(
|
|
119
|
+
write_file("static", "scalar", @graphql_scalar_landing_page, trim: false) unless @graphql_scalar_landing_page.nil?
|
|
83
120
|
|
|
84
|
-
write_file(
|
|
121
|
+
write_file("static", "directive", @graphql_directive_landing_page, trim: false) unless @graphql_directive_landing_page.nil?
|
|
85
122
|
|
|
86
123
|
if @options[:use_default_styles]
|
|
87
|
-
assets_dir = File.join(File.dirname(__FILE__),
|
|
88
|
-
FileUtils.mkdir_p(File.join(@options[:output_dir],
|
|
124
|
+
assets_dir = File.join(File.dirname(__FILE__), "layouts", "assets")
|
|
125
|
+
FileUtils.mkdir_p(File.join(@options[:output_dir], "assets"))
|
|
89
126
|
|
|
90
|
-
css = Sass.compile(File.join(assets_dir,
|
|
91
|
-
File.write(File.join(@options[:output_dir],
|
|
127
|
+
css = Sass.compile(File.join(assets_dir, "css", "screen.scss")).css
|
|
128
|
+
File.write(File.join(@options[:output_dir], "assets", "style.css"), css)
|
|
92
129
|
|
|
93
|
-
FileUtils.cp_r(File.join(assets_dir,
|
|
94
|
-
FileUtils.cp_r(File.join(assets_dir, 'webfonts'), File.join(@options[:output_dir], 'assets'))
|
|
130
|
+
FileUtils.cp_r(File.join(assets_dir, "images"), File.join(@options[:output_dir], "assets"))
|
|
95
131
|
end
|
|
96
132
|
|
|
97
133
|
true
|
|
98
134
|
end
|
|
99
135
|
|
|
136
|
+
# Creates HTML pages for GraphQL operation types (Query, Mutation).
|
|
137
|
+
#
|
|
138
|
+
# @return [Boolean] Returns true if Query type was found and generated
|
|
139
|
+
# @api private
|
|
100
140
|
def create_graphql_operation_pages
|
|
101
141
|
graphql_operation_types.each do |query_type|
|
|
102
|
-
metadata =
|
|
103
|
-
next unless query_type[:name] == graphql_root_types[
|
|
142
|
+
metadata = ""
|
|
143
|
+
next unless query_type[:name] == graphql_root_types["query"]
|
|
104
144
|
|
|
105
145
|
unless @options[:landing_pages][:query].nil?
|
|
106
146
|
query_landing_page = @options[:landing_pages][:query]
|
|
@@ -115,90 +155,117 @@ module GraphQLDocs
|
|
|
115
155
|
end
|
|
116
156
|
opts = default_generator_options(type: query_type)
|
|
117
157
|
contents = @graphql_operations_template.result(OpenStruct.new(opts).instance_eval { binding })
|
|
118
|
-
write_file(
|
|
158
|
+
write_file("operation", "query", metadata + contents)
|
|
119
159
|
return true
|
|
120
160
|
end
|
|
121
161
|
false
|
|
122
162
|
end
|
|
123
163
|
|
|
164
|
+
# Creates HTML pages for GraphQL object types.
|
|
165
|
+
# @return [void]
|
|
166
|
+
# @api private
|
|
124
167
|
def create_graphql_object_pages
|
|
125
168
|
graphql_object_types.each do |object_type|
|
|
126
169
|
opts = default_generator_options(type: object_type)
|
|
127
170
|
|
|
128
171
|
contents = @graphql_objects_template.result(OpenStruct.new(opts).instance_eval { binding })
|
|
129
|
-
write_file(
|
|
172
|
+
write_file("object", object_type[:name], contents)
|
|
130
173
|
end
|
|
131
174
|
end
|
|
132
175
|
|
|
176
|
+
# Creates HTML pages for GraphQL query fields.
|
|
177
|
+
# @return [void]
|
|
178
|
+
# @api private
|
|
133
179
|
def create_graphql_query_pages
|
|
134
180
|
graphql_query_types.each do |query|
|
|
135
181
|
opts = default_generator_options(type: query)
|
|
136
182
|
|
|
137
183
|
contents = @graphql_queries_template.result(OpenStruct.new(opts).instance_eval { binding })
|
|
138
|
-
write_file(
|
|
184
|
+
write_file("query", query[:name], contents)
|
|
139
185
|
end
|
|
140
186
|
end
|
|
141
187
|
|
|
188
|
+
# Creates HTML pages for GraphQL mutation fields.
|
|
189
|
+
# @return [void]
|
|
190
|
+
# @api private
|
|
142
191
|
def create_graphql_mutation_pages
|
|
143
192
|
graphql_mutation_types.each do |mutation|
|
|
144
193
|
opts = default_generator_options(type: mutation)
|
|
145
194
|
|
|
146
195
|
contents = @graphql_mutations_template.result(OpenStruct.new(opts).instance_eval { binding })
|
|
147
|
-
write_file(
|
|
196
|
+
write_file("mutation", mutation[:name], contents)
|
|
148
197
|
end
|
|
149
198
|
end
|
|
150
199
|
|
|
200
|
+
# Creates HTML pages for GraphQL interface types.
|
|
201
|
+
# @return [void]
|
|
202
|
+
# @api private
|
|
151
203
|
def create_graphql_interface_pages
|
|
152
204
|
graphql_interface_types.each do |interface_type|
|
|
153
205
|
opts = default_generator_options(type: interface_type)
|
|
154
206
|
|
|
155
207
|
contents = @graphql_interfaces_template.result(OpenStruct.new(opts).instance_eval { binding })
|
|
156
|
-
write_file(
|
|
208
|
+
write_file("interface", interface_type[:name], contents)
|
|
157
209
|
end
|
|
158
210
|
end
|
|
159
211
|
|
|
212
|
+
# Creates HTML pages for GraphQL enum types.
|
|
213
|
+
# @return [void]
|
|
214
|
+
# @api private
|
|
160
215
|
def create_graphql_enum_pages
|
|
161
216
|
graphql_enum_types.each do |enum_type|
|
|
162
217
|
opts = default_generator_options(type: enum_type)
|
|
163
218
|
|
|
164
219
|
contents = @graphql_enums_template.result(OpenStruct.new(opts).instance_eval { binding })
|
|
165
|
-
write_file(
|
|
220
|
+
write_file("enum", enum_type[:name], contents)
|
|
166
221
|
end
|
|
167
222
|
end
|
|
168
223
|
|
|
224
|
+
# Creates HTML pages for GraphQL union types.
|
|
225
|
+
# @return [void]
|
|
226
|
+
# @api private
|
|
169
227
|
def create_graphql_union_pages
|
|
170
228
|
graphql_union_types.each do |union_type|
|
|
171
229
|
opts = default_generator_options(type: union_type)
|
|
172
230
|
|
|
173
231
|
contents = @graphql_unions_template.result(OpenStruct.new(opts).instance_eval { binding })
|
|
174
|
-
write_file(
|
|
232
|
+
write_file("union", union_type[:name], contents)
|
|
175
233
|
end
|
|
176
234
|
end
|
|
177
235
|
|
|
236
|
+
# Creates HTML pages for GraphQL input object types.
|
|
237
|
+
# @return [void]
|
|
238
|
+
# @api private
|
|
178
239
|
def create_graphql_input_object_pages
|
|
179
240
|
graphql_input_object_types.each do |input_object_type|
|
|
180
241
|
opts = default_generator_options(type: input_object_type)
|
|
181
242
|
|
|
182
243
|
contents = @graphql_input_objects_template.result(OpenStruct.new(opts).instance_eval { binding })
|
|
183
|
-
write_file(
|
|
244
|
+
write_file("input_object", input_object_type[:name], contents)
|
|
184
245
|
end
|
|
185
246
|
end
|
|
186
247
|
|
|
248
|
+
# Creates HTML pages for GraphQL scalar types.
|
|
249
|
+
# @return [void]
|
|
250
|
+
# @api private
|
|
187
251
|
def create_graphql_scalar_pages
|
|
188
252
|
graphql_scalar_types.each do |scalar_type|
|
|
189
253
|
opts = default_generator_options(type: scalar_type)
|
|
190
254
|
|
|
191
255
|
contents = @graphql_scalars_template.result(OpenStruct.new(opts).instance_eval { binding })
|
|
192
|
-
write_file(
|
|
256
|
+
write_file("scalar", scalar_type[:name], contents)
|
|
193
257
|
end
|
|
194
258
|
end
|
|
195
259
|
|
|
260
|
+
# Creates HTML pages for GraphQL directives.
|
|
261
|
+
# @return [void]
|
|
262
|
+
# @api private
|
|
196
263
|
def create_graphql_directive_pages
|
|
197
264
|
graphql_directive_types.each do |directive_type|
|
|
198
265
|
opts = default_generator_options(type: directive_type)
|
|
199
266
|
|
|
200
267
|
contents = @graphql_directives_template.result(OpenStruct.new(opts).instance_eval { binding })
|
|
201
|
-
write_file(
|
|
268
|
+
write_file("directive", directive_type[:name], contents)
|
|
202
269
|
end
|
|
203
270
|
end
|
|
204
271
|
|
|
@@ -209,8 +276,8 @@ module GraphQLDocs
|
|
|
209
276
|
end
|
|
210
277
|
|
|
211
278
|
def write_file(type, name, contents, trim: true)
|
|
212
|
-
if type ==
|
|
213
|
-
if name ==
|
|
279
|
+
if type == "static"
|
|
280
|
+
if name == "index"
|
|
214
281
|
path = @options[:output_dir]
|
|
215
282
|
else
|
|
216
283
|
path = File.join(@options[:output_dir], name)
|
|
@@ -221,21 +288,59 @@ module GraphQLDocs
|
|
|
221
288
|
FileUtils.mkdir_p(path)
|
|
222
289
|
end
|
|
223
290
|
|
|
291
|
+
metadata = {}
|
|
224
292
|
if yaml?(contents)
|
|
225
293
|
# Split data
|
|
226
|
-
|
|
227
|
-
@options = @options.merge(meta)
|
|
294
|
+
metadata, contents = split_into_metadata_and_contents(contents)
|
|
228
295
|
end
|
|
229
296
|
|
|
230
297
|
if trim
|
|
231
298
|
# normalize spacing so that CommonMarker doesn't treat it as `pre`
|
|
232
|
-
contents.gsub!(/^\s+$/,
|
|
233
|
-
contents.gsub!(/^\s{4}/m,
|
|
299
|
+
contents.gsub!(/^\s+$/, "")
|
|
300
|
+
contents.gsub!(/^\s{4}/m, " ")
|
|
234
301
|
end
|
|
235
302
|
|
|
236
|
-
filename = File.join(path,
|
|
237
|
-
contents =
|
|
303
|
+
filename = File.join(path, "index.html")
|
|
304
|
+
contents = render_with_metadata(contents, metadata, type: type, name: name, filename: filename)
|
|
238
305
|
File.write(filename, contents) unless contents.nil?
|
|
239
306
|
end
|
|
307
|
+
|
|
308
|
+
# Renders content with optional metadata, without polluting @options.
|
|
309
|
+
#
|
|
310
|
+
# File Isolation:
|
|
311
|
+
# This method ensures that YAML frontmatter metadata from one file doesn't
|
|
312
|
+
# pollute or leak into other files during batch generation. It achieves this
|
|
313
|
+
# through an immutable options pattern:
|
|
314
|
+
#
|
|
315
|
+
# 1. When metadata is present (from YAML frontmatter), a NEW temporary renderer
|
|
316
|
+
# is created with merged options, keeping @options and @renderer unchanged.
|
|
317
|
+
# 2. When no metadata is present, the shared @renderer is used directly
|
|
318
|
+
# (safe because @options and @renderer remain unchanged throughout generation).
|
|
319
|
+
#
|
|
320
|
+
# This prevents a bug where title or other metadata from file1.md would
|
|
321
|
+
# persist and incorrectly appear in file2.md if they were processed sequentially.
|
|
322
|
+
#
|
|
323
|
+
# Example:
|
|
324
|
+
# # file1.md has "title: Custom Title"
|
|
325
|
+
# # file2.md has no YAML frontmatter
|
|
326
|
+
# # Without this isolation, file2.md would incorrectly get "Custom Title"
|
|
327
|
+
#
|
|
328
|
+
# @param contents [String] Content to render
|
|
329
|
+
# @param metadata [Hash] Metadata extracted from YAML frontmatter
|
|
330
|
+
# @param type [String] Type category
|
|
331
|
+
# @param name [String] Type name
|
|
332
|
+
# @param filename [String] Output filename
|
|
333
|
+
# @return [String, nil] Rendered HTML content
|
|
334
|
+
#
|
|
335
|
+
# @api private
|
|
336
|
+
def render_with_metadata(contents, metadata, type:, name:, filename:)
|
|
337
|
+
if metadata.is_a?(Hash) && metadata.any?
|
|
338
|
+
temp_options = @options.merge(metadata)
|
|
339
|
+
temp_renderer = @options[:renderer].new(@parsed_schema, temp_options)
|
|
340
|
+
temp_renderer.render(contents, type: type, name: name, filename: filename)
|
|
341
|
+
else
|
|
342
|
+
@renderer.render(contents, type: type, name: name, filename: filename)
|
|
343
|
+
end
|
|
344
|
+
end
|
|
240
345
|
end
|
|
241
346
|
end
|
data/lib/graphql-docs/helpers.rb
CHANGED
|
@@ -1,77 +1,183 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require
|
|
4
|
-
require
|
|
3
|
+
require "commonmarker"
|
|
4
|
+
require "gemoji"
|
|
5
|
+
require "ostruct"
|
|
5
6
|
|
|
6
7
|
module GraphQLDocs
|
|
8
|
+
# Helper methods module for use in ERB templates.
|
|
9
|
+
#
|
|
10
|
+
# This module provides utility methods that can be called from within ERB templates
|
|
11
|
+
# when generating documentation. Methods are available via dot notation in templates,
|
|
12
|
+
# such as `<%= slugify.(text) %>`.
|
|
13
|
+
#
|
|
14
|
+
# @example Using helper methods in templates
|
|
15
|
+
# <%= slugify.("My Type Name") %> # => "my-type-name"
|
|
16
|
+
# <%= markdownify.("**bold text**") %> # => "<strong>bold text</strong>"
|
|
7
17
|
module Helpers
|
|
18
|
+
# Regular expression for slugifying strings in a URL-friendly way.
|
|
19
|
+
# Matches all characters that are not alphanumeric or common URL-safe characters.
|
|
8
20
|
SLUGIFY_PRETTY_REGEXP = Regexp.new("[^[:alnum:]._~!$&'()+,;=@]+").freeze
|
|
9
21
|
|
|
22
|
+
# @!attribute [rw] templates
|
|
23
|
+
# @return [Hash] Cache of loaded ERB templates for includes
|
|
10
24
|
attr_accessor :templates
|
|
11
25
|
|
|
26
|
+
# Converts a string into a URL-friendly slug.
|
|
27
|
+
#
|
|
28
|
+
# @param str [String] The string to slugify
|
|
29
|
+
# @return [String] Lowercase slug with hyphens instead of spaces
|
|
30
|
+
#
|
|
31
|
+
# @example
|
|
32
|
+
# slugify("My Type Name") # => "my-type-name"
|
|
33
|
+
# slugify("Author.firstName") # => "author.firstname"
|
|
12
34
|
def slugify(str)
|
|
13
|
-
slug = str.gsub(SLUGIFY_PRETTY_REGEXP,
|
|
14
|
-
slug.gsub!(
|
|
35
|
+
slug = str.gsub(SLUGIFY_PRETTY_REGEXP, "-")
|
|
36
|
+
slug.gsub!(/^-|-$/i, "")
|
|
15
37
|
slug.downcase
|
|
16
38
|
end
|
|
17
39
|
|
|
40
|
+
# Includes and renders a partial template file.
|
|
41
|
+
#
|
|
42
|
+
# This method loads an ERB template from the includes directory and renders it
|
|
43
|
+
# with the provided options. Useful for reusing template fragments.
|
|
44
|
+
#
|
|
45
|
+
# @param filename [String] Name of the template file in the includes directory
|
|
46
|
+
# @param opts [Hash] Options to pass to the template
|
|
47
|
+
# @return [String] Rendered HTML content from the template
|
|
48
|
+
#
|
|
49
|
+
# @example In an ERB template
|
|
50
|
+
# <%= include.("field_table.html", fields: type[:fields]) %>
|
|
18
51
|
def include(filename, opts = {})
|
|
19
52
|
template = fetch_include(filename)
|
|
20
|
-
opts = {
|
|
53
|
+
opts = {base_url: @options[:base_url], classes: @options[:classes]}.merge(opts)
|
|
21
54
|
template.result(OpenStruct.new(opts.merge(helper_methods)).instance_eval { binding })
|
|
22
55
|
end
|
|
23
56
|
|
|
57
|
+
# Converts a Markdown string to HTML.
|
|
58
|
+
#
|
|
59
|
+
# @param string [String] Markdown content to convert
|
|
60
|
+
# @return [String] HTML output from the Markdown, empty string if input is nil
|
|
61
|
+
#
|
|
62
|
+
# @example
|
|
63
|
+
# markdownify("**bold**") # => "<strong>bold</strong>"
|
|
64
|
+
# markdownify(nil) # => ""
|
|
24
65
|
def markdownify(string)
|
|
25
|
-
return
|
|
66
|
+
return "" if string.nil?
|
|
26
67
|
|
|
27
|
-
|
|
28
|
-
|
|
68
|
+
begin
|
|
69
|
+
# Replace emoji shortcodes before markdown processing
|
|
70
|
+
string_with_emoji = emojify(string)
|
|
71
|
+
|
|
72
|
+
doc = ::Commonmarker.parse(string_with_emoji)
|
|
73
|
+
html = if @options[:pipeline_config][:context][:unsafe]
|
|
74
|
+
doc.to_html(options: {render: {unsafe: true}})
|
|
75
|
+
else
|
|
76
|
+
doc.to_html
|
|
77
|
+
end
|
|
78
|
+
html.strip
|
|
79
|
+
rescue => e
|
|
80
|
+
# Log error and return safe fallback
|
|
81
|
+
warn "Failed to parse markdown: #{e.message}"
|
|
82
|
+
require "cgi" unless defined?(CGI)
|
|
83
|
+
CGI.escapeHTML(string)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Converts emoji shortcodes like :smile: to emoji characters
|
|
88
|
+
def emojify(string)
|
|
89
|
+
string.gsub(/:([a-z0-9_+-]+):/) do |match|
|
|
90
|
+
emoji = Emoji.find_by_alias(Regexp.last_match(1))
|
|
91
|
+
emoji ? emoji.raw : match
|
|
92
|
+
end
|
|
29
93
|
end
|
|
30
94
|
|
|
95
|
+
# Returns the root types (query, mutation) from the parsed schema.
|
|
96
|
+
#
|
|
97
|
+
# @return [Hash] Hash containing 'query' and 'mutation' keys with type names
|
|
31
98
|
def graphql_root_types
|
|
32
99
|
@parsed_schema[:root_types] || []
|
|
33
100
|
end
|
|
34
101
|
|
|
102
|
+
# Returns all operation types (Query, Mutation) from the parsed schema.
|
|
103
|
+
#
|
|
104
|
+
# @return [Array<Hash>] Array of operation type hashes
|
|
35
105
|
def graphql_operation_types
|
|
36
106
|
@parsed_schema[:operation_types] || []
|
|
37
107
|
end
|
|
38
108
|
|
|
109
|
+
# Returns all query types from the parsed schema.
|
|
110
|
+
#
|
|
111
|
+
# @return [Array<Hash>] Array of query hashes with fields and arguments
|
|
39
112
|
def graphql_query_types
|
|
40
113
|
@parsed_schema[:query_types] || []
|
|
41
114
|
end
|
|
42
115
|
|
|
116
|
+
# Returns all mutation types from the parsed schema.
|
|
117
|
+
#
|
|
118
|
+
# @return [Array<Hash>] Array of mutation hashes with input and return fields
|
|
43
119
|
def graphql_mutation_types
|
|
44
120
|
@parsed_schema[:mutation_types] || []
|
|
45
121
|
end
|
|
46
122
|
|
|
123
|
+
# Returns all object types from the parsed schema.
|
|
124
|
+
#
|
|
125
|
+
# @return [Array<Hash>] Array of object type hashes
|
|
47
126
|
def graphql_object_types
|
|
48
127
|
@parsed_schema[:object_types] || []
|
|
49
128
|
end
|
|
50
129
|
|
|
130
|
+
# Returns all interface types from the parsed schema.
|
|
131
|
+
#
|
|
132
|
+
# @return [Array<Hash>] Array of interface type hashes
|
|
51
133
|
def graphql_interface_types
|
|
52
134
|
@parsed_schema[:interface_types] || []
|
|
53
135
|
end
|
|
54
136
|
|
|
137
|
+
# Returns all enum types from the parsed schema.
|
|
138
|
+
#
|
|
139
|
+
# @return [Array<Hash>] Array of enum type hashes with values
|
|
55
140
|
def graphql_enum_types
|
|
56
141
|
@parsed_schema[:enum_types] || []
|
|
57
142
|
end
|
|
58
143
|
|
|
144
|
+
# Returns all union types from the parsed schema.
|
|
145
|
+
#
|
|
146
|
+
# @return [Array<Hash>] Array of union type hashes with possible types
|
|
59
147
|
def graphql_union_types
|
|
60
148
|
@parsed_schema[:union_types] || []
|
|
61
149
|
end
|
|
62
150
|
|
|
151
|
+
# Returns all input object types from the parsed schema.
|
|
152
|
+
#
|
|
153
|
+
# @return [Array<Hash>] Array of input object type hashes
|
|
63
154
|
def graphql_input_object_types
|
|
64
155
|
@parsed_schema[:input_object_types] || []
|
|
65
156
|
end
|
|
66
157
|
|
|
158
|
+
# Returns all scalar types from the parsed schema.
|
|
159
|
+
#
|
|
160
|
+
# @return [Array<Hash>] Array of scalar type hashes
|
|
67
161
|
def graphql_scalar_types
|
|
68
162
|
@parsed_schema[:scalar_types] || []
|
|
69
163
|
end
|
|
70
164
|
|
|
165
|
+
# Returns all directive types from the parsed schema.
|
|
166
|
+
#
|
|
167
|
+
# @return [Array<Hash>] Array of directive hashes with locations and arguments
|
|
71
168
|
def graphql_directive_types
|
|
72
169
|
@parsed_schema[:directive_types] || []
|
|
73
170
|
end
|
|
74
171
|
|
|
172
|
+
# Splits content into YAML front matter metadata and body content.
|
|
173
|
+
#
|
|
174
|
+
# @param contents [String] Content string potentially starting with YAML front matter
|
|
175
|
+
# @param parse [Boolean] Whether to parse the YAML (true) or return it as a string (false)
|
|
176
|
+
# @return [Array<(Hash, String), (String, String)>] Tuple of [metadata, content]
|
|
177
|
+
#
|
|
178
|
+
# @raise [RuntimeError] If YAML front matter format is invalid
|
|
179
|
+
# @raise [RuntimeError] If YAML parsing fails
|
|
180
|
+
# @raise [TypeError] If parsed YAML is not a Hash
|
|
75
181
|
def split_into_metadata_and_contents(contents, parse: true)
|
|
76
182
|
pieces = yaml_split(contents)
|
|
77
183
|
raise "The file '#{content_filename}' appears to start with a metadata section (three or five dashes at the top) but it does not seem to be in the correct format." if pieces.size < 4
|
|
@@ -79,20 +185,34 @@ module GraphQLDocs
|
|
|
79
185
|
# Parse
|
|
80
186
|
begin
|
|
81
187
|
meta = if parse
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
188
|
+
YAML.safe_load(pieces[2]) || {}
|
|
189
|
+
else
|
|
190
|
+
pieces[2]
|
|
191
|
+
end
|
|
86
192
|
rescue Exception => e # rubocop:disable Lint/RescueException
|
|
87
193
|
raise "Could not parse YAML for #{name}: #{e.message}"
|
|
88
194
|
end
|
|
195
|
+
|
|
196
|
+
# Validate that parsed YAML is a Hash when parsing is enabled
|
|
197
|
+
if parse && !meta.is_a?(Hash)
|
|
198
|
+
raise TypeError, "Expected YAML front matter to be a hash, got #{meta.class}"
|
|
199
|
+
end
|
|
200
|
+
|
|
89
201
|
[meta, pieces[4]]
|
|
90
202
|
end
|
|
91
203
|
|
|
204
|
+
# Checks if content starts with YAML front matter.
|
|
205
|
+
#
|
|
206
|
+
# @param contents [String] Content to check
|
|
207
|
+
# @return [Boolean] True if content starts with YAML front matter delimiters
|
|
92
208
|
def yaml?(contents)
|
|
93
209
|
contents =~ /\A-{3,5}\s*$/
|
|
94
210
|
end
|
|
95
211
|
|
|
212
|
+
# Splits content by YAML front matter delimiters.
|
|
213
|
+
#
|
|
214
|
+
# @param contents [String] Content to split
|
|
215
|
+
# @return [Array<String>] Array of content pieces split by YAML delimiters
|
|
96
216
|
def yaml_split(contents)
|
|
97
217
|
contents.split(/^(-{5}|-{3})[ \t]*\r?\n?/, 3)
|
|
98
218
|
end
|