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.
Files changed (59) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +9 -0
  3. data/CHANGELOG.md +76 -0
  4. data/README.md +404 -8
  5. data/bin/console +6 -7
  6. data/bin/setup +8 -5
  7. data/exe/graphql-docs +9 -9
  8. data/graphql-docs.gemspec +50 -38
  9. data/lib/graphql-docs/app.rb +488 -0
  10. data/lib/graphql-docs/configuration.rb +28 -11
  11. data/lib/graphql-docs/generator.rb +147 -42
  12. data/lib/graphql-docs/helpers.rb +132 -12
  13. data/lib/graphql-docs/layouts/assets/_sass/_api-box.scss +2 -2
  14. data/lib/graphql-docs/layouts/assets/_sass/_content.scss +20 -19
  15. data/lib/graphql-docs/layouts/assets/_sass/_deprecations.scss +7 -0
  16. data/lib/graphql-docs/layouts/assets/_sass/_fonts.scss +6 -21
  17. data/lib/graphql-docs/layouts/assets/_sass/_header.scss +8 -2
  18. data/lib/graphql-docs/layouts/assets/_sass/_mobile.scss +10 -3
  19. data/lib/graphql-docs/layouts/assets/_sass/_search.scss +32 -10
  20. data/lib/graphql-docs/layouts/assets/_sass/_sidebar.scss +22 -15
  21. data/lib/graphql-docs/layouts/assets/_sass/_syntax.scss +1 -1
  22. data/lib/graphql-docs/layouts/assets/css/screen.scss +54 -5
  23. data/lib/graphql-docs/layouts/default.html +75 -1
  24. data/lib/graphql-docs/layouts/includes/sidebar.html +4 -2
  25. data/lib/graphql-docs/parser.rb +89 -33
  26. data/lib/graphql-docs/renderer.rb +98 -31
  27. data/lib/graphql-docs/version.rb +2 -1
  28. data/lib/graphql-docs.rb +78 -12
  29. data/lib/tasks/graphql-docs.rake +57 -0
  30. metadata +74 -78
  31. data/.github/ISSUE_TEMPLATE/bug_report.md +0 -31
  32. data/.github/ISSUE_TEMPLATE/feature_request.md +0 -20
  33. data/.github/workflows/tests.yml +0 -21
  34. data/.gitignore +0 -15
  35. data/.rubocop.yml +0 -11
  36. data/CODE_OF_CONDUCT.md +0 -84
  37. data/CONTRIBUTING.md +0 -18
  38. data/Gemfile +0 -6
  39. data/Rakefile +0 -86
  40. data/lib/graphql-docs/layouts/assets/images/graphiql-headers.png +0 -0
  41. data/lib/graphql-docs/layouts/assets/images/graphiql-variables.png +0 -0
  42. data/lib/graphql-docs/layouts/assets/images/graphiql.png +0 -0
  43. data/lib/graphql-docs/layouts/assets/images/search.svg +0 -3
  44. data/lib/graphql-docs/layouts/assets/webfonts/2C4B9D_B_0.eot +0 -0
  45. data/lib/graphql-docs/layouts/assets/webfonts/2C4B9D_B_0.ttf +0 -0
  46. data/lib/graphql-docs/layouts/assets/webfonts/2C4B9D_B_0.woff +0 -0
  47. data/lib/graphql-docs/layouts/assets/webfonts/2C4B9D_B_0.woff2 +0 -0
  48. data/lib/graphql-docs/layouts/assets/webfonts/2C4B9D_C_0.eot +0 -0
  49. data/lib/graphql-docs/layouts/assets/webfonts/2C4B9D_C_0.ttf +0 -0
  50. data/lib/graphql-docs/layouts/assets/webfonts/2C4B9D_C_0.woff +0 -0
  51. data/lib/graphql-docs/layouts/assets/webfonts/2C4B9D_C_0.woff2 +0 -0
  52. data/lib/graphql-docs/layouts/assets/webfonts/2C4B9D_D_0.eot +0 -0
  53. data/lib/graphql-docs/layouts/assets/webfonts/2C4B9D_D_0.ttf +0 -0
  54. data/lib/graphql-docs/layouts/assets/webfonts/2C4B9D_D_0.woff +0 -0
  55. data/lib/graphql-docs/layouts/assets/webfonts/2C4B9D_D_0.woff2 +0 -0
  56. data/lib/graphql-docs/layouts/assets/webfonts/2C4B9D_E_0.eot +0 -0
  57. data/lib/graphql-docs/layouts/assets/webfonts/2C4B9D_E_0.ttf +0 -0
  58. data/lib/graphql-docs/layouts/assets/webfonts/2C4B9D_E_0.woff +0 -0
  59. 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 'erb'
4
- require 'fileutils'
5
- require 'sass-embedded'
6
- require 'ostruct'
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((@options[:landing_pages][sym])) == '.erb'
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('static', 'index', @graphql_index_landing_page, trim: false) unless @graphql_index_landing_page.nil?
103
+ write_file("static", "index", @graphql_index_landing_page, trim: false) unless @graphql_index_landing_page.nil?
67
104
 
68
- write_file('static', 'object', @graphql_object_landing_page, trim: false) unless @graphql_object_landing_page.nil?
105
+ write_file("static", "object", @graphql_object_landing_page, trim: false) unless @graphql_object_landing_page.nil?
69
106
 
70
- write_file('operation', 'query', @graphql_query_landing_page, trim: false) if !@graphql_query_landing_page.nil? && !has_query
107
+ write_file("operation", "query", @graphql_query_landing_page, trim: false) if !@graphql_query_landing_page.nil? && !has_query
71
108
 
72
- write_file('operation', 'mutation', @graphql_mutation_landing_page, trim: false) unless @graphql_mutation_landing_page.nil?
109
+ write_file("operation", "mutation", @graphql_mutation_landing_page, trim: false) unless @graphql_mutation_landing_page.nil?
73
110
 
74
- write_file('static', 'interface', @graphql_interface_landing_page, trim: false) unless @graphql_interface_landing_page.nil?
111
+ write_file("static", "interface", @graphql_interface_landing_page, trim: false) unless @graphql_interface_landing_page.nil?
75
112
 
76
- write_file('static', 'enum', @graphql_enum_landing_page, trim: false) unless @graphql_enum_landing_page.nil?
113
+ write_file("static", "enum", @graphql_enum_landing_page, trim: false) unless @graphql_enum_landing_page.nil?
77
114
 
78
- write_file('static', 'union', @graphql_union_landing_page, trim: false) unless @graphql_union_landing_page.nil?
115
+ write_file("static", "union", @graphql_union_landing_page, trim: false) unless @graphql_union_landing_page.nil?
79
116
 
80
- write_file('static', 'input_object', @graphql_input_object_landing_page, trim: false) unless @graphql_input_object_landing_page.nil?
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('static', 'scalar', @graphql_scalar_landing_page, trim: false) unless @graphql_scalar_landing_page.nil?
119
+ write_file("static", "scalar", @graphql_scalar_landing_page, trim: false) unless @graphql_scalar_landing_page.nil?
83
120
 
84
- write_file('static', 'directive', @graphql_directive_landing_page, trim: false) unless @graphql_directive_landing_page.nil?
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__), 'layouts', 'assets')
88
- FileUtils.mkdir_p(File.join(@options[:output_dir], 'assets'))
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, 'css', 'screen.scss')).css
91
- File.write(File.join(@options[:output_dir], 'assets', 'style.css'), css)
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, 'images'), File.join(@options[:output_dir], 'assets'))
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['query']
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('operation', 'query', metadata + contents)
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('object', object_type[:name], contents)
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('query', query[:name], contents)
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('mutation', mutation[:name], contents)
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('interface', interface_type[:name], contents)
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('enum', enum_type[:name], contents)
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('union', union_type[:name], contents)
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('input_object', input_object_type[:name], contents)
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('scalar', scalar_type[:name], contents)
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('directive', directive_type[:name], contents)
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 == 'static'
213
- if name == 'index'
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
- meta, contents = split_into_metadata_and_contents(contents)
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, 'index.html')
237
- contents = @renderer.render(contents, type: type, name: name, filename: filename)
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
@@ -1,77 +1,183 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'commonmarker'
4
- require 'ostruct'
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!(/^\-|\-$/i, '')
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 = { base_url: @options[:base_url], classes: @options[:classes] }.merge(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 '' if string.nil?
66
+ return "" if string.nil?
26
67
 
27
- type = @options[:pipeline_config][:context][:unsafe] ? :UNSAFE : :DEFAULT
28
- ::CommonMarker.render_html(string, type).strip
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
- YAML.safe_load(pieces[2]) || {}
83
- else
84
- pieces[2]
85
- end
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
@@ -1,5 +1,5 @@
1
1
  .api {
2
- background: #fafafa;
2
+ background: var(--bg-tertiary);
3
3
  h3 {
4
4
  padding: 5px 10px;
5
5
  }
@@ -32,7 +32,7 @@
32
32
  margin-left: 15px;
33
33
  font-size: 0.9em;
34
34
  font-weight: 200;
35
- color: #000;
35
+ color: var(--text-primary);
36
36
  }
37
37
  }
38
38
  dd {