├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ └── tests.yml ├── .gitignore ├── .rubocop.yml ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── bin ├── console ├── rake └── setup ├── exe └── graphql-docs ├── graphql-docs.gemspec ├── lib ├── graphql-docs.rb └── graphql-docs │ ├── configuration.rb │ ├── generator.rb │ ├── helpers.rb │ ├── landing_pages │ ├── directive.md │ ├── enum.md │ ├── index.md │ ├── input_object.md │ ├── interface.md │ ├── mutation.md │ ├── object.md │ ├── query.md │ ├── scalar.md │ └── union.md │ ├── layouts │ ├── assets │ │ ├── _sass │ │ │ ├── _api-box.scss │ │ │ ├── _content.scss │ │ │ ├── _deprecations.scss │ │ │ ├── _fonts.scss │ │ │ ├── _header.scss │ │ │ ├── _mobile.scss │ │ │ ├── _normalize.scss │ │ │ ├── _search.scss │ │ │ ├── _sidebar.scss │ │ │ ├── _syntax.scss │ │ │ └── _types.scss │ │ ├── css │ │ │ └── screen.scss │ │ ├── images │ │ │ ├── graphiql-headers.png │ │ │ ├── graphiql-variables.png │ │ │ ├── graphiql.png │ │ │ ├── menu.png │ │ │ ├── navbar.png │ │ │ └── search.svg │ │ └── webfonts │ │ │ ├── 2C4B9D_B_0.eot │ │ │ ├── 2C4B9D_B_0.ttf │ │ │ ├── 2C4B9D_B_0.woff │ │ │ ├── 2C4B9D_B_0.woff2 │ │ │ ├── 2C4B9D_C_0.eot │ │ │ ├── 2C4B9D_C_0.ttf │ │ │ ├── 2C4B9D_C_0.woff │ │ │ ├── 2C4B9D_C_0.woff2 │ │ │ ├── 2C4B9D_D_0.eot │ │ │ ├── 2C4B9D_D_0.ttf │ │ │ ├── 2C4B9D_D_0.woff │ │ │ ├── 2C4B9D_D_0.woff2 │ │ │ ├── 2C4B9D_E_0.eot │ │ │ ├── 2C4B9D_E_0.ttf │ │ │ ├── 2C4B9D_E_0.woff │ │ │ └── 2C4B9D_E_0.woff2 │ ├── default.html │ ├── graphql_directives.html │ ├── graphql_enums.html │ ├── graphql_input_objects.html │ ├── graphql_interfaces.html │ ├── graphql_mutations.html │ ├── graphql_objects.html │ ├── graphql_operations.html │ ├── graphql_queries.html │ ├── graphql_scalars.html │ ├── graphql_unions.html │ └── includes │ │ ├── arguments.html │ │ ├── connections.html │ │ ├── deprecations.html │ │ ├── fields.html │ │ ├── input_fields.html │ │ ├── locations.html │ │ ├── notices.html │ │ ├── possible_types.html │ │ ├── sidebar.html │ │ └── values.html │ ├── parser.rb │ ├── renderer.rb │ └── version.rb └── test ├── cli_test.rb ├── graphql-docs ├── fixtures │ ├── gh-schema.graphql │ ├── landing_pages │ │ ├── broken_yaml.md │ │ ├── object.erb │ │ └── whitespace_template.md │ ├── sw-schema.graphql │ └── tiny-schema.graphql ├── generator_test.rb ├── graphql-docs_test.rb ├── parser_test.rb └── renderer_test.rb └── test_helper.rb /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Do x 16 | 2. Do y 17 | 18 | **Expected behavior** 19 | A clear and concise description of what you expected to happen. 20 | 21 | **Screenshots** 22 | If applicable, add screenshots to help explain your problem. 23 | 24 | **Additional context** 25 | Add any other context about the problem here. 26 | 27 | Examples: 28 | 29 | - Source code 30 | - Version of gem being used 31 | - Ruby version 32 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Ruby 2 | 3 | on: 4 | push: 5 | pull_request: 6 | 7 | jobs: 8 | test: 9 | runs-on: ubuntu-latest 10 | strategy: 11 | matrix: 12 | ruby-version: [3.4, 3.3, 3.2, 3.1] 13 | 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: Set up Ruby ${{ matrix.ruby-version }} 17 | uses: ruby/setup-ruby@v1 18 | with: 19 | ruby-version: ${{ matrix.ruby-version }} 20 | bundler-cache: true 21 | - name: Run test suite 22 | run: bundle exec rake 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /Gemfile.lock 4 | /_yardoc/ 5 | /coverage/ 6 | /doc/ 7 | /pkg/ 8 | /spec/reports/ 9 | /tmp/ 10 | /output/ 11 | /test/graphql-docs/fixtures/output/ 12 | /.sass-cache/ 13 | /output/ 14 | .byebug_history 15 | *.gem 16 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | require: rubocop-performance 2 | 3 | Style/StringLiterals: 4 | Enabled: true 5 | EnforcedStyle: single_quotes 6 | 7 | Naming/FileName: 8 | Enabled: false 9 | 10 | Layout/IndentationWidth: 11 | Width: 2 12 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # graphql-docs Changelog 2 | 3 | A concise overview of the public-facing changes to the gem from version to version. Does not include internal changes to the dev experience for the gem. 4 | 5 | ## Unreleased 6 | 7 | ## v5.2.0 - 2025-02-09 8 | 9 | - Add search filter to sidebar. Thanks @denisahearn! 10 | 11 | ## v5.1.0 - 2024-12-09 12 | 13 | - List queries in the sidebar, similar to mutations. See https://github.com/brettchalupa/graphql-docs/pull/156. Thanks @denisahearn! 14 | - Fix Sass `@import` deprecation 15 | - Add ostruct and logger gems to dependencies since they're being removed from the Ruby standard library in a future release 16 | - Test and fixture improvements 17 | 18 | ## v5.0.0 - 2024-07-03 19 | 20 | - **breaking**: The graphql gem 2.2.0+ breaks some of the parsing and displaying of comments from a GraphQL schema file 21 | - **breaking**: Ruby 2.6, 2.7, 3.0 are no longer supported as they are End of Life (EOL) 22 | - feat: CLI version flag: `graphql-docs --version` / `graphql-docs -v` 23 | - fix: CLI now works outside of a Bundler project 24 | - fix: test suite 25 | - chore: switch to sess-embedded gem for more maintained dependency 26 | 27 | ## v4.0.0 - 2023-01-26 28 | 29 | - **Breaking change**: drop support for Ruby 2.5 and earlier 30 | - CLI with limited options, e.g. `graphql-docs schema.graphql` 31 | - Dart Sass replaces Ruby Sass because Ruby Sass is deprecated 32 | - Fixes: 33 | - Upgrade commonmarker to latest ver to address security vulnerabilities 34 | - commonmarker pinned to version without security vulnerability 35 | - Chores: 36 | - Dev env refresh 37 | 38 | ## v3.0.1 - 2022-10-14 39 | 40 | - fix: Relieves `EscapeUtils.escape_html is deprecated. Use GCI.escapeHTML instead, it's faster` deprecation warning until it gets released in an downstream dependency 41 | - meta: Maintainership change from [gjtorikian](https://github.com/gjtorikian) to [brettchalupa](https://github.com/brettchalupa) 42 | 43 | ## v3.0.0 - 2022-03-23 44 | 45 | - Upgrades `graphql` gem to the 2.x series 46 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. 8 | 9 | ## Our Standards 10 | 11 | Examples of behavior that contributes to a positive environment for our community include: 12 | 13 | * Demonstrating empathy and kindness toward other people 14 | * Being respectful of differing opinions, viewpoints, and experiences 15 | * Giving and gracefully accepting constructive feedback 16 | * Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience 17 | * Focusing on what is best not just for us as individuals, but for the overall community 18 | 19 | Examples of unacceptable behavior include: 20 | 21 | * The use of sexualized language or imagery, and sexual attention or 22 | advances of any kind 23 | * Trolling, insulting or derogatory comments, and personal or political attacks 24 | * Public or private harassment 25 | * Publishing others' private information, such as a physical or email 26 | address, without their explicit permission 27 | * Other conduct which could reasonably be considered inappropriate in a 28 | professional setting 29 | 30 | ## Enforcement Responsibilities 31 | 32 | Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. 33 | 34 | Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate. 35 | 36 | ## Scope 37 | 38 | This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. 39 | 40 | ## Enforcement 41 | 42 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at brettchalupa@gmail.com. All complaints will be reviewed and investigated promptly and fairly. 43 | 44 | All community leaders are obligated to respect the privacy and security of the reporter of any incident. 45 | 46 | ## Enforcement Guidelines 47 | 48 | Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct: 49 | 50 | ### 1. Correction 51 | 52 | **Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community. 53 | 54 | **Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested. 55 | 56 | ### 2. Warning 57 | 58 | **Community Impact**: A violation through a single incident or series of actions. 59 | 60 | **Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban. 61 | 62 | ### 3. Temporary Ban 63 | 64 | **Community Impact**: A serious violation of community standards, including sustained inappropriate behavior. 65 | 66 | **Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. 67 | 68 | ### 4. Permanent Ban 69 | 70 | **Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. 71 | 72 | **Consequence**: A permanent ban from any sort of public interaction within the community. 73 | 74 | ## Attribution 75 | 76 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0, 77 | available at https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 78 | 79 | Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity). 80 | 81 | [homepage]: https://www.contributor-covenant.org 82 | 83 | For answers to common questions about this code of conduct, see the FAQ at 84 | https://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations. 85 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guide 2 | 3 | Contributions to this project are welcome. If you have an idea for a bigger change, [open an issue first](https://github.com/brettchalupa/graphql-docs/issues/new/choose) and we can discuss it. 4 | 5 | For fixes and small additions, follow the steps below to get developing and contributing: 6 | 7 | 1. Fork & clone the repository in GitHub 8 | 2. Run the `bin/setup` script to install development dependencies 9 | 3. Work on a branch 10 | 4. Make changes 11 | 5. Ensure tests pass by running `bin/rake` 12 | 6. Commit your changes, this project follows [the Conventional Commits spec](https://www.conventionalcommits.org/en/v1.0.0/) 13 | 7. Open up a pull request 14 | 15 | ## Finding Issues 16 | 17 | - Good First Issue — If you're new to the project or Ruby, check out the ["good first issue" tag](https://github.com/brettchalupa/graphql-docs/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22). They're smaller, approachable issues if you're just getting started. 18 | - Web — tasks that don't require much Ruby knowledge but require HTML and CSS have the ["web" tag](https://github.com/brettchalupa/graphql-docs/issues?q=is%3Aopen+is%3Aissue+label%3A%22web%22+) 19 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source 'https://rubygems.org' 4 | 5 | # Specify your gem's dependencies in graphql-docs.gemspec 6 | gemspec 7 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 Garen Torikian 2 | Copyright (c) 2022 Brett Chalupa 3 | 4 | MIT License 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining 7 | a copy of this software and associated documentation files (the 8 | "Software"), to deal in the Software without restriction, including 9 | without limitation the rights to use, copy, modify, merge, publish, 10 | distribute, sublicense, and/or sell copies of the Software, and to 11 | permit persons to whom the Software is furnished to do so, subject to 12 | the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be 15 | included in all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 19 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 20 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 21 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 22 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 23 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GraphQLDocs 2 | 3 | Ruby library and CLI for easily generating beautiful documentation from your GraphQL schema. 4 | 5 | ![sample](https://cloud.githubusercontent.com/assets/64050/23438604/6a23add0-fdc7-11e6-8852-ef41e8451033.png) 6 | 7 | ## Installation 8 | 9 | Add the gem to your project with this command: 10 | 11 | ```console 12 | bundle add graphql-docs 13 | ``` 14 | 15 | Or install it yourself as: 16 | 17 | ```console 18 | gem install graphql-docs 19 | ``` 20 | 21 | ## Usage 22 | 23 | GraphQLDocs can be used as a Ruby library to build the documentation website. Using it as a Ruby library allows for more control and using every supported option. Here's an example: 24 | 25 | ```ruby 26 | # pass in a filename 27 | GraphQLDocs.build(filename: filename) 28 | 29 | # or pass in a string 30 | GraphQLDocs.build(schema: contents) 31 | 32 | # or a schema class 33 | schema = GraphQL::Schema.define do 34 | query query_type 35 | end 36 | GraphQLDocs.build(schema: schema) 37 | ``` 38 | 39 | GraphQLDocs also has a simplified CLI (`graphql-docs`) that gets installed with the gem: 40 | 41 | ```console 42 | graphql-docs schema.graphql 43 | ``` 44 | 45 | That will generate the output in the `output` dir. 46 | 47 | See all of the supported CLI options with: 48 | 49 | ```console 50 | graphql-docs -h 51 | ``` 52 | 53 | ## Breakdown 54 | 55 | There are several phases going on the single `GraphQLDocs.build` call: 56 | 57 | - The GraphQL IDL file is read (if you passed `filename`) through `GraphQL::Client` (or simply read if you passed a string through `schema`). 58 | - `GraphQL::Parser` manipulates the IDL into a slightly saner format. 59 | - `GraphQL::Generator` takes that saner format and begins the process of applying items to the HTML templates. 60 | - `GraphQL::Renderer` technically runs as part of the generation phase. It passes the contents of each page and converts it into HTML. 61 | 62 | If you wanted to, you could break these calls up individually. For example: 63 | 64 | ```ruby 65 | options = {} 66 | options[:filename] = "#{File.dirname(__FILE__)}/../data/graphql/schema.idl" 67 | options[:renderer] = MySuperCoolRenderer 68 | 69 | options = GraphQLDocs::Configuration::GRAPHQLDOCS_DEFAULTS.merge(options) 70 | 71 | response = File.read(options[:filename]) 72 | 73 | parser = GraphQLDocs::Parser.new(response, options) 74 | parsed_schema = parser.parse 75 | 76 | generator = GraphQLDocs::Generator.new(parsed_schema, options) 77 | 78 | generator.generate 79 | ``` 80 | 81 | ## Generating output 82 | 83 | By default, the HTML generation process uses ERB to layout the content. There are a bunch of default options provided for you, but feel free to override any of these. The _Configuration_ section below has more information on what you can change. 84 | 85 | It also uses [html-pipeline](https://github.com/jch/html-pipeline) to perform the rendering by default. You can override this by providing a custom rendering class.You must implement two methods: 86 | 87 | - `initialize` - Takes two arguments, the parsed `schema` and the configuration `options`. 88 | - `render` Takes the contents of a template page. It also takes two optional kwargs, the GraphQL `type` and its `name`. For example: 89 | 90 | ```ruby 91 | class CustomRenderer 92 | def initialize(parsed_schema, options) 93 | @parsed_schema = parsed_schema 94 | @options = options 95 | end 96 | 97 | def render(contents, type: nil, name: nil) 98 | contents.sub(/Repository/i, 'Meow Woof!') 99 | 100 | opts[:content] = contents 101 | @graphql_default_layout.result(OpenStruct.new(opts).instance_eval { binding }) 102 | end 103 | end 104 | 105 | options[:filename] = 'location/to/sw-api.graphql' 106 | options[:renderer] = CustomRenderer 107 | 108 | GraphQLDocs.build(options) 109 | ``` 110 | 111 | If your `render` method returns `nil`, the `Generator` will not attempt to write any HTML file. 112 | 113 | ### Templates 114 | 115 | The layouts for the individual GraphQL pages are ERB templates, but you can also use ERB templates for your static landing pages. 116 | 117 | If you want to add additional variables for your landing pages, you can add define a `variables` hash within the `landing_pages` option. 118 | 119 | ### Helper methods 120 | 121 | In your ERB layouts, there are several helper methods you can use. The helper methods are: 122 | 123 | - `slugify(str)` - This slugifies the given string. 124 | - `include(filename, opts)` - This embeds a template from your `includes` folder, passing along the local options provided. 125 | - `markdownify(string)` - This converts a string into HTML via CommonMarker. 126 | - `graphql_operation_types`, `graphql_mutation_types`, `graphql_object_types`, `graphql_interface_types`, `graphql_enum_types`, `graphql_union_types`, `graphql_input_object_types`, `graphql_scalar_types`, `graphql_directive_types` - Collections of the various GraphQL types. 127 | 128 | To call these methods within templates, you must use the dot notation, such as `<%= slugify.(text) %>`. 129 | 130 | ## Configuration 131 | 132 | The following options are available: 133 | 134 | | Option | Description | Default | 135 | | :------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------- | 136 | | `filename` | The location of your schema's IDL file. | `nil` | 137 | | `schema` | A string representing a schema IDL file. | `nil` | 138 | | `output_dir` | The location of the output HTML. | `./output/` | 139 | | `use_default_styles` | Indicates if you want to use the default styles. | `true` | 140 | | `base_url` | Indicates the base URL to prepend for assets and links. | `""` | 141 | | `delete_output` | Deletes `output_dir` before generating content. | `false` | 142 | | `pipeline_config` | Defines two sub-keys, `pipeline` and `context`, which are used by `html-pipeline` when rendering your output. | `pipeline` has `ExtendedMarkdownFilter`, `EmojiFilter`, and `TableOfContentsFilter`. `context` has `gfm: false` and `asset_root` set to GitHub's CDN. | 143 | | `renderer` | The rendering class to use. | `GraphQLDocs::Renderer` | 144 | | `templates` | The templates to use when generating HTML. You may override any of the following keys: `default`, `includes`, `operations`, `objects`, `mutations`, `interfaces`, `enums`, `unions`, `input_objects`, `scalars`, `directives`. | The defaults are found in _lib/graphql-docs/layouts/_. | 145 | | `landing_pages` | The landing page to use when generating HTML for each type. You may override any of the following keys: `index`, `query`, `object`, `mutation`, `interface`, `enum`, `union`, `input_object`, `scalar`, `directive`. | The defaults are found in _lib/graphql-docs/landing_pages/_. | 146 | | `classes` | Additional class names you can provide to certain elements. | The full list is available in _lib/graphql-docs/configuration.rb_. | 147 | | `notices` | A proc used to add notices to schema members. See _Customizing Notices_ section below. | `nil` | 148 | 149 | ### Customizing Notices 150 | 151 | A notice is a block of CommonMark text that optionally has a title which is displayed above a schema member's description. The 152 | look of a notice block can be controlled by specifying a custom class for it and then styled via CSS. 153 | 154 | The `notices` option allows you to customize the notices that appear for a specific schema member using a proc. 155 | 156 | The proc will be called for each schema member and needs to return an array of notices or an empty array if there are none. 157 | 158 | A `notice` has the following options: 159 | 160 | | Option | Description | 161 | | :------------ | :-------------------------------------------------------- | 162 | | `body` | CommonMark body of the notice | 163 | | `title` | Optional title of the notice | 164 | | `class` | Optional CSS class for the wrapper `
` of the notice | 165 | | `title_class` | Optional CSS class for the `` of the notice's title | 166 | 167 | Example of a `notices` proc that adds a notice to the `TeamDiscussion` type: 168 | 169 | ```ruby 170 | options[:notices] = ->(schema_member_path) { 171 | notices = [] 172 | 173 | if schema_member_path == "TeamDiscussion" 174 | notices << { 175 | class: "preview-notice", 176 | body: "Available via the [Team Discussion](/previews/team-discussion) preview.", 177 | } 178 | end 179 | 180 | notices 181 | } 182 | ``` 183 | 184 | The format of `schema_member_path` is a dot delimited path to the schema member. For example: 185 | 186 | ```ruby 187 | "Author", # an object 188 | "ExtraInfo" # an interface, 189 | "Author.socialSecurityNumber" # a field 190 | "Book.author.includeMiddleInitial" # an argument 191 | "Likeable" # a union, 192 | "Cover" # an enum 193 | "Cover.DIGITAL" # an enum value 194 | "BookOrder" # an input object 195 | "Mutation.addLike" # a mutation 196 | ``` 197 | 198 | ## Supported Ruby Versions 199 | 200 | The gem officially supports **Ruby 3.1 and newer**. 201 | 202 | Any dropping of Ruby version support is considered a breaking change and means a major release for the gem. 203 | 204 | ## Upgrading 205 | 206 | This project aims to strictly follow [Semantic Versioning](https://semver.org/). 207 | Minor and patch level updates can be done with pretty high confidence that your usage won't break. 208 | 209 | Review the 210 | [Changelog](https://github.com/brettchalupa/graphql-docs/blob/main/CHANGELOG.md) 211 | for detailed changes for each release. The intent is to make upgrading as 212 | painless as possible. 213 | 214 | ## Roadmap 215 | 216 | Upcoming work for the project is organized publicly via [GitHub 217 | Projects](https://github.com/users/brettchalupa/projects/7/views/1). 218 | 219 | ## Development 220 | 221 | After checking out the repo, run `bin/setup` to install dependencies. Then, run 222 | `bin/rake test` to run the tests. You can also run `bin/console` for 223 | an interactive prompt that will allow you to experiment. 224 | 225 | ### Releasing a new version of the gem 226 | 227 | 1. Update CHANGELOG.md 228 | 2. Bump the version in `lib/graphql-docs/version.rb` 229 | 3. Make a commit and tag it with `git commit -a vX.X.X` 230 | 4. Push the commit and tags to GitHub: `git push origin main && git push --tags` 231 | 5. Build the gem: `gem build` 232 | 6. Push the gem to RubyGems.org: `gem push graphql-docs-X.X.X.gem` 233 | 234 | ## Sample Site 235 | 236 | Clone this repository and run: 237 | 238 | ``` 239 | bin/rake sample:generate 240 | ``` 241 | 242 | to see some sample output in the `output` dir. 243 | 244 | Boot up a server to view it: 245 | 246 | ``` 247 | bin/rake sample:serve 248 | ``` 249 | 250 | ## Credits 251 | 252 | Originally built by [gjtorikian](https://github.com/gjtorikian). Actively maintained by [brettchalupa](https://github.com/brettchalupa). 253 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'bundler/gem_tasks' 4 | require 'rake/testtask' 5 | 6 | require 'rubocop/rake_task' 7 | 8 | RuboCop::RakeTask.new(:rubocop) 9 | 10 | Rake::TestTask.new(:test) do |t| 11 | t.libs << 'test' 12 | t.libs << 'lib' 13 | t.warning = false 14 | t.test_files = FileList['test/**/*_test.rb'] 15 | end 16 | 17 | task default: :test 18 | 19 | desc 'Invoke HTML-Proofer' 20 | task :html_proofer do 21 | Rake::Task[:generate_sample].invoke('https://www.gjtorikian.com/graphql-docs') 22 | require 'html-proofer' 23 | output_dir = File.join(File.dirname(__FILE__), 'output') 24 | 25 | proofer_options = { disable_external: true, assume_extension: true } 26 | HTMLProofer.check_directory(output_dir, proofer_options).run 27 | end 28 | 29 | desc 'Set up a console' 30 | task :console do 31 | require 'graphql-docs' 32 | 33 | def reload! 34 | files = $LOADED_FEATURES.select { |feat| feat =~ %r{/graphql-docs/} } 35 | files.each { |file| load file } 36 | end 37 | 38 | require 'irb' 39 | ARGV.clear 40 | IRB.start 41 | end 42 | 43 | namespace :sample do 44 | desc 'Generate the sample documentation' 45 | task :generate do 46 | require 'graphql-docs' 47 | 48 | options = {} 49 | options[:delete_output] = true 50 | options[:base_url] = ENV.fetch('GQL_DOCS_BASE_URL', '') 51 | options[:filename] = File.join(File.dirname(__FILE__), 'test', 'graphql-docs', 'fixtures', 'gh-schema.graphql') 52 | 53 | puts "Generating sample docs" 54 | GraphQLDocs.build(options) 55 | end 56 | 57 | desc 'Generate the documentation and run a web server' 58 | task serve: [:generate] do 59 | require 'webrick' 60 | PORT = "5050" 61 | puts "Navigate to http://localhost:#{PORT} to view the sample docs" 62 | server = WEBrick::HTTPServer.new Port: PORT 63 | server.mount '/', WEBrick::HTTPServlet::FileHandler, 'output' 64 | trap('INT') { server.stop } 65 | server.start 66 | end 67 | task server: :serve 68 | end 69 | 70 | desc 'Generate and publish docs to gh-pages' 71 | task :publish do 72 | ENV['GQL_DOCS_BASE_URL'] = '/graphql-docs' 73 | Rake::Task[:generate_sample].invoke('https://www.gjtorikian.com/graphql-docs') 74 | Dir.mktmpdir do |tmp| 75 | system "mv output/* #{tmp}" 76 | system 'git checkout gh-pages' 77 | system 'rm -rf *' 78 | system "mv #{tmp}/* ." 79 | message = "Site updated at #{Time.now.utc}" 80 | system 'git add .' 81 | system "git commit -am #{message.shellescape}" 82 | system 'git push origin gh-pages --force' 83 | system 'git checkout master' 84 | system 'echo yolo' 85 | end 86 | end 87 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | echo "===> Bundling..." 6 | bin/setup --quiet 7 | 8 | echo "===> Launching..." 9 | bin/rake console 10 | -------------------------------------------------------------------------------- /bin/rake: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # 5 | # This file was generated by Bundler. 6 | # 7 | # The application 'rake' is installed as part of a gem, and 8 | # this file is here to facilitate running it. 9 | # 10 | 11 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) 12 | 13 | bundle_binstub = File.expand_path("bundle", __dir__) 14 | 15 | if File.file?(bundle_binstub) 16 | if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/ 17 | load(bundle_binstub) 18 | else 19 | abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. 20 | Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.") 21 | end 22 | end 23 | 24 | require "rubygems" 25 | require "bundler/setup" 26 | 27 | load Gem.bin_path("rake", "rake") 28 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install "$@" 7 | -------------------------------------------------------------------------------- /exe/graphql-docs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "graphql-docs" 4 | require "optparse" 5 | 6 | NAME = "graphql-docs".freeze 7 | 8 | opts = {} 9 | OptionParser.new do |parser| 10 | parser.program_name = NAME 11 | parser.banner = <<~EOS 12 | 13 | Generate GraphQL docs from the passed in schema file. 14 | 15 | Usage: graphql-docs SCHEMA 16 | 17 | The only required argument is the path to the schema file to generate the site from. 18 | 19 | Examples: 20 | $ graphql-docs schema.graphql 21 | $ graphql-docs schema.graphql -o _docs 22 | 23 | Options: 24 | EOS 25 | 26 | parser.version = GraphQLDocs::VERSION 27 | 28 | parser.on("-o", "--output-dir DIR", "Where the site is generated to, defaults to ./output") 29 | parser.on("-d", "--delete-output", "Delete the output-dir before generating, defaults to false") 30 | parser.on("-b", "--base-url URL", "URL to preprend for assets and links, defaults to \"\"") 31 | parser.on("-v", "--version", "Show the version") 32 | parser.on("--verbose", "Run in verbose mode") 33 | end.parse!(into: opts) 34 | 35 | if opts[:version] 36 | puts("v#{GraphQLDocs::VERSION}") 37 | exit 38 | end 39 | 40 | def err(msg) 41 | abort("#{NAME}: Error: #{msg}") 42 | end 43 | 44 | schema = ARGV[0] 45 | if schema.nil? 46 | err("schema must be specified") 47 | end 48 | opts[:filename] = schema 49 | 50 | verbose = opts.delete(:verbose) 51 | 52 | puts("Generating site with the following options: #{opts}") if verbose 53 | 54 | opts.transform_keys! { |k| k.to_s.gsub("-", "_").to_sym } 55 | GraphQLDocs.build(opts) 56 | 57 | puts("Site successfully generated in: #{opts[:output_dir] || 'output' }") if verbose 58 | -------------------------------------------------------------------------------- /graphql-docs.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | lib = File.expand_path('lib', __dir__) 4 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 5 | require 'graphql-docs/version' 6 | 7 | Gem::Specification.new do |spec| 8 | spec.name = 'graphql-docs' 9 | spec.version = GraphQLDocs::VERSION 10 | spec.authors = ['Brett Chalupa', 'Garen Torikian'] 11 | spec.email = ['brettchalupa@gmail.com'] 12 | 13 | spec.summary = 'Easily generate beautiful documentation from your GraphQL schema.' 14 | spec.description = <<-EOF 15 | Library and CLI for generating a website from a GraphQL API's schema 16 | definition. With ERB templating support and a plethora of configuration 17 | options, you can customize the output to your needs. The library easily 18 | integrates with your Ruby deployment toolchain to ensure the docs for your 19 | API are up to date. 20 | EOF 21 | spec.homepage = 'https://github.com/brettchalupa/graphql-docs' 22 | spec.license = 'MIT' 23 | spec.metadata = { 24 | "bug_tracker_uri" => "https://github.com/brettchalupa/graphql-docs/issues", 25 | "changelog_uri" => "https://github.com/brettchalupa/graphql-docs/blob/main/CHANGELOG.md", 26 | "wiki_uri" => "https://github.com/brettchalupa/graphql-docs/wiki", 27 | } 28 | 29 | spec.files = `git ls-files -z`.split("\x0").reject do |f| 30 | f.match(%r{^(test|spec|features)/}) 31 | end 32 | spec.bindir = 'exe' 33 | spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } 34 | spec.require_paths = ['lib'] 35 | 36 | spec.required_ruby_version = '>= 3.1' 37 | 38 | spec.add_dependency 'graphql', '~> 2.0' 39 | 40 | # rendering 41 | spec.add_dependency 'commonmarker', '>= 0.23.6', '~> 0.23' 42 | spec.add_dependency 'escape_utils', '~> 1.2' 43 | spec.add_dependency 'extended-markdown-filter', '~> 0.4' 44 | spec.add_dependency 'gemoji', '~> 3.0' 45 | spec.add_dependency 'html-pipeline', '>= 2.14.3', '~> 2.14' 46 | spec.add_dependency 'sass-embedded', '~> 1.58' 47 | spec.add_dependency 'ostruct', '~> 0.6' 48 | spec.add_dependency 'logger', '~> 1.6' 49 | 50 | spec.add_development_dependency 'html-proofer', '~> 3.4' 51 | spec.add_development_dependency 'minitest', '~> 5.24' 52 | spec.add_development_dependency 'minitest-focus', '~> 1.1' 53 | spec.add_development_dependency 'rake', '~> 13.0' 54 | spec.add_development_dependency 'rubocop', '~> 1.37' 55 | spec.add_development_dependency 'rubocop-performance', '~> 1.15' 56 | spec.add_development_dependency 'webmock', '~> 2.3' 57 | spec.add_development_dependency 'webrick', '~> 1.7' 58 | end 59 | -------------------------------------------------------------------------------- /lib/graphql-docs.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'graphql-docs/helpers' 4 | require 'graphql-docs/renderer' 5 | require 'graphql-docs/configuration' 6 | require 'graphql-docs/generator' 7 | require 'graphql-docs/parser' 8 | require 'graphql-docs/version' 9 | 10 | module GraphQLDocs 11 | class << self 12 | def build(options) 13 | # do not let user provided values overwrite every single value 14 | %i[templates landing_pages].each do |opt| 15 | next unless options.key?(opt) 16 | 17 | GraphQLDocs::Configuration::GRAPHQLDOCS_DEFAULTS[opt].each_pair do |key, value| 18 | options[opt][key] = value unless options[opt].key?(key) 19 | end 20 | end 21 | 22 | options = GraphQLDocs::Configuration::GRAPHQLDOCS_DEFAULTS.merge(options) 23 | 24 | filename = options[:filename] 25 | schema = options[:schema] 26 | 27 | raise ArgumentError, 'Pass in `filename` or `schema`, but not both!' if !filename.nil? && !schema.nil? 28 | 29 | raise ArgumentError, 'Pass in either `filename` or `schema`' if filename.nil? && schema.nil? 30 | 31 | if filename 32 | raise TypeError, "Expected `String`, got `#{filename.class}`" unless filename.is_a?(String) 33 | 34 | raise ArgumentError, "#{filename} does not exist!" unless File.exist?(filename) 35 | 36 | schema = File.read(filename) 37 | else 38 | raise TypeError, "Expected `String` or `GraphQL::Schema`, got `#{schema.class}`" if !schema.is_a?(String) && !schema_type?(schema) 39 | 40 | schema = schema 41 | end 42 | 43 | parser = GraphQLDocs::Parser.new(schema, options) 44 | parsed_schema = parser.parse 45 | 46 | generator = GraphQLDocs::Generator.new(parsed_schema, options) 47 | 48 | generator.generate 49 | end 50 | 51 | private def schema_type?(object) 52 | object.respond_to?(:ancestors) && object.ancestors.include?(GraphQL::Schema) 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /lib/graphql-docs/configuration.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module GraphQLDocs 4 | module Configuration 5 | GRAPHQLDOCS_DEFAULTS = { 6 | # initialize 7 | filename: nil, 8 | schema: nil, 9 | 10 | # Generating 11 | delete_output: false, 12 | output_dir: './output/', 13 | pipeline_config: { 14 | pipeline: 15 | %i[ExtendedMarkdownFilter 16 | EmojiFilter 17 | TableOfContentsFilter], 18 | context: { 19 | gfm: false, 20 | unsafe: true, # necessary for layout needs, given that it's all HTML templates 21 | asset_root: 'https://a248.e.akamai.net/assets.github.com/images/icons' 22 | } 23 | }, 24 | renderer: GraphQLDocs::Renderer, 25 | use_default_styles: true, 26 | base_url: '', 27 | 28 | templates: { 29 | default: "#{File.dirname(__FILE__)}/layouts/default.html", 30 | 31 | includes: "#{File.dirname(__FILE__)}/layouts/includes", 32 | 33 | operations: "#{File.dirname(__FILE__)}/layouts/graphql_operations.html", 34 | objects: "#{File.dirname(__FILE__)}/layouts/graphql_objects.html", 35 | queries: "#{File.dirname(__FILE__)}/layouts/graphql_queries.html", 36 | mutations: "#{File.dirname(__FILE__)}/layouts/graphql_mutations.html", 37 | interfaces: "#{File.dirname(__FILE__)}/layouts/graphql_interfaces.html", 38 | enums: "#{File.dirname(__FILE__)}/layouts/graphql_enums.html", 39 | unions: "#{File.dirname(__FILE__)}/layouts/graphql_unions.html", 40 | input_objects: "#{File.dirname(__FILE__)}/layouts/graphql_input_objects.html", 41 | scalars: "#{File.dirname(__FILE__)}/layouts/graphql_scalars.html", 42 | directives: "#{File.dirname(__FILE__)}/layouts/graphql_directives.html" 43 | }, 44 | 45 | landing_pages: { 46 | index: "#{File.dirname(__FILE__)}/landing_pages/index.md", 47 | query: "#{File.dirname(__FILE__)}/landing_pages/query.md", 48 | object: "#{File.dirname(__FILE__)}/landing_pages/object.md", 49 | mutation: "#{File.dirname(__FILE__)}/landing_pages/mutation.md", 50 | interface: "#{File.dirname(__FILE__)}/landing_pages/interface.md", 51 | enum: "#{File.dirname(__FILE__)}/landing_pages/enum.md", 52 | union: "#{File.dirname(__FILE__)}/landing_pages/union.md", 53 | input_object: "#{File.dirname(__FILE__)}/landing_pages/input_object.md", 54 | scalar: "#{File.dirname(__FILE__)}/landing_pages/scalar.md", 55 | directive: "#{File.dirname(__FILE__)}/landing_pages/directive.md", 56 | 57 | variables: {} # only used for ERB landing pages 58 | }, 59 | 60 | classes: { 61 | field_entry: '', 62 | deprecation_notice: '', 63 | notice: '', 64 | notice_title: '' 65 | } 66 | }.freeze 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /lib/graphql-docs/generator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'erb' 4 | require 'fileutils' 5 | require 'sass-embedded' 6 | require 'ostruct' 7 | 8 | module GraphQLDocs 9 | class Generator 10 | include Helpers 11 | 12 | attr_accessor :parsed_schema 13 | 14 | def initialize(parsed_schema, options) 15 | @parsed_schema = parsed_schema 16 | @options = options 17 | 18 | @renderer = @options[:renderer].new(@parsed_schema, @options) 19 | 20 | %i[operations objects queries mutations interfaces enums unions input_objects scalars directives].each do |sym| 21 | raise IOError, "`#{sym}` template #{@options[:templates][sym]} was not found" unless File.exist?(@options[:templates][sym]) 22 | 23 | instance_variable_set("@graphql_#{sym}_template", ERB.new(File.read(@options[:templates][sym]))) 24 | end 25 | 26 | %i[index object query mutation interface enum union input_object scalar directive].each do |sym| 27 | if @options[:landing_pages][sym].nil? 28 | instance_variable_set("@#{sym}_landing_page", nil) 29 | elsif !File.exist?(@options[:landing_pages][sym]) 30 | raise IOError, "`#{sym}` landing page #{@options[:landing_pages][sym]} was not found" 31 | end 32 | 33 | landing_page_contents = File.read(@options[:landing_pages][sym]) 34 | metadata = '' 35 | 36 | if File.extname((@options[:landing_pages][sym])) == '.erb' 37 | opts = @options.merge(@options[:landing_pages][:variables]).merge(helper_methods) 38 | if yaml?(landing_page_contents) 39 | metadata, landing_page = split_into_metadata_and_contents(landing_page_contents, parse: false) 40 | erb_template = ERB.new(landing_page) 41 | else 42 | erb_template = ERB.new(landing_page_contents) 43 | end 44 | 45 | landing_page_contents = erb_template.result(OpenStruct.new(opts).instance_eval { binding }) 46 | end 47 | 48 | instance_variable_set("@graphql_#{sym}_landing_page", metadata + landing_page_contents) 49 | end 50 | end 51 | 52 | def generate 53 | FileUtils.rm_rf(@options[:output_dir]) if @options[:delete_output] 54 | 55 | has_query = create_graphql_operation_pages 56 | create_graphql_object_pages 57 | create_graphql_query_pages 58 | create_graphql_mutation_pages 59 | create_graphql_interface_pages 60 | create_graphql_enum_pages 61 | create_graphql_union_pages 62 | create_graphql_input_object_pages 63 | create_graphql_scalar_pages 64 | create_graphql_directive_pages 65 | 66 | write_file('static', 'index', @graphql_index_landing_page, trim: false) unless @graphql_index_landing_page.nil? 67 | 68 | write_file('static', 'object', @graphql_object_landing_page, trim: false) unless @graphql_object_landing_page.nil? 69 | 70 | write_file('operation', 'query', @graphql_query_landing_page, trim: false) if !@graphql_query_landing_page.nil? && !has_query 71 | 72 | write_file('operation', 'mutation', @graphql_mutation_landing_page, trim: false) unless @graphql_mutation_landing_page.nil? 73 | 74 | write_file('static', 'interface', @graphql_interface_landing_page, trim: false) unless @graphql_interface_landing_page.nil? 75 | 76 | write_file('static', 'enum', @graphql_enum_landing_page, trim: false) unless @graphql_enum_landing_page.nil? 77 | 78 | write_file('static', 'union', @graphql_union_landing_page, trim: false) unless @graphql_union_landing_page.nil? 79 | 80 | write_file('static', 'input_object', @graphql_input_object_landing_page, trim: false) unless @graphql_input_object_landing_page.nil? 81 | 82 | write_file('static', 'scalar', @graphql_scalar_landing_page, trim: false) unless @graphql_scalar_landing_page.nil? 83 | 84 | write_file('static', 'directive', @graphql_directive_landing_page, trim: false) unless @graphql_directive_landing_page.nil? 85 | 86 | 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')) 89 | 90 | css = Sass.compile(File.join(assets_dir, 'css', 'screen.scss')).css 91 | File.write(File.join(@options[:output_dir], 'assets', 'style.css'), css) 92 | 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')) 95 | end 96 | 97 | true 98 | end 99 | 100 | def create_graphql_operation_pages 101 | graphql_operation_types.each do |query_type| 102 | metadata = '' 103 | next unless query_type[:name] == graphql_root_types['query'] 104 | 105 | unless @options[:landing_pages][:query].nil? 106 | query_landing_page = @options[:landing_pages][:query] 107 | query_landing_page = File.read(query_landing_page) 108 | if yaml?(query_landing_page) 109 | pieces = yaml_split(query_landing_page) 110 | pieces[2] = pieces[2].chomp 111 | metadata = pieces[1, 3].join("\n") 112 | query_landing_page = pieces[4] 113 | end 114 | query_type[:description] = query_landing_page 115 | end 116 | opts = default_generator_options(type: query_type) 117 | contents = @graphql_operations_template.result(OpenStruct.new(opts).instance_eval { binding }) 118 | write_file('operation', 'query', metadata + contents) 119 | return true 120 | end 121 | false 122 | end 123 | 124 | def create_graphql_object_pages 125 | graphql_object_types.each do |object_type| 126 | opts = default_generator_options(type: object_type) 127 | 128 | contents = @graphql_objects_template.result(OpenStruct.new(opts).instance_eval { binding }) 129 | write_file('object', object_type[:name], contents) 130 | end 131 | end 132 | 133 | def create_graphql_query_pages 134 | graphql_query_types.each do |query| 135 | opts = default_generator_options(type: query) 136 | 137 | contents = @graphql_queries_template.result(OpenStruct.new(opts).instance_eval { binding }) 138 | write_file('query', query[:name], contents) 139 | end 140 | end 141 | 142 | def create_graphql_mutation_pages 143 | graphql_mutation_types.each do |mutation| 144 | opts = default_generator_options(type: mutation) 145 | 146 | contents = @graphql_mutations_template.result(OpenStruct.new(opts).instance_eval { binding }) 147 | write_file('mutation', mutation[:name], contents) 148 | end 149 | end 150 | 151 | def create_graphql_interface_pages 152 | graphql_interface_types.each do |interface_type| 153 | opts = default_generator_options(type: interface_type) 154 | 155 | contents = @graphql_interfaces_template.result(OpenStruct.new(opts).instance_eval { binding }) 156 | write_file('interface', interface_type[:name], contents) 157 | end 158 | end 159 | 160 | def create_graphql_enum_pages 161 | graphql_enum_types.each do |enum_type| 162 | opts = default_generator_options(type: enum_type) 163 | 164 | contents = @graphql_enums_template.result(OpenStruct.new(opts).instance_eval { binding }) 165 | write_file('enum', enum_type[:name], contents) 166 | end 167 | end 168 | 169 | def create_graphql_union_pages 170 | graphql_union_types.each do |union_type| 171 | opts = default_generator_options(type: union_type) 172 | 173 | contents = @graphql_unions_template.result(OpenStruct.new(opts).instance_eval { binding }) 174 | write_file('union', union_type[:name], contents) 175 | end 176 | end 177 | 178 | def create_graphql_input_object_pages 179 | graphql_input_object_types.each do |input_object_type| 180 | opts = default_generator_options(type: input_object_type) 181 | 182 | contents = @graphql_input_objects_template.result(OpenStruct.new(opts).instance_eval { binding }) 183 | write_file('input_object', input_object_type[:name], contents) 184 | end 185 | end 186 | 187 | def create_graphql_scalar_pages 188 | graphql_scalar_types.each do |scalar_type| 189 | opts = default_generator_options(type: scalar_type) 190 | 191 | contents = @graphql_scalars_template.result(OpenStruct.new(opts).instance_eval { binding }) 192 | write_file('scalar', scalar_type[:name], contents) 193 | end 194 | end 195 | 196 | def create_graphql_directive_pages 197 | graphql_directive_types.each do |directive_type| 198 | opts = default_generator_options(type: directive_type) 199 | 200 | contents = @graphql_directives_template.result(OpenStruct.new(opts).instance_eval { binding }) 201 | write_file('directive', directive_type[:name], contents) 202 | end 203 | end 204 | 205 | private 206 | 207 | def default_generator_options(opts = {}) 208 | @options.merge(opts).merge(helper_methods) 209 | end 210 | 211 | def write_file(type, name, contents, trim: true) 212 | if type == 'static' 213 | if name == 'index' 214 | path = @options[:output_dir] 215 | else 216 | path = File.join(@options[:output_dir], name) 217 | FileUtils.mkdir_p(path) 218 | end 219 | else 220 | path = File.join(@options[:output_dir], type, name.downcase) 221 | FileUtils.mkdir_p(path) 222 | end 223 | 224 | if yaml?(contents) 225 | # Split data 226 | meta, contents = split_into_metadata_and_contents(contents) 227 | @options = @options.merge(meta) 228 | end 229 | 230 | if trim 231 | # normalize spacing so that CommonMarker doesn't treat it as `pre` 232 | contents.gsub!(/^\s+$/, '') 233 | contents.gsub!(/^\s{4}/m, ' ') 234 | end 235 | 236 | filename = File.join(path, 'index.html') 237 | contents = @renderer.render(contents, type: type, name: name, filename: filename) 238 | File.write(filename, contents) unless contents.nil? 239 | end 240 | end 241 | end 242 | -------------------------------------------------------------------------------- /lib/graphql-docs/helpers.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'commonmarker' 4 | require 'ostruct' 5 | 6 | module GraphQLDocs 7 | module Helpers 8 | SLUGIFY_PRETTY_REGEXP = Regexp.new("[^[:alnum:]._~!$&'()+,;=@]+").freeze 9 | 10 | attr_accessor :templates 11 | 12 | def slugify(str) 13 | slug = str.gsub(SLUGIFY_PRETTY_REGEXP, '-') 14 | slug.gsub!(/^\-|\-$/i, '') 15 | slug.downcase 16 | end 17 | 18 | def include(filename, opts = {}) 19 | template = fetch_include(filename) 20 | opts = { base_url: @options[:base_url], classes: @options[:classes] }.merge(opts) 21 | template.result(OpenStruct.new(opts.merge(helper_methods)).instance_eval { binding }) 22 | end 23 | 24 | def markdownify(string) 25 | return '' if string.nil? 26 | 27 | type = @options[:pipeline_config][:context][:unsafe] ? :UNSAFE : :DEFAULT 28 | ::CommonMarker.render_html(string, type).strip 29 | end 30 | 31 | def graphql_root_types 32 | @parsed_schema[:root_types] || [] 33 | end 34 | 35 | def graphql_operation_types 36 | @parsed_schema[:operation_types] || [] 37 | end 38 | 39 | def graphql_query_types 40 | @parsed_schema[:query_types] || [] 41 | end 42 | 43 | def graphql_mutation_types 44 | @parsed_schema[:mutation_types] || [] 45 | end 46 | 47 | def graphql_object_types 48 | @parsed_schema[:object_types] || [] 49 | end 50 | 51 | def graphql_interface_types 52 | @parsed_schema[:interface_types] || [] 53 | end 54 | 55 | def graphql_enum_types 56 | @parsed_schema[:enum_types] || [] 57 | end 58 | 59 | def graphql_union_types 60 | @parsed_schema[:union_types] || [] 61 | end 62 | 63 | def graphql_input_object_types 64 | @parsed_schema[:input_object_types] || [] 65 | end 66 | 67 | def graphql_scalar_types 68 | @parsed_schema[:scalar_types] || [] 69 | end 70 | 71 | def graphql_directive_types 72 | @parsed_schema[:directive_types] || [] 73 | end 74 | 75 | def split_into_metadata_and_contents(contents, parse: true) 76 | pieces = yaml_split(contents) 77 | 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 78 | 79 | # Parse 80 | begin 81 | meta = if parse 82 | YAML.safe_load(pieces[2]) || {} 83 | else 84 | pieces[2] 85 | end 86 | rescue Exception => e # rubocop:disable Lint/RescueException 87 | raise "Could not parse YAML for #{name}: #{e.message}" 88 | end 89 | [meta, pieces[4]] 90 | end 91 | 92 | def yaml?(contents) 93 | contents =~ /\A-{3,5}\s*$/ 94 | end 95 | 96 | def yaml_split(contents) 97 | contents.split(/^(-{5}|-{3})[ \t]*\r?\n?/, 3) 98 | end 99 | 100 | private 101 | 102 | def fetch_include(filename) 103 | @templates ||= {} 104 | 105 | return @templates[filename] unless @templates[filename].nil? 106 | 107 | contents = File.read(File.join(@options[:templates][:includes], filename)) 108 | 109 | @templates[filename] = ERB.new(contents) 110 | end 111 | 112 | def helper_methods 113 | return @helper_methods if defined?(@helper_methods) 114 | 115 | @helper_methods = {} 116 | 117 | Helpers.instance_methods.each do |name| 118 | next if name == :helper_methods 119 | 120 | @helper_methods[name] = method(name) 121 | end 122 | 123 | @helper_methods 124 | end 125 | end 126 | end 127 | -------------------------------------------------------------------------------- /lib/graphql-docs/landing_pages/directive.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Directives 3 | --- 4 | 5 | # Directives 6 | 7 | Directives provide a way to describe alternate runtime execution and type validation behavior in a GraphQL document. 8 | 9 | For more information, see [the GraphQL spec](https://graphql.github.io/graphql-spec/draft/#sec-Language.Directives). 10 | -------------------------------------------------------------------------------- /lib/graphql-docs/landing_pages/enum.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Enums 3 | --- 4 | 5 | # Enums 6 | 7 | Enums represent a possible set of values for a field. For example, the `Issue` object has a field called `state`. The state of an issue may be `OPEN` or `CLOSED`. 8 | 9 | For more information, see [the GraphQL spec](http://spec.graphql.org/draft/#sec-Enums). 10 | -------------------------------------------------------------------------------- /lib/graphql-docs/landing_pages/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: GraphQL documentation 3 | --- 4 | 5 | # GraphQL Reference 6 | 7 | These docs were generated by [graphql-docs](https://github.com/gjtorikian/graphql-docs). 8 | 9 | ## New to GraphQL? 10 | 11 | Just starting out with GraphQL? Check out GraphQL's [official documentation](https://graphql.org/)! 12 | -------------------------------------------------------------------------------- /lib/graphql-docs/landing_pages/input_object.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Input Objects 3 | --- 4 | 5 | # Input Objects 6 | 7 | Input objects are best described as "composable objects" in that they contain a set of input fields that define a particular object. For example, the `AuthorInput` takes a field called `emails`. Providing a value for `emails` will transform the `AuthorInput` into a list of `User` objects which contain that email address/ 8 | 9 | For more information, see [the GraphQL spec](http://spec.graphql.org/draft/#sec-Input-Objects). 10 | -------------------------------------------------------------------------------- /lib/graphql-docs/landing_pages/interface.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Interfaces 3 | --- 4 | 5 | # Interfaces 6 | 7 | GraphQL Interfaces are a sort of "parent object" from which other objects can "inherit" from. For example, `Stars` is considered an interface, because both `Repository` and `Gist` can be starred. An interface has its own list of named fields that are shared by implementing objects. 8 | 9 | For more information, see [the GraphQL spec](http://spec.graphql.org/draft/#sec-Interfaces). 10 | -------------------------------------------------------------------------------- /lib/graphql-docs/landing_pages/mutation.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Mutations 3 | --- 4 | 5 | # Mutations 6 | 7 | Every GraphQL schema has a root type for both queries and mutations. 8 | 9 | The mutation type defines how GraphQL operations change data. It is analogous to performing HTTP verbs such as `POST`, `PATCH`, and `DELETE`. 10 | 11 | For more information, see [the GraphQL spec](http://spec.graphql.org/draft/#sec-Type-System). 12 | -------------------------------------------------------------------------------- /lib/graphql-docs/landing_pages/object.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Objects 3 | --- 4 | # Objects 5 | 6 | Objects in GraphQL represent the resources that you can access. Objects can contain a list of fields, which are specifically typed. For example, the `Repository` object has a field called `name`, which is a `String`. 7 | 8 | For more information, see [the GraphQL spec](http://spec.graphql.org/draft/#sec-Objects). 9 | -------------------------------------------------------------------------------- /lib/graphql-docs/landing_pages/query.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Queries 3 | --- 4 | Every GraphQL schema has a root type for both queries and mutations. 5 | 6 | The query type defines GraphQL operations that retrieve data from the server. 7 | 8 | For more information, see [the GraphQL spec](http://spec.graphql.org/draft/#sec-Type-System). 9 | -------------------------------------------------------------------------------- /lib/graphql-docs/landing_pages/scalar.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Scalars 3 | --- 4 | 5 | # Scalars 6 | 7 | Scalars are primitive values such as `Int` or `String`. 8 | 9 | For more information, see [the GraphQL spec](http://spec.graphql.org/draft/#sec-Scalars). 10 | -------------------------------------------------------------------------------- /lib/graphql-docs/landing_pages/union.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Unions 3 | --- 4 | 5 | # Unions 6 | 7 | A union is a type of object that can represent one of many kinds of objects. For example, a field marked as a `ReactableUnion` could be a `CommitComment`, an `Issue`, an `IssueComment`, or a `PullRequestReviewComment`, because each of those objects can be reacted on. 8 | 9 | For more information, see [the GraphQL spec](http://spec.graphql.org/draft/#sec-Unions). 10 | -------------------------------------------------------------------------------- /lib/graphql-docs/layouts/assets/_sass/_api-box.scss: -------------------------------------------------------------------------------- 1 | .api { 2 | background: #fafafa; 3 | h3 { 4 | padding: 5px 10px; 5 | } 6 | h3.api-title { 7 | margin: 0; 8 | overflow: auto; 9 | } 10 | h4 { 11 | font-weight: normal; 12 | font-style: italic; 13 | margin-bottom: 0.25em; 14 | text-decoration: underline; 15 | margin-left: 20px; 16 | } 17 | dl { 18 | margin-top: 0.25em; 19 | } 20 | dl.args { 21 | margin-left: 40px; 22 | } 23 | dl.constants { 24 | margin-left: 20px; 25 | } 26 | dt { 27 | margin-top: 1em; 28 | .name { 29 | font-weight: bold; 30 | } 31 | .type { 32 | margin-left: 15px; 33 | font-size: 0.9em; 34 | font-weight: 200; 35 | color: #000; 36 | } 37 | } 38 | dd { 39 | margin-bottom: 1em; 40 | margin-left: 0; 41 | } 42 | .desc { 43 | margin: 1em; 44 | } 45 | pre { 46 | margin-right: 10px; 47 | } 48 | } 49 | h3.api-title { 50 | padding: 5px 10px; 51 | margin-top: 2em; 52 | } 53 | .api-title { 54 | .locus { 55 | float: right; 56 | font-weight: normal; 57 | padding-right: 5px; 58 | font-style: italic; 59 | } 60 | .subtext { 61 | font-size: 11px; 62 | text-align: left; 63 | clear: both; 64 | display: block; 65 | font-weight: normal; 66 | >code { 67 | font-size: 11px; 68 | margin-right: 12px; 69 | } 70 | } 71 | .src-code { 72 | color: #20338a !important; 73 | border-bottom: none !important; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /lib/graphql-docs/layouts/assets/_sass/_content.scss: -------------------------------------------------------------------------------- 1 | #content { 2 | padding: 20px 30px; 3 | max-width: 760px; 4 | margin: 0px auto; 5 | -webkit-text-size-adjust: 100%; 6 | em { 7 | font-style: italic; 8 | } 9 | strong { 10 | font-family: 'ProximaNova-Bold'; 11 | } 12 | h1 { 13 | margin: 15px 0; 14 | line-height: 1.4em; 15 | font-size: 2em; 16 | margin-top: 0; 17 | margin-bottom: 30px; 18 | } 19 | h2 { 20 | margin: 15px 0; 21 | line-height: 1.4em; 22 | font-size: 1.5em; 23 | margin-top: 30px; 24 | padding-bottom: 10px; 25 | border-bottom: 1px solid #eee; 26 | position: relative; 27 | .anchor { 28 | opacity: 0; 29 | position: absolute; 30 | font-size: 16px; 31 | top: 2px; 32 | left: -21px; 33 | } 34 | &:hover { 35 | .anchor { 36 | opacity: 1; 37 | } 38 | } 39 | } 40 | h3 { 41 | margin: 15px 0; 42 | line-height: 1.4em; 43 | font-size: 1.2em; 44 | margin-top: 30px; 45 | position: relative; 46 | .anchor { 47 | opacity: 0; 48 | position: absolute; 49 | font-size: 16px; 50 | top: 2px; 51 | left: -21px; 52 | } 53 | &:hover { 54 | .anchor { 55 | opacity: 1; 56 | } 57 | } 58 | } 59 | h4 { 60 | margin: 15px 0; 61 | line-height: 1.4em; 62 | } 63 | h5 { 64 | margin: 15px 0; 65 | line-height: 1.4em; 66 | } 67 | h6 { 68 | margin: 15px 0; 69 | line-height: 1.4em; 70 | } 71 | p { 72 | margin: 15px 0; 73 | line-height: 1.4em; 74 | } 75 | ul { 76 | margin: 15px 0; 77 | line-height: 1.4em; 78 | padding-left: 1.5em; 79 | list-style-type: disc; 80 | li { 81 | margin-bottom: 5px; 82 | } 83 | } 84 | ol { 85 | margin: 15px 0; 86 | line-height: 1.4em; 87 | padding-left: 1.5em; 88 | list-style-type: decimal; 89 | li { 90 | margin-bottom: 5px; 91 | } 92 | } 93 | figure { 94 | margin: 15px 0; 95 | line-height: 1.4em; 96 | } 97 | a { 98 | color: #de4f4f; 99 | } 100 | img { 101 | max-width: 100%; 102 | } 103 | code { 104 | font-family: Menlo, Monaco, Consolas, "Courier New", monospace; 105 | font-size: 0.8em; 106 | line-height: 1.6em; 107 | padding: 1px 4px; 108 | background-color: #eee; 109 | margin: 0 2px; 110 | } 111 | blockquote { 112 | padding-left: 1.3em; 113 | border-left: #eee solid 0.2em; 114 | font-style: italic; 115 | } 116 | blockquote.warning { 117 | border-color: #f00; 118 | color: #f00; 119 | } 120 | dl { 121 | margin-left: 1.5em; 122 | dt { 123 | .name { 124 | font-family: monospace; 125 | } 126 | .type { 127 | margin-left: 0.5em; 128 | } 129 | } 130 | dd { 131 | margin-left: 1.5em; 132 | } 133 | } 134 | .edit-discuss-links { 135 | margin-top: -25px; 136 | margin-bottom: 40px; 137 | } 138 | table { 139 | margin-top: 10px; 140 | th { 141 | text-align: left; 142 | padding: 0 25px 0 25px; 143 | } 144 | thead th:first-child { 145 | padding: 0; 146 | } 147 | td p { 148 | padding: 0 25px 0 25px; 149 | } 150 | } 151 | pre { 152 | font-family: Menlo, Monaco, Consolas, "Courier New", monospace; 153 | line-height: 1.6em; 154 | margin: 20px 0; 155 | overflow-x: auto; 156 | position: relative; 157 | padding: 20px 30px; 158 | table { 159 | width: 100%; 160 | border-collapse: collapse; 161 | padding: 0; 162 | margin: 0; 163 | } 164 | tr { 165 | width: 100%; 166 | border-collapse: collapse; 167 | padding: 0; 168 | margin: 0; 169 | } 170 | td { 171 | width: 100%; 172 | border-collapse: collapse; 173 | padding: 0; 174 | margin: 0; 175 | } 176 | code { 177 | background-color: #272822; 178 | padding: 0; 179 | margin: 0; 180 | } 181 | .gutter { 182 | user-select: none; 183 | width: 1.5em; 184 | padding-right: 30px; 185 | } 186 | } 187 | .highlight.html { 188 | .code { 189 | &:after { 190 | font-family: 'ProximaNova-Semibold'; 191 | position: absolute; 192 | top: 0; 193 | right: 0; 194 | color: #ccc; 195 | text-align: right; 196 | font-size: 0.75em; 197 | padding: 5px 10px 0; 198 | letter-spacing: 1.5px; 199 | line-height: 15px; 200 | height: 15px; 201 | font-weight: 600; 202 | } 203 | } 204 | } 205 | .highlight.js { 206 | .code { 207 | &:after { 208 | font-family: 'ProximaNova-Semibold'; 209 | position: absolute; 210 | top: 0; 211 | right: 0; 212 | color: #ccc; 213 | text-align: right; 214 | font-size: 0.75em; 215 | padding: 5px 10px 0; 216 | letter-spacing: 1.5px; 217 | line-height: 15px; 218 | height: 15px; 219 | font-weight: 600; 220 | } 221 | } 222 | } 223 | .highlight.bash { 224 | .code { 225 | &:after { 226 | font-family: 'ProximaNova-Semibold'; 227 | position: absolute; 228 | top: 0; 229 | right: 0; 230 | color: #ccc; 231 | text-align: right; 232 | font-size: 0.75em; 233 | padding: 5px 10px 0; 234 | letter-spacing: 1.5px; 235 | line-height: 15px; 236 | height: 15px; 237 | font-weight: 600; 238 | } 239 | } 240 | } 241 | .highlight.css { 242 | .code { 243 | &:after { 244 | font-family: 'ProximaNova-Semibold'; 245 | position: absolute; 246 | top: 0; 247 | right: 0; 248 | color: #ccc; 249 | text-align: right; 250 | font-size: 0.75em; 251 | padding: 5px 10px 0; 252 | letter-spacing: 1.5px; 253 | line-height: 15px; 254 | height: 15px; 255 | font-weight: 600; 256 | } 257 | } 258 | } 259 | .highlight.jsx { 260 | .code { 261 | &:after { 262 | font-family: 'ProximaNova-Semibold'; 263 | position: absolute; 264 | top: 0; 265 | right: 0; 266 | color: #ccc; 267 | text-align: right; 268 | font-size: 0.75em; 269 | padding: 5px 10px 0; 270 | letter-spacing: 1.5px; 271 | line-height: 15px; 272 | height: 15px; 273 | font-weight: 600; 274 | } 275 | } 276 | } 277 | .highlight.html.html { 278 | .code { 279 | &:after { 280 | content: 'HTML'; 281 | } 282 | } 283 | } 284 | .highlight.js.html { 285 | .code { 286 | &:after { 287 | content: 'HTML'; 288 | } 289 | } 290 | } 291 | .highlight.bash.html { 292 | .code { 293 | &:after { 294 | content: 'HTML'; 295 | } 296 | } 297 | } 298 | .highlight.css.html { 299 | .code { 300 | &:after { 301 | content: 'HTML'; 302 | } 303 | } 304 | } 305 | .highlight.jsx.html { 306 | .code { 307 | &:after { 308 | content: 'HTML'; 309 | } 310 | } 311 | } 312 | .highlight.html.js { 313 | .code { 314 | &:after { 315 | content: 'JS'; 316 | } 317 | } 318 | } 319 | .highlight.js.js { 320 | .code { 321 | &:after { 322 | content: 'JS'; 323 | } 324 | } 325 | } 326 | .highlight.bash.js { 327 | .code { 328 | &:after { 329 | content: 'JS'; 330 | } 331 | } 332 | } 333 | .highlight.css.js { 334 | .code { 335 | &:after { 336 | content: 'JS'; 337 | } 338 | } 339 | } 340 | .highlight.jsx.js { 341 | .code { 342 | &:after { 343 | content: 'JS'; 344 | } 345 | } 346 | } 347 | .highlight.html.bash { 348 | .code { 349 | &:after { 350 | content: 'Shell'; 351 | } 352 | } 353 | } 354 | .highlight.js.bash { 355 | .code { 356 | &:after { 357 | content: 'Shell'; 358 | } 359 | } 360 | } 361 | .highlight.bash.bash { 362 | .code { 363 | &:after { 364 | content: 'Shell'; 365 | } 366 | } 367 | } 368 | .highlight.css.bash { 369 | .code { 370 | &:after { 371 | content: 'Shell'; 372 | } 373 | } 374 | } 375 | .highlight.jsx.bash { 376 | .code { 377 | &:after { 378 | content: 'Shell'; 379 | } 380 | } 381 | } 382 | .highlight.html.css { 383 | .code { 384 | &:after { 385 | content: 'CSS'; 386 | } 387 | } 388 | } 389 | .highlight.js.css { 390 | .code { 391 | &:after { 392 | content: 'CSS'; 393 | } 394 | } 395 | } 396 | .highlight.bash.css { 397 | .code { 398 | &:after { 399 | content: 'CSS'; 400 | } 401 | } 402 | } 403 | .highlight.css.css { 404 | .code { 405 | &:after { 406 | content: 'CSS'; 407 | } 408 | } 409 | } 410 | .highlight.jsx.css { 411 | .code { 412 | &:after { 413 | content: 'CSS'; 414 | } 415 | } 416 | } 417 | .highlight.html.jsx { 418 | .code { 419 | &:after { 420 | content: 'JSX'; 421 | } 422 | } 423 | } 424 | .highlight.js.jsx { 425 | .code { 426 | &:after { 427 | content: 'JSX'; 428 | } 429 | } 430 | } 431 | .highlight.bash.jsx { 432 | .code { 433 | &:after { 434 | content: 'JSX'; 435 | } 436 | } 437 | } 438 | .highlight.css.jsx { 439 | .code { 440 | &:after { 441 | content: 'JSX'; 442 | } 443 | } 444 | } 445 | .highlight.jsx.jsx { 446 | .code { 447 | &:after { 448 | content: 'JSX'; 449 | } 450 | } 451 | } 452 | >table { 453 | width: 100%; 454 | margin: 20px 0; 455 | tr { 456 | border-top: 1px solid #eee; 457 | &:nth-child(2n) { 458 | background-color: #f8f8f8; 459 | } 460 | } 461 | th { 462 | font-family: 'ProximaNova-Semibold'; 463 | padding: 12px 13px; 464 | border: 1px solid #eee; 465 | vertical-align: middle; 466 | text-align: left; 467 | } 468 | td { 469 | border: 1px solid #eee; 470 | vertical-align: middle; 471 | padding: 6px 13px; 472 | font-family: Menlo, Monaco, Consolas, "Courier New", monospace; 473 | font-size: 0.8em; 474 | line-height: 1.6em; 475 | } 476 | } 477 | .bottom-nav { 478 | height: 44px; 479 | margin: 30px 0 25px; 480 | border-bottom: 1px solid #eee; 481 | padding-bottom: 25px; 482 | a { 483 | font-family: 'ProximaNova-Semibold'; 484 | margin: 0 5px; 485 | } 486 | } 487 | .edit-link { 488 | text-align: center; 489 | a { 490 | color: #aaa; 491 | font-family: 'ProximaNova-Semibold'; 492 | &:before { 493 | content: ''; 494 | display: inline-block; 495 | width: 16px; 496 | height: 16px; 497 | background-size: 16px; 498 | opacity: 0.3; 499 | margin-right: 8px; 500 | position: relative; 501 | top: 2px; 502 | } 503 | } 504 | } 505 | 506 | .field-name { 507 | font-weight: bold; 508 | } 509 | 510 | .field-entry { 511 | margin-bottom: 4rem; 512 | } 513 | .description-wrapper { 514 | >p { 515 | padding-left: 1rem; 516 | margin-bottom: 1rem; 517 | } 518 | } 519 | } 520 | -------------------------------------------------------------------------------- /lib/graphql-docs/layouts/assets/_sass/_deprecations.scss: -------------------------------------------------------------------------------- 1 | .deprecation-notice { 2 | padding-left: 5px; 3 | border-left: 2px solid #e8400d; 4 | background: #fdf2ec; 5 | 6 | span { 7 | font-weight: bold; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /lib/graphql-docs/layouts/assets/_sass/_fonts.scss: -------------------------------------------------------------------------------- 1 | @import "//hello.myfonts.net/count/2c4b9d"; 2 | @font-face { 3 | font-family: 'ProximaNova-Light'; 4 | src: url("webfonts/2C4B9D_B_0.eot"); 5 | src: url("webfonts/2C4B9D_B_0.eot?#iefix") format('embedded-opentype'), url("webfonts/2C4B9D_B_0.woff2") format('woff2'), url("webfonts/2C4B9D_B_0.woff") format('woff'), url("webfonts/2C4B9D_B_0.ttf") format('truetype'); 6 | } 7 | @font-face { 8 | font-family: 'ProximaNova-Semibold'; 9 | src: url("webfonts/2C4B9D_C_0.eot"); 10 | src: url("webfonts/2C4B9D_C_0.eot?#iefix") format('embedded-opentype'), url("webfonts/2C4B9D_C_0.woff2") format('woff2'), url("webfonts/2C4B9D_C_0.woff") format('woff'), url("webfonts/2C4B9D_C_0.ttf") format('truetype'); 11 | } 12 | @font-face { 13 | font-family: 'ProximaNova-Regular'; 14 | src: url("webfonts/2C4B9D_D_0.eot"); 15 | src: url("webfonts/2C4B9D_D_0.eot?#iefix") format('embedded-opentype'), url("webfonts/2C4B9D_D_0.woff2") format('woff2'), url("webfonts/2C4B9D_D_0.woff") format('woff'), url("webfonts/2C4B9D_D_0.ttf") format('truetype'); 16 | } 17 | @font-face { 18 | font-family: 'ProximaNova-Bold'; 19 | src: url("webfonts/2C4B9D_E_0.eot"); 20 | src: url("webfonts/2C4B9D_E_0.eot?#iefix") format('embedded-opentype'), url("webfonts/2C4B9D_E_0.woff2") format('woff2'), url("webfonts/2C4B9D_E_0.woff") format('woff'), url("webfonts/2C4B9D_E_0.ttf") format('truetype'); 21 | } 22 | -------------------------------------------------------------------------------- /lib/graphql-docs/layouts/assets/_sass/_header.scss: -------------------------------------------------------------------------------- 1 | #top-nav { 2 | height: 30px; 3 | line-height: 30px; 4 | background-color: #27272b; 5 | a { 6 | text-decoration: none; 7 | } 8 | } 9 | #top-nav-links { 10 | list-style-type: none; 11 | position: absolute; 12 | top: 0; 13 | right: 30px; 14 | li { 15 | float: left; 16 | margin-left: 20px; 17 | } 18 | a { 19 | display: inline-block; 20 | height: 30px; 21 | padding: 0 5px; 22 | color: #fff; 23 | font-size: 10px; 24 | letter-spacing: 1.5px; 25 | text-transform: uppercase; 26 | } 27 | } 28 | #site-nav { 29 | position: relative; 30 | height: 70px; 31 | background-color: #fff; 32 | border-bottom: 1px solid #eee; 33 | padding: 14px 30px; 34 | a { 35 | vertical-align: bottom; 36 | } 37 | span { 38 | vertical-align: bottom; 39 | } 40 | select { 41 | vertical-align: bottom; 42 | } 43 | .sub-title { 44 | margin: 0 8px; 45 | position: relative; 46 | top: 1px; 47 | } 48 | .logo { 49 | img { 50 | height: 50px; 51 | margin-bottom: -20px; 52 | } 53 | } 54 | .search-box { 55 | position: absolute; 56 | right: 30px; 57 | top: 20px; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /lib/graphql-docs/layouts/assets/_sass/_mobile.scss: -------------------------------------------------------------------------------- 1 | #mobile-header { 2 | z-index: 3; 3 | position: fixed; 4 | top: 0; 5 | left: 0; 6 | width: 100%; 7 | height: 40px; 8 | background-color: #fff; 9 | display: none; 10 | box-shadow: 0 0 4px rgba(0,0,0,0.25); 11 | .menu-button { 12 | position: absolute; 13 | width: 24px; 14 | height: 24px; 15 | top: 8px; 16 | left: 12px; 17 | background: url("../assets/images/menu.png") center center no-repeat; 18 | background-size: 24px; 19 | opacity: 0.5; 20 | } 21 | .logo { 22 | position: absolute; 23 | top: 5px; 24 | left: 50%; 25 | margin-left: -15px; 26 | background-size: 30px; 27 | img { 28 | width: 30px; 29 | height: 30px; 30 | } 31 | } 32 | } 33 | #mobile-shade { 34 | z-index: 1; 35 | display: none; 36 | pointer-events: none; 37 | opacity: 0; 38 | transition: opacity 0.3s ease; 39 | position: fixed; 40 | top: 0; 41 | left: 0; 42 | right: 0; 43 | bottom: 0; 44 | background-color: rgba(0,0,0,0.4); 45 | } 46 | 47 | @media screen and (max-width: 560px) { 48 | body { 49 | font-size: 14px; 50 | } 51 | body.sidebar-open { 52 | #sidebar { 53 | transform: translate3d(0, 0, 0); 54 | } 55 | #mobile-shade { 56 | opacity: 1; 57 | pointer-events: auto; 58 | } 59 | } 60 | #header { 61 | height: 40px; 62 | } 63 | #top-nav { 64 | display: none; 65 | } 66 | #site-nav { 67 | display: none; 68 | } 69 | #mobile-header { 70 | display: block; 71 | } 72 | #mobile-shade { 73 | display: block; 74 | } 75 | #sidebar-mobile { 76 | display: block; 77 | } 78 | #wrap { 79 | padding-top: 40px; 80 | padding-left: 0; 81 | } 82 | #sidebar { 83 | top: 0; 84 | left: 0; 85 | padding-top: 60px; 86 | border-right: none; 87 | box-shadow: 0 0 4px rgba(0,0,0,0.25); 88 | transition: transform 0.3s ease; 89 | transform: translate3d(-120%, 0, 0); 90 | display: block; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /lib/graphql-docs/layouts/assets/_sass/_normalize.scss: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | } 4 | html, 5 | body, 6 | div, 7 | span, 8 | applet, 9 | object, 10 | iframe, 11 | h1, 12 | h2, 13 | h3, 14 | h4, 15 | h5, 16 | h6, 17 | p, 18 | blockquote, 19 | pre, 20 | a, 21 | abbr, 22 | acronym, 23 | address, 24 | big, 25 | cite, 26 | code, 27 | del, 28 | dfn, 29 | em, 30 | img, 31 | ins, 32 | kbd, 33 | q, 34 | s, 35 | samp, 36 | small, 37 | strike, 38 | strong, 39 | sub, 40 | sup, 41 | tt, 42 | var, 43 | b, 44 | u, 45 | i, 46 | center, 47 | dl, 48 | dt, 49 | dd, 50 | ol, 51 | ul, 52 | li, 53 | fieldset, 54 | form, 55 | label, 56 | legend, 57 | table, 58 | caption, 59 | tbody, 60 | tfoot, 61 | thead, 62 | tr, 63 | th, 64 | td, 65 | article, 66 | aside, 67 | canvas, 68 | details, 69 | embed, 70 | figure, 71 | figcaption, 72 | footer, 73 | header, 74 | hgroup, 75 | menu, 76 | nav, 77 | output, 78 | ruby, 79 | section, 80 | summary, 81 | time, 82 | mark, 83 | audio, 84 | video { 85 | margin: 0; 86 | padding: 0; 87 | border: 0; 88 | font-size: 100%; 89 | font: inherit; 90 | vertical-align: baseline; 91 | } 92 | /* HTML5 display-role reset for older browsers */ 93 | article, 94 | aside, 95 | details, 96 | figcaption, 97 | figure, 98 | footer, 99 | header, 100 | hgroup, 101 | menu, 102 | nav, 103 | section { 104 | display: block; 105 | } 106 | body { 107 | line-height: 1; 108 | } 109 | ol, 110 | ul { 111 | list-style: none; 112 | } 113 | blockquote, 114 | q { 115 | quotes: none; 116 | } 117 | blockquote:before, 118 | blockquote:after, 119 | q:before, 120 | q:after { 121 | content: ''; 122 | content: none; 123 | } 124 | table { 125 | border-collapse: collapse; 126 | border-spacing: 0; 127 | } 128 | -------------------------------------------------------------------------------- /lib/graphql-docs/layouts/assets/_sass/_search.scss: -------------------------------------------------------------------------------- 1 | .search-box { 2 | input { 3 | width: 200px; 4 | background-color: #fff; 5 | outline: none; 6 | font-family: 'ProximaNova-Regular'; 7 | font-size: 14px; 8 | padding: 7px 12px 6px 32px; 9 | border-radius: 20px; 10 | border: 1px solid #ddd; 11 | background: url("../assets/images/search.png") 8px 6px no-repeat; 12 | background-size: 20px; 13 | transition: border-color 0.25s ease; 14 | &:focus { 15 | border-color: #de4f4f; 16 | } 17 | } 18 | } 19 | .search-box.st-default-search-input { 20 | width: 200px; 21 | background-color: #fff; 22 | outline: none; 23 | font-family: 'ProximaNova-Regular'; 24 | font-size: 14px; 25 | padding: 7px 12px 6px 32px; 26 | border-radius: 20px; 27 | border: 1px solid #ddd; 28 | background: url("../assets/images/search.png") 8px 6px no-repeat; 29 | background-size: 20px; 30 | transition: border-color 0.25s ease; 31 | &:focus { 32 | border-color: #de4f4f; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /lib/graphql-docs/layouts/assets/_sass/_sidebar.scss: -------------------------------------------------------------------------------- 1 | #sidebar { 2 | background-color: #fff; 3 | position: fixed; 4 | z-index: 2; 5 | top: 30px; 6 | left: 0; 7 | bottom: 0; 8 | width: 300px; 9 | padding: 20px 30px; 10 | overflow-x: hidden; 11 | overflow-y: scroll; 12 | -webkit-overflow-scrolling: touch; 13 | -ms-overflow-style: none; 14 | font-family: 'ProximaNova-Semibold'; 15 | border-right: 1px solid #eee; 16 | font-size: 16px; 17 | line-height: 1.1em; 18 | &::-webkit-scrollbar { 19 | width: 0 !important; 20 | } 21 | li { 22 | margin-bottom: 0.6em; 23 | } 24 | a { 25 | color: #444; 26 | text-decoration: none; 27 | &:hover { 28 | color: #de4f4f; 29 | } 30 | } 31 | a.current { 32 | color: #de4f4f; 33 | } 34 | a.H2 { 35 | font-weight: bold; 36 | } 37 | .categories { 38 | >li { 39 | >p { 40 | margin-top: 1.5em; 41 | border-top: 1px solid #eee; 42 | text-transform: uppercase; 43 | padding-top: 1.2em; 44 | margin-bottom: 1em; 45 | color: #999; 46 | font-size: 0.8em; 47 | } 48 | } 49 | } 50 | .sub-menu { 51 | font-family: 'ProximaNova-Regular'; 52 | padding-left: 20px; 53 | margin: 0.6em 0; 54 | font-size: 14px; 55 | .active { 56 | position: relative; 57 | color: #de4f4f; 58 | &:before { 59 | content: ""; 60 | position: absolute; 61 | top: 2px; 62 | left: -15px; 63 | display: inline-block; 64 | width: 0; 65 | height: 0; 66 | border-top: 4px solid transparent; 67 | border-bottom: 4px solid transparent; 68 | border-left: 6px solid #de4f4f; 69 | } 70 | } 71 | } 72 | #search { 73 | display: flex; 74 | position: relative; 75 | align-items: center; 76 | border: 1px solid #ddd; 77 | border-radius: 5px; 78 | padding: 0.01em 16px; 79 | margin-bottom: 20px; 80 | 81 | img { 82 | position: absolute; 83 | left: 10px; 84 | height: 16px; 85 | width: 16px; 86 | } 87 | 88 | input { 89 | height: 24px; 90 | line-height: 1.5; 91 | width: 100%; 92 | padding-left: 15px; 93 | background-color: transparent; 94 | color: #444; 95 | border: none; 96 | font-size: 14px; 97 | font-family: 'ProximaNova-Semibold'; 98 | } 99 | 100 | input:focus { 101 | outline: none; 102 | } 103 | } 104 | } 105 | 106 | #sidebar-mobile { 107 | display: none; 108 | margin-bottom: 20px; 109 | .search-box { 110 | width: 200px; 111 | margin-bottom: 20px; 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /lib/graphql-docs/layouts/assets/_sass/_syntax.scss: -------------------------------------------------------------------------------- 1 | .gutter { 2 | pre { 3 | color: #999; 4 | } 5 | } 6 | pre { 7 | color: #525252; 8 | .function { 9 | .keyword { 10 | color: #0092db; 11 | } 12 | } 13 | .constant { 14 | color: #0092db; 15 | } 16 | .keyword { 17 | color: #e96900; 18 | } 19 | .attribute { 20 | color: #e96900; 21 | } 22 | .number { 23 | color: #ae81ff; 24 | } 25 | .literal { 26 | color: #ae81ff; 27 | } 28 | .tag { 29 | color: #2973b7; 30 | .title { 31 | color: #2973b7; 32 | } 33 | .value { 34 | color: #90a959; 35 | } 36 | } 37 | .change { 38 | color: #2973b7; 39 | } 40 | .winutils { 41 | color: #2973b7; 42 | } 43 | .flow { 44 | color: #2973b7; 45 | } 46 | .lisp { 47 | .title { 48 | color: #2973b7; 49 | } 50 | } 51 | .clojure { 52 | .built_in { 53 | color: #2973b7; 54 | } 55 | } 56 | .nginx { 57 | .title { 58 | color: #2973b7; 59 | } 60 | } 61 | .tex { 62 | .special { 63 | color: #2973b7; 64 | } 65 | .command { 66 | color: #90a959; 67 | } 68 | .formula { 69 | color: #b3b3b3; 70 | opacity: 0.5; 71 | } 72 | } 73 | .class { 74 | .title { 75 | color: #4077bf; 76 | } 77 | } 78 | .symbol { 79 | color: #90a959; 80 | .string { 81 | color: #90a959; 82 | } 83 | } 84 | .value { 85 | color: #90a959; 86 | } 87 | .regexp { 88 | color: #90a959; 89 | } 90 | .title { 91 | color: #a6e22e; 92 | } 93 | .string { 94 | color: #90a959; 95 | } 96 | .subst { 97 | color: #90a959; 98 | } 99 | .haskell { 100 | .type { 101 | color: #90a959; 102 | } 103 | } 104 | .preprocessor { 105 | color: #90a959; 106 | } 107 | .ruby { 108 | .class { 109 | .parent { 110 | color: #90a959; 111 | } 112 | } 113 | } 114 | .built_in { 115 | color: #90a959; 116 | } 117 | .sql { 118 | .aggregate { 119 | color: #90a959; 120 | } 121 | } 122 | .django { 123 | .template_tag { 124 | color: #90a959; 125 | } 126 | .variable { 127 | color: #90a959; 128 | } 129 | .filter { 130 | .argument { 131 | color: #90a959; 132 | } 133 | } 134 | } 135 | .smalltalk { 136 | .class { 137 | color: #90a959; 138 | } 139 | .localvars { 140 | color: #90a959; 141 | } 142 | .array { 143 | color: #90a959; 144 | } 145 | } 146 | .javadoc { 147 | color: #90a959; 148 | } 149 | .attr_selector { 150 | color: #90a959; 151 | } 152 | .pseudo { 153 | color: #90a959; 154 | } 155 | .addition { 156 | color: #90a959; 157 | } 158 | .stream { 159 | color: #90a959; 160 | } 161 | .envvar { 162 | color: #90a959; 163 | } 164 | .apache { 165 | .tag { 166 | color: #90a959; 167 | } 168 | .cbracket { 169 | color: #90a959; 170 | } 171 | .sqbracket { 172 | color: #b3b3b3; 173 | } 174 | } 175 | .prompt { 176 | color: #90a959; 177 | } 178 | .comment { 179 | color: #b3b3b3; 180 | } 181 | .java { 182 | .annotation { 183 | color: #b3b3b3; 184 | } 185 | } 186 | .python { 187 | .decorator { 188 | color: #b3b3b3; 189 | } 190 | } 191 | .template_comment { 192 | color: #b3b3b3; 193 | } 194 | .pi { 195 | color: #b3b3b3; 196 | } 197 | .doctype { 198 | color: #b3b3b3; 199 | } 200 | .deletion { 201 | color: #b3b3b3; 202 | } 203 | .shebang { 204 | color: #b3b3b3; 205 | } 206 | .coffeescript { 207 | .javascript { 208 | opacity: 0.5; 209 | } 210 | } 211 | .javascript { 212 | .xml { 213 | opacity: 0.5; 214 | } 215 | } 216 | .xml { 217 | .javascript { 218 | opacity: 0.5; 219 | } 220 | .vbscript { 221 | opacity: 0.5; 222 | } 223 | .css { 224 | opacity: 0.5; 225 | } 226 | .cdata { 227 | opacity: 0.5; 228 | } 229 | } 230 | } 231 | 232 | .highlight .hll { background-color: #49483e } 233 | pre { background: #272822; color: #f8f8f2 } 234 | .highlight .c { color: #75715e } /* Comment */ 235 | .highlight .err { color: #960050; background-color: #1e0010 } /* Error */ 236 | .highlight .k { color: #66d9ef } /* Keyword */ 237 | .highlight .l { color: #ae81ff } /* Literal */ 238 | .highlight .n { color: #f8f8f2 } /* Name */ 239 | .highlight .o { color: #f92672 } /* Operator */ 240 | .highlight .p { color: #f8f8f2 } /* Punctuation */ 241 | .highlight .cm { color: #75715e } /* Comment.Multiline */ 242 | .highlight .cp { color: #75715e } /* Comment.Preproc */ 243 | .highlight .c1 { color: #75715e } /* Comment.Single */ 244 | .highlight .cs { color: #75715e } /* Comment.Special */ 245 | .highlight .ge { font-style: italic } /* Generic.Emph */ 246 | .highlight .gs { font-weight: bold } /* Generic.Strong */ 247 | .highlight .kc { color: #66d9ef } /* Keyword.Constant */ 248 | .highlight .kd { color: #66d9ef } /* Keyword.Declaration */ 249 | .highlight .kn { color: #f92672 } /* Keyword.Namespace */ 250 | .highlight .kp { color: #66d9ef } /* Keyword.Pseudo */ 251 | .highlight .kr { color: #66d9ef } /* Keyword.Reserved */ 252 | .highlight .kt { color: #66d9ef } /* Keyword.Type */ 253 | .highlight .ld { color: #e6db74 } /* Literal.Date */ 254 | .highlight .m { color: #ae81ff } /* Literal.Number */ 255 | .highlight .s { color: #e6db74 } /* Literal.String */ 256 | .highlight .na { color: #a6e22e } /* Name.Attribute */ 257 | .highlight .nb { color: #f8f8f2 } /* Name.Builtin */ 258 | .highlight .nc { color: #a6e22e } /* Name.Class */ 259 | .highlight .no { color: #66d9ef } /* Name.Constant */ 260 | .highlight .nd { color: #a6e22e } /* Name.Decorator */ 261 | .highlight .ni { color: #f8f8f2 } /* Name.Entity */ 262 | .highlight .ne { color: #a6e22e } /* Name.Exception */ 263 | .highlight .nf { color: #a6e22e } /* Name.Function */ 264 | .highlight .nl { color: #f8f8f2 } /* Name.Label */ 265 | .highlight .nn { color: #f8f8f2 } /* Name.Namespace */ 266 | .highlight .nx { color: #a6e22e } /* Name.Other */ 267 | .highlight .py { color: #f8f8f2 } /* Name.Property */ 268 | .highlight .nt { color: #f92672 } /* Name.Tag */ 269 | .highlight .nv { color: #f8f8f2 } /* Name.Variable */ 270 | .highlight .ow { color: #f92672 } /* Operator.Word */ 271 | .highlight .w { color: #f8f8f2 } /* Text.Whitespace */ 272 | .highlight .mf { color: #ae81ff } /* Literal.Number.Float */ 273 | .highlight .mh { color: #ae81ff } /* Literal.Number.Hex */ 274 | .highlight .mi { color: #ae81ff } /* Literal.Number.Integer */ 275 | .highlight .mo { color: #ae81ff } /* Literal.Number.Oct */ 276 | .highlight .sb { color: #e6db74 } /* Literal.String.Backtick */ 277 | .highlight .sc { color: #e6db74 } /* Literal.String.Char */ 278 | .highlight .sd { color: #e6db74 } /* Literal.String.Doc */ 279 | .highlight .s2 { color: #e6db74 } /* Literal.String.Double */ 280 | .highlight .se { color: #ae81ff } /* Literal.String.Escape */ 281 | .highlight .sh { color: #e6db74 } /* Literal.String.Heredoc */ 282 | .highlight .si { color: #e6db74 } /* Literal.String.Interpol */ 283 | .highlight .sx { color: #e6db74 } /* Literal.String.Other */ 284 | .highlight .sr { color: #e6db74 } /* Literal.String.Regex */ 285 | .highlight .s1 { color: #e6db74 } /* Literal.String.Single */ 286 | .highlight .ss { color: #e6db74 } /* Literal.String.Symbol */ 287 | .highlight .bp { color: #f8f8f2 } /* Name.Builtin.Pseudo */ 288 | .highlight .vc { color: #f8f8f2 } /* Name.Variable.Class */ 289 | .highlight .vg { color: #f8f8f2 } /* Name.Variable.Global */ 290 | .highlight .vi { color: #f8f8f2 } /* Name.Variable.Instance */ 291 | .highlight .il { color: #ae81ff } /* Literal.Number.Integer.Long */ 292 | -------------------------------------------------------------------------------- /lib/graphql-docs/layouts/assets/_sass/_types.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brettchalupa/graphql-docs/27097afd2c0af6e7a596a27ea41be943f132c1ad/lib/graphql-docs/layouts/assets/_sass/_types.scss -------------------------------------------------------------------------------- /lib/graphql-docs/layouts/assets/css/screen.scss: -------------------------------------------------------------------------------- 1 | @charset "utf-8"; 2 | 3 | @use "sass:meta"; 4 | @use "../_sass/_normalize.scss"; 5 | @use "../_sass/_fonts"; 6 | 7 | body { 8 | font-family: 'Source Sans Pro', 'Helvetica Neue', Helvetica, Arial, sans-serif; 9 | -webkit-font-smoothing: antialiased; 10 | -moz-osx-font-smoothing: grayscale; 11 | font-size: 16px; 12 | font-weight: 400; 13 | color: #444; 14 | } 15 | h1, 16 | h2, 17 | h3, 18 | h4, 19 | h5, 20 | h6 { 21 | font-family: 'ProximaNova-Semibold'; 22 | font-weight: 200; 23 | } 24 | em { 25 | font-style: italic; 26 | } 27 | 28 | #wrap { 29 | padding-top: 100px; 30 | padding-left: 300px; 31 | height: 100%; 32 | } 33 | #header { 34 | position: fixed; 35 | z-index: 2; 36 | top: 0; 37 | left: 0; 38 | width: 100%; 39 | height: 100px; 40 | font-family: 'ProximaNova-Semibold'; 41 | } 42 | 43 | @include meta.load-css('../_sass/_header'); 44 | @include meta.load-css('../_sass/_sidebar'); 45 | @include meta.load-css('../_sass/_content'); 46 | @include meta.load-css('../_sass/_types'); 47 | @include meta.load-css('../_sass/_mobile'); 48 | @include meta.load-css('../_sass/_api-box'); 49 | @include meta.load-css('../_sass/_syntax'); 50 | @include meta.load-css('../_sass/_deprecations'); 51 | -------------------------------------------------------------------------------- /lib/graphql-docs/layouts/assets/images/graphiql-headers.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brettchalupa/graphql-docs/27097afd2c0af6e7a596a27ea41be943f132c1ad/lib/graphql-docs/layouts/assets/images/graphiql-headers.png -------------------------------------------------------------------------------- /lib/graphql-docs/layouts/assets/images/graphiql-variables.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brettchalupa/graphql-docs/27097afd2c0af6e7a596a27ea41be943f132c1ad/lib/graphql-docs/layouts/assets/images/graphiql-variables.png -------------------------------------------------------------------------------- /lib/graphql-docs/layouts/assets/images/graphiql.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brettchalupa/graphql-docs/27097afd2c0af6e7a596a27ea41be943f132c1ad/lib/graphql-docs/layouts/assets/images/graphiql.png -------------------------------------------------------------------------------- /lib/graphql-docs/layouts/assets/images/menu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brettchalupa/graphql-docs/27097afd2c0af6e7a596a27ea41be943f132c1ad/lib/graphql-docs/layouts/assets/images/menu.png -------------------------------------------------------------------------------- /lib/graphql-docs/layouts/assets/images/navbar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brettchalupa/graphql-docs/27097afd2c0af6e7a596a27ea41be943f132c1ad/lib/graphql-docs/layouts/assets/images/navbar.png -------------------------------------------------------------------------------- /lib/graphql-docs/layouts/assets/images/search.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /lib/graphql-docs/layouts/assets/webfonts/2C4B9D_B_0.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brettchalupa/graphql-docs/27097afd2c0af6e7a596a27ea41be943f132c1ad/lib/graphql-docs/layouts/assets/webfonts/2C4B9D_B_0.eot -------------------------------------------------------------------------------- /lib/graphql-docs/layouts/assets/webfonts/2C4B9D_B_0.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brettchalupa/graphql-docs/27097afd2c0af6e7a596a27ea41be943f132c1ad/lib/graphql-docs/layouts/assets/webfonts/2C4B9D_B_0.ttf -------------------------------------------------------------------------------- /lib/graphql-docs/layouts/assets/webfonts/2C4B9D_B_0.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brettchalupa/graphql-docs/27097afd2c0af6e7a596a27ea41be943f132c1ad/lib/graphql-docs/layouts/assets/webfonts/2C4B9D_B_0.woff -------------------------------------------------------------------------------- /lib/graphql-docs/layouts/assets/webfonts/2C4B9D_B_0.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brettchalupa/graphql-docs/27097afd2c0af6e7a596a27ea41be943f132c1ad/lib/graphql-docs/layouts/assets/webfonts/2C4B9D_B_0.woff2 -------------------------------------------------------------------------------- /lib/graphql-docs/layouts/assets/webfonts/2C4B9D_C_0.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brettchalupa/graphql-docs/27097afd2c0af6e7a596a27ea41be943f132c1ad/lib/graphql-docs/layouts/assets/webfonts/2C4B9D_C_0.eot -------------------------------------------------------------------------------- /lib/graphql-docs/layouts/assets/webfonts/2C4B9D_C_0.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brettchalupa/graphql-docs/27097afd2c0af6e7a596a27ea41be943f132c1ad/lib/graphql-docs/layouts/assets/webfonts/2C4B9D_C_0.ttf -------------------------------------------------------------------------------- /lib/graphql-docs/layouts/assets/webfonts/2C4B9D_C_0.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brettchalupa/graphql-docs/27097afd2c0af6e7a596a27ea41be943f132c1ad/lib/graphql-docs/layouts/assets/webfonts/2C4B9D_C_0.woff -------------------------------------------------------------------------------- /lib/graphql-docs/layouts/assets/webfonts/2C4B9D_C_0.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brettchalupa/graphql-docs/27097afd2c0af6e7a596a27ea41be943f132c1ad/lib/graphql-docs/layouts/assets/webfonts/2C4B9D_C_0.woff2 -------------------------------------------------------------------------------- /lib/graphql-docs/layouts/assets/webfonts/2C4B9D_D_0.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brettchalupa/graphql-docs/27097afd2c0af6e7a596a27ea41be943f132c1ad/lib/graphql-docs/layouts/assets/webfonts/2C4B9D_D_0.eot -------------------------------------------------------------------------------- /lib/graphql-docs/layouts/assets/webfonts/2C4B9D_D_0.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brettchalupa/graphql-docs/27097afd2c0af6e7a596a27ea41be943f132c1ad/lib/graphql-docs/layouts/assets/webfonts/2C4B9D_D_0.ttf -------------------------------------------------------------------------------- /lib/graphql-docs/layouts/assets/webfonts/2C4B9D_D_0.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brettchalupa/graphql-docs/27097afd2c0af6e7a596a27ea41be943f132c1ad/lib/graphql-docs/layouts/assets/webfonts/2C4B9D_D_0.woff -------------------------------------------------------------------------------- /lib/graphql-docs/layouts/assets/webfonts/2C4B9D_D_0.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brettchalupa/graphql-docs/27097afd2c0af6e7a596a27ea41be943f132c1ad/lib/graphql-docs/layouts/assets/webfonts/2C4B9D_D_0.woff2 -------------------------------------------------------------------------------- /lib/graphql-docs/layouts/assets/webfonts/2C4B9D_E_0.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brettchalupa/graphql-docs/27097afd2c0af6e7a596a27ea41be943f132c1ad/lib/graphql-docs/layouts/assets/webfonts/2C4B9D_E_0.eot -------------------------------------------------------------------------------- /lib/graphql-docs/layouts/assets/webfonts/2C4B9D_E_0.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brettchalupa/graphql-docs/27097afd2c0af6e7a596a27ea41be943f132c1ad/lib/graphql-docs/layouts/assets/webfonts/2C4B9D_E_0.ttf -------------------------------------------------------------------------------- /lib/graphql-docs/layouts/assets/webfonts/2C4B9D_E_0.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brettchalupa/graphql-docs/27097afd2c0af6e7a596a27ea41be943f132c1ad/lib/graphql-docs/layouts/assets/webfonts/2C4B9D_E_0.woff -------------------------------------------------------------------------------- /lib/graphql-docs/layouts/assets/webfonts/2C4B9D_E_0.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brettchalupa/graphql-docs/27097afd2c0af6e7a596a27ea41be943f132c1ad/lib/graphql-docs/layouts/assets/webfonts/2C4B9D_E_0.woff2 -------------------------------------------------------------------------------- /lib/graphql-docs/layouts/default.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | <%= title || name %> 8 | 9 | 10 | 78 | 79 | 80 | 81 |
82 | 84 | 87 |
88 | 89 | <%= contents %> 90 | 91 |
92 | 93 | 94 |
95 | 96 | 99 |
100 |
101 | 102 |
103 | 104 | 105 | 106 | -------------------------------------------------------------------------------- /lib/graphql-docs/layouts/graphql_directives.html: -------------------------------------------------------------------------------- 1 |

<%= type[:name] %>

2 | 3 | <%= include.('notices.html', notices: type[:notices]) %> 4 | 5 | <%= type[:description] %> 6 | 7 | <%= include.('locations.html', locations: type[:locations]) %> 8 | 9 | <% unless type[:arguments].empty? %> 10 | 11 |

Arguments

12 | 13 | <%= include.('arguments.html', arguments: type[:arguments]) %> 14 | 15 | <% end %> 16 | -------------------------------------------------------------------------------- /lib/graphql-docs/layouts/graphql_enums.html: -------------------------------------------------------------------------------- 1 |

<%= type[:name] %>

2 | 3 | <%= include.('notices.html', notices: type[:notices]) %> 4 | 5 | <%= type[:description] %> 6 | 7 | <%= include.('values.html', values: type[:values]) %> 8 | -------------------------------------------------------------------------------- /lib/graphql-docs/layouts/graphql_input_objects.html: -------------------------------------------------------------------------------- 1 |

<%= type[:name] %>

2 | 3 | <%= include.('notices.html', notices: type[:notices]) %> 4 | 5 | <%= type[:description] %> 6 | 7 | <% unless type[:input_fields].nil? %> 8 | 9 |

Input Fields

10 | 11 | <%= include.('input_fields.html', input_fields: type[:input_fields]) %> 12 | 13 | <% end %> 14 | -------------------------------------------------------------------------------- /lib/graphql-docs/layouts/graphql_interfaces.html: -------------------------------------------------------------------------------- 1 |

<%= type[:name] %>

2 | 3 | <%= include.('notices.html', notices: type[:notices]) %> 4 | 5 | <%= type[:description] %> 6 | 7 | <% unless type[:implemented_by].empty? %> 8 | 9 |

Implemented by

10 | 11 | 16 | 17 | <% end %> 18 | 19 | <% unless type[:fields].empty? %> 20 | 21 |

Fields

22 | 23 | <%= include.('fields.html', fields: type[:fields]) %> 24 | 25 | <% end %> 26 | -------------------------------------------------------------------------------- /lib/graphql-docs/layouts/graphql_mutations.html: -------------------------------------------------------------------------------- 1 |

<%= type[:name] %>

2 | 3 | <%= include.('notices.html', notices: type[:notices]) %> 4 | 5 | <%= type[:description] %> 6 | 7 | <% if !type[:input_fields].empty? %> 8 | 9 |

Input fields

10 | 11 | <%= include.('fields.html', fields: type[:input_fields]) %> 12 | 13 | <% end %> 14 | 15 | 16 | <% if !type[:return_fields].empty? %> 17 | 18 |

Return fields

19 | 20 | <%= include.('fields.html', fields: type[:return_fields]) %> 21 | 22 | <% end %> 23 | -------------------------------------------------------------------------------- /lib/graphql-docs/layouts/graphql_objects.html: -------------------------------------------------------------------------------- 1 |

<%= type[:name] %>

2 | 3 | <%= include.('notices.html', notices: type[:notices]) %> 4 | 5 | <%= type[:description] %> 6 | 7 | <% unless type[:interfaces].empty? %> 8 | 9 |

Implements

10 | 11 | 16 | 17 | <% end %> 18 | 19 | <% unless type[:connections].empty? %> 20 | 21 |

Connections

22 | 23 | <%= include.('connections.html', connections: type[:connections]) %> 24 | 25 | <% end %> 26 | 27 | <% unless type[:fields].empty? %> 28 | 29 |

Fields

30 | 31 | <%= include.('fields.html', fields: type[:fields]) %> 32 | 33 | <% end %> 34 | -------------------------------------------------------------------------------- /lib/graphql-docs/layouts/graphql_operations.html: -------------------------------------------------------------------------------- 1 |

<%= type[:name] %>

2 | 3 | <%= type[:description] %> 4 | -------------------------------------------------------------------------------- /lib/graphql-docs/layouts/graphql_queries.html: -------------------------------------------------------------------------------- 1 |

<%= type[:name] %>

2 | 3 | <%= include.('notices.html', notices: type[:notices]) %> 4 | 5 | <%= type[:description] %> 6 | 7 | <% if !type[:arguments].empty? %> 8 | 9 |

Arguments

10 | 11 | <%= include.('fields.html', fields: type[:arguments]) %> 12 | 13 | <% end %> 14 | 15 | 16 | <% if !type[:return_fields].empty? %> 17 | 18 |

Return fields

19 | 20 | <%= include.('fields.html', fields: type[:return_fields]) %> 21 | 22 | <% end %> 23 | -------------------------------------------------------------------------------- /lib/graphql-docs/layouts/graphql_scalars.html: -------------------------------------------------------------------------------- 1 | # <%= type[:name] %> 2 | 3 | <%= type[:description] %> 4 | -------------------------------------------------------------------------------- /lib/graphql-docs/layouts/graphql_unions.html: -------------------------------------------------------------------------------- 1 |

<%= type[:name] %>

2 | 3 | <%= include.('notices.html', notices: type[:notices]) %> 4 | 5 | <%= type[:description] %> 6 | 7 | <%= include.('possible_types.html', possible_types: type[:possible_types]) %> 8 | -------------------------------------------------------------------------------- /lib/graphql-docs/layouts/includes/arguments.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | <% arguments.each do |argument| %> 11 | 12 | 13 | 16 | 22 | 23 | <% end %> 24 | 25 |
ArgumentTypeDescription
<%= argument[:name] %> 14 | <%= argument[:type][:info] %> 15 | 17 | <%= markdownify.(argument[:description]) %> 18 | <% unless (default_value = argument[:default_value]).nil? %> 19 |

The default value is <%= argument[:default_value] %>.

20 | <% end %> 21 |
26 | -------------------------------------------------------------------------------- /lib/graphql-docs/layouts/includes/connections.html: -------------------------------------------------------------------------------- 1 | <% connections.each do |connection| %> 2 | 3 |
4 | <%= connection[:name] %> (<%= connection[:type][:info] %>) 5 | 6 |
7 | <%= include.('deprecations.html', item: connection) %> 8 | 9 | <%= include.('notices.html', notices: connection[:notices]) %> 10 | 11 | <%= markdownify.(connection[:description]) %> 12 | 13 | <% unless connection[:arguments].empty? %> 14 | <%= include.('arguments.html', arguments: connection[:arguments]) %> 15 | <% end %> 16 |
17 |
18 | 19 | <% end %> 20 | -------------------------------------------------------------------------------- /lib/graphql-docs/layouts/includes/deprecations.html: -------------------------------------------------------------------------------- 1 | <% if item[:is_deprecated] %> 2 |
3 | Deprecation notice 4 | <%= markdownify.(item[:deprecation_reason]) %> 5 |
6 | <% end %> 7 | -------------------------------------------------------------------------------- /lib/graphql-docs/layouts/includes/fields.html: -------------------------------------------------------------------------------- 1 | <% fields.each do |field| %> 2 | 3 |
4 | <%= field[:name] %> (<%= field[:type][:info] %>) 5 | 6 |
7 | <%= include.('deprecations.html', item: field) %> 8 | 9 | <%= include.('notices.html', notices: field[:notices]) %> 10 | 11 | <%= markdownify.(field[:description]) %> 12 | 13 | <% unless field[:arguments].empty? %> 14 | <%= include.('arguments.html', arguments: field[:arguments]) %> 15 | <% end %> 16 |
17 |
18 | 19 | <% end %> 20 | -------------------------------------------------------------------------------- /lib/graphql-docs/layouts/includes/input_fields.html: -------------------------------------------------------------------------------- 1 | <% input_fields.each do |field| %> 2 | 3 |
4 | <%= field[:name] %> (<%= field[:type][:info] %>) 5 | 6 |
7 | <%= include.('notices.html', notices: field[:notices]) %> 8 | 9 | <%= markdownify.(field[:description]) %> 10 | 11 | <% unless field[:arguments].empty? %> 12 | <%= include.('arguments.html', arguments: field[:arguments]) %> 13 | <% end %> 14 |
15 |
16 | 17 | <% end %> 18 | -------------------------------------------------------------------------------- /lib/graphql-docs/layouts/includes/locations.html: -------------------------------------------------------------------------------- 1 |

Locations

2 | 3 | 10 | -------------------------------------------------------------------------------- /lib/graphql-docs/layouts/includes/notices.html: -------------------------------------------------------------------------------- 1 | <% notices.each do |notice| %> 2 |
3 | <% if notice[:title] %> 4 | <%= notice[:title] %> 5 | <% end %> 6 | <%= markdownify.(notice[:body]) %> 7 |
8 | <% end %> 9 | -------------------------------------------------------------------------------- /lib/graphql-docs/layouts/includes/possible_types.html: -------------------------------------------------------------------------------- 1 |

Possible Types

2 | 3 | 10 | -------------------------------------------------------------------------------- /lib/graphql-docs/layouts/includes/sidebar.html: -------------------------------------------------------------------------------- 1 | 5 | 155 | -------------------------------------------------------------------------------- /lib/graphql-docs/layouts/includes/values.html: -------------------------------------------------------------------------------- 1 |

Values

2 | 3 | <% values.each do |value| %> 4 | 5 |

<%= value[:name] %>

6 | 7 |
8 | <%= include.('deprecations.html', item: value) %> 9 | 10 | <%= include.('notices.html', notices: value[:notices]) %> 11 | 12 | <%= markdownify.(value[:description]) %> 13 |
14 | 15 | <% end %> 16 | -------------------------------------------------------------------------------- /lib/graphql-docs/parser.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'graphql' 4 | 5 | module GraphQLDocs 6 | class Parser 7 | include Helpers 8 | 9 | attr_reader :processed_schema 10 | 11 | def initialize(schema, options) 12 | @options = options 13 | 14 | @options[:notices] ||= ->(_schema_member_path) { [] } 15 | 16 | @schema = if schema.is_a?(String) 17 | GraphQL::Schema.from_definition(schema) 18 | elsif schema < GraphQL::Schema 19 | schema 20 | end 21 | 22 | @processed_schema = { 23 | operation_types: [], 24 | query_types: [], 25 | mutation_types: [], 26 | object_types: [], 27 | interface_types: [], 28 | enum_types: [], 29 | union_types: [], 30 | input_object_types: [], 31 | scalar_types: [], 32 | directive_types: [] 33 | } 34 | end 35 | 36 | def parse 37 | root_types = {} 38 | %w[query mutation].each do |operation| 39 | root_types[operation] = @schema.root_type_for_operation(operation).graphql_name unless @schema.root_type_for_operation(operation).nil? 40 | end 41 | @processed_schema[:root_types] = root_types 42 | 43 | @schema.types.each_value do |object| 44 | data = {} 45 | 46 | data[:notices] = @options[:notices].call(object.graphql_name) 47 | 48 | if object < ::GraphQL::Schema::Object 49 | data[:name] = object.graphql_name 50 | data[:description] = object.description 51 | 52 | if data[:name] == root_types['query'] 53 | data[:interfaces] = object.interfaces.map(&:graphql_name).sort 54 | data[:fields], data[:connections] = fetch_fields(object.fields, object.graphql_name) 55 | @processed_schema[:operation_types] << data 56 | 57 | object.fields.each_value do |query| 58 | h = {} 59 | 60 | h[:notices] = @options[:notices].call([object.graphql_name, query.graphql_name].join('.')) 61 | h[:name] = query.graphql_name 62 | h[:description] = query.description 63 | h[:arguments], = fetch_fields(query.arguments, [object.graphql_name, query.graphql_name].join('.')) 64 | 65 | return_type = query.type 66 | if return_type.unwrap.respond_to?(:fields) 67 | h[:return_fields], = fetch_fields(return_type.unwrap.fields, return_type.graphql_name) 68 | else # it is a scalar return type 69 | h[:return_fields], = fetch_fields({ return_type.graphql_name => query }, return_type.graphql_name) 70 | end 71 | 72 | @processed_schema[:query_types] << h 73 | end 74 | elsif data[:name] == root_types['mutation'] 75 | @processed_schema[:operation_types] << data 76 | 77 | object.fields.each_value do |mutation| 78 | h = {} 79 | 80 | h[:notices] = @options[:notices].call([object.graphql_name, mutation.graphql_name].join('.')) 81 | h[:name] = mutation.graphql_name 82 | h[:description] = mutation.description 83 | h[:input_fields], = fetch_fields(mutation.arguments, [object.graphql_name, mutation.graphql_name].join('.')) 84 | 85 | return_type = mutation.type 86 | if return_type.unwrap.respond_to?(:fields) 87 | h[:return_fields], = fetch_fields(return_type.unwrap.fields, return_type.graphql_name) 88 | else # it is a scalar return type 89 | h[:return_fields], = fetch_fields({ return_type.graphql_name => mutation }, return_type.graphql_name) 90 | end 91 | 92 | @processed_schema[:mutation_types] << h 93 | end 94 | else 95 | data[:interfaces] = object.interfaces.map(&:graphql_name).sort 96 | data[:fields], data[:connections] = fetch_fields(object.fields, object.graphql_name) 97 | 98 | @processed_schema[:object_types] << data 99 | end 100 | elsif object < ::GraphQL::Schema::Interface 101 | data[:name] = object.graphql_name 102 | data[:description] = object.description 103 | data[:fields], data[:connections] = fetch_fields(object.fields, object.graphql_name) 104 | 105 | @processed_schema[:interface_types] << data 106 | elsif object < ::GraphQL::Schema::Enum 107 | data[:name] = object.graphql_name 108 | data[:description] = object.description 109 | 110 | data[:values] = object.values.values.map do |val| 111 | h = {} 112 | h[:notices] = @options[:notices].call([object.graphql_name, val.graphql_name].join('.')) 113 | h[:name] = val.graphql_name 114 | h[:description] = val.description 115 | unless val.deprecation_reason.nil? 116 | h[:is_deprecated] = true 117 | h[:deprecation_reason] = val.deprecation_reason 118 | end 119 | h 120 | end 121 | 122 | @processed_schema[:enum_types] << data 123 | elsif object < ::GraphQL::Schema::Union 124 | data[:name] = object.graphql_name 125 | data[:description] = object.description 126 | data[:possible_types] = object.possible_types.map(&:graphql_name).sort 127 | 128 | @processed_schema[:union_types] << data 129 | elsif object < GraphQL::Schema::InputObject 130 | data[:name] = object.graphql_name 131 | data[:description] = object.description 132 | 133 | data[:input_fields], = fetch_fields(object.arguments, object.graphql_name) 134 | 135 | @processed_schema[:input_object_types] << data 136 | elsif object < GraphQL::Schema::Scalar 137 | data[:name] = object.graphql_name 138 | data[:description] = object.description 139 | 140 | @processed_schema[:scalar_types] << data 141 | else 142 | raise TypeError, "I'm not sure what #{object.class} < #{object.superclass.name} is!" 143 | end 144 | end 145 | 146 | @schema.directives.each_value do |directive| 147 | data = {} 148 | data[:notices] = @options[:notices].call(directive.graphql_name) 149 | 150 | data[:name] = directive.graphql_name 151 | data[:description] = directive.description 152 | data[:locations] = directive.locations 153 | 154 | data[:arguments], = fetch_fields(directive.arguments, directive.graphql_name) 155 | 156 | @processed_schema[:directive_types] << data 157 | end 158 | 159 | sort_by_name! 160 | 161 | @processed_schema[:interface_types].each do |interface| 162 | interface[:implemented_by] = [] 163 | @processed_schema[:object_types].each do |obj| 164 | interface[:implemented_by] << obj[:name] if obj[:interfaces].include?(interface[:name]) 165 | end 166 | end 167 | 168 | @processed_schema 169 | end 170 | 171 | private 172 | 173 | def fetch_fields(object_fields, parent_path) 174 | fields = [] 175 | connections = [] 176 | 177 | object_fields.each_value do |field| 178 | hash = {} 179 | 180 | hash[:notices] = @options[:notices].call([parent_path, field.graphql_name].join('.')) 181 | hash[:name] = field.graphql_name 182 | hash[:description] = field.description 183 | if field.respond_to?(:deprecation_reason) && !field.deprecation_reason.nil? 184 | hash[:is_deprecated] = true 185 | hash[:deprecation_reason] = field.deprecation_reason 186 | end 187 | 188 | hash[:type] = generate_type(field.type) 189 | 190 | hash[:arguments] = [] 191 | if field.respond_to?(:arguments) 192 | field.arguments.each_value do |arg| 193 | h = {} 194 | h[:name] = arg.graphql_name 195 | h[:description] = arg.description 196 | h[:type] = generate_type(arg.type) 197 | h[:default_value] = arg.default_value if arg.default_value? 198 | if arg.respond_to?(:deprecation_reason) && arg.deprecation_reason 199 | h[:is_deprecated] = true 200 | h[:deprecation_reason] = arg.deprecation_reason 201 | end 202 | hash[:arguments] << h 203 | end 204 | end 205 | 206 | if !argument?(field) && connection?(field) 207 | connections << hash 208 | else 209 | fields << hash 210 | end 211 | end 212 | 213 | [fields, connections] 214 | end 215 | 216 | def generate_type(type) 217 | name = type.unwrap.graphql_name 218 | 219 | path = if type.unwrap < GraphQL::Schema::Object 220 | if name == 'Query' 221 | 'operation' 222 | else 223 | 'object' 224 | end 225 | elsif type.unwrap < GraphQL::Schema::Scalar 226 | 'scalar' 227 | elsif type.unwrap < GraphQL::Schema::Interface 228 | 'interface' 229 | elsif type.unwrap < GraphQL::Schema::Enum 230 | 'enum' 231 | elsif type.unwrap < GraphQL::Schema::InputObject 232 | 'input_object' 233 | elsif type.unwrap < GraphQL::Schema::Union 234 | 'union' 235 | else 236 | raise TypeError, "Unknown type for `#{name}`: `#{type.unwrap.class}`" 237 | end 238 | 239 | { 240 | name: name, 241 | path: "#{path}/#{slugify(name)}", 242 | info: type.to_type_signature 243 | } 244 | end 245 | 246 | def argument?(field) 247 | field.is_a?(::GraphQL::Schema::Argument) 248 | end 249 | 250 | def connection?(field) 251 | field.respond_to?(:connection?) && field.connection? 252 | end 253 | 254 | def sort_by_name! 255 | @processed_schema.each_pair do |key, value| 256 | next if value.empty? 257 | next if %i[operation_types root_types].include?(key) 258 | 259 | value.sort_by! { |o| o[:name] } 260 | end 261 | end 262 | end 263 | end 264 | -------------------------------------------------------------------------------- /lib/graphql-docs/renderer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'html/pipeline' 4 | require 'yaml' 5 | require 'extended-markdown-filter' 6 | require 'ostruct' 7 | 8 | module GraphQLDocs 9 | class Renderer 10 | include Helpers 11 | 12 | attr_reader :options 13 | 14 | def initialize(parsed_schema, options) 15 | @parsed_schema = parsed_schema 16 | @options = options 17 | 18 | @graphql_default_layout = ERB.new(File.read(@options[:templates][:default])) unless @options[:templates][:default].nil? 19 | 20 | @pipeline_config = @options[:pipeline_config] || {} 21 | pipeline = @pipeline_config[:pipeline] || {} 22 | context = @pipeline_config[:context] || {} 23 | 24 | filters = pipeline.map do |f| 25 | if filter?(f) 26 | f 27 | else 28 | key = filter_key(f) 29 | filter = HTML::Pipeline.constants.find { |c| c.downcase == key } 30 | # possibly a custom filter 31 | if filter.nil? 32 | Kernel.const_get(f) 33 | else 34 | HTML::Pipeline.const_get(filter) 35 | end 36 | end 37 | end 38 | 39 | @pipeline = HTML::Pipeline.new(filters, context) 40 | end 41 | 42 | def render(contents, type: nil, name: nil, filename: nil) 43 | opts = { base_url: @options[:base_url], output_dir: @options[:output_dir] }.merge({ type: type, name: name, filename: filename }).merge(helper_methods) 44 | 45 | contents = to_html(contents, context: { filename: filename }) 46 | return contents if @graphql_default_layout.nil? 47 | 48 | opts[:content] = contents 49 | @graphql_default_layout.result(OpenStruct.new(opts).instance_eval { binding }) 50 | end 51 | 52 | def to_html(string, context: {}) 53 | @pipeline.to_html(string, context) 54 | end 55 | 56 | private 57 | 58 | def filter_key(str) 59 | str.downcase 60 | end 61 | 62 | def filter?(filter) 63 | filter < HTML::Pipeline::Filter 64 | rescue LoadError, ArgumentError 65 | false 66 | end 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /lib/graphql-docs/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module GraphQLDocs 4 | VERSION = '5.2.0' 5 | end 6 | -------------------------------------------------------------------------------- /test/cli_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'test_helper' 4 | 5 | class CliTest < Minitest::Test 6 | def setup 7 | @schema = File.join(fixtures_dir, 'sw-schema.graphql') 8 | end 9 | 10 | def test_cli_works 11 | FileUtils.rm_rf("output") 12 | assert cmd("#{@schema}") 13 | assert File.exist?(File.join("output", 'index.html')) 14 | end 15 | 16 | def test_cli_works_with_output_dir 17 | clean_up_output # NOTE: this is the fixture output dir 18 | schema = File.join(fixtures_dir, 'sw-schema.graphql') 19 | assert cmd("#{@schema} -o #{output_dir}") 20 | assert File.exist?(File.join(output_dir, 'index.html')) 21 | end 22 | 23 | def test_cli_requires_schema 24 | _out, err = capture_subprocess_io do 25 | refute cmd("", exception: false) 26 | end 27 | assert_match /schema must be specified/, err 28 | end 29 | 30 | def test_cli_help 31 | out, err = capture_subprocess_io do 32 | assert cmd("--help") 33 | end 34 | assert_match /Usage/, out 35 | assert err.empty?, "errors not empty: #{err}" 36 | end 37 | 38 | def test_cli_verbose 39 | out, err = capture_subprocess_io do 40 | assert cmd("#{@schema} --verbose") 41 | end 42 | assert_match /Generating site/, out 43 | assert_match /Site successfully generated/, out 44 | assert err.empty?, "errors not empty: #{err}" 45 | end 46 | 47 | def test_cli_base_url 48 | out, err = capture_subprocess_io do 49 | assert cmd("#{@schema} -b https://example.com") 50 | end 51 | assert err.empty?, "errors not empty: #{err}" 52 | assert File.read(File.join("output", 'index.html')).include?("") 53 | end 54 | 55 | def test_cli_version 56 | out, err = capture_subprocess_io do 57 | assert cmd("--version") 58 | end 59 | assert_match /#{GraphQLDocs::VERSION}/, out 60 | assert err.empty?, "errors not empty: #{err}" 61 | end 62 | 63 | private 64 | 65 | def cmd(args, exception: true) 66 | system("ruby -Ilib ./exe/graphql-docs #{args}", exception: exception) 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /test/graphql-docs/fixtures/landing_pages/broken_yaml.md: -------------------------------------------------------------------------------- 1 | --- 2 | hey now 3 | --- 4 | 5 | # Words, words 6 | -------------------------------------------------------------------------------- /test/graphql-docs/fixtures/landing_pages/object.erb: -------------------------------------------------------------------------------- 1 | --- 2 | title: Variable Objects 3 | --- 4 | # Objects 5 | 6 | <%= some_var %> in GraphQL represent the resources that you can access. 7 | -------------------------------------------------------------------------------- /test/graphql-docs/fixtures/landing_pages/whitespace_template.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: GraphQL documentation 3 | --- 4 | 5 | # GraphQL Reference 6 | 7 | ```json 8 | { 9 | "nest1": { 10 | "nest2": { 11 | "nest3": true 12 | } 13 | } 14 | } 15 | ``` 16 | -------------------------------------------------------------------------------- /test/graphql-docs/fixtures/sw-schema.graphql: -------------------------------------------------------------------------------- 1 | """ 2 | A character in the Star Wars Trilogy 3 | """ 4 | interface Character { 5 | """ 6 | Which movies they appear in. 7 | """ 8 | appearsIn: [Episode] 9 | 10 | """ 11 | The friends of the character, or an empty list if they have none. 12 | """ 13 | friends: [Character] 14 | 15 | """ 16 | The id of the character. 17 | """ 18 | id: ID! 19 | 20 | """ 21 | The name of the character. 22 | """ 23 | name: String 24 | 25 | """ 26 | All secrets about their past. 27 | """ 28 | secretBackstory: String 29 | } 30 | 31 | """ 32 | A mechanical creature in the Star Wars universe. 33 | """ 34 | type Droid implements Character { 35 | """ 36 | Which movies they appear in. 37 | """ 38 | appearsIn: [Episode] 39 | 40 | """ 41 | The friends of the droid, or an empty list if they have none. 42 | """ 43 | friends: [Character] 44 | 45 | """ 46 | The id of the droid. 47 | """ 48 | id: ID! 49 | 50 | """ 51 | The name of the droid. 52 | """ 53 | name: String 54 | 55 | """ 56 | The primary function of the droid. 57 | """ 58 | primaryFunction: String 59 | 60 | """ 61 | Construction date and the name of the designer. 62 | """ 63 | secretBackstory: String 64 | } 65 | 66 | """ 67 | One of the films in the Star Wars Trilogy 68 | """ 69 | enum Episode { 70 | """ 71 | Released in 1980. 72 | """ 73 | EMPIRE 74 | 75 | """ 76 | Released in 1983. 77 | """ 78 | JEDI 79 | 80 | """ 81 | Released in 1977. 82 | """ 83 | NEWHOPE 84 | } 85 | 86 | """ 87 | A humanoid creature in the Star Wars universe. 88 | """ 89 | type Human implements Character { 90 | """ 91 | Which movies they appear in. 92 | """ 93 | appearsIn: [Episode] 94 | 95 | """ 96 | The friends of the human, or an empty list if they have none. 97 | """ 98 | friends: [Character] 99 | 100 | """ 101 | The home planet of the human, or null if unknown. 102 | """ 103 | homePlanet: String 104 | 105 | """ 106 | The id of the human. 107 | """ 108 | id: ID! 109 | 110 | """ 111 | The name of the human. 112 | """ 113 | name: String 114 | 115 | """ 116 | Where are they from and how they came to be who they are. 117 | """ 118 | secretBackstory: String 119 | } 120 | 121 | """ 122 | Root query 123 | """ 124 | type Query { 125 | """ 126 | Return the Droid by ID. 127 | """ 128 | droid( 129 | """ 130 | id of the droid 131 | """ 132 | id: ID! 133 | ): Droid 134 | 135 | """ 136 | Return the hero by episode. 137 | """ 138 | hero( 139 | """ 140 | If omitted, returns the hero of the whole saga. If provided, returns the hero of that particular episode. 141 | """ 142 | episode: Episode 143 | ): Character 144 | 145 | """ 146 | Return the Human by ID. 147 | """ 148 | human( 149 | """ 150 | id of the human 151 | """ 152 | id: ID! 153 | ): Human 154 | } 155 | -------------------------------------------------------------------------------- /test/graphql-docs/fixtures/tiny-schema.graphql: -------------------------------------------------------------------------------- 1 | """ 2 | The Code of Conduct for a repository 3 | """ 4 | type CodeOfConduct { 5 | """ 6 | The body of the CoC 7 | """ 8 | body: String 9 | 10 | """ 11 | The key for the CoC 12 | """ 13 | key: String! 14 | 15 | """ 16 | The formal name of the CoC 17 | """ 18 | name: String! 19 | 20 | """ 21 | The path to the CoC 22 | """ 23 | url: URI 24 | } 25 | 26 | """ 27 | The query root of GitHub's GraphQL interface. 28 | """ 29 | type Query { 30 | """ 31 | Look up a code of conduct by its key 32 | """ 33 | codeOfConduct( 34 | """ 35 | The code of conduct's key 36 | """ 37 | key: String! 38 | ): CodeOfConduct 39 | } 40 | 41 | """ 42 | An RFC 3986, RFC 3987, and RFC 6570 (level 4) compliant URI string. 43 | """ 44 | scalar URI 45 | -------------------------------------------------------------------------------- /test/graphql-docs/generator_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'test_helper' 4 | 5 | class GeneratorTest < Minitest::Test 6 | class CustomRenderer 7 | def initialize(_, _); end 8 | 9 | def render(contents, type: nil, name: nil, filename: nil) # rubocop:disable Lint/UnusedMethodArgument 10 | to_html(contents) 11 | end 12 | 13 | def to_html(contents) 14 | return '' if contents.nil? 15 | 16 | contents.sub(/CodeOfConduct/i, 'CoC!!!!!') 17 | end 18 | end 19 | 20 | def setup 21 | schema = File.read(File.join(fixtures_dir, 'gh-schema.graphql')) 22 | @parser = GraphQLDocs::Parser.new(schema, {}) 23 | @results = @parser.parse 24 | 25 | tiny_schema = File.read(File.join(fixtures_dir, 'tiny-schema.graphql')) 26 | @tiny_parser = GraphQLDocs::Parser.new(tiny_schema, {}) 27 | @tiny_results = @tiny_parser.parse 28 | 29 | @output_dir = File.join(fixtures_dir, 'output') 30 | end 31 | 32 | def deep_copy(hash) 33 | Marshal.load(Marshal.dump(hash)) 34 | end 35 | 36 | def test_that_it_requires_templates 37 | options = deep_copy(GraphQLDocs::Configuration::GRAPHQLDOCS_DEFAULTS) 38 | options[:templates][:objects] = 'BOGUS' 39 | 40 | assert_raises IOError do 41 | GraphQLDocs::Generator.new(@results, options) 42 | end 43 | end 44 | 45 | def test_that_it_does_not_require_default 46 | options = deep_copy(GraphQLDocs::Configuration::GRAPHQLDOCS_DEFAULTS) 47 | options[:templates][:default] = nil 48 | 49 | GraphQLDocs::Generator.new(@results, options) 50 | end 51 | 52 | def test_that_it_works 53 | options = deep_copy(GraphQLDocs::Configuration::GRAPHQLDOCS_DEFAULTS) 54 | options[:output_dir] = @output_dir 55 | options[:delete_output] = true 56 | 57 | generator = GraphQLDocs::Generator.new(@results, options) 58 | generator.generate 59 | 60 | assert File.exist? File.join(@output_dir, 'index.html') 61 | assert File.exist? File.join(@output_dir, 'assets', 'style.css') 62 | assert File.exist? File.join(@output_dir, 'enum', 'issuestate', 'index.html') 63 | assert File.exist? File.join(@output_dir, 'input_object', 'projectorder', 'index.html') 64 | assert File.exist? File.join(@output_dir, 'interface', 'reactable', 'index.html') 65 | assert File.exist? File.join(@output_dir, 'query', 'codeofconduct', 'index.html') 66 | assert File.exist? File.join(@output_dir, 'mutation', 'addcomment', 'index.html') 67 | assert File.exist? File.join(@output_dir, 'object', 'repository', 'index.html') 68 | assert File.exist? File.join(@output_dir, 'scalar', 'boolean', 'index.html') 69 | assert File.exist? File.join(@output_dir, 'union', 'issuetimelineitem', 'index.html') 70 | assert File.exist? File.join(@output_dir, 'directive', 'deprecated', 'index.html') 71 | 72 | # content sanity checks 73 | Dir.glob("#{@output_dir}/**/*.html") do |file| 74 | contents = File.read(file) 75 | # no empty types 76 | refute_match %r{}, contents 77 | end 78 | end 79 | 80 | def test_that_turning_off_styles_works 81 | options = deep_copy(GraphQLDocs::Configuration::GRAPHQLDOCS_DEFAULTS) 82 | options[:output_dir] = @output_dir 83 | options[:delete_output] = true 84 | options[:use_default_styles] = false 85 | 86 | generator = GraphQLDocs::Generator.new(@tiny_results, options) 87 | generator.generate 88 | 89 | refute File.exist? File.join(@output_dir, 'assets', 'style.css') 90 | end 91 | 92 | def test_that_setting_base_url_works 93 | options = deep_copy(GraphQLDocs::Configuration::GRAPHQLDOCS_DEFAULTS) 94 | options[:output_dir] = @output_dir 95 | options[:delete_output] = true 96 | options[:base_url] = 'wowzers' 97 | 98 | generator = GraphQLDocs::Generator.new(@tiny_results, options) 99 | generator.generate 100 | 101 | contents = File.read File.join(@output_dir, 'index.html') 102 | assert_match %r{}, contents 103 | 104 | contents = File.read File.join(@output_dir, 'object', 'codeofconduct', 'index.html') 105 | assert_match %r{href="wowzers/object/codeofconduct/"}, contents 106 | end 107 | 108 | def test_that_custom_renderer_can_be_used 109 | options = deep_copy(GraphQLDocs::Configuration::GRAPHQLDOCS_DEFAULTS) 110 | options[:output_dir] = @output_dir 111 | 112 | options[:renderer] = CustomRenderer 113 | 114 | generator = GraphQLDocs::Generator.new(@tiny_results, options) 115 | generator.generate 116 | 117 | contents = File.read(File.join(@output_dir, 'object', 'codeofconduct', 'index.html')) 118 | 119 | assert_match(/CoC!!!!!/, contents) 120 | end 121 | 122 | def test_that_it_sets_classes 123 | options = deep_copy(GraphQLDocs::Configuration::GRAPHQLDOCS_DEFAULTS) 124 | options[:output_dir] = @output_dir 125 | options[:delete_output] = true 126 | options[:classes][:field_entry] = 'my-4' 127 | 128 | generator = GraphQLDocs::Generator.new(@tiny_results, options) 129 | generator.generate 130 | 131 | object = File.read File.join(@output_dir, 'object', 'codeofconduct', 'index.html') 132 | 133 | assert_match(/
/, object) 134 | end 135 | 136 | def test_that_missing_landing_pages_are_reported 137 | options = deep_copy(GraphQLDocs::Configuration::GRAPHQLDOCS_DEFAULTS) 138 | options[:landing_pages][:index] = 'BOGUS' 139 | 140 | assert_raises IOError do 141 | GraphQLDocs::Generator.new(@tiny_results, options) 142 | end 143 | end 144 | 145 | def test_that_erb_landing_pages_work 146 | options = deep_copy(GraphQLDocs::Configuration::GRAPHQLDOCS_DEFAULTS) 147 | options[:output_dir] = @output_dir 148 | options[:landing_pages][:object] = File.join(fixtures_dir, 'landing_pages', 'object.erb') 149 | options[:landing_pages][:variables] = { some_var: 'wowie!!' } 150 | 151 | generator = GraphQLDocs::Generator.new(@tiny_results, options) 152 | generator.generate 153 | 154 | object = File.read File.join(@output_dir, 'object', 'index.html') 155 | 156 | assert_match(/Variable Objects/, object) 157 | assert_match(/wowie!!/, object) 158 | end 159 | 160 | def test_that_broken_yaml_is_caught 161 | options = deep_copy(GraphQLDocs::Configuration::GRAPHQLDOCS_DEFAULTS) 162 | options[:landing_pages][:index] = File.join(fixtures_dir, 'landing_pages', 'broken_yaml.md') 163 | generator = GraphQLDocs::Generator.new(@tiny_results, options) 164 | 165 | assert_raises TypeError do 166 | generator.generate 167 | end 168 | end 169 | 170 | def test_that_markdown_preserves_whitespace 171 | options = deep_copy(GraphQLDocs::Configuration::GRAPHQLDOCS_DEFAULTS) 172 | options[:output_dir] = @output_dir 173 | options[:landing_pages][:index] = File.join(fixtures_dir, 'landing_pages', 'whitespace_template.md') 174 | 175 | generator = GraphQLDocs::Generator.new(@tiny_results, options) 176 | generator.generate 177 | 178 | contents = File.read File.join(@output_dir, 'index.html') 179 | 180 | assert_match(/ "nest2": \{/, contents) 181 | end 182 | 183 | def test_that_empty_html_lines_not_interpreted_by_markdown 184 | options = deep_copy(GraphQLDocs::Configuration::GRAPHQLDOCS_DEFAULTS) 185 | options[:output_dir] = @output_dir 186 | 187 | generator = GraphQLDocs::Generator.new(@tiny_results, options) 188 | generator.generate 189 | 190 | contents = File.read File.join(@output_dir, 'query', 'codeofconduct', 'index.html') 191 | 192 | assert_match(%r{

The code of conduct's key

}, contents) 193 | end 194 | 195 | def test_that_non_empty_html_lines_not_interpreted_by_markdown 196 | options = deep_copy(GraphQLDocs::Configuration::GRAPHQLDOCS_DEFAULTS) 197 | options[:output_dir] = @output_dir 198 | 199 | generator = GraphQLDocs::Generator.new(@results, options) 200 | generator.generate 201 | 202 | contents = File.read File.join(@output_dir, 'input_object', 'projectorder', 'index.html') 203 | 204 | assert_match %r{
\n

The direction in which to order projects by the specified field.

\s+
}, 205 | contents 206 | end 207 | end 208 | -------------------------------------------------------------------------------- /test/graphql-docs/graphql-docs_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'test_helper' 4 | 5 | class GraphQLDocsTest < Minitest::Test 6 | def test_that_it_requires_a_file_or_string 7 | assert_raises ArgumentError do 8 | GraphQLDocs.build({}) 9 | end 10 | end 11 | 12 | def test_it_demands_string_argument 13 | assert_raises TypeError do 14 | GraphQLDocs.build(filename: 43) 15 | end 16 | 17 | assert_raises TypeError do 18 | GraphQLDocs.build(schema: 43) 19 | end 20 | end 21 | 22 | def test_it_needs_a_file_that_exists 23 | assert_raises ArgumentError do 24 | GraphQLDocs.build(filename: 'not/a/real/file') 25 | end 26 | end 27 | 28 | def test_it_needs_one_or_the_other 29 | assert_raises ArgumentError do 30 | GraphQLDocs.build(filename: 'http://graphql.org/swapi-graphql/', schema: File.join(fixtures_dir, 'gh-api.json')) 31 | end 32 | 33 | assert_raises ArgumentError do 34 | GraphQLDocs.build 35 | end 36 | end 37 | 38 | def test_it_accepts_class_schema 39 | GraphQLDocs.build(schema: MySchema) 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /test/graphql-docs/parser_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'test_helper' 4 | 5 | class ParserTest < Minitest::Test 6 | def setup 7 | @ghapi = File.read(File.join(fixtures_dir, 'gh-schema.graphql')) 8 | @swapi = File.read(File.join(fixtures_dir, 'sw-schema.graphql')) 9 | @gh_parser = GraphQLDocs::Parser.new(@ghapi, {}) 10 | @gh_results = @gh_parser.parse 11 | end 12 | 13 | def test_it_accepts_schema_class 14 | schema = MySchema 15 | 16 | results = GraphQLDocs::Parser.new(schema, {}).parse 17 | assert_equal 'myField', results[:operation_types][0][:fields][0][:name] 18 | assert_equal "Title paragraph.\n ```\n line1\n line2\n line3\n ```", results[:operation_types][0][:fields][0][:description] 19 | end 20 | 21 | def test_types_are_sorted 22 | names = @gh_results[:object_types].map { |t| t[:name] } 23 | assert_equal names.sort, names 24 | end 25 | 26 | def test_connections_are_plucked 27 | issue = @gh_results[:object_types].find { |t| t[:name] == 'Issue' } 28 | refute issue[:connections].empty? 29 | end 30 | 31 | def test_knows_implementers_for_interfaces 32 | comment = @gh_results[:interface_types].find { |t| t[:name] == 'Comment' } 33 | refute comment[:implemented_by].empty? 34 | end 35 | 36 | def test_groups_items_by_type 37 | assert @gh_results[:input_object_types] 38 | assert @gh_results[:object_types] 39 | assert @gh_results[:scalar_types] 40 | assert @gh_results[:interface_types] 41 | assert @gh_results[:enum_types] 42 | assert @gh_results[:union_types] 43 | assert @gh_results[:mutation_types] 44 | assert @gh_results[:directive_types] 45 | end 46 | 47 | def test_directives 48 | names = @gh_results[:directive_types].map { |t| t[:name] } 49 | assert_equal %w[deprecated include oneOf preview skip specifiedBy], names 50 | 51 | preview_directive = @gh_results[:directive_types].find { |t| t[:name] == 'deprecated' } 52 | assert_equal %i[FIELD_DEFINITION ENUM_VALUE ARGUMENT_DEFINITION INPUT_FIELD_DEFINITION], preview_directive[:locations] 53 | 54 | assert_equal 'Marks an element of a GraphQL schema as no longer supported.', preview_directive[:description] 55 | reason_arg = preview_directive[:arguments].first 56 | assert_equal 'reason', reason_arg[:name] 57 | assert_equal 'Explains why this element was deprecated, usually also including a suggestion for how to access supported similar data. Formatted in [Markdown](https://daringfireball.net/projects/markdown/).', reason_arg[:description] 58 | end 59 | 60 | def test_mutationless_schemas_do_not_explode 61 | parser = GraphQLDocs::Parser.new(@swapi, {}) 62 | results = parser.parse 63 | 64 | assert_empty results[:mutation_types] 65 | end 66 | 67 | def test_scalar_inputs_for_mutations_are_supported 68 | schema = <<-SCHEMA 69 | type Query { 70 | foo : ID 71 | } 72 | input MessageInput { 73 | content: String 74 | author: String 75 | } 76 | type Mutation { 77 | bar(id: ID!, input: MessageInput) : ID 78 | } 79 | SCHEMA 80 | 81 | parser = GraphQLDocs::Parser.new(schema, {}) 82 | results = parser.parse 83 | 84 | assert results[:mutation_types] 85 | end 86 | 87 | def test_schemas_with_quote_style_comments_works 88 | schema = <<-SCHEMA 89 | type Query { 90 | profile: User 91 | } 92 | 93 | """ 94 | A user 95 | """ 96 | type User { 97 | """ 98 | The id of the user 99 | """ 100 | id: String! 101 | 102 | """ 103 | The email of user 104 | """ 105 | email: String 106 | } 107 | SCHEMA 108 | 109 | parser = GraphQLDocs::Parser.new(schema, {}) 110 | 111 | results = parser.parse 112 | assert results[:object_types] 113 | user = results[:object_types].first 114 | assert_equal 'The id of the user', user[:fields].first[:description] 115 | end 116 | 117 | def test_deprecations 118 | schema = MySchema 119 | 120 | fields = GraphQLDocs::Parser.new(schema, {}).parse[:operation_types][0][:fields] 121 | 122 | refute fields[0][:is_deprecated] 123 | assert fields[1][:is_deprecated] 124 | assert fields[2][:arguments][0][:is_deprecated] 125 | end 126 | end 127 | -------------------------------------------------------------------------------- /test/graphql-docs/renderer_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'test_helper' 4 | 5 | class RendererTest < Minitest::Test 6 | def setup 7 | @swapi = File.read(File.join(fixtures_dir, 'sw-schema.graphql')) 8 | @parsed_schema = GraphQLDocs::Parser.new(@swapi, {}).parse 9 | @renderer = GraphQLDocs::Renderer.new(@parsed_schema, GraphQLDocs::Configuration::GRAPHQLDOCS_DEFAULTS) 10 | end 11 | 12 | def test_that_rendering_works 13 | contents = @renderer.render('R2D2', type: 'Droid', name: 'R2D2') 14 | 15 | assert_match %r{R2D2}, contents 16 | end 17 | 18 | def test_that_html_conversion_works 19 | contents = @renderer.to_html('**R2D2**') 20 | 21 | assert_equal '

R2D2

', contents 22 | end 23 | 24 | def test_that_unsafe_html_is_not_blocked_by_default 25 | contents = @renderer.to_html('Oh hello') 26 | 27 | assert_equal '

Oh hello

', contents 28 | end 29 | 30 | def test_that_unsafe_html_is_blocked_when_asked 31 | renderer = GraphQLDocs::Renderer.new(@parsed_schema, GraphQLDocs::Configuration::GRAPHQLDOCS_DEFAULTS.merge({ 32 | pipeline_config: { 33 | pipeline: 34 | %i[ExtendedMarkdownFilter 35 | EmojiFilter 36 | TableOfContentsFilter], 37 | context: { 38 | gfm: false, 39 | unsafe: false, 40 | asset_root: 'https://a248.e.akamai.net/assets.github.com/images/icons' 41 | } 42 | } 43 | })) 44 | contents = renderer.to_html('Oh **hello**') 45 | 46 | assert_equal '

Oh hello

', contents 47 | end 48 | 49 | def test_that_filename_accessible_to_filters 50 | renderer = GraphQLDocs::Renderer.new(@parsed_schema, GraphQLDocs::Configuration::GRAPHQLDOCS_DEFAULTS.merge({ 51 | pipeline_config: { 52 | pipeline: 53 | %i[ExtendedMarkdownFilter 54 | EmojiFilter 55 | TableOfContentsFilter 56 | AddFilenameFilter], 57 | context: { 58 | gfm: false, 59 | unsafe: true, 60 | asset_root: 'https://a248.e.akamai.net/assets.github.com/images/icons' 61 | } 62 | } 63 | })) 64 | contents = renderer.render('', type: 'Droid', name: 'R2D2', filename: '/this/is/the/filename') 65 | assert_match %r{/this/is/the/filename}, contents 66 | end 67 | end 68 | 69 | class AddFilenameFilter < HTML::Pipeline::Filter 70 | def call 71 | doc.search('span[@id="fill-me"]').each do |span| 72 | span.inner_html = context[:filename] 73 | end 74 | doc 75 | end 76 | 77 | def validate 78 | needs :filename 79 | end 80 | end 81 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | $LOAD_PATH.unshift File.expand_path('../lib', __dir__) 4 | require 'graphql-docs' 5 | 6 | require 'minitest/autorun' 7 | require 'minitest/pride' 8 | require 'minitest/focus' 9 | 10 | def fixtures_dir 11 | File.join(File.dirname(__FILE__), 'graphql-docs', 'fixtures') 12 | end 13 | 14 | def output_dir 15 | File.join(fixtures_dir, 'output') 16 | end 17 | def clean_up_output 18 | FileUtils.rm_rf(output_dir) 19 | end 20 | clean_up_output 21 | 22 | class QueryType < GraphQL::Schema::Object 23 | field :my_field, Int, "Title paragraph. 24 | ``` 25 | line1 26 | line2 27 | line3 28 | ```", null: false 29 | 30 | field :deprecated_field, Int, deprecation_reason: "Not useful any more" 31 | 32 | field :field_with_deprecated_arg, Int do 33 | argument :my_arg, Int, required: false, deprecation_reason: "Not useful any more" 34 | end 35 | end 36 | 37 | class MySchema < GraphQL::Schema 38 | query QueryType 39 | end 40 | --------------------------------------------------------------------------------